From 99e2323a493647e577dfa0a410c78c08eab803d9 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 27 Mar 2025 16:18:49 -0600 Subject: [PATCH 01/25] Refactored tools into a shared component. Removed 3.9 support. Added MCP support. Added prompt caching. Cleaned content_parts behaviors on messages. Made ruff rules stricter. --- examples/rag.py | 65 ++- poetry.lock | 151 ++++-- pyproject.toml | 74 +-- rigging/__init__.py | 15 +- rigging/chat.py | 772 ++++++++++++++++------------- rigging/completion.py | 198 +++++--- rigging/data.py | 58 ++- rigging/error.py | 16 +- rigging/generator/__init__.py | 19 +- rigging/generator/base.py | 151 ++++-- rigging/generator/http.py | 67 ++- rigging/generator/litellm_.py | 118 ++++- rigging/generator/transformers_.py | 38 +- rigging/generator/vllm_.py | 34 +- rigging/integrations/__init__.py | 6 - rigging/integrations/robopages.py | 168 ------- rigging/interact.py | 24 +- rigging/logging.py | 8 +- rigging/message.py | 356 ++++++++++--- rigging/model.py | 349 +++++++++++-- rigging/parsing.py | 44 +- rigging/prompt.py | 248 ++++++--- rigging/tool/__init__.py | 13 +- rigging/tool/api.py | 181 +------ rigging/tool/base.py | 379 ++++++++++++++ rigging/tool/mcp.py | 225 +++++++++ rigging/tool/native.py | 378 +++++--------- rigging/tool/robopages.py | 104 ++++ rigging/util.py | 17 +- rigging/watchers.py | 43 +- tests/test_chat.py | 48 +- tests/test_completion.py | 2 - tests/test_tool.py | 340 +++++++++++++ tests/test_watchers.py | 30 +- tests/test_xml_parsing.py | 4 +- 35 files changed, 3160 insertions(+), 1583 deletions(-) delete mode 100644 rigging/integrations/__init__.py delete mode 100644 rigging/integrations/robopages.py create mode 100644 rigging/tool/base.py create mode 100644 rigging/tool/mcp.py create mode 100644 rigging/tool/robopages.py create mode 100644 tests/test_tool.py diff --git a/examples/rag.py b/examples/rag.py index c2bf9bf..b68cbe7 100644 --- a/examples/rag.py +++ b/examples/rag.py @@ -2,8 +2,6 @@ # Credit to https://github.com/kyleavery for the contribution # -from __future__ import annotations - import asyncio import os import time @@ -80,7 +78,9 @@ def chunk_document(document: RawDocument) -> t.Generator[Document, None, None]: } -def read_documents_from_path(directory: Path, extensions: list[str]) -> t.Generator[Document, None, None]: +def read_documents_from_path( + directory: Path, extensions: list[str] +) -> t.Generator[Document, None, None]: if not directory.is_dir(): raise ValueError(f"{directory} is not a directory") @@ -171,7 +171,7 @@ def populate_db(self, directories: list[Path], extensions: list[str]) -> None: "title": {"type": "text"}, "slice_start": {"type": "integer"}, "slice_end": {"type": "integer"}, - } + }, } self.es_client.indices.create(index=VECTOR_INDEX, mappings=index_mapping) @@ -237,10 +237,10 @@ def fuzzy_search(self, search_phrase: str, max_results: int) -> list[Reference]: "query": search_phrase, "fields": ["content", "title"], "fuzziness": "AUTO", - } - } - ] - } + }, + }, + ], + }, } try: @@ -271,7 +271,9 @@ def semantic_search(self, search_phrase: str, max_results: int) -> list[Referenc return self.handle_elastic_results(es_response) def reciprocal_rank_fusion( - self, fuzzy_results: list[Reference], semantic_results: list[Reference] + self, + fuzzy_results: list[Reference], + semantic_results: list[Reference], ) -> list[Reference]: """ Implementation copied from Nemesis: @@ -287,13 +289,19 @@ def reciprocal_rank_fusion( unique_refs_dict = {result.id: result for result in fuzzy_results + semantic_results} fused_scores: dict[str, float] = {} - for rank, result in enumerate(sorted(fuzzy_results, key=lambda x: x.score, reverse=True), 1): + for rank, result in enumerate( + sorted(fuzzy_results, key=lambda x: x.score, reverse=True), 1 + ): fused_scores[result.id] = fused_scores.get(result.id, 0) + 1 / (k_fuzzy + rank) - for rank, result in enumerate(sorted(semantic_results, key=lambda x: x.score, reverse=True), 1): + for rank, result in enumerate( + sorted(semantic_results, key=lambda x: x.score, reverse=True), 1 + ): fused_scores[result.id] = fused_scores.get(result.id, 0) + 1 / (k_semantic + rank) - combined_results_sorted = sorted(unique_refs_dict.values(), key=lambda x: fused_scores[x.id], reverse=True) + combined_results_sorted = sorted( + unique_refs_dict.values(), key=lambda x: fused_scores[x.id], reverse=True + ) for result in combined_results_sorted: result.score = round( @@ -302,7 +310,7 @@ def reciprocal_rank_fusion( ) logger.trace( - f"Combined results:\n{chr(10).join([f' {ref.title} ({ref.score})' for ref in combined_results_sorted])}" + f"Combined results:\n{chr(10).join([f' {ref.title} ({ref.score})' for ref in combined_results_sorted])}", ) return combined_results_sorted @@ -335,7 +343,7 @@ def hybrid_search(self, search_phrase: str, max_refs: int) -> str: """ for ref in sorted_refs - ] + ], ) return f"""\ @@ -363,9 +371,23 @@ def hybrid_search(self, search_phrase: str, max_refs: int) -> str: default="mistral/mistral-embed", help="LiteLLM embedding model identifier", ) -@click.option("-eH", "--elastic-host", envvar="ES_HOST", required=True, help="Elasticsearch host (ES_HOST)") -@click.option("-eU", "--elastic-username", envvar="ES_USER", required=True, help="Elasticsearch username (ES_USER)") -@click.option("-eP", "--elastic-password", envvar="ES_PASS", required=True, help="Elasticsearch password (ES_PASS)") +@click.option( + "-eH", "--elastic-host", envvar="ES_HOST", required=True, help="Elasticsearch host (ES_HOST)" +) +@click.option( + "-eU", + "--elastic-username", + envvar="ES_USER", + required=True, + help="Elasticsearch username (ES_USER)", +) +@click.option( + "-eP", + "--elastic-password", + envvar="ES_PASS", + required=True, + help="Elasticsearch password (ES_PASS)", +) @click.option( "--log-level", type=click.Choice(logging.LogLevelList), @@ -433,14 +455,19 @@ async def search(ctx: click.Context, query: str, generator_id: str, max_refs: in ref_db = ReferenceDB(embedding_id, elastic_host, elastic_username, elastic_password) if not ref_db.index_exists(VECTOR_INDEX): - logger.error(f"Elasticsearch index '{VECTOR_INDEX}' is empty. Please run the 'populate' command first.") + logger.error( + f"Elasticsearch index '{VECTOR_INDEX}' is empty. Please run the 'populate' command first." + ) return generator = rg.get_generator(generator_id) chat = await generator.chat( [ - {"role": "system", "content": SYSTEM_PROMPT + "\n\n" + ref_db.hybrid_search(query, max_refs)}, + { + "role": "system", + "content": SYSTEM_PROMPT + "\n\n" + ref_db.hybrid_search(query, max_refs), + }, {"role": "user", "content": query}, ], ).run() diff --git a/poetry.lock b/poetry.lock index d093ce2..62836fd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -269,7 +269,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -818,10 +818,7 @@ files = [ [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" -urllib3 = [ - {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, - {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, -] +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] crt = ["awscrt (==0.23.8)"] @@ -1467,7 +1464,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "dev"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -1835,6 +1832,18 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "httpx-sse" +version = "0.4.0" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, + {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, +] + [[package]] name = "huggingface-hub" version = "0.29.3" @@ -1891,12 +1900,11 @@ version = "8.6.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" -groups = ["main", "dev", "docs"] +groups = ["main"] files = [ {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, ] -markers = {dev = "python_version < \"3.10\"", docs = "python_version < \"3.10\""} [package.dependencies] zipp = ">=3.20" @@ -2008,7 +2016,6 @@ prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] @@ -2259,7 +2266,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" @@ -2324,14 +2330,14 @@ regex = ["regex"] [[package]] name = "litellm" -version = "1.63.11" +version = "1.63.12" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] files = [ - {file = "litellm-1.63.11-py3-none-any.whl", hash = "sha256:f3915dc35309b164ef2419ad05e5241ddd97f3f47aa036df28365bf889d8ea23"}, - {file = "litellm-1.63.11.tar.gz", hash = "sha256:89930895121d0cbf5553e560ed886c45be480ceec0eca3c53ae441473d5d46a4"}, + {file = "litellm-1.63.12-py3-none-any.whl", hash = "sha256:ae72a9d7099100b4b1172aaa2954bf6d7b205d47ba76beec5cd53f62dd57913e"}, + {file = "litellm-1.63.12.tar.gz", hash = "sha256:db875fb0b5d2bebdcf68805bc0bd4733dcebf3630e9eef4753cfe414a53120fc"}, ] [package.dependencies] @@ -2349,7 +2355,7 @@ tokenizers = "*" [package.extras] extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "resend (>=0.8.0,<0.9.0)"] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=22.0.0,<23.0.0)", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=22.0.0,<23.0.0)", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"] [[package]] name = "lm-format-enforcer" @@ -2413,9 +2419,6 @@ files = [ {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - [package.extras] docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] @@ -2506,6 +2509,33 @@ files = [ [package.dependencies] traitlets = "*" +[[package]] +name = "mcp" +version = "1.5.0" +description = "Model Context Protocol SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527"}, + {file = "mcp-1.5.0.tar.gz", hash = "sha256:5b2766c05e68e01a2034875e250139839498c61792163a7b221fc170c12f5aa9"}, +] + +[package.dependencies] +anyio = ">=4.5" +httpx = ">=0.27" +httpx-sse = ">=0.4" +pydantic = ">=2.7.2,<3.0.0" +pydantic-settings = ">=2.5.2" +sse-starlette = ">=1.6.1" +starlette = ">=0.27" +uvicorn = ">=0.23.1" + +[package.extras] +cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] +rich = ["rich (>=13.9.4)"] +ws = ["websockets (>=15.0.1)"] + [[package]] name = "mergedeep" version = "1.3.4" @@ -2534,7 +2564,6 @@ files = [ click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" markdown = ">=3.3.6" markupsafe = ">=2.0.1" @@ -2580,7 +2609,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} mergedeep = ">=1.3.4" platformdirs = ">=2.2.0" pyyaml = ">=5.1" @@ -2658,7 +2686,6 @@ files = [ [package.dependencies] click = ">=7.0" -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} Jinja2 = ">=2.11.1" Markdown = ">=3.3" MarkupSafe = ">=1.1" @@ -2666,7 +2693,6 @@ mkdocs = ">=1.4" mkdocs-autorefs = ">=0.3.1" platformdirs = ">=2.2.0" pymdown-extensions = ">=6.3" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} [package.extras] crystal = ["mkdocstrings-crystal (>=0.3.4)"] @@ -4122,6 +4148,27 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.8.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"}, + {file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pydantic-xml" version = "2.14.3" @@ -4814,7 +4861,7 @@ description = "C version of reader, parser and emitter for ruamel.yaml derived f optional = false python-versions = ">=3.9" groups = ["main"] -markers = "platform_python_implementation == \"CPython\" and python_version <= \"3.12\"" +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, @@ -5031,6 +5078,26 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sse-starlette" +version = "2.2.1" +description = "SSE plugin for Starlette" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99"}, + {file = "sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419"}, +] + +[package.dependencies] +anyio = ">=4.7.0" +starlette = ">=0.41.3" + +[package.extras] +examples = ["fastapi"] +uvicorn = ["uvicorn (>=0.34.0)"] + [[package]] name = "stack-data" version = "0.6.3" @@ -5055,10 +5122,9 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] name = "starlette" version = "0.46.1" description = "The little ASGI library that shines." -optional = true +optional = false python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"all\"" files = [ {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"}, {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"}, @@ -5066,7 +5132,6 @@ files = [ [package.dependencies] anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] @@ -5212,7 +5277,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version <= \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -5441,7 +5506,7 @@ description = "A language and compiler for custom Deep Learning operations" optional = true python-versions = "*" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.12\" and extra == \"all\"" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version <= \"3.11\" and extra == \"all\"" files = [ {file = "triton-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ce4b8ff70c48e47274c66f269cce8861cf1dc347ceeb7a67414ca151b1822d8"}, {file = "triton-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c3d9607f85103afdb279938fc1dd2a66e4f5999a58eb48a346bd42738f986dd"}, @@ -5540,12 +5605,11 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] 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"}, ] -markers = {docs = "python_version < \"3.10\""} [[package]] name = "tzdata" @@ -5559,24 +5623,6 @@ files = [ {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, ] -[[package]] -name = "urllib3" -version = "1.26.20" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["main", "docs"] -markers = "python_version < \"3.10\"" -files = [ - {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, - {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, -] - -[package.extras] -brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - [[package]] name = "urllib3" version = "2.3.0" @@ -5584,7 +5630,6 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["main", "docs"] -markers = "python_version >= \"3.10\"" files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, @@ -5600,10 +5645,9 @@ zstd = ["zstandard (>=0.18.0)"] name = "uvicorn" version = "0.34.0" description = "The lightning-fast ASGI server." -optional = true +optional = false python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"all\"" files = [ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, @@ -6159,12 +6203,11 @@ version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" -groups = ["main", "dev", "docs"] +groups = ["main"] files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] -markers = {dev = "python_version < \"3.10\"", docs = "python_version < \"3.10\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] @@ -6181,5 +6224,5 @@ tracing = [] [metadata] lock-version = "2.1" -python-versions = "^3.9" -content-hash = "510aca5e1c1dad11e15173973e5cb5e8771475647b874383ba16d6f47a7e105c" +python-versions = "^3.10" +content-hash = "762aeaf734b37469815d6adbc151875abd6a9c078ddd7a9bcc8075ee43d24e0c" diff --git a/pyproject.toml b/pyproject.toml index 0518f3b..1e394d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ packages = [{ include = "rigging" }] # Dependencies [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" pydantic = "^2.7.3" pydantic-xml = "^2.11.0" loguru = "^0.7.2" @@ -37,6 +37,7 @@ click = { version = "^8.1.7", optional = true } httpx = { version = "^0.27.0", optional = true } aiodocker = { version = "^0.22.2", optional = true } websockets = { version = "^13.0", optional = true } +mcp = "^1.5.0" [tool.poetry.extras] tracing = ["logfire"] @@ -109,62 +110,31 @@ strict = true # Formatting / Linting [tool.ruff] -line-length = 120 -indent-width = 4 target-version = "py310" -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", +line-length = 100 +extend-exclude = [ + "*.ipynb", # jupyter notebooks + "tests/*" ] [tool.ruff.lint] -fixable = ["ALL"] -unfixable = ["B"] -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "C", # flake8-comprehensions - "B", # flake8-bugbear - "UP", # pyupgrade - "NPY", # numpydoc - "TCH", # typecheck - "A", # flake8-annotations -] +select = [ "ALL" ] ignore = [ - "E501", # line too long, handled by black - "B008", # do not perform function calls in argument defaults - "C901", # too complex - "W191", # indentation contains tabs - "F722", # syntax error in forward annotation - "UP007", # X | Y syntax while we're still supporting 3.9 - "UP038", # isinstance() X | Y instance ^ - "B905", # zip() without strict (isn't supported in 3.9) + "E501", # line too long (we make best effort) + "TRY003", # long messages in exception classes + "EM", # picky message construction for exceptions + "C90", # mccabe complexity + "A002", # shadowing built-in + "D", # docstrings + "ANN", # annotations (handled by mypy) + "PLR0913", # too many arguments + "ERA001", # commented out code + "FIX002", # contains todo, consider fixing + "TD002", # TODO + "TD003", # TODO + "PLR0911", # too many return statements + "FBT003", # boolean positional in function call ] [tool.ruff.format] -quote-style = "double" -indent-style = "space" -skip-magic-trailing-comma = false -line-ending = "auto" +skip-magic-trailing-comma = false \ No newline at end of file diff --git a/rigging/__init__.py b/rigging/__init__.py index 675a1a5..69e2dd3 100644 --- a/rigging/__init__.py +++ b/rigging/__init__.py @@ -1,6 +1,11 @@ -from rigging import data, error, generator, integrations, logging, model, parsing, tool, watchers +from rigging import data, error, generator, logging, model, parsing, watchers from rigging.chat import Chat, ChatPipeline, MapChatCallback, ThenChatCallback -from rigging.completion import Completion, CompletionPipeline, MapCompletionCallback, ThenCompletionCallback +from rigging.completion import ( + Completion, + CompletionPipeline, + MapCompletionCallback, + ThenCompletionCallback, +) from rigging.generator import ( GeneratedMessage, GeneratedText, @@ -15,7 +20,7 @@ from rigging.message import ContentImageUrl, ContentText, Message, MessageDict, Messages from rigging.model import Model, attr, element, wrapped from rigging.prompt import Ctx, Prompt, prompt -from rigging.tool import ApiTool, Tool +from rigging.tool import Tool, mcp, robopages, tool from rigging.util import await_ # TODO: Migrate to importlib for this @@ -29,7 +34,6 @@ "ContentText", "ContentImageUrl", "Tool", - "ApiTool", "Model", "attr", "element", @@ -54,7 +58,6 @@ "error", "parsing", "tool", - "integrations", "logging", "await_", "interact", @@ -63,6 +66,8 @@ "ThenCompletionCallback", "MapCompletionCallback", "generator", + "mcp", + "robopages", ] from loguru import logger diff --git a/rigging/chat.py b/rigging/chat.py index 806b08a..1e0983d 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -4,35 +4,42 @@ They are the primary way to interact with the generator. """ -from __future__ import annotations - import asyncio +import types import typing as t -import warnings from copy import deepcopy from dataclasses import dataclass from datetime import datetime from typing import runtime_checkable from uuid import UUID, uuid4 +from elasticsearch import AsyncElasticsearch from loguru import logger from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, ValidationError, computed_field from rigging.error import MessagesExhaustedMaxRoundsError, UnknownToolError from rigging.generator import GenerateParams, Generator, get_generator -from rigging.generator.base import GeneratedMessage, StopReason, Usage # noqa: TCH001 -from rigging.message import Message, MessageDict, Messages +from rigging.generator.base import GeneratedMessage, StopReason, Usage +from rigging.message import Content, Message, MessageDict, Messages from rigging.model import Model, ModelT, SystemErrorModel, ValidationErrorModel -from rigging.tool.api import ApiTool, ToolChoice -from rigging.tool.native import Tool, ToolCalls, ToolDescriptionList, ToolResult, ToolResults, system_tool_extension +from rigging.tool.api import ApiToolChoice +from rigging.tool.base import Tool, ToolMode +from rigging.tool.native import ( + JsonInXmlToolCall, + JsonInXmlToolDefinition, + XmlToolCall, + XmlToolDefinition, + tool_description_prompt_part, +) from rigging.tracing import Span, tracer from rigging.util import get_qualified_name if t.TYPE_CHECKING: - from elasticsearch import AsyncElasticsearch - from rigging.data import ElasticOpType - from rigging.prompt import P, Prompt, R + from rigging.prompt import Prompt + +P = t.ParamSpec("P") +R = t.TypeVar("R") CallableT = t.TypeVar("CallableT", bound=t.Callable[..., t.Any]) @@ -48,6 +55,13 @@ - include: Mark the message as failed and include it in the final output. """ +CacheMode = t.Literal["latest"] +""" +How to handle cache_control entries on messages. + +- latest: Assign cache_control to the latest 2 non-assistant messages in the pipeline before inference. +""" + class Chat(BaseModel): """ @@ -69,19 +83,20 @@ class Chat(BaseModel): stop_reason: StopReason = Field(default="unknown") """The reason the generation stopped.""" - usage: t.Optional[Usage] = Field(None, repr=False) + usage: Usage | None = Field(None, repr=False) """The usage statistics for the generation if available.""" extra: dict[str, t.Any] = Field(default_factory=dict, repr=False) """Any additional information from the generation.""" - generator: t.Optional[Generator] = Field(None, exclude=True, repr=False) + generator: Generator | None = Field(None, exclude=True, repr=False) """The generator associated with the chat.""" - params: t.Optional[GenerateParams] = Field(None, exclude=True, repr=False) + params: GenerateParams | None = Field(None, exclude=True, repr=False) """Any additional generation params used for this chat.""" - error: t.Optional[ - t.Annotated[Exception, PlainSerializer(lambda x: str(x), return_type=str, when_used="json-unless-none")] - ] = Field(None, repr=False) + error: t.Annotated[ + Exception, + PlainSerializer(lambda x: str(x), return_type=str, when_used="json-unless-none"), + ] | None = Field(None, repr=False) """Holds any exception that was caught during the generation pipeline.""" failed: bool = Field(False, exclude=False, repr=True) """ @@ -89,6 +104,8 @@ class Chat(BaseModel): This is typically used for graceful error handling when parsing. """ + _pipeline: "ChatPipeline | None" + @computed_field(repr=False) # type: ignore [prop-decorator] @property def generator_id(self) -> str | None: @@ -101,7 +118,8 @@ def __init__( self, messages: Messages, generated: Messages | None = None, - generator: t.Optional[Generator] = None, + generator: Generator | None = None, + pipeline: "ChatPipeline | None" = None, **kwargs: t.Any, ): """ @@ -131,6 +149,8 @@ def __init__( **kwargs, ) + self._pipeline = pipeline + def __len__(self) -> int: return len(self.all) @@ -167,9 +187,12 @@ def message_dicts(self) -> list[MessageDict]: Returns: The MessageDict list """ - return [t.cast(MessageDict, m.model_dump(include={"role", "all_content"})) for m in self.all] + return [ + t.cast(MessageDict, m.model_dump(include={"role", "content_parts"}, exclude_none=True)) + for m in self.all + ] - def meta(self, **kwargs: t.Any) -> Chat: + def meta(self, **kwargs: t.Any) -> "Chat": """ Updates the metadata of the chat with the provided key-value pairs. @@ -182,7 +205,12 @@ def meta(self, **kwargs: t.Any) -> Chat: self.metadata.update(kwargs) return self - def restart(self, *, generator: t.Optional[Generator] = None, include_all: bool = False) -> ChatPipeline: + def restart( + self, + *, + generator: Generator | None = None, + include_all: bool = False, + ) -> "ChatPipeline": """ Attempt to convert back to a ChatPipeline for further generation. @@ -199,6 +227,8 @@ def restart(self, *, generator: t.Optional[Generator] = None, include_all: bool """ messages = self.all if include_all else self.messages if generator is None: + if self._pipeline is not None: + return self._pipeline.clone(chat=Chat(messages)) generator = self.generator if generator is None: raise ValueError("Cannot restart a chat without an associated generator") @@ -209,7 +239,7 @@ def fork( messages: t.Sequence[Message] | t.Sequence[MessageDict] | Message | MessageDict | str, *, include_all: bool = False, - ) -> ChatPipeline: + ) -> "ChatPipeline": """ Forks the chat by creating calling [rigging.chat.Chat.restart][] and appending the specified messages. @@ -224,11 +254,14 @@ def fork( """ return self.restart(include_all=include_all).add(messages) - def continue_(self, messages: t.Sequence[Message] | t.Sequence[MessageDict] | Message | str) -> ChatPipeline: + def continue_( + self, + messages: t.Sequence[Message] | t.Sequence[MessageDict] | Message | str, + ) -> "ChatPipeline": """Alias for the [rigging.chat.Chat.fork][] with `include_all=True`.""" return self.fork(messages, include_all=True) - def clone(self, *, only_messages: bool = False) -> Chat: + def clone(self, *, only_messages: bool = False) -> "Chat": """ Creates a deep copy of the chat. @@ -254,7 +287,7 @@ def clone(self, *, only_messages: bool = False) -> Chat: new.error = self.error return new - def apply(self, **kwargs: str) -> Chat: + def apply(self, **kwargs: str) -> "Chat": """ Calls [rigging.message.Message.apply][] on the last message in the chat with the given keyword arguments. @@ -270,7 +303,7 @@ def apply(self, **kwargs: str) -> Chat: self.messages[-1] = self.messages[-1].apply(**kwargs) return self - def apply_to_all(self, **kwargs: str) -> Chat: + def apply_to_all(self, **kwargs: str) -> "Chat": """ Calls [rigging.message.Message.apply][] on all messages in the chat with the given keyword arguments. @@ -284,7 +317,7 @@ def apply_to_all(self, **kwargs: str) -> Chat: self.generated = Message.apply_to_list(self.generated, **kwargs) return self - def strip(self, model_type: type[Model], fail_on_missing: bool = False) -> Chat: + def strip(self, model_type: type[Model], fail_on_missing: bool = False) -> "Chat": # noqa: FBT001, FBT002 (historical) """ Strips all parsed parts of a particular type from the message content. @@ -317,20 +350,30 @@ def inject_system_content(self, content: str) -> Message: """ if len(self.messages) == 0 or self.messages[0].role != "system": self.messages.insert(0, Message(role="system", content=content)) - elif self.messages[0].role == "system": + elif self.messages[0].role == "system" and content not in self.messages[0].content: self.messages[0].content += "\n\n" + content return self.messages[0] - def inject_tool_prompt(self, tools: t.Sequence[Tool]) -> None: + def inject_tool_prompt(self, tools: t.Sequence[Tool], mode: ToolMode) -> None: """ Injects a default tool use prompt into the system prompt. Args: tools: A sequence of Tool objects. """ - call_format = ToolCalls.xml_example() - tool_description_list = ToolDescriptionList(tools=[t.get_description() for t in tools]) - tool_system_prompt = system_tool_extension(call_format, tool_description_list.to_pretty_xml()) + if mode not in ["xml", "json-in-xml"]: + return + + definitions: list[XmlToolDefinition] | list[JsonInXmlToolDefinition] + if mode == "xml": + definitions = [tool.xml_definition for tool in tools] + else: + definitions = [tool.json_definition for tool in tools] + + tool_system_prompt = tool_description_prompt_part( + definitions, + t.cast(t.Literal["xml", "json-in-xml"], mode), + ) self.inject_system_content(tool_system_prompt) def to_df(self) -> t.Any: @@ -352,7 +395,7 @@ async def to_elastic( index: str, client: AsyncElasticsearch, *, - op_type: ElasticOpType = "index", + op_type: "ElasticOpType" = "index", create_index: bool = True, **kwargs: t.Any, ) -> int: @@ -366,7 +409,14 @@ async def to_elastic( """ from rigging.data import chats_to_elastic - return await chats_to_elastic(self, index, client, op_type=op_type, create_index=create_index, **kwargs) + return await chats_to_elastic( + self, + index, + client, + op_type=op_type, + create_index=create_index, + **kwargs, + ) # List Helper Type @@ -398,7 +448,7 @@ async def to_elastic( index: str, client: AsyncElasticsearch, *, - op_type: ElasticOpType = "index", + op_type: "ElasticOpType" = "index", create_index: bool = True, **kwargs: t.Any, ) -> int: @@ -412,7 +462,14 @@ async def to_elastic( """ from rigging.data import chats_to_elastic - return await chats_to_elastic(self, index, client, op_type=op_type, create_index=create_index, **kwargs) + return await chats_to_elastic( + self, + index, + client, + op_type=op_type, + create_index=create_index, + **kwargs, + ) def to_json(self) -> list[dict[str, t.Any]]: """ @@ -485,8 +542,8 @@ def __init__( generator: Generator, messages: t.Sequence[Message], *, - params: t.Optional[GenerateParams] = None, - watch_callbacks: t.Optional[list[WatchChatCallback]] = None, + params: GenerateParams | None = None, + watch_callbacks: list[WatchChatCallback] | None = None, ): self.generator: Generator = generator """The generator object responsible for generating the chat.""" @@ -506,14 +563,15 @@ def __init__( """The list of exceptions to exclude from the catch list.""" self.on_failed: FailMode = "raise" """How to handle failures in the pipeline unless overriden in calls.""" + self.caching: CacheMode | None = None # (callback, attempt_recovery, drop_dialog, max_rounds) self.until_callbacks: list[tuple[UntilMessageCallback, bool, bool, int]] = [] self.until_types: list[type[Model]] = [] - self.api_tools: list[ApiTool] = [] - self.native_tools: list[Tool] = [] - self.inject_native_tool_prompt: bool = True - self.force_native_tool: bool = False + self.tools: list[Tool] = [] + self.tool_mode: ToolMode = "auto" + self.api_tool_choice: ApiToolChoice | None = None + self.inject_tool_prompt = True self.then_callbacks: list[ThenChatCallback] = [] self.map_callbacks: list[MapChatCallback] = [] self.watch_callbacks: list[WatchChatCallback] = watch_callbacks or [] @@ -521,7 +579,7 @@ def __init__( def __len__(self) -> int: return len(self.chat) - def with_(self, params: t.Optional[GenerateParams] = None, **kwargs: t.Any) -> ChatPipeline: + def with_(self, params: GenerateParams | None = None, **kwargs: t.Any) -> "ChatPipeline": """ Assign specific generation parameter overloads for this chat. @@ -547,8 +605,11 @@ def with_(self, params: t.Optional[GenerateParams] = None, **kwargs: t.Any) -> C return self def catch( - self, *errors: type[Exception], on_failed: FailMode | None = None, exclude: list[type[Exception]] | None = None - ) -> ChatPipeline: + self, + *errors: type[Exception], + on_failed: FailMode | None = None, + exclude: list[type[Exception]] | None = None, + ) -> "ChatPipeline": """ Adds exceptions to catch during generation when including or skipping failures. @@ -564,7 +625,11 @@ def catch( self.on_failed = on_failed or self.on_failed return self - def watch(self, *callbacks: WatchChatCallback, allow_duplicates: bool = False) -> ChatPipeline: + def watch( + self, + *callbacks: WatchChatCallback, + allow_duplicates: bool = False, + ) -> "ChatPipeline": """ Registers a callback to monitor any chats produced. @@ -589,10 +654,15 @@ async def log(chats: list[Chat]) -> None: def add( self, - messages: t.Sequence[MessageDict] | t.Sequence[Message] | MessageDict | Message | str, + messages: t.Sequence[MessageDict] + | t.Sequence[Message] + | MessageDict + | Message + | Content + | str, *, merge_strategy: t.Literal["only-user-role", "all", "none"] = "only-user-role", - ) -> ChatPipeline: + ) -> "ChatPipeline": """ Appends new message(s) to the internal chat before generation. @@ -615,17 +685,26 @@ def add( """ message_list = Message.fit_as_list(messages) - if merge_strategy != "none" and self.chat.all and self.chat.all[-1].role == message_list[0].role: - if merge_strategy == "all" or (merge_strategy == "only-user-role" and self.chat.all[-1].role == "user"): - self.chat.all[-1].content += "\n" + message_list[0].content - message_list = message_list[1:] + if ( + merge_strategy != "none" + and self.chat.all + and self.chat.all[-1].role == message_list[0].role + and ( + merge_strategy == "all" + or merge_strategy == "only-user-role" + and self.chat.all[-1].role == "user" + ) + ): + self.chat.all[-1].content += "\n" + message_list[0].content + message_list = message_list[1:] self.chat.generated += message_list return self def fork( - self, messages: t.Sequence[MessageDict] | t.Sequence[Message] | MessageDict | Message | str - ) -> ChatPipeline: + self, + messages: t.Sequence[MessageDict] | t.Sequence[Message] | MessageDict | Message | str, + ) -> "ChatPipeline": """ Creates a new instance of `ChatPipeline` by forking the current chat and adding the specified messages. @@ -639,7 +718,7 @@ def fork( """ return self.clone().add(messages) - def clone(self, *, only_messages: bool = False) -> ChatPipeline: + def clone(self, *, only_messages: bool = False, chat: Chat | None = None) -> "ChatPipeline": """ Creates a clone of the current `ChatPipeline` instance. @@ -647,6 +726,8 @@ def clone(self, *, only_messages: bool = False) -> ChatPipeline: only_messages: If True, only the messages will be cloned. If False (default), the entire `ChatPipeline` instance will be cloned including until callbacks, types, tools, metadata, etc. + chat: An optional chat object clone for use in the new pipeline, otherwise the current + internal chat object will be cloned. Returns: A new instance of `ChatPipeline` that is a clone of the current instance. @@ -657,23 +738,41 @@ def clone(self, *, only_messages: bool = False) -> ChatPipeline: params=self.params.model_copy() if self.params is not None else None, watch_callbacks=self.watch_callbacks, ) - new.chat = self.chat.clone() + new.chat = (chat or self.chat).clone() if not only_messages: new.until_callbacks = self.until_callbacks.copy() new.until_types = self.until_types.copy() - new.native_tools = self.native_tools.copy() - new.api_tools = self.api_tools.copy() - new.inject_native_tool_prompt = self.inject_native_tool_prompt - new.force_native_tool = self.force_native_tool + new.tools = self.tools.copy() + new.tool_mode = self.tool_mode new.metadata = deepcopy(self.metadata) - new.then_callbacks = self.then_callbacks.copy() new.map_callbacks = self.map_callbacks.copy() new.on_failed = self.on_failed new.errors_to_fail_on = self.errors_to_fail_on.copy() new.errors_to_exclude = self.errors_to_exclude.copy() + new.caching = self.caching + + # Check if any of our callbacks are bound methods to a ChatPipline. + # If so, we should rebind them to `self` to ensure they work correctly + # and aren't operating with old state. + + new.then_callbacks = [ + callback + if not hasattr(callback, "__self__") + or not isinstance(callback.__self__, ChatPipeline) + else types.MethodType(callback.__func__, self) # type: ignore [attr-defined] + for callback in self.then_callbacks.copy() + ] + new.map_callbacks = [ + callback + if not hasattr(callback, "__self__") + or not isinstance(callback.__self__, ChatPipeline) + else types.MethodType(callback.__func__, self) # type: ignore [attr-defined] + for callback in self.map_callbacks.copy() + ] + return new - def meta(self, **kwargs: t.Any) -> ChatPipeline: + def meta(self, **kwargs: t.Any) -> "ChatPipeline": """ Updates the metadata of the chat with the provided key-value pairs. @@ -686,7 +785,7 @@ def meta(self, **kwargs: t.Any) -> ChatPipeline: self.metadata.update(kwargs) return self - def then(self, callback: ThenChatCallback) -> ChatPipeline: + def then(self, callback: ThenChatCallback) -> "ChatPipeline": """ Registers a callback to be executed after the generation process completes. @@ -714,7 +813,7 @@ async def process(chat: Chat) -> Chat | None: self.then_callbacks.append(callback) return self - def map(self, callback: MapChatCallback) -> ChatPipeline: + def map(self, callback: MapChatCallback) -> "ChatPipeline": """ Registers a callback to be executed after the generation process completes. @@ -742,7 +841,7 @@ async def process(chats: list[Chat]) -> list[Chat]: self.map_callbacks.append(callback) return self - def apply(self, **kwargs: str) -> ChatPipeline: + def apply(self, **kwargs: str) -> "ChatPipeline": """ Clones this chat pipeline and calls [rigging.chat.Chat.apply][] with the given keyword arguments. @@ -756,7 +855,7 @@ def apply(self, **kwargs: str) -> ChatPipeline: new.chat.apply(**kwargs) return new - def apply_to_all(self, **kwargs: str) -> ChatPipeline: + def apply_to_all(self, **kwargs: str) -> "ChatPipeline": """ Clones this chat pipeline and calls [rigging.chat.Chat.apply_to_all][] with the given keyword arguments. @@ -770,6 +869,21 @@ def apply_to_all(self, **kwargs: str) -> ChatPipeline: new.chat.apply_to_all(**kwargs) return new + def cache(self, mode: CacheMode | None | t.Literal[False] = "latest") -> "ChatPipeline": + """ + Sets the caching mode for the pipeline. + + Args: + mode: The caching mode to use. Defaults to "latest". + + Returns: + The updated pipeline. + """ + if mode is False: + mode = None + self.caching = mode + return self + def until( self, callback: UntilMessageCallback, @@ -777,7 +891,7 @@ def until( attempt_recovery: bool = True, drop_dialog: bool = True, max_rounds: int = DEFAULT_MAX_ROUNDS, - ) -> ChatPipeline: + ) -> "ChatPipeline": """ Registers a callback to participate in validating the generation process. @@ -817,7 +931,7 @@ def callback(message: Message) -> tuple[bool, list[Message]]: self.until_callbacks.append((callback, attempt_recovery, drop_dialog, max_rounds)) return self - def wrap(self, func: t.Callable[[CallableT], CallableT]) -> ChatPipeline: + def wrap(self, func: t.Callable[[CallableT], CallableT]) -> "ChatPipeline": """ Helper for [rigging.generator.base.Generator.wrap][]. @@ -830,144 +944,62 @@ def wrap(self, func: t.Callable[[CallableT], CallableT]) -> ChatPipeline: self.generator = self.generator.wrap(func) return self - @t.overload - def using(self, *tools: t.Callable[..., t.Any], choice: ToolChoice | None = None) -> ChatPipeline: - ... - - @t.overload - def using( - self, - *tools: Tool, - force: bool = False, - attempt_recovery: bool = True, - drop_dialog: bool = False, - max_rounds: int = DEFAULT_MAX_ROUNDS, - inject_prompt: bool | None = None, - ) -> ChatPipeline: - ... - def using( self, - *tools: Tool | ApiTool | t.Callable[..., t.Any], - choice: ToolChoice | None = None, - force: bool = False, - attempt_recovery: bool = True, - drop_dialog: bool = False, - max_rounds: int = DEFAULT_MAX_ROUNDS, - inject_prompt: bool | None = None, - ) -> ChatPipeline: + *tools: Tool | t.Callable[..., t.Any], + mode: ToolMode | None = None, + choice: ApiToolChoice | None = None, + ) -> "ChatPipeline": """ Adds a tool or a sequence of tools to participate in the generation process. - These can be either: - - Native tools (rigging.tool.native.Tool) which use manual parsing and schema insertion - - API tools (rigging.tool.api.ApiTool or any callable) which uses api-provided tool integrations + Note: + By default, the tool mode is set to "auto" which will attempt to use + api function calling if available, otherwise it will fallback to `xml`. Args: - tools: The tool or sequence of tools to be added. - force: Whether to force the use of the tool(s) at least once. - attempt_recovery: Whether to attempt recovery if the tool(s) fail by providing - validation feedback to the model before the next round. - drop_dialog: Whether to drop the intermediate dialog of recovery efforts - before returning the final chat to the caller. - max_rounds: The maximum number of rounds to attempt recovery. - inject_prompt: Whether to inject the tool guidance prompt into a - system message.and will override self.inject_tool_prompt if provided. + *tools: The tools to be added to the pipeline. + mode: The tool mode to use (e.g., "xml", "json-in-xml", "api"). + choice: The API tool choice to use. This is only relevant when using the "api" tool mode. Returns: - The updated ChatPipeline object. - """ - native_tools = [tool for tool in tools if isinstance(tool, Tool)] - if native_tools and len(native_tools) != len(tools): - raise ValueError("All tools must be of the same type (api or native)") - - if native_tools: - return self.using_native_tools( - *t.cast(list[Tool], tools), - force=force, - attempt_recovery=attempt_recovery, - drop_dialog=drop_dialog, - max_rounds=max_rounds, - inject_prompt=inject_prompt, + The updated pipeline. + + Example: + ```python + async def get_weather(city: Annotated[str, "The city name to get weather for"]) -> str: + "Get the weather" + city = city.replace(" ", "+") + return requests.get(f"http://wttr.in/{city}?format=2").text.strip() + + chat = ( + await rg.get_generator("openai/gpt-4o-mini") + .chat("What's the weather in london?") + .using(get_weather) + .run() ) - else: - return self.using_api_tools(*t.cast(list[ApiTool], tools), choice=choice) - - def using_native_tools( - self, - *tools: Tool, - force: bool = False, - attempt_recovery: bool = True, - drop_dialog: bool = False, - max_rounds: int = DEFAULT_MAX_ROUNDS, - inject_prompt: bool | None = None, - ) -> ChatPipeline: - """ - Adds a tool or a sequence of tools to participate in the generation process. - - Args: - tools: The tool or sequence of tools to be added. - force: Whether to force the use of the tool(s) at least once. - attempt_recovery: Whether to attempt recovery if the tool(s) fail by providing - validation feedback to the model before the next round. - drop_dialog: Whether to drop the intermediate dialog of recovery efforts - before returning the final chat to the caller. - max_rounds: The maximum number of rounds to attempt recovery. - inject_prompt: Whether to inject the tool guidance prompt into a - system message.and will override self.inject_tool_prompt if provided. - - Returns: - The updated ChatPipeline object. + ``` """ if len(tools) == 0: return self - if self.api_tools: - raise ValueError("Cannot mix native and API tools in the same pipeline") + new_tools = [tool if isinstance(tool, Tool) else Tool.from_callable(tool) for tool in tools] - self.native_tools += tools - self.inject_native_tool_prompt = inject_prompt or self.inject_native_tool_prompt - self.force_native_tool = force - if next((c for c in self.until_callbacks if c[0] == self._until_native_tools_callback), None) is None: - self.until_callbacks.append( - ( - self._until_native_tools_callback, - attempt_recovery, - drop_dialog, - max_rounds, - ) - ) - return self + existing_names = {tool.name for tool in self.tools} + for tool in new_tools: + if tool.name in existing_names: + raise ValueError(f"Tool with name '{tool.name}' already exists in the pipeline.") - def using_api_tools( - self, *tools: ApiTool | t.Callable[..., t.Any], choice: ToolChoice | None = None - ) -> ChatPipeline: - """ - Adds an API tool or a sequence of API tools to participate in the generation process. + self.tools += new_tools - Args: - tools: The API tool or sequence of API tools to be added. + if next((c for c in self.then_callbacks if c == self._then_tools), None) is None: + self.then_callbacks.append(self._then_tools) - Returns: - The updated ChatPipeline object. - """ - if len(tools) == 0: - return self - - if self.native_tools: - raise ValueError("Cannot mix native and API tools in the same pipeline") - - self.api_tools += [tool if isinstance(tool, ApiTool) else ApiTool(tool) for tool in tools] - - if self.params is None: - self.params = GenerateParams() - self.params.tools = [tool.definition for tool in self.api_tools] + if mode is not None: + self.tool_mode = mode if choice is not None: - self.params.tool_choice = choice - - if next((c for c in self.then_callbacks if c == self._then_api_tools), None) is None: - self.then_callbacks.append(self._then_api_tools) + self.api_tool_choice = choice return self @@ -977,7 +1009,7 @@ def until_parsed_as( attempt_recovery: bool = False, drop_dialog: bool = True, max_rounds: int = DEFAULT_MAX_ROUNDS, - ) -> ChatPipeline: + ) -> "ChatPipeline": """ Adds the specified types to the list of types which should successfully parse before the generation process completes. @@ -994,70 +1026,40 @@ def until_parsed_as( The updated ChatPipeline object. """ self.until_types += types - if next((c for c in self.until_callbacks if c[0] == self._until_parse_callback), None) is None: - self.until_callbacks.append((self._until_parse_callback, attempt_recovery, drop_dialog, max_rounds)) + if ( + next((c for c in self.until_callbacks if c[0] == self._until_parse_callback), None) + is None + ): + self.until_callbacks.append( + (self._until_parse_callback, attempt_recovery, drop_dialog, max_rounds), + ) return self - def _until_native_tools_callback(self, message: Message) -> tuple[bool, list[Message]]: - generated: list[Message] = [message] - - try: - tool_calls = message.try_parse(ToolCalls) - except ValidationError as e: - generated.append(Message.from_model(ValidationErrorModel(content=str(e)))) - return (True, generated) - - if tool_calls is None: - if self.force_native_tool: - logger.debug("No tool calls or types, returning error") - generated.append(Message.from_model(SystemErrorModel(content="You must use a tool"))) - else: - logger.debug("No tool calls or types, returning message") - return (self.force_native_tool, generated) - - self.force_native_tool = False - - tool_results: list[ToolResult] = [] - errors: list[SystemErrorModel] = [] - for call in tool_calls.calls: - if call.tool not in [tool.name for tool in self.native_tools]: - errors.append(SystemErrorModel(content=f"Tool '{call.tool}' does not exist")) - continue + def _has_tool_calls_to_process(self, message: Message) -> bool: + if not self.tools: + return False - tool = next(t for t in self.native_tools if t.name == call.tool) - tool_description = tool.get_description() + xml_tool_calls = message.try_parse_set(XmlToolCall) + json_tool_calls = message.try_parse_set(JsonInXmlToolCall) + api_tool_calls = message.tool_calls - if call.function not in [f.name for f in tool_description.functions]: - errors.append(SystemErrorModel(content=f"Function '{call.function}' does not exist on '{tool.name}'")) - continue + return bool(xml_tool_calls or json_tool_calls or api_tool_calls) - tool_results.append(tool(call)) - - if errors: - generated.append(Message.from_model(errors, suffix="Rewrite your message with all the required tags.")) - else: - generated.append(Message.from_model(ToolResults(results=tool_results))) - - return (True, generated) + # TODO: There is a lot of duplicated code between these _then_*_tools methods + # and we should clean it up. async def _then_api_tools(self, chat: Chat) -> Chat | None: - # If there are no tool calls, we can continue if not chat.last.tool_calls: return None - # Slightly strange cloning behavior here, but we are abusing - # the .then() mechanic slightly and want our pipeline to maintain - # all existing state - - next_pipeline = self.clone() - next_pipeline.chat = chat.clone() + next_pipeline = self.clone(chat=chat) for tool_call in chat.last.tool_calls: - tool = next((t for t in self.api_tools if t.name == tool_call.function.name), None) + tool = next((t for t in self.tools if t.name == tool_call.function.name), None) if tool is None: raise UnknownToolError(tool_call.function.name) - next_pipeline.add(await tool.execute(tool_call)) + next_pipeline.add(await tool.handle_tool_call(tool_call)) # Need to prevent infinite loops and treat tool_choice like # an ephemeral setting which resets after each tool call. @@ -1069,6 +1071,48 @@ async def _then_api_tools(self, chat: Chat) -> Chat | None: return await next_pipeline.run() + async def _then_xml_tools(self, chat: Chat) -> Chat | None: + tool_calls = chat.last.try_parse_set(XmlToolCall) + if not tool_calls: + return None + + next_pipeline = self.clone(chat=chat) + + for tool_call in tool_calls: + tool = next((t for t in self.tools if t.name == tool_call.name), None) + if tool is None: + raise UnknownToolError(tool_call.name) + next_pipeline.add(await tool.handle_tool_call(tool_call)) + + return await next_pipeline.run() + + async def _then_json_in_xml_tools(self, chat: Chat) -> Chat | None: + tool_calls = chat.last.try_parse_set(JsonInXmlToolCall) + if not tool_calls: + return None + + next_pipeline = self.clone(chat=chat) + + for tool_call in tool_calls: + tool = next((t for t in self.tools if t.name == tool_call.name), None) + if tool is None: + raise UnknownToolError(tool_call.name) + next_pipeline.add(await tool.handle_tool_call(tool_call)) + + return await next_pipeline.run() + + async def _then_tools(self, chat: Chat) -> Chat | None: + if self.tool_mode == "api": + return await self._then_api_tools(chat) + if self.tool_mode == "xml": + return await self._then_xml_tools(chat) + if self.tool_mode == "json-in-xml": + return await self._then_json_in_xml_tools(chat) + + raise RuntimeError( + "tool_mode appears incorrect and must be one of 'api', 'xml', or 'json-in-xml'", + ) + def _until_parse_callback(self, message: Message) -> tuple[bool, list[Message]]: should_continue: bool = False generated: list[Message] = [message] @@ -1081,15 +1125,15 @@ def _until_parse_callback(self, message: Message) -> tuple[bool, list[Message]]: Message.from_model( ValidationErrorModel(content=str(e)), suffix="Rewrite your entire message with all the required elements.", - ) + ), ) - except Exception as e: + except Exception as e: # noqa: BLE001 should_continue = True generated.append( Message.from_model( SystemErrorModel(content=str(e)), suffix="Rewrite your entire message with all the required elements.", - ) + ), ) return (should_continue, generated) @@ -1098,8 +1142,8 @@ def _until( self, message: Message, callback: UntilMessageCallback, - attempt_recovery: bool, - drop_dialog: bool, + attempt_recovery: bool, # noqa: FBT001 + drop_dialog: bool, # noqa: FBT001 max_rounds: int, ) -> t.Generator[list[Message], Message, list[Message]]: should_continue, step_messages = callback(message) @@ -1120,11 +1164,13 @@ def _until( drop_dialog=drop_dialog, ): logger.trace( - f"_until({callback_name}) round {_round}/{max_rounds} (attempt_recovery={attempt_recovery})" + f"_until({callback_name}) round {_round}/{max_rounds} (attempt_recovery={attempt_recovery})", ) next_message = yield running_messages should_continue, step_messages = callback(next_message) - logger.trace(f" |- returned {should_continue} with {len(step_messages)} new messages)") + logger.trace( + f" |- returned {should_continue} with {len(step_messages)} new messages)", + ) if attempt_recovery: running_messages += step_messages @@ -1138,27 +1184,34 @@ def _until( logger.warning(f"Exhausted max rounds ({max_rounds})") raise MessagesExhaustedMaxRoundsError( - max_rounds, [next_message] if not attempt_recovery and next_message else running_messages[:-1] + max_rounds, + [next_message] if not attempt_recovery and next_message else running_messages[:-1], ) - # TODO: Much like the CompletionPipeline code, it's opaque - # exactly how multiple callbacks should be blended together - # when generating. I think we should look at limiting it to - # one callback in total, but I'll leave the behavior as is - # for now with the knowledge that behavior might be a bit - # unpredictable. + # TODO: Much like the CompletionPipeline code, it's opaque exactly how + # multiple callbacks should be blended together when generating. + # + # I think we should look at limiting it to one callback in total, + # but I'll leave the behavior as is for now with the warning that it + # might be a bit unpredictable. + def _process(self) -> t.Generator[list[Message], Message, list[Message]]: - self._pre_run() first_response = yield [] new_messages = [first_response] # If we need to process tool calls, we should do that first # before proceeding with our until callbacks - if first_response.tool_calls and self.api_tools: + if self._has_tool_calls_to_process(first_response): return new_messages for callback, reset_between, drop_internal, max_rounds in self.until_callbacks: - generated = yield from self._until(new_messages[-1], callback, reset_between, drop_internal, max_rounds) + generated = yield from self._until( + new_messages[-1], + callback, + reset_between, + drop_internal, + max_rounds, + ) new_messages = new_messages[:-1] + generated return new_messages @@ -1203,12 +1256,18 @@ async def _post_run(self, chats: list[Chat], on_failed: FailMode) -> ChatList: ): chats = await map_callback(chats) if not all(isinstance(c, Chat) for c in chats): - raise ValueError(f".map() callback must return a Chat object or None ({callback_name})") + raise ValueError( + f".map() callback must return a Chat object or None ({callback_name})", + ) def wrap_then_callback(callback: ThenChatCallback) -> ThenChatCallback: async def traced_then_callback(chat: Chat) -> Chat | None: callback_name = get_qualified_name(callback) - with tracer.span(f"Then with {callback_name}()", callback=callback_name, chat_id=str(chat.uuid)): + with tracer.span( + f"Then with {callback_name}()", + callback=callback_name, + chat_id=str(chat.uuid), + ): return await callback(chat) return traced_then_callback @@ -1218,28 +1277,31 @@ async def traced_then_callback(chat: Chat) -> Chat | None: new_chats = await asyncio.gather(*coros) if not all(isinstance(c, Chat) or c is None for c in new_chats): raise ValueError( - f".then() callback must return a Chat object or None ({get_qualified_name(then_callback)})" + f".then() callback must return a Chat object or None ({get_qualified_name(then_callback)})", ) - chats = [new or chat for new, chat in zip(new_chats, chats)] + chats = [new or chat for new, chat in zip(new_chats, chats, strict=False)] return ChatList(chats) - def _pre_run(self) -> None: - if self.native_tools: - if self.inject_native_tool_prompt: - self.chat.inject_tool_prompt(self.native_tools) - self.inject_native_tool_prompt = False + async def _pre_run(self) -> None: + if self.tool_mode == "auto" and self.tools: + self.tool_mode = "api" if await self.generator.supports_function_calling() else "xml" - # TODO: This can cause issues when certain APIs do not return - # the stop sequence as part of the response. This behavior - # seems like a larger issue than the model continuining after - # requesting a tool call, so we'll remove it for now. - # - # self.params.stop = [ToolCalls.xml_end_tag()] + if self.tools and self.tool_mode in ["xml", "json-in-xml"] and self.inject_tool_prompt: + self.chat.inject_tool_prompt(self.tools, self.tool_mode) + self.inject_native_tool_prompt = False + + if self.tools and self.tool_mode == "api": + if self.params is None: + self.params = GenerateParams() + self.params.tools = [tool.api_definition for tool in self.tools] + self.params.tool_choice = self.api_tool_choice def _fit_params( - self, count: int, params: t.Sequence[t.Optional[GenerateParams] | None] | None = None + self, + count: int, + params: t.Sequence[GenerateParams | None] | None = None, ) -> list[GenerateParams]: params = [None] * count if params is None else list(params) if len(params) != count: @@ -1251,7 +1313,9 @@ def _fit_params( # Run methods def _initialize_states( - self, count: int, params: t.Sequence[t.Optional[GenerateParams]] | None = None + self, + count: int, + params: t.Sequence[GenerateParams | None] | None = None, ) -> list[RunState]: states = [RunState([], [], p, self._process()) for p in self._fit_params(count, params)] for state in states: @@ -1266,10 +1330,10 @@ def _initialize_batch_states( | t.Sequence[str] | MessageDict | str, - params: t.Sequence[t.Optional[GenerateParams]] | None = None, + params: t.Sequence[GenerateParams | None] | None = None, ) -> list[RunState]: - if isinstance(many, dict) or isinstance(many, str): # Some strange typechecking here - many = t.cast(t.Union[t.Sequence[str], t.Sequence[MessageDict]], [many]) + if isinstance(many, dict | str): # Some strange typechecking here + many = t.cast(t.Sequence[str] | t.Sequence[MessageDict], [many]) count = max(len(many), len(params) if params is not None else 0) @@ -1281,7 +1345,10 @@ def _initialize_batch_states( params = self._fit_params(count, params) - states: list[RunState] = [RunState(self.chat.all + m, [], p, self._process()) for m, p in zip(many, params)] + states: list[RunState] = [ + RunState(self.chat.all + m, [], p, self._process()) + for m, p in zip(many, params, strict=False) + ] for state in states: next(state.processor) @@ -1292,14 +1359,15 @@ def _create_chat( state: RunState, outputs: list[Message], inbound: GeneratedMessage, - batch_mode: bool, - failed: bool = False, + batch_mode: bool, # noqa: FBT001 + failed: bool = False, # noqa: FBT001, FBT002 error: Exception | None = None, ) -> Chat: return Chat( state.inputs if batch_mode else self.chat.all, outputs, generator=self.generator, + pipeline=self, metadata=self.metadata, params=state.params, stop_reason=inbound.stop_reason, @@ -1309,7 +1377,7 @@ def _create_chat( error=error, ) - def _create_failed_chat(self, state: RunState, error: Exception, batch_mode: bool) -> Chat: + def _create_failed_chat(self, state: RunState, error: Exception, batch_mode: bool) -> Chat: # noqa: FBT001 return Chat( state.inputs if batch_mode else self.chat.all, [], @@ -1320,16 +1388,46 @@ def _create_failed_chat(self, state: RunState, error: Exception, batch_mode: boo error=error, ) - async def _run(self, span: Span, states: list[RunState], on_failed: FailMode, batch_mode: bool = False) -> ChatList: - pending_states = states + def _apply_cache_mode_to_messages(self, messages: list[list[Message]]) -> list[list[Message]]: + if self.caching is None: + return messages + + if self.caching != "latest": + logger.warning(f"Unknown caching mode '{self.caching}', defaulting to 'latest'") + + # first remove existing cache settings + updated: list[list[Message]] = [] + for _messages in messages: + updated = [*updated, [m.clone().cache(cache_control=False) for m in _messages]] + + # then apply the latest cache settings + for _messages in updated: + for message in [m for m in _messages if m.role != "assistant"][-2:]: + message.cache(cache_control=True) + + return updated + + async def _run( # noqa: PLR0912 + self, + span: Span, + states: list[RunState], + on_failed: FailMode, + batch_mode: bool = False, # noqa: FBT002, FBT001 + ) -> ChatList: + pending_states: list[RunState] = states while pending_states: try: + messages = [ + (s.inputs if batch_mode else self.chat.all) + s.messages for s in pending_states + ] + messages = self._apply_cache_mode_to_messages(messages) + inbounds = await self.generator.generate_messages( - [(s.inputs if batch_mode else self.chat.all) + s.messages for s in pending_states], + messages, [s.params for s in pending_states], ) - except Exception as error: + except Exception as error: # noqa: BLE001 # Handle core generator errors if ( on_failed == "raise" @@ -1349,11 +1447,11 @@ async def _run(self, span: Span, states: list[RunState], on_failed: FailMode, ba else: # Process each inbound message and individual errors - for inbound, state in zip(inbounds, pending_states): + for inbound, state in zip(inbounds, pending_states, strict=False): try: # Process for parsing callbacks, etc. state.messages = state.processor.send(inbound.message) - except StopIteration as stop: + except StopIteration as stop: # noqa: PERF203 # StopIteration implies we are done and the chat is good to go outputs = t.cast(list[Message], stop.value) state.chat = self._create_chat(state, outputs, inbound, batch_mode) @@ -1364,18 +1462,33 @@ async def _run(self, span: Span, states: list[RunState], on_failed: FailMode, ba # exhausted.messages holds the current messages when the error occured, # so we'll pass them into the chat as the last generated messages. state.chat = self._create_chat( - state, exhausted.messages, inbound, batch_mode, failed=True, error=exhausted + state, + exhausted.messages, + inbound, + batch_mode, + failed=True, + error=exhausted, ) if on_failed == "raise": # Set the attribute for troubleshooting - span.set_attribute("chats", [s.chat for s in states if s.chat is not None]) + span.set_attribute( + "chats", + [s.chat for s in states if s.chat is not None], + ) raise - except Exception as error: + except Exception as error: # noqa: BLE001 span.set_attribute("failed", True) span.set_attribute("error", error) - state.chat = self._create_chat(state, [], inbound, batch_mode, failed=True, error=error) + state.chat = self._create_chat( + state, + [], + inbound, + batch_mode, + failed=True, + error=error, + ) # Check to see if we should be handling any specific errors # and gracefully marking the chat as failed instead of raising (.catch) @@ -1385,11 +1498,16 @@ async def _run(self, span: Span, states: list[RunState], on_failed: FailMode, ba or any(isinstance(error, t) for t in self.errors_to_exclude) ): # Set the attribute for troubleshooting - span.set_attribute("chats", [s.chat for s in states if s.chat is not None]) + span.set_attribute( + "chats", + [s.chat for s in states if s.chat is not None], + ) raise pending_states = [s for s in pending_states if s.chat is None] - completed_states = [s for s in states if s.chat is not None and not s.watched] + completed_states: list[RunState] = [ + s for s in states if s.chat is not None and not s.watched + ] if not completed_states: continue @@ -1414,7 +1532,7 @@ async def run(self, *, allow_failed: bool = False, on_failed: FailMode | None = """ Execute the generation process to produce the final chat. - Parameters: + Args: allow_failed: Ignore any errors and potentially return the chat in a failed state. on_failed: The behavior when a message fails to generate. @@ -1428,9 +1546,11 @@ async def run(self, *, allow_failed: bool = False, on_failed: FailMode | None = if on_failed == "skip": raise ValueError( - "Cannot use 'skip' mode with single message generation (pass allow_failed=True or on_failed='include'/'raise')" + "Cannot use 'skip' mode with single message generation (pass allow_failed=True or on_failed='include'/'raise')", ) + await self._pre_run() + on_failed = on_failed or self.on_failed states = self._initialize_states(1) @@ -1449,13 +1569,13 @@ async def run_many( self, count: int, *, - params: t.Sequence[t.Optional[GenerateParams]] | None = None, + params: t.Sequence[GenerateParams | None] | None = None, on_failed: FailMode | None = None, ) -> ChatList: """ Executes the generation process multiple times with the same inputs. - Parameters: + Args: count: The number of times to execute the generation process. params: A sequence of parameters to be used for each execution. on_failed: The behavior when a message fails to generate. @@ -1463,6 +1583,8 @@ async def run_many( Returns: A list of generatated Chats. """ + await self._pre_run() + on_failed = on_failed or self.on_failed states = self._initialize_states(count, params) @@ -1484,7 +1606,7 @@ async def run_batch( | t.Sequence[str] | MessageDict | str, - params: t.Sequence[t.Optional[GenerateParams]] | None = None, + params: t.Sequence[GenerateParams | None] | None = None, *, on_failed: FailMode | None = None, ) -> ChatList: @@ -1494,7 +1616,7 @@ async def run_batch( Note: Anything already in this chat pipeline will be prepended to the input messages. - Parameters: + Args: many: A sequence of sequences of messages to be generated. params: A sequence of parameters to be used for each set of messages. on_failed: The behavior when a message fails to generate. @@ -1502,6 +1624,8 @@ async def run_batch( Returns: A list of generatated Chats. """ + await self._pre_run() + on_failed = on_failed or self.on_failed states = self._initialize_batch_states(many, params) @@ -1516,7 +1640,10 @@ async def run_batch( # Generator iteration async def run_over( - self, *generators: Generator | str, include_original: bool = True, on_failed: FailMode | None = None + self, + *generators: Generator | str, + include_original: bool = True, + on_failed: FailMode | None = None, ) -> ChatList: """ Executes the generation process across multiple generators. @@ -1524,7 +1651,7 @@ async def run_over( For each generator, this pipeline is cloned and the generator is replaced before the run call. All callbacks and parameters are preserved. - Parameters: + Args: *generators: A sequence of generators to be used for the generation process. include_original: Whether to include the original generator in the list of runs. on_failed: The behavior when a message fails to generate. @@ -1532,9 +1659,13 @@ async def run_over( Returns: A list of generatated Chats. """ + await self._pre_run() + on_failed = on_failed or self.on_failed - _generators: list[Generator] = [g if isinstance(g, Generator) else get_generator(g) for g in generators] + _generators: list[Generator] = [ + g if isinstance(g, Generator) else get_generator(g) for g in generators + ] if include_original: _generators.append(self.generator) @@ -1548,9 +1679,9 @@ async def run_over( chats = await asyncio.gather(*coros) return await self._post_run(chats, on_failed) - # Prompt functions + # Prompt binding - def prompt(self, func: t.Callable[P, t.Coroutine[None, None, R]]) -> Prompt[P, R]: + def prompt(self, func: t.Callable[P, t.Coroutine[None, None, R]]) -> "Prompt[P, R]": """ Decorator to convert a function into a prompt bound to this pipeline. @@ -1565,46 +1696,3 @@ def prompt(self, func: t.Callable[P, t.Coroutine[None, None, R]]) -> Prompt[P, R from rigging.prompt import prompt return prompt(func, pipeline=self) - - async def run_prompt(self, prompt: Prompt[P, R], /, *args: P.args, **kwargs: P.kwargs) -> R: - """ - Calls [rigging.prompt.Prompt.run][] with this pipeline. - - Warning: - This method is deprecated and will be removed in a future release. - Use [Prompt.bind(pipeline)][rigging.prompt.Prompt.bind] instead. - """ - warnings.warn("run_prompt is deprecated, use Prompt.bind(pipeline) instead", DeprecationWarning, stacklevel=2) - return await prompt.bind(self)(*args, **kwargs) - - async def run_prompt_many(self, prompt: Prompt[P, R], count: int, /, *args: P.args, **kwargs: P.kwargs) -> list[R]: - """ - Calls [rigging.prompt.Prompt.run_many][] with this pipeline. - - Warning: - This method is deprecated and will be removed in a future release. - Use [Prompt.bind_many(pipeline)][rigging.prompt.Prompt.bind_many] instead. - """ - warnings.warn( - "run_prompt_many is deprecated, use Prompt.bind_many(pipeline) instead", - DeprecationWarning, - stacklevel=2, - ) - return await prompt.bind_many(self)(count, *args, **kwargs) - - async def run_prompt_over( - self, prompt: Prompt[P, R], generators: t.Sequence[Generator | str], /, *args: P.args, **kwargs: P.kwargs - ) -> list[R]: - """ - Calls [rigging.prompt.Prompt.run_over][] with this pipeline. - - Warning: - This method is deprecated and will be removed in a future release. - Use [Prompt.bind_over(pipeline)][rigging.prompt.Prompt.bind_over] instead. - """ - warnings.warn( - "run_prompt_over is deprecated, use Prompt.bind_over(pipeline) instead", - DeprecationWarning, - stacklevel=2, - ) - return await prompt.bind_over(self)(generators, *args, **kwargs) diff --git a/rigging/completion.py b/rigging/completion.py index 57668e6..2d4d6ae 100644 --- a/rigging/completion.py +++ b/rigging/completion.py @@ -2,8 +2,6 @@ Completions work with isolated strings of text pre and post generation. """ -from __future__ import annotations - import asyncio import string import typing as t @@ -18,7 +16,7 @@ from rigging.error import CompletionExhaustedMaxRoundsError from rigging.generator import GenerateParams, Generator, get_generator -from rigging.generator.base import GeneratedText, StopReason, Usage # noqa: TCH001 +from rigging.generator.base import GeneratedText, StopReason, Usage from rigging.parsing import parse_many from rigging.tracing import Span, tracer from rigging.util import get_qualified_name @@ -56,17 +54,17 @@ class Completion(BaseModel): stop_reason: StopReason = Field(default="unknown") """The reason the generation stopped.""" - usage: t.Optional[Usage] = Field(None, repr=False) + usage: Usage | None = Field(None, repr=False) """The usage statistics for the generation if available.""" extra: dict[str, t.Any] = Field(default_factory=dict, repr=False) """Any additional information from the generation.""" - generator: t.Optional[Generator] = Field(None, exclude=True, repr=False) + generator: Generator | None = Field(None, exclude=True, repr=False) """The generator associated with the completion.""" - params: t.Optional[GenerateParams] = Field(None, exclude=True, repr=False) + params: GenerateParams | None = Field(None, exclude=True, repr=False) """Any additional generation params used for this completion.""" - error: t.Optional[Exception] = Field(None, exclude=True, repr=False) + error: Exception | None = Field(None, exclude=True, repr=False) """Holds any exception that was caught during the generation pipeline.""" failed: bool = Field(False, exclude=False, repr=False) """ @@ -86,7 +84,7 @@ def __init__( self, text: str, generated: str, - generator: t.Optional[Generator] = None, + generator: Generator | None = None, **kwargs: t.Any, ): """ @@ -117,7 +115,12 @@ def all(self) -> str: """Returns both the text and the generation.""" return self.text + self.generated - def restart(self, *, generator: t.Optional[Generator] = None, include_all: bool = False) -> CompletionPipeline: + def restart( + self, + *, + generator: Generator | None = None, + include_all: bool = False, + ) -> "CompletionPipeline": """ Attempt to convert back to a CompletionPipeline for further generation. @@ -139,7 +142,7 @@ def restart(self, *, generator: t.Optional[Generator] = None, include_all: bool raise ValueError("Cannot restart a completion without an associated generator") return generator.complete(text, self.params) - def fork(self, text: str, *, include_all: bool = False) -> CompletionPipeline: + def fork(self, text: str, *, include_all: bool = False) -> "CompletionPipeline": """ Forks the completion by creating calling [rigging.completion.Completion.restart][] and appends the specified text. @@ -151,11 +154,11 @@ def fork(self, text: str, *, include_all: bool = False) -> CompletionPipeline: """ return self.restart(include_all=include_all).add(text) - def continue_(self, text: str) -> CompletionPipeline: + def continue_(self, text: str) -> "CompletionPipeline": """Alias for the [rigging.completion.Completion.fork][] with `include_all=True`.""" return self.fork(text, include_all=True) - def clone(self, *, only_messages: bool = False) -> Completion: + def clone(self, *, only_messages: bool = False) -> "Completion": """Creates a deep copy of the completion.""" new = Completion(self.text, self.generated, self.generator) if not only_messages: @@ -167,7 +170,7 @@ def clone(self, *, only_messages: bool = False) -> Completion: new.failed = self.failed return new - def meta(self, **kwargs: t.Any) -> Completion: + def meta(self, **kwargs: t.Any) -> "Completion": """ Updates the metadata of the completion with the provided key-value pairs. @@ -243,8 +246,8 @@ def __init__( generator: Generator, text: str, *, - params: t.Optional[GenerateParams] = None, - watch_callbacks: t.Optional[list[WatchCompletionCallback]] = None, + params: GenerateParams | None = None, + watch_callbacks: list[WatchCompletionCallback] | None = None, ): self.generator: Generator = generator """The generator object responsible for generating the completion.""" @@ -260,7 +263,7 @@ def __init__( ExhuastedMaxRounds is implicitly included. """ - self.on_failed: FailMode = "raise" + self.on_failed: "FailMode" = "raise" """How to handle failures in the pipeline unless overriden in calls.""" # (callback, all_text, max_rounds) @@ -273,7 +276,11 @@ def __init__( def __len__(self) -> int: return len(self.text) - def with_(self, params: t.Optional[GenerateParams] = None, **kwargs: t.Any) -> CompletionPipeline: + def with_( + self, + params: GenerateParams | None = None, + **kwargs: t.Any, + ) -> "CompletionPipeline": """ Assign specific generation parameter overloads for this completion. @@ -298,7 +305,11 @@ def with_(self, params: t.Optional[GenerateParams] = None, **kwargs: t.Any) -> C self.params = params return self - def catch(self, *errors: type[Exception], on_failed: FailMode | None = None) -> CompletionPipeline: + def catch( + self, + *errors: type[Exception], + on_failed: "FailMode | None" = None, + ) -> "CompletionPipeline": """ Adds exceptions to catch during generation when including or skipping failures. @@ -313,7 +324,11 @@ def catch(self, *errors: type[Exception], on_failed: FailMode | None = None) -> self.on_failed = on_failed or self.on_failed return self - def watch(self, *callbacks: WatchCompletionCallback, allow_duplicates: bool = False) -> CompletionPipeline: + def watch( + self, + *callbacks: WatchCompletionCallback, + allow_duplicates: bool = False, + ) -> "CompletionPipeline": """ Registers a callback to monitor any completions produced. @@ -336,7 +351,7 @@ async def log(completions: list[Completion]) -> None: self.watch_callbacks.append(callback) return self - def then(self, callback: ThenCompletionCallback) -> CompletionPipeline: + def then(self, callback: ThenCompletionCallback) -> "CompletionPipeline": """ Registers a callback to be executed after the generation process completes. @@ -360,7 +375,7 @@ async def process(completion: Completion) -> Completion | None: self.then_callbacks.append(callback) return self - def map(self, callback: MapCompletionCallback) -> CompletionPipeline: + def map(self, callback: MapCompletionCallback) -> "CompletionPipeline": """ Registers a callback to be executed after the generation process completes. @@ -384,7 +399,7 @@ async def process(completions: list[Completion]) -> list[Completion]: self.map_callbacks.append(callback) return self - def add(self, text: str) -> CompletionPipeline: + def add(self, text: str) -> "CompletionPipeline": """ Appends new text to the internal text before generation. @@ -397,7 +412,7 @@ def add(self, text: str) -> CompletionPipeline: self.text += text return self - def fork(self, text: str) -> CompletionPipeline: + def fork(self, text: str) -> "CompletionPipeline": """ Creates a new instance of `CompletionPipeline` by forking the current completion and adding the specified text. @@ -411,7 +426,7 @@ def fork(self, text: str) -> CompletionPipeline: """ return self.clone().add(text) - def clone(self, *, only_text: bool = False) -> CompletionPipeline: + def clone(self, *, only_text: bool = False) -> "CompletionPipeline": """ Creates a clone of the current `CompletionPipeline` instance. @@ -437,7 +452,7 @@ def clone(self, *, only_text: bool = False) -> CompletionPipeline: new.map_callbacks = self.map_callbacks.copy() return new - def meta(self, **kwargs: t.Any) -> CompletionPipeline: + def meta(self, **kwargs: t.Any) -> "CompletionPipeline": """ Updates the metadata of the completion with the provided key-value pairs. @@ -450,7 +465,7 @@ def meta(self, **kwargs: t.Any) -> CompletionPipeline: self.metadata.update(kwargs) return self - def apply(self, **kwargs: str) -> CompletionPipeline: + def apply(self, **kwargs: str) -> "CompletionPipeline": """ Applies keyword arguments to the text using string template substitution. @@ -474,7 +489,7 @@ def until( *, use_all_text: bool = False, max_rounds: int = DEFAULT_MAX_ROUNDS, - ) -> CompletionPipeline: + ) -> "CompletionPipeline": """ Registers a callback to participate in validating the generation process. @@ -505,10 +520,10 @@ def callback(text: str) -> bool: def until_parsed_as( self, - *types: type[ModelT], + *types: type["ModelT"], use_all_text: bool = False, max_rounds: int = DEFAULT_MAX_ROUNDS, - ) -> CompletionPipeline: + ) -> "CompletionPipeline": """ Adds the specified types to the list of types which should successfully parse before the generation process completes. @@ -522,12 +537,15 @@ def until_parsed_as( The updated CompletionPipeline object. """ self.until_types += types - if next((c for c in self.until_callbacks if c[0] == self._until_parse_callback), None) is None: + if ( + next((c for c in self.until_callbacks if c[0] == self._until_parse_callback), None) + is None + ): self.until_callbacks.append((self._until_parse_callback, use_all_text, max_rounds)) return self - def wrap(self, func: t.Callable[[CallableT], CallableT]) -> CompletionPipeline: + def wrap(self, func: t.Callable[[CallableT], CallableT]) -> "CompletionPipeline": """ Helper for [rigging.generator.base.Generator.wrap][]. @@ -542,8 +560,9 @@ def wrap(self, func: t.Callable[[CallableT], CallableT]) -> CompletionPipeline: def _until_parse_callback(self, text: str) -> bool: try: + # TODO: try_parse_many here? parse_many(text, *self.until_types) - except Exception: + except Exception: # noqa: BLE001 return True return False @@ -596,7 +615,11 @@ def _process(self) -> t.Generator[None, str, str]: return generated - async def _post_run(self, completions: list[Completion], on_failed: FailMode) -> list[Completion]: + async def _post_run( + self, + completions: list[Completion], + on_failed: "FailMode", + ) -> list[Completion]: if on_failed == "skip": completions = [c for c in completions if not c.failed] @@ -614,14 +637,18 @@ async def _post_run(self, completions: list[Completion], on_failed: FailMode) -> ): completions = await map_callback(completions) if not all(isinstance(c, Completion) for c in completions): - raise ValueError(f".map() callback must return a Completion object or None ({callback_name})") + raise ValueError( + f".map() callback must return a Completion object or None ({callback_name})", + ) def wrap_then_callback(callback: ThenCompletionCallback) -> ThenCompletionCallback: callback_name = get_qualified_name(callback) async def traced_then_callback(completion: Completion) -> Completion | None: with tracer.span( - f"Then with {callback_name}()", callback=callback_name, completion_id=str(completion.uuid) + f"Then with {callback_name}()", + callback=callback_name, + completion_id=str(completion.uuid), ): return await callback(completion) @@ -632,15 +659,23 @@ async def traced_then_callback(completion: Completion) -> Completion | None: new_completions = await asyncio.gather(*coros) if not all(isinstance(c, Completion) or c is None for c in new_completions): raise ValueError( - f".then() callback must return a Completion object or None ({get_qualified_name(then_callback)})" + f".then() callback must return a Completion object or None ({get_qualified_name(then_callback)})", ) - completions = [new or completion for new, completion in zip(new_completions, completions)] + completions = [ + new or completion + for new, completion in zip(new_completions, completions, strict=False) + ] return completions def _create_completion( - self, state: RunState, output: str, inbound: GeneratedText, failed: bool = False, error: Exception | None = None + self, + state: RunState, + output: str, + inbound: GeneratedText, + failed: bool = False, # noqa: FBT001, FBT002 + error: Exception | None = None, ) -> Completion: return Completion( self.text, @@ -667,7 +702,9 @@ def _create_failed_completion(self, state: RunState, error: Exception) -> Comple ) def _fit_params( - self, count: int, params: t.Sequence[t.Optional[GenerateParams] | None] | None = None + self, + count: int, + params: t.Sequence[GenerateParams | None] | None = None, ) -> list[GenerateParams]: params = [None] * count if params is None else list(params) if len(params) != count: @@ -677,15 +714,21 @@ def _fit_params( return [(p or GenerateParams()) for p in params] def _initialize_states( - self, count: int, params: t.Sequence[t.Optional[GenerateParams]] | None = None + self, + count: int, + params: t.Sequence[GenerateParams | None] | None = None, ) -> list[RunState]: states = [RunState(self.text, p, self._process()) for p in self._fit_params(count, params)] for state in states: next(state.processor) return states - async def _run( - self, span: Span, states: list[RunState], on_failed: FailMode, batch_mode: bool = False + async def _run( # noqa: PLR0912 + self, + span: Span, + states: list[RunState], + on_failed: "FailMode", + batch_mode: bool = False, # noqa: FBT001, FBT002 ) -> list[Completion]: pending_states = states while pending_states: @@ -694,8 +737,10 @@ async def _run( [(self.text + s.text) if batch_mode else s.text for s in pending_states], [s.params for s in pending_states], ) - except Exception as e: - if on_failed == "raise" or not any(isinstance(e, t) for t in self.errors_to_fail_on): + except Exception as e: # noqa: BLE001 + if on_failed == "raise" or not any( + isinstance(e, t) for t in self.errors_to_fail_on + ): raise span.set_attribute("failed", True) @@ -704,7 +749,7 @@ async def _run( for state in pending_states: state.completion = self._create_failed_completion(state, e) else: - for inbound, state in zip(inbounds, pending_states): + for inbound, state in zip(inbounds, pending_states, strict=False): output: str = "" failed: bool = False error: Exception | None = None @@ -720,8 +765,10 @@ async def _run( output = exhausted.completion failed = True error = exhausted - except Exception as e: - if on_failed == "raise" or not any(isinstance(e, t) for t in self.errors_to_fail_on): + except Exception as e: # noqa: BLE001 + if on_failed == "raise" or not any( + isinstance(e, t) for t in self.errors_to_fail_on + ): raise failed = True error = e @@ -730,27 +777,43 @@ async def _run( span.set_attribute("failed", True) span.set_attribute("error", error) - state.completion = self._create_completion(state, output, inbound, failed, error) + state.completion = self._create_completion( + state, + output, + inbound, + failed, + error, + ) pending_states = [s for s in pending_states if s.completion is None] to_watch_states = [s for s in states if s.completion is not None and not s.watched] - await self._watch_callback([s.completion for s in to_watch_states if s.completion is not None]) + await self._watch_callback( + [s.completion for s in to_watch_states if s.completion is not None], + ) for state in to_watch_states: state.watched = True - completions = await self._post_run([s.completion for s in states if s.completion is not None], on_failed) + completions = await self._post_run( + [s.completion for s in states if s.completion is not None], + on_failed, + ) span.set_attribute("completions", completions) return completions - async def run(self, *, allow_failed: bool = False, on_failed: FailMode | None = None) -> Completion: + async def run( + self, + *, + allow_failed: bool = False, + on_failed: "FailMode | None" = None, + ) -> Completion: """ Execute the generation process to produce the final chat. - Parameters: + Args: allow_failed: Ignore any errors and potentially - return the chat in a failed state. + return the chat in a failed state on_failed: The behavior when a message fails to generate. (this is used as an alternative to allow_failed) @@ -762,7 +825,7 @@ async def run(self, *, allow_failed: bool = False, on_failed: FailMode | None = if on_failed == "skip": raise ValueError( - "Cannot use 'skip' mode with single completion generation (pass allow_failed=True or on_failed='include'/'raise')" + "Cannot use 'skip' mode with single completion generation (pass allow_failed=True or on_failed='include'/'raise')", ) on_failed = on_failed or self.on_failed @@ -783,13 +846,13 @@ async def run_many( self, count: int, *, - params: t.Sequence[t.Optional[GenerateParams]] | None = None, - on_failed: FailMode | None = None, + params: t.Sequence[GenerateParams | None] | None = None, + on_failed: "FailMode | None" = None, ) -> list[Completion]: """ Executes the generation process multiple times with the same inputs. - Parameters: + Args: count: The number of times to execute the generation process. params: A sequence of parameters to be used for each execution. on_failed: How to handle failures in the pipeline unless overriden in calls. @@ -813,9 +876,9 @@ async def run_many( async def run_batch( self, many: t.Sequence[str], - params: t.Sequence[t.Optional[GenerateParams]] | None = None, + params: t.Sequence[GenerateParams | None] | None = None, *, - on_failed: FailMode = "raise", + on_failed: "FailMode" = "raise", ) -> list[Completion]: """ Executes the generation process accross multiple input messages. @@ -823,7 +886,7 @@ async def run_batch( Note: Anything already in this pending completion will be prepended to the text. - Parameters: + Args: many: A sequence of texts to generate with. params: A sequence of parameters to be used for each text. on_failed: How to handle failures in the pipeline unless overriden in calls. @@ -834,7 +897,9 @@ async def run_batch( on_failed = on_failed or self.on_failed params = self._fit_params(len(many), params) - states: list[RunState] = [RunState(m, p, self._process()) for m, p in zip(many, params)] + states: list[RunState] = [ + RunState(m, p, self._process()) for m, p in zip(many, params, strict=False) + ] for state in states: next(state.processor) @@ -849,7 +914,10 @@ async def run_batch( # Generator iteration async def run_over( - self, *generators: Generator | str, include_original: bool = True, on_failed: FailMode | None = None + self, + *generators: Generator | str, + include_original: bool = True, + on_failed: "FailMode | None" = None, ) -> list[Completion]: """ Executes the generation process across multiple generators. @@ -857,7 +925,7 @@ async def run_over( For each generator, this pipeline is cloned and the generator is replaced before the run call. All callbacks and parameters are preserved. - Parameters: + Args: *generators: A sequence of generators to be used for the generation process. include_original: Whether to include the original generator in the list of runs. on_failed: The behavior when a message fails to generate. @@ -867,7 +935,9 @@ async def run_over( """ on_failed = on_failed or self.on_failed - _generators: list[Generator] = [g if isinstance(g, Generator) else get_generator(g) for g in generators] + _generators: list[Generator] = [ + g if isinstance(g, Generator) else get_generator(g) for g in generators + ] if include_original: _generators.append(self.generator) diff --git a/rigging/data.py b/rigging/data.py index 116dfee..21071dc 100644 --- a/rigging/data.py +++ b/rigging/data.py @@ -1,4 +1,6 @@ -from __future__ import annotations +""" +Utilities for converting chat data between different formats. +""" import itertools import json @@ -7,14 +9,12 @@ import elasticsearch as es import elasticsearch.helpers import pandas as pd +from elastic_transport import ObjectApiResponse +from mypy_boto3_s3 import S3Client from rigging.chat import Chat from rigging.message import Message -if t.TYPE_CHECKING: - from elastic_transport import ObjectApiResponse - from mypy_boto3_s3 import S3Client - def flatten_chats(chats: Chat | t.Sequence[Chat]) -> list[dict[t.Any, t.Any]]: """ @@ -34,7 +34,10 @@ def flatten_chats(chats: Chat | t.Sequence[Chat]) -> list[dict[t.Any, t.Any]]: generator_id = chat.generator_id # We let pydantic do the heavy lifting here - chat_json = chat.model_dump(include={"uuid", "timestamp", "metadata", "usage", "extra"}, mode="json") + chat_json = chat.model_dump( + include={"uuid", "timestamp", "metadata", "usage", "extra"}, + mode="json", + ) metadata = chat_json.pop("metadata") usage = chat_json.pop("usage") extra = chat_json.pop("extra") @@ -56,7 +59,7 @@ def flatten_chats(chats: Chat | t.Sequence[Chat]) -> list[dict[t.Any, t.Any]]: "generated": generated, "message_id": message_id, **message_dict, - } + }, ) generated = True @@ -93,7 +96,7 @@ def by_chat_id(message: dict[t.Any, t.Any]) -> t.Any: message = Message( role=message_data["role"], content=message_data["content"], - **{"uuid": message_data["message_id"]}, + uuid=message_data["message_id"], ) if message_data["generated"]: _generated.append(message) @@ -112,7 +115,7 @@ def by_chat_id(message: dict[t.Any, t.Any]) -> t.Any: stop_reason=_first_message["chat_stop_reason"], usage=json.loads(_first_message["chat_usage"]), extra=json.loads(_first_message["chat_extra"]), - **{"generator_id": _first_message["chat_generator_id"]}, + generator_id=_first_message["chat_generator_id"], ) chats.append(chat) @@ -141,7 +144,9 @@ def chats_to_df(chats: Chat | t.Sequence[Chat]) -> pd.DataFrame: flattened = flatten_chats(chats) - df = pd.DataFrame(flattened).astype( + # TODO: Come back to indexing + + return pd.DataFrame(flattened).astype( { "chat_id": "string", "chat_metadata": "string", @@ -155,13 +160,9 @@ def chats_to_df(chats: Chat | t.Sequence[Chat]) -> pd.DataFrame: "role": "category", "content": "string", "parts": "string", - } + }, ) - # TODO: Come back to indexing - - return df - def df_to_chats(df: pd.DataFrame) -> list[Chat]: """ @@ -188,7 +189,7 @@ def df_to_chats(df: pd.DataFrame) -> list[Chat]: message = Message( role=message_data["role"], content=message_data["content"], - **{"uuid": message_data["message_id"]}, + uuid=message_data["message_id"], # TODO: I don't believe this is safe to deserialize # here as we aren't bonded to the underlying rg.Model # which was the original object. Skipping for now. @@ -208,7 +209,7 @@ def df_to_chats(df: pd.DataFrame) -> list[Chat]: stop_reason=chat_data["chat_stop_reason"], usage=json.loads(chat_data["chat_usage"]), extra=json.loads(chat_data["chat_extra"]), - **{"generator_id": chat_data["chat_generator_id"]}, + generator_id=chat_data["chat_generator_id"], ) chats.append(chat) @@ -225,7 +226,10 @@ def df_to_chats(df: pd.DataFrame) -> list[Chat]: def chats_to_elastic_data( - chats: Chat | t.Sequence[Chat], index: str, *, op_type: ElasticOpType = "index" + chats: Chat | t.Sequence[Chat], + index: str, + *, + op_type: ElasticOpType = "index", ) -> list[dict[str, t.Any]]: """ Convert chat data to Elasticsearch bulk operation format. @@ -275,7 +279,7 @@ async def chats_to_elastic( """ es_data = chats_to_elastic_data(chats, index, op_type=op_type) if create_index: - if (await client.indices.exists(index=index)).meta.status != 200: + if (await client.indices.exists(index=index)).meta.status != 200: # noqa: PLR2004 await client.indices.create(index=index, mappings=ElasticMapping) else: await client.indices.put_mapping(index=index, properties=ElasticMapping["properties"]) @@ -295,7 +299,9 @@ def elastic_data_to_chats( objects = t.cast(t.Sequence[t.Mapping[str, t.Any]], data) if not isinstance(objects, t.Sequence): - raise ValueError(f"Expected to find a sequence of objects (optionally under hits), found: {type(data)}") + raise TypeError( + f"Expected to find a sequence of objects (optionally under hits), found: {type(data)}", + ) chats: list[Chat] = [] for obj in objects: @@ -351,12 +357,12 @@ async def s3_bucket_exists(client: S3Client, bucket: str) -> bool: """ try: client.head_bucket(Bucket=bucket) - return True except client.exceptions.ClientError as e: if e.response["Error"]["Code"] == "404": return False - else: - raise + raise + + return True async def s3_object_exists(client: S3Client, bucket: str, key: str) -> bool: @@ -373,9 +379,9 @@ async def s3_object_exists(client: S3Client, bucket: str, key: str) -> bool: """ try: client.head_object(Bucket=bucket, Key=key) - return True except client.exceptions.ClientError as e: if e.response["Error"]["Code"] == "404": return False - else: - raise + raise + + return True diff --git a/rigging/error.py b/rigging/error.py index 0ecba7e..398c48d 100644 --- a/rigging/error.py +++ b/rigging/error.py @@ -24,6 +24,15 @@ def __init__(self, tool_name: str): """The name of the tool which was unknown.""" +class ToolDefinitionError(Exception): + """ + Raised when a tool cannot be properly defined. + """ + + def __init__(self, message: str): + super().__init__(message) + + class ExhaustedMaxRoundsError(Exception): """ Raised when the maximum number of rounds is exceeded while generating. @@ -88,7 +97,10 @@ def __init__(self, content: str): R = t.TypeVar("R") -def raise_as(error_type: type[Exception], message: str) -> t.Callable[[t.Callable[P, R]], t.Callable[P, R]]: +def raise_as( + error_type: type[Exception], + message: str, +) -> t.Callable[[t.Callable[P, R]], t.Callable[P, R]]: "When the wrapped function raises an exception, `raise ... from` with the new error type." def _raise_as(func: t.Callable[P, R]) -> t.Callable[P, R]: @@ -96,7 +108,7 @@ def _raise_as(func: t.Callable[P, R]) -> t.Callable[P, R]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: try: return func(*args, **kwargs) - except Exception as e: + except Exception as e: # noqa: BLE001 error = error_type(message) raise error from e diff --git a/rigging/generator/__init__.py b/rigging/generator/__init__.py index 8e755ec..225836d 100644 --- a/rigging/generator/__init__.py +++ b/rigging/generator/__init__.py @@ -20,16 +20,21 @@ register_generator("litellm", LiteLLMGenerator) register_generator("http", HTTPGenerator) -register_generator("base", Generator) # TODO: Helper while we sort out generators being required so many places. +register_generator( + "base", + Generator, +) # TODO: Helper while we sort out generators being required so many places. def get_vllm_lazy() -> type[Generator]: try: from rigging.generator.vllm_ import VLLMGenerator - - return VLLMGenerator except ImportError as e: - raise ImportError("VLLMGenerator is not available. Please install `vllm` or use `rigging[extra]`.") from e + raise ImportError( + "VLLMGenerator is not available. Please install `vllm` or use `rigging[extra]`.", + ) from e + + return VLLMGenerator register_generator("vllm", get_vllm_lazy) @@ -38,13 +43,13 @@ def get_vllm_lazy() -> type[Generator]: def get_transformers_lazy() -> type[Generator]: try: from rigging.generator.transformers_ import TransformersGenerator - - return TransformersGenerator except ImportError as e: raise ImportError( - "TransformersGenerator is not available. Please install `transformers` or use `rigging[extra]`." + "TransformersGenerator is not available. Please install `transformers` or use `rigging[extra]`.", ) from e + return TransformersGenerator + register_generator("transformers", get_transformers_lazy) diff --git a/rigging/generator/base.py b/rigging/generator/base.py index 74f992f..9b3b625 100644 --- a/rigging/generator/base.py +++ b/rigging/generator/base.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import abc import inspect import typing as t @@ -11,18 +9,17 @@ from rigging.error import InvalidModelSpecifiedError from rigging.message import Message, MessageDict -from rigging.tool.api import ToolChoice, ToolDefinition +from rigging.tool.api import ApiToolChoice, ApiToolDefinition if t.TYPE_CHECKING: from rigging.chat import ChatPipeline, WatchChatCallback from rigging.completion import CompletionPipeline, WatchCompletionCallback - from rigging.prompt import P, Prompt, R - - WatchCallbacks = t.Union[WatchChatCallback, WatchCompletionCallback] - + from rigging.prompt import Prompt CallableT = t.TypeVar("CallableT", bound=t.Callable[..., t.Any]) +P = t.ParamSpec("P") +R = t.TypeVar("R") T = t.TypeVar("T") # Global provider map @@ -30,11 +27,11 @@ @t.runtime_checkable class LazyGenerator(t.Protocol): - def __call__(self) -> type[Generator]: + def __call__(self) -> type["Generator"]: ... -g_providers: dict[str, type[Generator] | LazyGenerator] = {} +g_providers: dict[str, type["Generator"] | LazyGenerator] = {} # Fixups @@ -125,10 +122,10 @@ class GenerateParams(BaseModel): seed: int | None = None """The random seed.""" - tools: list[ToolDefinition] | None = None + tools: list[ApiToolDefinition] | None = None """The tools to be used in the generation.""" - tool_choice: ToolChoice | None = None + tool_choice: ApiToolChoice | None = None """The tool choice to be used in the generation.""" parallel_tool_calls: bool | None = None @@ -138,24 +135,26 @@ class GenerateParams(BaseModel): """Extra parameters to be passed to the API.""" @field_validator("tools", mode="before") + @classmethod def validate_tools(cls, value: t.Any) -> t.Any: if isinstance(value, list) and all(isinstance(v, dict) for v in value): - return [ToolDefinition.model_validate(v) for v in value] - elif isinstance(value, list) and all(isinstance(v, str) for v in value): - return [ToolDefinition.model_validate_json(v) for v in value] + return [ApiToolDefinition.model_validate(v) for v in value] + if isinstance(value, list) and all(isinstance(v, str) for v in value): + return [ApiToolDefinition.model_validate_json(v) for v in value] return value @field_validator("stop", mode="before") + @classmethod def validate_stop(cls, value: t.Any) -> t.Any: if value is None: return None if isinstance(value, str): return value.split(";") - elif isinstance(value, list) and all(isinstance(v, str) for v in value): + if isinstance(value, list) and all(isinstance(v, str) for v in value): return value raise ValueError("Stop sequences must be a list or a string separated by ';'") - def merge_with(self, *others: t.Optional[GenerateParams]) -> GenerateParams: + def merge_with(self, *others: "GenerateParams | None") -> "GenerateParams": """ Apply a series of parameter overrides to the current instance and return a copy. @@ -172,7 +171,7 @@ def merge_with(self, *others: t.Optional[GenerateParams]) -> GenerateParams: updates: dict[str, t.Any] = {} for other in [o for o in others if o is not None]: other_dict = other.model_dump(exclude_unset=True, exclude_none=True) - for name in other_dict.keys(): + for name in other_dict: updates[name] = getattr(other, name) return self.model_copy(update=updates) @@ -194,21 +193,25 @@ def to_dict(self) -> dict[str, t.Any]: """Reporting reason for generation completing.""" -def convert_stop_reason(reason: t.Optional[str]) -> StopReason: +def convert_stop_reason(reason: str | None) -> StopReason: if reason in ["stop", "eos"]: return "stop" - elif reason in ["model_length"]: + if reason in ["model_length"]: return "length" - elif reason in ["length"]: + if reason in ["length"]: return "length" - elif reason in ["content_filter"]: + if reason in ["content_filter"]: return "content_filter" - elif reason and "tool" in reason: + if reason and "tool" in reason: return "tool_calls" return "unknown" class Usage(BaseModel): + """Usage statistics for a generation.""" + + model_config = ConfigDict(extra="allow") + input_tokens: int """The number of input tokens.""" output_tokens: int @@ -229,7 +232,7 @@ class GeneratedMessage(BaseModel): stop_reason: t.Annotated[StopReason, BeforeValidator(convert_stop_reason)] = "unknown" """The reason for stopping generation.""" - usage: t.Optional[Usage] = None + usage: Usage | None = None """The usage statistics for the generation if available.""" extra: dict[str, t.Any] = Field(default_factory=dict) @@ -239,7 +242,7 @@ def __str__(self) -> str: return str(self.message) @classmethod - def from_text(cls, text: str, stop_reason: StopReason = "unknown") -> GeneratedMessage: + def from_text(cls, text: str, stop_reason: StopReason = "unknown") -> "GeneratedMessage": return cls(message=Message(role="assistant", content=text), stop_reason=stop_reason) @@ -252,7 +255,7 @@ class GeneratedText(BaseModel): stop_reason: t.Annotated[StopReason, BeforeValidator(convert_stop_reason)] = "unknown" """The reason for stopping generation.""" - usage: t.Optional[Usage] = None + usage: Usage | None = None """The usage statistics for the generation if available.""" extra: dict[str, t.Any] = Field(default_factory=dict) @@ -262,7 +265,7 @@ def __str__(self) -> str: return self.text @classmethod - def from_text(cls, text: str, stop_reason: StopReason = "unknown") -> GeneratedText: + def from_text(cls, text: str, stop_reason: StopReason = "unknown") -> "GeneratedText": return cls(text=text, stop_reason=stop_reason) def to_generated_message(self) -> GeneratedMessage: @@ -293,7 +296,7 @@ class Generator(BaseModel): params: GenerateParams """The parameters used for generating completion messages.""" - _watch_callbacks: list[WatchCallbacks] = [] + _watch_callbacks: list["WatchChatCallback | WatchCompletionCallback"] = [] _wrap: t.Callable[[CallableT], CallableT] | None = None _fixups: Fixups = Fixups() @@ -312,7 +315,11 @@ def to_identifier(self, params: GenerateParams | None = None) -> str: """ return get_identifier(self, params) - def watch(self, *callbacks: WatchCallbacks, allow_duplicates: bool = False) -> Generator: + def watch( + self, + *callbacks: "WatchChatCallback | WatchCompletionCallback", + allow_duplicates: bool = False, + ) -> "Generator": """ Registers watch callbacks to be passed to any created [rigging.chat.ChatPipeline][] or [rigging.completion.CompletionPipeline][]. @@ -370,6 +377,15 @@ def wrap(self, func: t.Callable[[CallableT], CallableT] | None) -> Self: self._wrap = func # type: ignore [assignment] return self + async def supports_function_calling(self) -> bool | None: + """ + Check if the generator supports calling functions explicitly or is unknown. + + Returns: + True/False if the generator supports function calling, None if unknown. + """ + return None + def _check_fixups(self, error: Exception) -> bool: """ Check if any fixer can handle this error. @@ -460,7 +476,7 @@ def chat( self, messages: t.Sequence[MessageDict], params: GenerateParams | None = None, - ) -> ChatPipeline: + ) -> "ChatPipeline": ... @t.overload @@ -468,14 +484,19 @@ def chat( self, messages: t.Sequence[Message] | MessageDict | Message | str | None = None, params: GenerateParams | None = None, - ) -> ChatPipeline: + ) -> "ChatPipeline": ... def chat( self, - messages: t.Sequence[MessageDict] | t.Sequence[Message] | MessageDict | Message | str | None = None, + messages: t.Sequence[MessageDict] + | t.Sequence[Message] + | MessageDict + | Message + | str + | None = None, params: GenerateParams | None = None, - ) -> ChatPipeline: + ) -> "ChatPipeline": """ Build a chat pipeline with the given messages and optional params overloads. @@ -488,7 +509,9 @@ def chat( """ from rigging.chat import ChatPipeline, WatchChatCallback - chat_watch_callbacks = [cb for cb in self._watch_callbacks if isinstance(cb, (WatchChatCallback))] + chat_watch_callbacks = [ + cb for cb in self._watch_callbacks if isinstance(cb, (WatchChatCallback)) + ] return ChatPipeline( self, @@ -499,7 +522,7 @@ def chat( # Helper alternative to complete(generator) -> generator.complete(...) - def complete(self, text: str, params: GenerateParams | None = None) -> CompletionPipeline: + def complete(self, text: str, params: GenerateParams | None = None) -> "CompletionPipeline": """ Build a completion pipeline of the given text with optional param overloads. @@ -512,11 +535,18 @@ def complete(self, text: str, params: GenerateParams | None = None) -> Completio """ from rigging.completion import CompletionPipeline, WatchCompletionCallback - completion_watch_callbacks = [cb for cb in self._watch_callbacks if isinstance(cb, (WatchCompletionCallback))] + completion_watch_callbacks = [ + cb for cb in self._watch_callbacks if isinstance(cb, (WatchCompletionCallback)) + ] - return CompletionPipeline(self, text, params=params, watch_callbacks=completion_watch_callbacks) + return CompletionPipeline( + self, + text, + params=params, + watch_callbacks=completion_watch_callbacks, + ) - def prompt(self, func: t.Callable[P, t.Coroutine[None, None, R]]) -> Prompt[P, R]: + def prompt(self, func: t.Callable[P, t.Coroutine[None, None, R]]) -> "Prompt[P, R]": """ Decorator to convert a function into a prompt bound to this generator. @@ -538,7 +568,7 @@ def chat( generator: Generator, messages: t.Sequence[MessageDict], params: GenerateParams | None = None, -) -> ChatPipeline: +) -> "ChatPipeline": ... @@ -547,15 +577,20 @@ def chat( generator: Generator, messages: t.Sequence[Message] | MessageDict | Message | str | None = None, params: GenerateParams | None = None, -) -> ChatPipeline: +) -> "ChatPipeline": ... def chat( generator: Generator, - messages: t.Sequence[MessageDict] | t.Sequence[Message] | MessageDict | Message | str | None = None, + messages: t.Sequence[MessageDict] + | t.Sequence[Message] + | MessageDict + | Message + | str + | None = None, params: GenerateParams | None = None, -) -> ChatPipeline: +) -> "ChatPipeline": """ Creates a chat pipeline using the given generator, messages, and params. @@ -575,7 +610,7 @@ def complete( generator: Generator, text: str, params: GenerateParams | None = None, -) -> CompletionPipeline: +) -> "CompletionPipeline": return generator.complete(text, params) @@ -595,11 +630,16 @@ def get_identifier(generator: Generator, params: GenerateParams | None = None) - """ provider = next( - name for name, klass in g_providers.items() if isinstance(klass, type) and isinstance(generator, klass) + name + for name, klass in g_providers.items() + if isinstance(klass, type) and isinstance(generator, klass) ) identifier = f"{provider}!{generator.model}" - extra_cls_args = generator.model_dump(exclude_unset=True, exclude={"model", "api_key", "params"}) + extra_cls_args = generator.model_dump( + exclude_unset=True, + exclude={"model", "api_key", "params"}, + ) if extra_cls_args: identifier += f",{','.join([f'{k}={v}' for k, v in extra_cls_args.items()])}" @@ -617,7 +657,7 @@ def get_identifier(generator: Generator, params: GenerateParams | None = None) - return identifier -def get_generator(identifier: str, *, params: GenerateParams | None = None) -> Generator: +def get_generator(identifier: str, *, params: GenerateParams | None = None) -> Generator: # noqa: PLR0912 """ Get a generator by an identifier string. Uses LiteLLM by default. @@ -651,7 +691,7 @@ def get_generator(identifier: str, *, params: GenerateParams | None = None) -> G InvalidModelSpecified: If the identifier is invalid. """ - provider: str = list(g_providers.keys())[0] + provider: str = next(iter(g_providers.keys())) model: str = identifier if not identifier: @@ -662,7 +702,7 @@ def get_generator(identifier: str, *, params: GenerateParams | None = None) -> G if "!" in identifier: try: provider, model = identifier.split("!") - except Exception as e: + except Exception as e: # noqa: BLE001 raise InvalidModelSpecifiedError(identifier) from e if provider not in g_providers: @@ -679,12 +719,14 @@ def get_generator(identifier: str, *, params: GenerateParams | None = None) -> G try: model, kwargs_str = model.split(",", 1) kwargs = dict(arg.split("=", 1) for arg in kwargs_str.split(",")) - except Exception as e: + except Exception as e: # noqa: BLE001 raise InvalidModelSpecifiedError(identifier) from e # See if any of the kwargs would apply to the cls constructor directly init_signature = inspect.signature(generator_cls) - init_kwargs: dict[str, t.Any] = {k: kwargs.pop(k) for k in list(kwargs.keys())[:] if k in init_signature.parameters} + init_kwargs: dict[str, t.Any] = { + k: kwargs.pop(k) for k in list(kwargs.keys())[:] if k in init_signature.parameters + } # Do some subtle type conversion for k, v in init_kwargs.items(): @@ -705,7 +747,7 @@ def get_generator(identifier: str, *, params: GenerateParams | None = None) -> G try: merged_params = GenerateParams(**kwargs).merge_with(params) - except Exception as e: + except Exception as e: # noqa: BLE001 raise InvalidModelSpecifiedError(identifier) from e return generator_cls(model=model, params=merged_params, **init_kwargs) @@ -721,11 +763,14 @@ def register_generator(provider: str, generator_cls: type[Generator] | LazyGener provider: The name of the provider. generator_cls: The generator class to register. """ - global g_providers + global g_providers # noqa: PLW0602 g_providers[provider] = generator_cls -def trace_messages(messages: t.Sequence[Message] | t.Sequence[GeneratedMessage], title: str) -> None: +def trace_messages( + messages: t.Sequence[Message] | t.Sequence[GeneratedMessage], + title: str, +) -> None: """ Helper function to trace log a sequence of Message objects. @@ -745,7 +790,7 @@ def trace_str(content: str | GeneratedText, title: str) -> None: """ Helper function to trace log a string. - Parameters: + Args: content: The string content to be logged. title: The title of the log entry. diff --git a/rigging/generator/http.py b/rigging/generator/http.py index d53fd7f..388f3f5 100644 --- a/rigging/generator/http.py +++ b/rigging/generator/http.py @@ -1,14 +1,13 @@ -from __future__ import annotations - import asyncio import base64 +import contextlib import json import re import typing as t import httpx import jinja2 -import jsonpath_ng # type: ignore +import jsonpath_ng # type: ignore [import-untyped] from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator from ruamel.yaml import YAML @@ -21,7 +20,7 @@ ) from rigging.message import Message, Role -# TODO: +# TODO: Look at: # - Add request retry mechanics # - Add request timeout mechanics # - Add maximum concurrent requests @@ -154,9 +153,9 @@ def replace_vars(obj: t.Any) -> t.Any: for part in parts: val = val[part] return val - elif isinstance(obj, dict): + if isinstance(obj, dict): return {k: replace_vars(v) for k, v in obj.items()} - elif isinstance(obj, list): + if isinstance(obj, list): return [replace_vars(v) for v in obj] return obj @@ -172,14 +171,17 @@ def replace_vars(obj: t.Any) -> t.Any: "data": _result, "output": _result, "body": _result, - } + }, ) - template = jinja2.Template(_to_str(transform.pattern), undefined=jinja2.StrictUndefined) + template = jinja2.Template( + _to_str(transform.pattern), + undefined=jinja2.StrictUndefined, + ) result = template.render(**merged_context) if result is None: - raise Exception("No valid input transform found") + raise RuntimeError("No valid input transform found") return result @@ -192,7 +194,10 @@ def parse_response_body(self, data: str) -> str: for transform in self.response.transforms: if transform.type == "jinja": - template = jinja2.Template(_to_str(transform.pattern), undefined=jinja2.StrictUndefined) + template = jinja2.Template( + _to_str(transform.pattern), + undefined=jinja2.StrictUndefined, + ) _result = _to_dict_or_str(result) result = template.render( # Duplicates here for convenience @@ -208,8 +213,10 @@ def parse_response_body(self, data: str) -> str: result = json.loads(result) matches = [match.value for match in jsonpath_expr.find(result)] if len(matches) == 0: - raise Exception(f"No matches found for JSONPath: {transform.pattern} from {result}") - elif len(matches) == 1: + raise RuntimeError( + f"No matches found for JSONPath: {transform.pattern} from {result}", + ) + if len(matches) == 1: matches = matches[0] result = matches if isinstance(matches, str) else json.dumps(matches) @@ -279,27 +286,22 @@ class HTTPGenerator(Generator): """Specification for building/parsing HTTP interactions.""" @field_validator("spec", mode="before") + @classmethod def process_spec(cls, v: t.Any) -> t.Any: if not isinstance(v, str): return v # Check if the string is base64 encoded - try: + with contextlib.suppress(Exception): v = base64.b64decode(v).decode() - except Exception: - pass # Try to load as JSON - try: + with contextlib.suppress(json.JSONDecodeError): return json.loads(v) - except json.JSONDecodeError: - pass # Try to load as YAML - try: + with contextlib.suppress(Exception): return YAML(typ="safe").load(v) - except Exception: - pass return v @@ -349,15 +351,23 @@ async def _generate_message( content = response.text - if self.spec.response is not None: - if response.status_code not in self.spec.response.valid_status_codes: - raise ProcessingError(f"Received invalid status code: {response.status_code} for {response.url}") + if ( + self.spec.response is not None + and response.status_code not in self.spec.response.valid_status_codes + ): + raise ProcessingError( + f"Received invalid status code: {response.status_code} for {response.url}", + ) return GeneratedMessage( message=Message(role="assistant", content=self.spec.parse_response_body(content)), stop_reason="stop", usage=None, - extra={"status_code": response.status_code, "url": response.url, "headers": response.headers}, + extra={ + "status_code": response.status_code, + "url": response.url, + "headers": response.headers, + }, ) async def generate_messages( @@ -365,10 +375,13 @@ async def generate_messages( messages: t.Sequence[t.Sequence[Message]], params: t.Sequence[GenerateParams], ) -> t.Sequence[GeneratedMessage]: - coros = [self._generate_message(_messages, _params) for _messages, _params in zip(messages, params)] + coros = [ + self._generate_message(_messages, _params) + for _messages, _params in zip(messages, params, strict=False) + ] generated = await asyncio.gather(*coros) - for i, (_messages, response) in enumerate(zip(messages, generated)): + for i, (_messages, response) in enumerate(zip(messages, generated, strict=False)): trace_messages(_messages, f"Messages {i + 1}/{len(messages)}") trace_messages([response], f"Response {i + 1}/{len(messages)}") diff --git a/rigging/generator/litellm_.py b/rigging/generator/litellm_.py index c02867e..66532c0 100644 --- a/rigging/generator/litellm_.py +++ b/rigging/generator/litellm_.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import asyncio import datetime import typing as t @@ -18,7 +16,9 @@ trace_messages, trace_str, ) -from rigging.message import Message +from rigging.message import ContentText, Message +from rigging.tool.api import ApiFunctionDefinition, ApiToolDefinition +from rigging.tracing import tracer # We should probably let people configure # this independently, but for now we'll @@ -50,8 +50,13 @@ def fix(self, items: t.Sequence[Message]) -> t.Sequence[Message]: updated_messages: list[Message] = [] append_queue: list[Message] = [] for message in items: - if message.role == "tool" and isinstance(message.all_content, list): - updated_messages.append(message.model_copy(deep=True, update={"all_content": "See next message"})) + if message.role == "tool" and isinstance(message.content_parts, list): + updated_messages.append( + message.model_copy( + deep=True, + update={"content_parts": [ContentText(text="See next message")]}, + ), + ) append_queue.append(message.model_copy(deep=True, update={"role": "user"})) else: updated_messages.extend(append_queue) @@ -102,6 +107,7 @@ class LiteLLMGenerator(Generator): _semaphore: asyncio.Semaphore | None = None _last_request_time: datetime.datetime | None = None + _supports_function_calling: bool | None = None _fixups = Fixups(available=[OpenAIToolsWithImageURLsFixup()]) @@ -113,11 +119,51 @@ def semaphore(self) -> asyncio.Semaphore: self._semaphore = asyncio.Semaphore(max_connections) return self._semaphore + async def supports_function_calling(self) -> bool | None: + if self._supports_function_calling is not None: + return self._supports_function_calling + + self._supports_function_calling = litellm.utils.supports_function_calling(self.model) + if self._supports_function_calling: + return self._supports_function_calling + + self._supports_function_calling = False + + # Otherwise we'll run a small check to see if we can + + with tracer.span(f"Checking '{self.model}' for function calling support") as span: + try: + generated = await self.generate_messages( + [[Message(role="user", content="Call the test function")]], + [ + GenerateParams( + tools=[ + ApiToolDefinition( + function=ApiFunctionDefinition( + name="test_function", + description="Test function", + ), + ), + ], + ), + ], + ) + + if generated and generated[0].message.tool_calls: + self._supports_function_calling = True + except Exception as e: # noqa: BLE001 + logger.warning(f"Failed to check for function calling support: {e}") + span.set_attribute("error", str(e)) + + span.set_attribute("supports_function_calling", self._supports_function_calling) + + return self._supports_function_calling + async def _ensure_delay_between_requests(self) -> None: if self._last_request_time is None: return - delta = datetime.datetime.now() - self._last_request_time + delta = datetime.datetime.now(tz=datetime.timezone.utc) - self._last_request_time delta_ms = delta.total_seconds() * 1000 if delta_ms < self.min_delay_between_requests: @@ -136,16 +182,19 @@ async def _ensure_delay_between_requests(self) -> None: # This seems like a brittle feature at the moment, so we'll # leave it out for now. - def _parse_model_response(self, response: litellm.types.utils.ModelResponse) -> GeneratedMessage: + def _parse_model_response( + self, + response: litellm.types.utils.ModelResponse, + ) -> GeneratedMessage: choice = response.choices[-1] usage = None if getattr(response, "usage", None) is not None: - usage = response.usage.model_dump() # type: ignore + usage = response.usage.model_dump() # type: ignore [attr-defined] usage["input_tokens"] = usage.pop("prompt_tokens") usage["output_tokens"] = usage.pop("completion_tokens") if isinstance(choice, litellm.types.utils.StreamingChoices): - raise ValueError("Streaming choices are not supported") + raise TypeError("Streaming choices are not supported") tool_calls: list[dict[str, t.Any]] | None = None if ( @@ -161,17 +210,27 @@ def _parse_model_response(self, response: litellm.types.utils.ModelResponse) -> extra = {"response_id": response.id} if hasattr(response, "provider"): extra["provider"] = response.provider - if hasattr(choice.message, "provider_specific_fields") and choice.message.provider_specific_fields is not None: + if ( + hasattr(choice.message, "provider_specific_fields") + and choice.message.provider_specific_fields is not None + ): extra.update(choice.message.provider_specific_fields) return GeneratedMessage( - message=Message(role="assistant", content=choice.message.content, tool_calls=tool_calls), + message=Message( + role="assistant", + content=choice.message.content, + tool_calls=tool_calls, + ), stop_reason=choice.finish_reason, usage=usage, extra=extra, ) - def _parse_text_completion_response(self, response: litellm.types.utils.TextCompletionResponse) -> GeneratedText: + def _parse_text_completion_response( + self, + response: litellm.types.utils.TextCompletionResponse, + ) -> GeneratedText: choice = response.choices[-1] usage = None if response.usage is not None: @@ -185,7 +244,11 @@ def _parse_text_completion_response(self, response: litellm.types.utils.TextComp extra={"response_id": response.id}, ) - async def _generate_message(self, messages: t.Sequence[Message], params: GenerateParams) -> GeneratedMessage: + async def _generate_message( + self, + messages: t.Sequence[Message], + params: GenerateParams, + ) -> GeneratedMessage: async with self.semaphore: # if params.max_tokens is None: # params.max_tokens = get_max_tokens_for_model(self.model) @@ -210,7 +273,7 @@ async def _generate_message(self, messages: t.Sequence[Message], params: Generat return await self._generate_message(messages, params) raise - self._last_request_time = datetime.datetime.now() + self._last_request_time = datetime.datetime.now(tz=datetime.timezone.utc) return self._parse_model_response(response) async def _generate_text(self, text: str, params: GenerateParams) -> GeneratedText: @@ -224,10 +287,13 @@ async def _generate_text(self, text: str, params: GenerateParams) -> GeneratedTe atext_completion = self._wrap(atext_completion) response = await atext_completion( - prompt=text, model=self.model, api_key=self.api_key, **self.params.merge_with(params).to_dict() + prompt=text, + model=self.model, + api_key=self.api_key, + **self.params.merge_with(params).to_dict(), ) - self._last_request_time = datetime.datetime.now() + self._last_request_time = datetime.datetime.now(tz=datetime.timezone.utc) return self._parse_text_completion_response(response) async def generate_messages( @@ -235,10 +301,13 @@ async def generate_messages( messages: t.Sequence[t.Sequence[Message]], params: t.Sequence[GenerateParams], ) -> t.Sequence[GeneratedMessage]: - coros = [self._generate_message(_messages, _params) for _messages, _params in zip(messages, params)] + coros = [ + self._generate_message(_messages, _params) + for _messages, _params in zip(messages, params, strict=False) + ] generated = await asyncio.gather(*coros) - for i, (_messages, response) in enumerate(zip(messages, generated)): + for i, (_messages, response) in enumerate(zip(messages, generated, strict=False)): trace_messages(_messages, f"Messages {i+1}/{len(messages)}") trace_messages([response], f"Response {i+1}/{len(messages)}") @@ -255,13 +324,16 @@ async def generate_texts( chunk_texts = texts[i : i + max_connections] chunk_params = params[i : i + max_connections] chunk_generated = await asyncio.gather( - *[self._generate_text(text, _params) for text, _params in zip(chunk_texts, chunk_params)] + *[ + self._generate_text(text, _params) + for text, _params in zip(chunk_texts, chunk_params, strict=False) + ], ) generated.extend(chunk_generated) - for i, (text, response) in enumerate(zip(chunk_texts, chunk_generated)): - trace_str(text, f"Text {i+1}/{len(texts)}") - trace_str(response, f"Generated {i+1}/{len(texts)}") + for k, (text, response) in enumerate(zip(chunk_texts, chunk_generated, strict=False)): + trace_str(text, f"Text {k+1}/{len(texts)}") + trace_str(response, f"Generated {k+1}/{len(texts)}") return generated @@ -281,4 +353,4 @@ def get_max_tokens_for_model(model: str) -> int | None: return None model = "/".join(model.split("/")[1:]) - return litellm.model_cost[model].get("max_tokens") # type: ignore + return litellm.model_cost[model].get("max_tokens") # type: ignore [no-any-return] diff --git a/rigging/generator/transformers_.py b/rigging/generator/transformers_.py index 247b641..9c93612 100644 --- a/rigging/generator/transformers_.py +++ b/rigging/generator/transformers_.py @@ -1,11 +1,14 @@ -from __future__ import annotations - import gc import typing as t import torch -import transformers # type: ignore -from transformers import AutoModelForCausalLM, AutoTokenizer, PreTrainedTokenizer, TextGenerationPipeline +import transformers +from transformers import ( + AutoModelForCausalLM, + AutoTokenizer, + PreTrainedTokenizer, + TextGenerationPipeline, +) from rigging.generator.base import ( GeneratedMessage, @@ -70,7 +73,13 @@ def llm(self) -> AutoModelForCausalLM: if self._llm is None: llm_kwargs = self.model_dump( exclude_unset=True, - include={"torch_dtype", "device_map", "trust_remote_code", "load_in_8bit", "load_in_4bit"}, + include={ + "torch_dtype", + "device_map", + "trust_remote_code", + "load_in_8bit", + "load_in_4bit", + }, ) self._llm = AutoModelForCausalLM.from_pretrained(self.model, **llm_kwargs) return self._llm @@ -98,12 +107,11 @@ def pipeline(self) -> TextGenerationPipeline: def from_obj( cls, model: str, - llm: AutoModelForCausalLM, tokenizer: PreTrainedTokenizer, *, pipeline: TextGenerationPipeline | None = None, params: GenerateParams | None = None, - ) -> TransformersGenerator: + ) -> "TransformersGenerator": """ Create a new instance of TransformersGenerator from an already loaded model and tokenizer. @@ -116,16 +124,16 @@ def from_obj( The TransformersGenerator instance. """ instance = cls(model=model, params=params or GenerateParams()) - instance._llm = model - instance._tokenizer = tokenizer - instance._pipeline = pipeline + instance._llm = model # noqa: SLF001 + instance._tokenizer = tokenizer # noqa: SLF001 + instance._pipeline = pipeline # noqa: SLF001 return instance - def load(self) -> TransformersGenerator: + def load(self) -> "TransformersGenerator": _ = self.pipeline return self - def unload(self) -> TransformersGenerator: + def unload(self) -> "TransformersGenerator": del self._pipeline del self._llm del self._tokenizer @@ -160,14 +168,14 @@ def _generate( async def generate_messages( self, - messages: t.Sequence[t.Sequence[Message]], + messages: t.Sequence[t.Sequence["Message"]], params: t.Sequence[GenerateParams], ) -> t.Sequence[GeneratedMessage]: message_dicts = [[m.to_openai_spec() for m in _messages] for _messages in messages] outputs = self._generate(message_dicts, params) generated = [o.to_generated_message() for o in outputs] - for i, (in_messages, out_message) in enumerate(zip(messages, generated)): + for i, (in_messages, out_message) in enumerate(zip(messages, generated, strict=False)): trace_messages(in_messages, f"Messages {i+1}/{len(in_messages)}") trace_messages([out_message], f"Response {i+1}/{len(in_messages)}") @@ -180,7 +188,7 @@ async def generate_texts( ) -> t.Sequence[GeneratedText]: generated = self._generate(texts, params) - for i, (text, response) in enumerate(zip(texts, generated)): + for i, (text, response) in enumerate(zip(texts, generated, strict=False)): trace_str(text, f"Text {i+1}/{len(texts)}") trace_str(response, f"Generated {i+1}/{len(texts)}") diff --git a/rigging/generator/vllm_.py b/rigging/generator/vllm_.py index 087673d..bac71b8 100644 --- a/rigging/generator/vllm_.py +++ b/rigging/generator/vllm_.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import gc import inspect import typing as t @@ -77,7 +75,13 @@ def llm(self) -> vllm.LLM: return self._llm @classmethod - def from_obj(cls, model: str, llm: vllm.LLM, *, params: GenerateParams | None = None) -> VLLMGenerator: + def from_obj( + cls, + model: str, + llm: vllm.LLM, + *, + params: GenerateParams | None = None, + ) -> VLLMGenerator: """Create a generator from an existing vLLM instance. Args: @@ -87,7 +91,7 @@ def from_obj(cls, model: str, llm: vllm.LLM, *, params: GenerateParams | None = The VLLMGenerator instance. """ generator = cls(model=model, params=params or GenerateParams()) - generator._llm = llm + generator._llm = llm # noqa: SLF001 return generator def load(self) -> VLLMGenerator: @@ -105,11 +109,17 @@ def _generate( texts: list[str], params: t.Sequence[GenerateParams], ) -> list[GeneratedText]: - sampling_params_args = list(inspect.signature(vllm.SamplingParams.__init__).parameters.keys()) + sampling_params_args = list( + inspect.signature(vllm.SamplingParams.__init__).parameters.keys(), + ) sampling_params = ( [ vllm.SamplingParams( - **{k: v for k, v in self.params.merge_with(p).to_dict().items() if k in sampling_params_args} + **{ + k: v + for k, v in self.params.merge_with(p).to_dict().items() + if k in sampling_params_args + }, ) for p in params ] @@ -144,15 +154,19 @@ def _generate( async def generate_messages( self, - messages: t.Sequence[t.Sequence[Message]], + messages: t.Sequence[t.Sequence["Message"]], params: t.Sequence[GenerateParams], ) -> t.Sequence[GeneratedMessage]: message_dicts = [[m.to_openai_spec() for m in _messages] for _messages in messages] - texts = self.llm.get_tokenizer().apply_chat_template(message_dicts, add_generation_prompt=True, tokenize=False) + texts = self.llm.get_tokenizer().apply_chat_template( + message_dicts, + add_generation_prompt=True, + tokenize=False, + ) generated_texts = self._generate(texts, params=params) generated = [g.to_generated_message() for g in generated_texts] - for i, (in_messages, out_message) in enumerate(zip(messages, generated)): + for i, (in_messages, out_message) in enumerate(zip(messages, generated, strict=False)): trace_messages(in_messages, f"Messages {i+1}/{len(in_messages)}") trace_messages([out_message], f"Response {i+1}/{len(in_messages)}") @@ -165,7 +179,7 @@ async def generate_texts( ) -> t.Sequence[GeneratedText]: generated = self._generate(list(texts), params=params) - for i, (text, response) in enumerate(zip(texts, generated)): + for i, (text, response) in enumerate(zip(texts, generated, strict=False)): trace_str(text, f"Text {i+1}/{len(texts)}") trace_str(response, f"Generated {i+1}/{len(texts)}") diff --git a/rigging/integrations/__init__.py b/rigging/integrations/__init__.py deleted file mode 100644 index 562606c..0000000 --- a/rigging/integrations/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .robopages import RobopagesApiTool, robopages - -__all__ = [ - "RobopagesApiTool", - "robopages", -] diff --git a/rigging/integrations/robopages.py b/rigging/integrations/robopages.py deleted file mode 100644 index 3f89432..0000000 --- a/rigging/integrations/robopages.py +++ /dev/null @@ -1,168 +0,0 @@ -import json -import re -import typing as t -from functools import cached_property -from urllib.parse import urlparse - -import requests -import typing_extensions as te -from loguru import logger - -from rigging.message import Message -from rigging.tool import ApiTool, Tool, ToolType -from rigging.tool.api import FunctionDefinition, ToolCall, ToolDefinition -from rigging.tracing import tracer - - -class RobopagesApiTool(ApiTool): - """ - Overload of the ApiTool class to support making tool calls through a Robopages server. - """ - - def __init__(self, url: str, name: str, description: str, parameters: t.Optional[dict[str, t.Any]] = None) -> None: - self._url = urlparse(url)._replace(path="").geturl() - self._name = name - self._description = description - self._parameters = parameters - - @cached_property - def name(self) -> str: - return self._name - - @cached_property - def description(self) -> str: - return self._name - - @cached_property - def schema(self) -> dict[str, t.Any]: - return self._parameters or {} - - @cached_property - def definition(self) -> ToolDefinition: - return ToolDefinition( - function=FunctionDefinition(name=self.name, description=self.description, parameters=self._parameters) - ) - - async def execute(self, tool_call: ToolCall) -> Message: - """Executes a function call on the tool.""" - - from rigging.message import Message - - if tool_call.function.name != self.name: - raise ValueError(f"Function name {tool_call.function.name} does not match {self.name}") - - with tracer.span(f"Robopages Tool {self.name}()", name=self.name, tool_call_id=tool_call.id) as span: - args = json.loads(tool_call.function.arguments) - span.set_attribute("arguments", args) - - response = requests.post( - f"{self._url}/process", - json=[ - { - "type": "function", - "id": tool_call.id, - "function": { - "name": self._name, - "arguments": args, - }, - } - ], - ) - if response.status_code not in [200, 400]: - response.raise_for_status() - - if response.status_code == 400: - result = response.content.decode() - else: - result = response.json()[0]["content"] - - span.set_attribute("result", result) - - return Message(role="tool", tool_call_id=tool_call.id, content=str(result)) - - __call__ = execute - - -class OpenAIFunction(te.TypedDict): - name: str - description: str - parameters: dict[str, t.Any] - - -class OpenAIFormat(te.TypedDict): - type: str - function: OpenAIFunction - - -class RobopagesFunction(te.TypedDict): - name: str - description: str - parameters: list[dict[str, t.Any]] - page_name: str - page_description: str - - -@t.overload -def robopages(url: str, *, tool_type: t.Literal["api"] = "api") -> list[RobopagesApiTool]: - ... - - -@t.overload -def robopages(url: str, *, tool_type: t.Literal["native"]) -> list[Tool]: - ... - - -def robopages( - url: str, *, tool_type: ToolType = "api", name_filter: t.Optional[str] = None -) -> t.Union[list[RobopagesApiTool], list[Tool]]: - """ - Create a list of tools from a Robopages server. - - Args: - url: The URL of the Robopages server. - tool_type: The type of tools to fetch. - name_filter: A regular expression to filter the tools by name. - - Returns: - A list of tools from the Robopages server. - - Example: - - ```python - import rigging as rg - - tools = rg.integrations.robopages("http://localhost:8080") - - chat = ( - await rg.get_generator('gpt-4o') - .chat('Please use tools') - .using(*tools) - .run() - ) - - print(chat.conversation) - ``` - """ - - filter_regex = re.compile(name_filter) if name_filter else None - - response = requests.get(url, params={"flavor": "rigging" if tool_type == "native" else "openai"}) - response.raise_for_status() - tools_data = response.json() - - if tool_type == "api": - openai_tools = t.cast(list[OpenAIFormat], tools_data) - - logger.info(f"Fetched {len(openai_tools)} functions from Robopages ({url})") - - tools: list[RobopagesApiTool] = [] - for tool in openai_tools: - function = tool["function"] - if filter_regex and not filter_regex.search(function["name"]): - logger.debug(f"Skipping function {function['name']}") - continue - tools.append(RobopagesApiTool(url, function["name"], function["description"], function.get("parameters"))) - - return tools - - raise NotImplementedError("Only API tools are supported right now") diff --git a/rigging/interact.py b/rigging/interact.py index 6f40993..f65a2a1 100644 --- a/rigging/interact.py +++ b/rigging/interact.py @@ -1,4 +1,6 @@ -from __future__ import annotations +""" +Utility functions for interactive chat sessions. +""" import asyncio import itertools @@ -6,14 +8,18 @@ from colorama import Fore, Style -from rigging.chat import ChatPipeline +from rigging.chat import Chat, ChatPipeline from rigging.generator.base import Generator, get_generator -if t.TYPE_CHECKING: - from rigging.chat import Chat +# ruff: noqa: T201 (we use print() here for simplicity) -async def _animate(*, delay: float = 0.5, chars: list[str] | None = None, color: str = Fore.BLUE) -> None: +async def _animate( + *, + delay: float = 0.5, + chars: list[str] | None = None, + color: str = Fore.BLUE, +) -> None: cycle = itertools.cycle(chars or [" ", ". ", ".. ", "..."]) while True: print(f"{color}{next(cycle)}{Style.RESET_ALL}", end="\r", flush=True) @@ -21,7 +27,9 @@ async def _animate(*, delay: float = 0.5, chars: list[str] | None = None, color: async def interact( - entrypoint: ChatPipeline | Generator | str, *, reset_callback: t.Callable[[Chat | None], None] | None = None + entrypoint: ChatPipeline | Generator | str, + *, + reset_callback: t.Callable[[Chat | None], None] | None = None, ) -> Chat | None: """ Start an interactive chat session using the given pipeline, generator, or generator id. @@ -92,8 +100,8 @@ async def interact( except KeyboardInterrupt: print(f"\n\n{Fore.YELLOW}Chat interrupted. Exiting.{Style.RESET_ALL}") break - except Exception as e: - print(f"\n\n{Fore.RED}An error occurred: {str(e)}{Style.RESET_ALL}") + except Exception as e: # noqa: BLE001 + print(f"\n\n{Fore.RED}An error occurred: {e!s}{Style.RESET_ALL}") break return chat diff --git a/rigging/logging.py b/rigging/logging.py index 3016126..6fe8468 100644 --- a/rigging/logging.py +++ b/rigging/logging.py @@ -4,16 +4,12 @@ To just enable rigging logs to flow, call `logger.enable("rigging")` after importing the module. """ -from __future__ import annotations - +import pathlib import sys import typing as t from loguru import logger -if t.TYPE_CHECKING: - import pathlib - g_configured: bool = False LogLevelList = ["trace", "debug", "info", "success", "warning", "error", "critical"] @@ -35,7 +31,7 @@ def configure_logging( will only be done to the console. log_file_level: The log level for the log file. """ - global g_configured + global g_configured # noqa: PLW0603 if g_configured: return diff --git a/rigging/message.py b/rigging/message.py index 83d5b24..f18c3b7 100644 --- a/rigging/message.py +++ b/rigging/message.py @@ -2,8 +2,6 @@ This module covers core message objects and handling. """ -from __future__ import annotations - import base64 import copy import mimetypes @@ -27,14 +25,17 @@ ) from rigging.error import MissingModelError -from rigging.model import Model, ModelT # noqa: TCH001 +from rigging.model import Model, ModelT from rigging.parsing import try_parse_many -from rigging.tool.api import ToolCall +from rigging.tool.api import ApiToolCall from rigging.util import truncate_string Role = t.Literal["system", "user", "assistant", "tool"] """The role of a message. Can be 'system', 'user', 'assistant', or 'tool'.""" +EPHERMAL_CACHE_CONTROL = {"type": "ephemeral"} +"""Cache control entry for ephemeral messages.""" + # Helper type for messages structured # more similarly to other libraries @@ -73,7 +74,7 @@ def validate_slice(cls, value: t.Any) -> slice: if isinstance(value, slice): return value if not isinstance(value, list): - raise ValueError("slice must be a list or a slice") + raise TypeError("slice must be a list or a slice") return slice(value[0], value[1]) @@ -84,6 +85,8 @@ class ContentText(BaseModel): """The type of content (always `text`).""" text: str """The text content.""" + cache_control: dict[str, str] | None = None + """Cache control entry for prompt caching.""" def __str__(self) -> str: return self.text @@ -102,12 +105,31 @@ class ImageUrl(BaseModel): """The type of content (always `image_url`).""" image_url: ImageUrl """The image URL content.""" + cache_control: dict[str, str] | None = None + """Cache control entry for prompt caching.""" def __str__(self) -> str: return f"" @classmethod - def from_file(cls, file: Path | str, *, mimetype: str | None = None) -> ContentImageUrl: + def from_file( + cls, + file: Path | str, + *, + mimetype: str | None = None, + detail: t.Literal["auto", "low", "high"] = "auto", + ) -> "ContentImageUrl": + """ + Creates a ContentImageUrl object from a file. + + Args: + file: The file to create the content from. + mimetype: The mimetype of the file. If not provided, it will be guessed. + + Returns: + The created ContentImageUrl object. + """ + file = Path(file) if not file.exists(): raise FileNotFoundError(f"File '{file}' does not exist") @@ -121,14 +143,53 @@ def from_file(cls, file: Path | str, *, mimetype: str | None = None) -> ContentI encoded = base64.b64encode(file.read_bytes()).decode() url = f"data:{mimetype};base64,{encoded}" - return cls(image_url=cls.ImageUrl(url=url)) + return cls(image_url=cls.ImageUrl(url=url, detail=detail)) + + @classmethod + def from_bytes( + cls, + data: bytes, + mimetype: str, + *, + detail: t.Literal["auto", "low", "high"] = "auto", + ) -> "ContentImageUrl": + """ + Creates a ContentImageUrl object from raw bytes. + + Args: + data: The raw bytes of the image. + mimetype: The mimetype of the image. + detail: The detail level of the image. + + Returns: + The created ContentImageUrl + """ + + encoded = base64.b64encode(data).decode() + url = f"data:{mimetype};base64,{encoded}" + return cls(image_url=cls.ImageUrl(url=url, detail=detail)) @classmethod - def from_url(cls, url: str, *, detail: t.Literal["auto", "low", "high"] = "auto") -> ContentImageUrl: + def from_url( + cls, + url: str, + *, + detail: t.Literal["auto", "low", "high"] = "auto", + ) -> "ContentImageUrl": + """ + Creates a ContentImageUrl object from a URL. + + Args: + url: The URL of the image. + detail: The detail level of the image. + + Returns: + The created ContentImageUrl object. + """ return cls(image_url=cls.ImageUrl(url=url, detail=detail)) -Content = t.Union[ContentText, ContentImageUrl] +Content = ContentText | ContentImageUrl """The types of content that can be included in a message.""" ContentTypes = (ContentText, ContentImageUrl) @@ -143,8 +204,8 @@ class Message(BaseModel): For interface stability, `content` will remain a property accessor for the text of a message, but the "real" content - is available in `all_content`. During serialization, we rename - `all_content` to `content` for compatibility. + is available in `content_parts`. During serialization, we rename + `content_parts` to `content` for compatibility. """ uuid: UUID = Field(default_factory=uuid4, repr=False) @@ -153,9 +214,9 @@ class Message(BaseModel): """The role of the message.""" parts: list[ParsedMessagePart] = Field(default_factory=list) """The parsed message parts.""" - all_content: str | list[Content] = Field("", repr=False) + content_parts: list[Content] = Field([], repr=False) """Interior str content or structured content parts.""" - tool_calls: list[ToolCall] | None = Field(None) + tool_calls: list[ApiToolCall] | None = Field(None) """The tool calls associated with the message.""" tool_call_id: str | None = Field(None) """Associated call id if this message is a response to a tool call.""" @@ -163,10 +224,11 @@ class Message(BaseModel): def __init__( self, role: Role, - content: str | list[str | Content] | None = None, + content: str | t.Sequence[str | Content] | None = None, parts: t.Sequence[ParsedMessagePart] | None = None, - tool_calls: list[ToolCall] | list[dict[str, t.Any]] | None = None, + tool_calls: t.Sequence[ApiToolCall] | t.Sequence[dict[str, t.Any]] | None = None, tool_call_id: str | None = None, + cache_control: t.Literal["ephemeral"] | dict[str, str] | None = None, **kwargs: t.Any, ): # TODO: We default to an empty string, but this technically isn't @@ -174,17 +236,25 @@ def __init__( if content is None: content = "" - if isinstance(content, str): - content = dedent(content) - else: - content = [ContentText(text=dedent(part)) if isinstance(part, str) else part for part in content] + content = [content] if isinstance(content, str) else content + content_parts = [ + ContentText(text=dedent(part)) if isinstance(part, str) else part for part in content + ] - if tool_calls is not None and not all(isinstance(call, ToolCall) for call in tool_calls): - tool_calls = [ToolCall.model_validate(call) if isinstance(call, dict) else call for call in tool_calls] + if tool_calls is not None and not all(isinstance(call, ApiToolCall) for call in tool_calls): + tool_calls = [ + ApiToolCall.model_validate(call) if isinstance(call, dict) else call + for call in tool_calls + ] + + if cache_control is not None and content_parts: + content_parts[-1].cache_control = ( + cache_control if isinstance(cache_control, dict) else EPHERMAL_CACHE_CONTROL + ) super().__init__( role=role, - all_content=content, + content_parts=content_parts, parts=parts or [], tool_calls=tool_calls, tool_call_id=tool_call_id, @@ -193,10 +263,11 @@ def __init__( def __str__(self) -> str: formatted = f"[{self.role}]:" - if isinstance(self.all_content, list): - formatted += "\n |- " + "\n |- ".join(str(content) for content in self.all_content) + + if len(self.content_parts) == 1 and isinstance(self.content_parts[0], ContentText): + formatted += f" {self.content_parts[0].text}" else: - formatted += f" {self.content}" + formatted += "\n |- " + "\n |- ".join(str(content) for content in self.content_parts) for tool_call in self.tool_calls or []: formatted += f"\n |- {tool_call}" @@ -215,21 +286,43 @@ def __repr_args__(self) -> t.Iterable[tuple[str | None, t.Any]]: @model_serializer(mode="wrap") def rename_content(self, handler: SerializerFunctionWrapHandler) -> t.Any: serialized = handler(self) - if "all_content" in serialized: - serialized["content"] = serialized.pop("all_content") + if "content_parts" in serialized: + serialized["content"] = serialized.pop("content_parts") + + # Some backwards compatibility for single text content + # which we'll load straight into the content value as opposed + # to a list of content parts. + if ( + len(serialized["content"]) == 1 + and list(serialized["content"][0].keys()) == ["type", "text"] + and serialized["content"][0].get("type") == "text" + ): + serialized["content"] = serialized["content"][0]["text"] + return serialized + @property + def all_content(self) -> str | list[Content]: + """ + Returns all content parts of the message or the single text content part as a string. + + Deprecated: + Use `.content_parts` instead + """ + if len(self.content_parts) == 1 and isinstance(self.content_parts[0], ContentText): + return self.content_parts[0].text + return self.content_parts + @property def content(self) -> str: """ - The content of the message. + The content of the message as a string. - If the interior of the message content is stored as a list of Content objects, - this property will return the concatenated text of any ContentText parts. + If you need to access the structured content parts, use `.content_parts`. """ - if isinstance(self.all_content, str): - return self.all_content - return "\n".join([content.text for content in self.all_content if isinstance(content, ContentText)]) + return "\n".join( + [content.text for content in self.content_parts if isinstance(content, ContentText)], + ) @content.setter def content(self, value: str) -> None: @@ -246,16 +339,20 @@ def content(self, value: str) -> None: # when content is changed. self.parts = [] - if isinstance(self.all_content, list): - text_parts = [c for c in self.all_content if isinstance(c, ContentText)] - if len(text_parts) == 1: - text_parts[0].text = value - return + text_parts = [c for c in self.content_parts if isinstance(c, ContentText)] - self.all_content = dedent(value) + # If we have a single text part, we can just update it + if len(text_parts) == 1: + text_parts[0].text = value + return + + # Otherwise we need to remove text parts without + # removing other content parts + other_parts = [c for c in self.content_parts if not isinstance(c, ContentText)] + self.content_parts = [*other_parts, ContentText(text=value)] @model_validator(mode="after") - def validate_parts(self) -> Message: + def validate_parts(self) -> "Message": from rigging.model import Model # TODO: For now, we don't want to keep parts @@ -279,19 +376,32 @@ def to_openai_spec(self) -> dict[str, t.Any]: Returns: The serialized message. """ - # `all_content` will be moved to `content` - return self.model_dump(include={"role", "all_content", "tool_calls", "tool_call_id"}, exclude_none=True) + # `content_parts` will be moved to `content` + obj = self.model_dump( + include={"role", "content_parts", "tool_calls", "tool_call_id"}, + exclude_none=True, + ) - def force_str_content(self) -> Message: - """ - Forces the content of the message to be a string by stripping - any structured content parts like images. + # Walk content parts and add a `\n` to the end of any text parts + # which are followed by another text part (if not already present). + # + # This prevents model API behaviors from just concatenating the text + # parts together without any separation which confuses the model. - Returns: - The modified message. - """ - self.all_content = self.content - return self + for i, current in enumerate(obj.get("content", [])): + if i == len(obj["content"]) - 1: + break + + next_ = obj["content"][i + 1] + + if ( + current.get("type") == "text" + and next_.get("type") == "text" + and not current.get("text", "").endswith("\n") + ): + current["text"] += "\n" + + return obj # TODO: In general the add/remove/sync_part methods are # overly complicated. We should probably just update content, @@ -299,28 +409,41 @@ def force_str_content(self) -> Message: # # I don't like all this manual slice recalculation logic, seems brittle. - def _remove_part(self, part: ParsedMessagePart) -> str: - if not isinstance(self.all_content, str): - raise NotImplementedError("Managing parsed parts from multi-content messages is not supported") + def _remove_part(self, part: ParsedMessagePart) -> None: + text_parts = [p for p in self.content_parts if isinstance(p, ContentText)] + if len(text_parts) > 1: + raise NotImplementedError( + "Managing parsed parts in messages with multiple content text parts is not supported", + ) + + if len(text_parts) == 0: + raise ValueError("No text content to remove part from") + + text_part = text_parts[0] removed_length = part.slice_.stop - part.slice_.start - self.all_content = self.all_content[: part.slice_.start] + self.all_content[part.slice_.stop :] + text_part.text = text_part.text[: part.slice_.start] + text_part.text[part.slice_.stop :] self.parts.remove(part) # Update slices of any parts that come after the removed part for other_part in self.parts: if other_part.slice_.start > part.slice_.start: other_part.slice_ = slice( - other_part.slice_.start - removed_length, other_part.slice_.stop - removed_length + other_part.slice_.start - removed_length, + other_part.slice_.stop - removed_length, ) - return self.all_content - def _add_part(self, part: ParsedMessagePart) -> None: for existing in self.parts: - if part.slice_ == existing.slice_ and part.model.xml_tags() == existing.model.xml_tags(): + if ( + part.slice_ == existing.slice_ + and part.model.xml_tags() == existing.model.xml_tags() + ): return # We clearly already have this part defined - if max(part.slice_.start, existing.slice_.start) < min(part.slice_.stop, existing.slice_.stop): + if max(part.slice_.start, existing.slice_.start) < min( + part.slice_.stop, + existing.slice_.stop, + ): raise ValueError("Incoming part overlaps with an existing part") self.parts.append(part) @@ -329,15 +452,24 @@ def _add_part(self, part: ParsedMessagePart) -> None: # to watch for the total size of our message shifting and update the slices # of the following parts accordingly. In other words, as A expands, B which # follows will have a new start slice and end slice. + def _sync_parts(self) -> None: - if not isinstance(self.all_content, str): - raise NotImplementedError("Managing parsed parts from multi-content messages is not supported") + text_parts = [p for p in self.content_parts if isinstance(p, ContentText)] + if len(text_parts) > 1: + raise NotImplementedError( + "Managing parsed parts in messages with multiple content text parts is not supported", + ) + + if len(text_parts) == 0: + raise ValueError("No text content to remove part from") + + text_part = text_parts[0] self.parts = sorted(self.parts, key=lambda p: p.slice_.start) shift = 0 for part in self.parts: - existing = self.all_content[part.slice_] + existing = text_part.text[part.slice_] # Adjust for any previous shifts part.slice_ = slice(part.slice_.start + shift, part.slice_.stop + shift) @@ -351,8 +483,10 @@ def _sync_parts(self) -> None: old_length = part.slice_.stop - part.slice_.start new_length = len(xml_content) - self.all_content = ( - self.all_content[: part.slice_.start] + xml_content + self.all_content[part.slice_.stop :] + text_part.text = ( + text_part.text[: part.slice_.start] + + xml_content + + text_part.text[part.slice_.stop :] ) part.slice_ = slice(part.slice_.start, part.slice_.start + new_length) @@ -360,11 +494,46 @@ def _sync_parts(self) -> None: self.parts = sorted(self.parts, key=lambda p: p.slice_.start) - def clone(self) -> Message: + def clone(self) -> "Message": """Creates a copy of the message.""" - return Message(self.role, self.content, parts=copy.deepcopy(self.parts)) + return Message( + self.role, + copy.deepcopy(self.content_parts), + parts=copy.deepcopy(self.parts), + ) - def apply(self, **kwargs: str) -> Message: + def cache(self, cache_control: dict[str, str] | bool = True) -> "Message": # noqa: FBT002 + """ + Update cache control settings for this message. + + Args: + cache_control: The cache control settings to + apply to the message. If `False`, all cache + control settings will be removed. If `True`, + the default ephemeral cache control will be applied. + If a dictionary, it will be applied as the cache control settings. + + Returns: + The updated message. + """ + + for part in self.content_parts: + part.cache_control = None + + if cache_control is False: + return self + + if cache_control is True: + cache_control = EPHERMAL_CACHE_CONTROL + + if not isinstance(cache_control, dict): + raise TypeError(f"Invalid cache control: {cache_control}") + + self.content_parts[-1].cache_control = cache_control + + return self + + def apply(self, **kwargs: str) -> "Message": """ Applies the given keyword arguments with string templating to the content of the message. @@ -381,7 +550,12 @@ def apply(self, **kwargs: str) -> Message: new.content = template.safe_substitute(**kwargs) return new - def strip(self, model_type: type[Model], *, fail_on_missing: bool = False) -> list[ParsedMessagePart]: + def strip( + self, + model_type: type[Model], + *, + fail_on_missing: bool = False, + ) -> list[ParsedMessagePart]: """ Removes and returns a list of ParsedMessagePart objects from the message that match the specified model type. @@ -402,7 +576,9 @@ def strip(self, model_type: type[Model], *, fail_on_missing: bool = False) -> li removed.append(part) if not removed and fail_on_missing: - raise TypeError(f"Could not find <{model_type.__xml_tag__}> ({model_type.__name__}) in message") + raise TypeError( + f"Could not find <{model_type.__xml_tag__}> ({model_type.__name__}) in message", + ) return removed @@ -459,7 +635,10 @@ def parse_set(self, model_type: type[ModelT], minimum: int | None = None) -> lis return self.try_parse_set(model_type, minimum=minimum, fail_on_missing=True) def try_parse_set( - self, model_type: type[ModelT], minimum: int | None = None, fail_on_missing: bool = False + self, + model_type: type[ModelT], + minimum: int | None = None, + fail_on_missing: bool = False, # noqa: FBT001, FBT002 (historical) ) -> list[ModelT]: """ Tries to parse a set of models from the message content. @@ -510,7 +689,11 @@ def try_parse_many(self, *types: type[ModelT], fail_on_missing: bool = False) -> MissingModelError: If a model type is missing and `fail_on_missing` is True. """ model: ModelT - parsed: list[tuple[ModelT, slice]] = try_parse_many(self.content, *types, fail_on_missing=fail_on_missing) + parsed: list[tuple[ModelT, slice]] = try_parse_many( + self.content, + *types, + fail_on_missing=fail_on_missing, + ) for model, slice_ in parsed: self._add_part(ParsedMessagePart(model=model, slice_=slice_)) self._sync_parts() @@ -518,8 +701,11 @@ def try_parse_many(self, *types: type[ModelT], fail_on_missing: bool = False) -> @classmethod def from_model( - cls: type[Message], models: Model | t.Sequence[Model], role: Role = "user", suffix: str | None = None - ) -> Message: + cls: type["Message"], + models: Model | t.Sequence[Model], + role: Role = "user", + suffix: str | None = None, + ) -> "Message": """ Create a Message object from one or more Model objects. @@ -546,24 +732,30 @@ def from_model( @classmethod def fit_as_list( - cls, messages: t.Sequence[MessageDict] | t.Sequence[Message] | MessageDict | Message | str - ) -> list[Message]: + cls, + messages: t.Sequence[MessageDict] + | t.Sequence["Message"] + | MessageDict + | "Message" + | Content + | str, + ) -> list["Message"]: """Helper function to convert various common types to a strict list of Message objects.""" - if isinstance(messages, (Message, dict, str)): + if isinstance(messages, (Message, dict, str, *ContentTypes)): return [cls.fit(messages)] return [cls.fit(message) for message in messages] @classmethod - def fit(cls, message: t.Union[Message, MessageDict, str]) -> Message: + def fit(cls, message: t.Union["Message", MessageDict, Content, str]) -> "Message": """Helper function to convert various common types to a Message object.""" - if isinstance(message, str): - return cls(role="user", content=message) + if isinstance(message, (str, *ContentTypes)): + return cls(role="user", content=[message]) return cls(**message) if isinstance(message, dict) else message.model_copy(deep=True) @classmethod - def apply_to_list(cls, messages: t.Sequence[Message], **kwargs: str) -> list[Message]: + def apply_to_list(cls, messages: t.Sequence["Message"], **kwargs: str) -> list["Message"]: """Helper function to apply keyword arguments to a list of Message objects.""" return [message.apply(**kwargs) for message in messages] -Messages = t.Union[t.Sequence[MessageDict], t.Sequence[Message]] +Messages = t.Sequence[MessageDict] | t.Sequence[Message] diff --git a/rigging/model.py b/rigging/model.py index 0176bf7..e140ae7 100644 --- a/rigging/model.py +++ b/rigging/model.py @@ -2,35 +2,37 @@ Models are the core datatypes for structured parsing. """ -from __future__ import annotations - +import contextlib +import dataclasses +import inspect import re import typing as t -from xml.etree import ElementTree as ET +from xml.etree import ElementTree as ET # noqa: N817 import typing_extensions as te -import xmltodict # type: ignore +import xmltodict # type: ignore [import-untyped] from pydantic import ( + BaseModel, BeforeValidator, + Field, SerializationInfo, ValidationError, - create_model, field_serializer, field_validator, ) +from pydantic import create_model as create_pydantic_model from pydantic_xml import BaseXmlModel -from pydantic_xml import attr as attr -from pydantic_xml import element as element -from pydantic_xml import wrapped as wrapped +from pydantic_xml import attr as attr # noqa: PLC0414 +from pydantic_xml import create_model as create_pydantic_xml_model +from pydantic_xml import element as element # noqa: PLC0414 +from pydantic_xml import wrapped as wrapped # noqa: PLC0414 +from pydantic_xml.element import SearchMode # type: ignore [attr-defined] from pydantic_xml.model import XmlEntityInfo from pydantic_xml.typedefs import EntityLocation, NsMap from rigging.error import MissingModelError from rigging.util import escape_xml, to_xml_tag, unescape_xml -if t.TYPE_CHECKING: - from pydantic_xml.element import SearchMode # type: ignore [attr-defined] - # # Core XML serializable models for messages # @@ -99,14 +101,35 @@ def to_pretty_xml(self) -> str: """ tree = self.to_xml_tree() ET.indent(tree, " ") - pretty_encoded_xml = ET.tostring(tree, short_empty_elements=False, encoding="utf-8").decode() + pretty_encoded_xml = str( + ET.tostring( + tree, + short_empty_elements=False, + encoding="utf-8", + ).decode(), + ) + + # If this is a complex model, we'll return exactly what we get + if not self.__class__.is_simple() and not self.__class__.is_simple_with_attrs(): + return pretty_encoded_xml + + # Otherwise we can try to do some cleaning by unescaping and + # re-indenting the XML (helpful for nested-xml) - if self.__class__.is_simple(): - # We only expect to use this in our "simple" - # models, but I'd like a better long-term solution - return unescape_xml(pretty_encoded_xml) - else: - return pretty_encoded_xml # type: ignore [no-any-return] + pretty_encoded_xml = unescape_xml(pretty_encoded_xml) + + with contextlib.suppress(ET.ParseError): + tree = ET.fromstring(pretty_encoded_xml) # noqa: S314 + ET.indent(tree, " ") + pretty_encoded_xml = str( + ET.tostring( + tree, + short_empty_elements=False, + encoding="utf-8", + ).decode(), + ) + + return pretty_encoded_xml def __str__(self) -> str: return self.to_pretty_xml() @@ -144,6 +167,32 @@ def is_simple(cls) -> bool: return annotation in BASIC_TYPES + @classmethod + def is_simple_with_attrs(cls) -> bool: + """ + Check if the model would otherwise be marked as "simple", but has other fields which are + all attributes. If so, we can do some parsing magic below and make sure our non-element + field is updated with the extracted content properly, while pydantic-xml takes care + of the attributes. + + Returns: + True if the model is simple with attrs, False otherwise. + """ + field_values = list(cls.model_fields.values()) + + none_entity_fields = [f for f in field_values if not isinstance(f, XmlEntityInfo)] + entity_fields = [f for f in field_values if isinstance(f, XmlEntityInfo)] + attr_fields = [f for f in entity_fields if f.location == EntityLocation.ATTRIBUTE] + + if len(none_entity_fields) != 1 or len(attr_fields) != len(entity_fields): + return False + + annotation = field_values[0].annotation + if t.get_origin(annotation) == t.Annotated: + annotation = t.get_args(annotation)[0] + + return annotation in BASIC_TYPES + @classmethod def xml_start_tag(cls) -> str: """Helper method which wrapped the class tag in XML braces.""" @@ -181,7 +230,11 @@ def xml_example(cls) -> str: properties = schema["properties"] structure = {cls.__xml_tag__: {field: None for field in properties}} xml_string = xmltodict.unparse( - structure, pretty=True, full_document=False, indent=" ", short_empty_elements=True + structure, + pretty=True, + full_document=False, + indent=" ", + short_empty_elements=True, ) return t.cast(str, xml_string) # Bad type hints in xmltodict @@ -195,7 +248,9 @@ def ensure_valid(cls) -> None: if len(cls.model_fields) == 1: field_info = next(iter(cls.model_fields.values())) if hasattr(field_info, "location") and field_info.location == EntityLocation.ATTRIBUTE: - raise ValueError(f"Model '{cls.__name__}' has a single attr() field which is not supported") + raise ValueError( + f"Model '{cls.__name__}' has a single attr() field which is not supported", + ) # Attempt to extract this object from an arbitrary string # which may contain other XML elements or text, returns @@ -227,7 +282,11 @@ def from_text(cls, content: str) -> list[tuple[te.Self, slice]]: tag_name = cls.__xml_tag__ pattern = f"(<({tag_name}).*?>((.*?)))" - matches = [m for m in re.finditer(pattern, content, flags=re.DOTALL) if m.group(2) == cls.__xml_tag__] + matches = [ + m + for m in re.finditer(pattern, content, flags=re.DOTALL) + if m.group(2) == cls.__xml_tag__ + ] if not matches: raise MissingModelError(f"Failed to find '{cls.xml_tags()}' in message") @@ -255,7 +314,10 @@ def from_text(cls, content: str) -> list[tuple[te.Self, slice]]: inner_match: re.Match[str] | None = match while inner_match is not None: inner_matches = re.finditer(pattern, inner_with_end_tag, flags=re.DOTALL) - inner_match = next((m for m in inner_matches if m.group(2) == cls.__xml_tag__), None) + inner_match = next( + (m for m in inner_matches if m.group(2) == cls.__xml_tag__), + None, + ) if inner_match is not None: full_text, _, inner_with_end_tag, inner = inner_match.groups() @@ -265,8 +327,21 @@ def from_text(cls, content: str) -> list[tuple[te.Self, slice]]: if cls.is_simple() else cls.from_xml(escape_xml(full_text)) ) + + # If our model is relatively simple (only attributes and a single non-element field) + # we should go back and update our non-element field with the extracted content. + + if cls.is_simple_with_attrs(): + name, field = next( + (name, field) + for name, field in cls.model_fields.items() + if not isinstance(field, XmlEntityInfo) + ) + if field.annotation in BASIC_TYPES: + model.__dict__[name] = field.annotation(unescape_xml(inner)) + extracted.append((model, slice(match.start(), match.end()))) - except Exception as e: + except Exception as e: # noqa: BLE001 exceptions.append(e) continue @@ -308,7 +383,10 @@ def one_from_text(cls, content: str, *, fail_on_many: bool = False) -> tuple[te. class Primitive(Model, t.Generic[PrimitiveT]): - content: t.Annotated[PrimitiveT, BeforeValidator(lambda x: x.strip() if isinstance(x, str) else x)] + content: t.Annotated[ + PrimitiveT, + BeforeValidator(lambda x: x.strip() if isinstance(x, str) else x), + ] def make_primitive( @@ -343,18 +421,212 @@ def _validate(value: str) -> str: return value if strip_content: - type_ = t.Annotated[type_, BeforeValidator(lambda x: x.strip() if isinstance(x, str) else x)] # type: ignore + type_ = t.Annotated[ + type_, + BeforeValidator(lambda x: x.strip() if isinstance(x, str) else x), + ] # type: ignore [assignment] - return create_model( + return create_pydantic_model( name, - __base__=Primitive[type_], # type: ignore + __base__=Primitive[type_], # type: ignore [valid-type, arg-type] __doc__=doc, __cls_kwargs__={"tag": tag}, content=(type_, ...), - __validators__={"content_validator": field_validator("content")(_validate)} if validator else {}, + __validators__={"content_validator": field_validator("content")(_validate)} + if validator + else {}, ) +# +# Conversion from JSON schemas +# + +FieldType = t.Any # aliases for my sanity +FieldInfo = t.Any + + +def _process_field(field_name: str, field_schema: dict[str, t.Any]) -> tuple[FieldType, FieldInfo]: + """Process a field schema and return appropriate type and field info.""" + field_info: FieldInfo = {} + + # enums + if "enum" in field_schema: + return t.Literal[tuple(field_schema["enum"])], field_info + + # arrays + if field_schema.get("type") == "array": + item_schema = field_schema["items"] + item_type, _ = _process_field(f"{field_name}Item", item_schema) + + if item_schema.get("type") == "object": + return list[item_type], wrapped(field_name, **field_info) # type: ignore [valid-type] + + return list[item_type], wrapped(field_name, element("item", **field_info)) # type: ignore [valid-type] + + # objects + if field_schema.get("type") == "object": + # dictionaries + additional_schema = field_schema.get("additionalProperties") + if additional_schema: + dict_type, _ = _process_field(f"{field_name}Item", additional_schema) + return dict[str, dict_type], field_info # type: ignore [valid-type] + + # Otherwise a nested model + return make_from_schema( + field_schema, + field_schema.get("title"), + allow_primitive=True, + ), field_info + + # unions + for union_type in ["anyOf", "oneOf", "allOf"]: + if union_type in field_schema: + types = [ + _process_field(field_name, sub_schema)[0] for sub_schema in field_schema[union_type] + ] + return t.Union[tuple(types)], field_info # noqa: UP007 + + type_mapping: dict[str, FieldType] = { + "string": str, + "integer": int, + "number": float, + "boolean": bool, + "null": type(None), + "object": dict, + "array": list, + } + + # tuples + field_type = field_schema.get("type", "string") + if isinstance(field_type, list): + types = [type_mapping.get(t, t.Any) for t in field_type] + return t.Union[tuple(types)], field_info # noqa: UP007 + + # primitives + return type_mapping.get(field_type, t.Any), field_info + + +def make_from_schema( + schema: dict[str, t.Any], + name: str | None = None, + *, + allow_primitive: bool = False, +) -> type[Model]: + """ + Helper to build a Rigging model dynamically from a JSON schema. + + Note: + There are plenty of edge cases this doesn't handle, consider this + very experimental and only suitable for simple schemas. + + Args: + schema: The JSON schema to build the model from. + model_name: The name of the model (otherwise inferred from the schema). + + Returns: + The Pydantic model class. + """ + properties = schema.get("properties", {}) + required = set(schema.get("required", [])) + fields: dict[str, tuple[FieldType, FieldInfo]] = {} + + # If we only have one property and the caller allows it, + # make the model "simple/primitive" by not using the element() wrapper + field_cls = Field if len(properties) == 1 and allow_primitive else element + + for field_name, field_schema in properties.items(): + field_type, field_info = _process_field(field_name, field_schema) + + fields[field_name] = ( + field_type, + field_cls( + default=... if field_name in required else None, + description=field_schema.get("description", ""), + title=field_schema.get("title", ""), + **field_info, + ) + if isinstance(field_info, dict) + else field_info, + ) + + name = name or schema.get("title", "SchemaModel") + return create_pydantic_xml_model(name, __base__=Model, **fields) # type: ignore [arg-type] + + +# +# Conversion from callable signatures +# + + +def _safe_issubclass(cls: t.Any, class_or_tuple: t.Any) -> bool: + """Safely check if a class is a subclass of another class or tuple.""" + try: + return isinstance(cls, type) and issubclass(cls, class_or_tuple) + except TypeError: + return False + + +def _is_complex_type(typ: t.Any) -> bool: + """Check if a type is a complex type (class-based, not primitive).""" + try: + return _safe_issubclass(typ, (str, int, float, bool, bytes)) and typ is not t.Any + except TypeError: + return False + + +def make_from_signature(signature: inspect.Signature, name: str | None = None) -> type[Model]: + fields = {} + for param_name, param in signature.parameters.items(): + param_type = param.annotation + param_origin = t.get_origin(param_type) + param_args = t.get_args(param_type) + + # Sanity checks + for type_ in (param_type, param_origin, *param_args): + if _safe_issubclass(type_, BaseModel) and not _safe_issubclass(type_, BaseXmlModel): + raise ValueError( + f"Function arguments which are Pydantic models must inherit from `BaseXmlModel` ({param_name})", + ) + + if dataclasses.is_dataclass(type_): + raise ValueError( + f"Function arguments which are dataclasses are not supported ({param_name})", + ) + + # Extract description from Annotated types + description = "" + if param_origin is t.Annotated: + param_type = param_args[0] # The actual type + if len(param_args) > 1 and isinstance(param_args[1], str): + description = param_args[1] # The description + + # Add default value if available + default = ... if param.default is inspect.Parameter.empty else param.default + field = element(default=default, description=description) + + # Handle List types (really just for the wrapped() component) + if param_origin is list or param_origin is t.List: # noqa: UP006 + item_tag = "item" + + if ( + param_args + and _is_complex_type(param_args[0]) + and param_name.endswith("s") + and (singular := param_name[:-1]) + ): + item_tag = singular + + field = wrapped( + param_name, + element(tag=item_tag, default=default, description=description), + ) + + fields[param_name] = (param_type, field) + + return create_pydantic_xml_model(name, __base__=Model, **fields) # type: ignore [arg-type] + + # # Helpers for passing structured errors to models # @@ -364,6 +636,7 @@ class ErrorModel(Model, tag="error"): content: str @field_validator("content", mode="before") + @classmethod def parse_exception(cls, value: t.Any) -> t.Any: if isinstance(value, Exception): return str(value) @@ -426,39 +699,34 @@ class DelimitedAnswer(Model): content: str _delimiters: t.ClassVar[list[str]] = [",", "-", "/", "|"] - _items: list[str] = [] - @property def items(self) -> list[str]: """Parsed items from the content.""" - if self._items: - return self._items - split_sizes: dict[str, int] = {} for delimiter in self._delimiters: split_sizes[delimiter] = len(self.content.split(delimiter)) delimiter = max(split_sizes, key=split_sizes.get) # type: ignore [arg-type] split = [i.strip(" \"'\t\r\n") for i in self.content.split(delimiter)] - self._items = [s for s in split if s] - return self._items + return [s for s in split if s] @field_validator("content", mode="before") + @classmethod def parse_str_to_list(cls, v: t.Any) -> t.Any: if not isinstance(v, str): - raise ValueError(f"Cannot parse content as a delimited list: {v}") + raise TypeError(f"Cannot parse content as a delimited list: {v}") return v class CommaDelimitedAnswer(DelimitedAnswer): "Comma delimited answer (,)" - _delimiters = [","] + _delimiters: t.ClassVar = [","] class NewlineDelimitedAnswer(DelimitedAnswer): "Newline delimited answer (\n)" - _delimiters = ["\n"] + _delimiters: t.ClassVar = ["\n"] class YesNoAnswer(Model): @@ -468,15 +736,16 @@ class YesNoAnswer(Model): """The boolean value of the answer.""" @field_validator("boolean", mode="before") + @classmethod def parse_str_to_bool(cls, v: t.Any) -> t.Any: if isinstance(v, str): simple = v.strip().lower() if any(simple.startswith(s) for s in ["yes", "true"]): return True - elif any(simple.startswith(s) for s in ["no", "false"]): + if any(simple.startswith(s) for s in ["no", "false"]): return False raise ValueError(f"Cannot parse '{v}' as a boolean") @field_serializer("boolean") - def serialize_bool_to_str(self, v: bool, _info: SerializationInfo) -> str: + def serialize_bool_to_str(self, v: bool, _info: SerializationInfo) -> str: # noqa: FBT001 return "yes" if v else "no" diff --git a/rigging/parsing.py b/rigging/parsing.py index e6c22dd..d58ba12 100644 --- a/rigging/parsing.py +++ b/rigging/parsing.py @@ -2,8 +2,6 @@ Parsing helpers for extracting rigging models from text """ -from __future__ import annotations - import typing as t from rigging.error import MissingModelError @@ -12,7 +10,7 @@ from rigging.model import ModelT -def parse(text: str, model_type: type[ModelT]) -> tuple[ModelT, slice]: +def parse(text: str, model_type: type["ModelT"]) -> tuple["ModelT", slice]: """ Parses a single model from text. @@ -29,7 +27,7 @@ def parse(text: str, model_type: type[ModelT]) -> tuple[ModelT, slice]: return try_parse_many(text, model_type, fail_on_missing=True)[0] -def try_parse(text: str, model_type: type[ModelT]) -> tuple[ModelT, slice] | None: +def try_parse(text: str, model_type: type["ModelT"]) -> tuple["ModelT", slice] | None: """ Tries to parse a model from text. @@ -43,7 +41,12 @@ def try_parse(text: str, model_type: type[ModelT]) -> tuple[ModelT, slice] | Non return next(iter(try_parse_many(text, model_type)), None) -def parse_set(text: str, model_type: type[ModelT], *, minimum: int | None = None) -> list[tuple[ModelT, slice]]: +def parse_set( + text: str, + model_type: type["ModelT"], + *, + minimum: int | None = None, +) -> list[tuple["ModelT", slice]]: """ Parses a set of models with the specified identical type from text. @@ -62,8 +65,12 @@ def parse_set(text: str, model_type: type[ModelT], *, minimum: int | None = None def try_parse_set( - text: str, model_type: type[ModelT], *, minimum: int | None = None, fail_on_missing: bool = False -) -> list[tuple[ModelT, slice]]: + text: str, + model_type: type["ModelT"], + *, + minimum: int | None = None, + fail_on_missing: bool = False, +) -> list[tuple["ModelT", slice]]: """ Tries to parse a set of models with the specified identical type from text. @@ -85,7 +92,7 @@ def try_parse_set( return models -def parse_many(text: str, *types: type[ModelT]) -> list[tuple[ModelT, slice]]: +def parse_many(text: str, *types: type["ModelT"]) -> list[tuple["ModelT", slice]]: """ Parses multiple models of the specified non-identical types from text. @@ -102,7 +109,11 @@ def parse_many(text: str, *types: type[ModelT]) -> list[tuple[ModelT, slice]]: return try_parse_many(text, *types, fail_on_missing=True) -def try_parse_many(text: str, *types: type[ModelT], fail_on_missing: bool = False) -> list[tuple[ModelT, slice]]: +def try_parse_many( + text: str, + *types: type["ModelT"], + fail_on_missing: bool = False, +) -> list[tuple["ModelT", slice]]: """ Tries to parses multiple models of the specified non-identical types from text. @@ -118,14 +129,15 @@ def try_parse_many(text: str, *types: type[ModelT], fail_on_missing: bool = Fals MissingModelError: If a model type is missing and `fail_on_missing` is True. Exception: If the model is malformed and `fail_on_missing` is True. """ - model: ModelT - parsed: list[tuple[ModelT, slice]] = [] - for model_class in types: - try: + model: "ModelT" + parsed: list[tuple["ModelT", slice]] = [] + + try: + for model_class in types: for model, slice_ in model_class.from_text(text): parsed.append((model, slice_)) - except Exception as e: - if fail_on_missing: - raise e + except Exception: # noqa: BLE001 + if fail_on_missing: + raise return sorted(parsed, key=lambda x: x[1].start) diff --git a/rigging/prompt.py b/rigging/prompt.py index 1fe5707..9bc49cf 100644 --- a/rigging/prompt.py +++ b/rigging/prompt.py @@ -2,8 +2,6 @@ Treat empty function signatures as prompts for structured chat interfaces. """ -from __future__ import annotations - import asyncio import dataclasses import inspect @@ -19,7 +17,7 @@ from rigging.generator.base import GenerateParams, Generator, get_generator from rigging.message import Message from rigging.model import Model, SystemErrorModel, ValidationErrorModel, make_primitive -from rigging.tool.api import ApiTool +from rigging.tool import Tool from rigging.tracing import tracer from rigging.util import escape_xml, get_qualified_name, to_snake, to_xml_tag @@ -59,7 +57,7 @@ class Ctx: # Utilities -def unwrap_annotated(annotation: t.Any) -> tuple[t.Any, t.Optional[Ctx]]: +def unwrap_annotated(annotation: t.Any) -> tuple[t.Any, Ctx | None]: if t.get_origin(annotation) is t.Annotated: base_type, *meta = t.get_args(annotation) for m in meta: @@ -70,13 +68,16 @@ def unwrap_annotated(annotation: t.Any) -> tuple[t.Any, t.Optional[Ctx]]: def get_undeclared_variables(template: str) -> set[str]: - env = Environment() + env = Environment() # noqa: S701 parsed_template = env.parse(template) return meta.find_undeclared_variables(parsed_template) def make_parameter( - annotation: t.Any, *, name: str = "nested", kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD + annotation: t.Any, + *, + name: str = "nested", + kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD, ) -> inspect.Parameter: return inspect.Parameter(name=name, kind=kind, annotation=annotation) @@ -111,8 +112,8 @@ def to_xml(self, value: t.Any) -> str: @dataclasses.dataclass class BasicInput(Input): def to_str(self, value: t.Any) -> str: - if not isinstance(value, (int, float, str, bool)): - raise ValueError(f"Value must be a basic type, got: {type(value)}") + if not isinstance(value, int | float | str | bool): + raise TypeError(f"Value must be a basic type, got: {type(value)}") return str(value) @@ -120,7 +121,7 @@ def to_str(self, value: t.Any) -> str: class ModelInput(Input): def to_str(self, value: t.Any) -> str: if not isinstance(value, Model): - raise ValueError(f"Value must be a Model instance, got: {type(value)}") + raise TypeError(f"Value must be a Model instance, got: {type(value)}") return value.to_pretty_xml() def to_xml(self, value: t.Any) -> str: @@ -141,8 +142,8 @@ class DictInput(Input): def to_str(self, value: t.Any) -> str: if not isinstance(value, dict): - raise ValueError(f"Value must be a dictionary, got: {type(value)}") - if not all(isinstance(k, str) for k in value.keys()): + raise TypeError(f"Value must be a dictionary, got: {type(value)}") + if not all(isinstance(k, str) for k in value): raise ValueError("Dictionary keys must be strings") return "\n".join(f"<{k}>{self.interior.to_str(v)}" for k, v in value.items()) @@ -172,20 +173,26 @@ def parse_parameter(param: inspect.Parameter, error_name: str) -> Input: arg_type, arg_context = unwrap_annotated(args[0]) return ListInput( - param.name, arg_context or context or Ctx(), parse_parameter(make_parameter(arg_type), error_name) + param.name, + arg_context or context or Ctx(), + parse_parameter(make_parameter(arg_type), error_name), ) - elif t.get_origin(annotation) is dict: + if t.get_origin(annotation) is dict: if param.name == "nested": raise TypeError(f"Nested dict parameters are not supported: {error_name}") args = t.get_args(annotation) - if not args or len(args) != 2: + if not args or len(args) != 2: # noqa: PLR2004 raise TypeError(f"Dict param must be fully typed: {error_name}") if args[0] is not str: raise TypeError(f"Dict param keys must be strings: {error_name}") - return DictInput(param.name, context or Ctx(), parse_parameter(make_parameter(args[1]), error_name)) + return DictInput( + param.name, + context or Ctx(), + parse_parameter(make_parameter(args[1]), error_name), + ) if inspect.isclass(annotation) and issubclass(annotation, Model): return ModelInput(param.name, context or Ctx()) @@ -215,7 +222,8 @@ def guidance(self) -> str: def to_format(self) -> str: tag = self.context.tag or self.tag - assert not isinstance(self.context.example, Model) + if isinstance(self.context.example, Model): + raise NotImplementedError("Model examples are not supported for this output") return self._prefix(f"<{tag}>{escape_xml(self.context.example or '')}") def from_chat(self, chat: Chat) -> t.Any: @@ -233,8 +241,8 @@ class BasicOutput(Output): type_: type[t.Any] # TODO: We should be able to scope this down def from_chat(self, chat: Chat) -> t.Any: - Temp = make_primitive("Model", self.type_, tag=self.context.tag or self.tag) - return chat.last.parse(Temp).content + model_cls = make_primitive("Model", self.type_, tag=self.context.tag or self.tag) + return chat.last.parse(model_cls).content @dataclasses.dataclass @@ -243,8 +251,8 @@ def guidance(self) -> str: return "Produce the following output for each item (use xml tags):" def from_chat(self, chat: Chat) -> t.Any: - Model = make_primitive("Model", self.type_, tag=self.context.tag or self.tag) - return [m.content for m in chat.last.parse_set(Model)] + model_cls = make_primitive("Model", self.type_, tag=self.context.tag or self.tag) + return [m.content for m in chat.last.parse_set(model_cls)] @dataclasses.dataclass @@ -308,7 +316,7 @@ def from_chat(self, chat: Chat) -> t.Any: return self.type_(*super().from_chat(chat)) -def parse_output(annotation: t.Any, error_name: str, *, allow_nested: bool = True) -> Output: +def parse_output(annotation: t.Any, error_name: str, *, allow_nested: bool = True) -> Output: # noqa: PLR0912 from rigging.chat import Chat if annotation in [None, inspect.Parameter.empty]: @@ -335,10 +343,18 @@ def parse_output(annotation: t.Any, error_name: str, *, allow_nested: bool = Tru arg_type, arg_context = unwrap_annotated(args[0]) if arg_type in [int, float, str, bool]: - return BasicListOutput(id=arg_type.__name__, context=arg_context or context or Ctx(), type_=arg_type) + return BasicListOutput( + id=arg_type.__name__, + context=arg_context or context or Ctx(), + type_=arg_type, + ) if inspect.isclass(arg_type) and issubclass(arg_type, Model): - return ModelListOutput(id=arg_type.__name__, context=arg_context or context or Ctx(), type_=arg_type) + return ModelListOutput( + id=arg_type.__name__, + context=arg_context or context or Ctx(), + type_=arg_type, + ) if t.get_origin(annotation) is tuple: if not allow_nested: @@ -354,7 +370,7 @@ def parse_output(annotation: t.Any, error_name: str, *, allow_nested: bool = Tru raise TypeError( f"Tuple return annotations must have unique internal types\n" "or use Annotated[..., Ctx(tag=...)] overrides to\n" - f"make them differentiable ({error_name})" + f"make them differentiable ({error_name})", ) return TupleOutput(id="tuple", context=context or Ctx(), interiors=tuple_interiors) @@ -371,8 +387,16 @@ def parse_output(annotation: t.Any, error_name: str, *, allow_nested: bool = Tru interior_annotations.append(t.Annotated[field_annotation, Ctx(**ctx_dict)]) dataclass_interiors: list[Output] = [] - for field, field_annotation in zip(dataclasses.fields(annotation), interior_annotations): - interior = parse_output(field_annotation, f"{error_name}#{field.name}", allow_nested=False) + for field, field_annotation in zip( + dataclasses.fields(annotation), + interior_annotations, + strict=False, + ): + interior = parse_output( + field_annotation, + f"{error_name}#{field.name}", + allow_nested=False, + ) if interior is None: raise TypeError(f"Dataclass field type is invalid ({error_name}#{field.name}") dataclass_interiors.append(interior) @@ -381,11 +405,14 @@ def parse_output(annotation: t.Any, error_name: str, *, allow_nested: bool = Tru raise TypeError( f"Dataclass return annotations must have unique internal types\n" "or use Annotated[..., Ctx(tag=...)] overrides to\n" - f"make them differentiable ({error_name})" + f"make them differentiable ({error_name})", ) return DataclassOutput( - id=annotation.__name__, type_=annotation, context=context or Ctx(), interiors=dataclass_interiors + id=annotation.__name__, + type_=annotation, + context=context or Ctx(), + interiors=dataclass_interiors, ) # This has to come after our list/tuple checks as they pass isclass @@ -420,12 +447,14 @@ class Prompt(t.Generic[P, R]): output: Output = dataclasses.field(default_factory=lambda: ChatOutput(id="chat", context=Ctx())) """The structured output handler for the prompt.""" - watch_callbacks: list[WatchChatCallback] = dataclasses.field(default_factory=list) + watch_callbacks: "list[WatchChatCallback]" = dataclasses.field(default_factory=list) """Callbacks to be passed any chats produced while executing this prompt.""" params: GenerateParams | None = None """The parameters to be used when generating chats for this prompt.""" - api_tools: list[ApiTool] = dataclasses.field(default_factory=list) + tools: list[Tool] = dataclasses.field(default_factory=list) """The API tools to be made available when generating chats for this prompt.""" + system_prompt: str | None = None + """A system prompt fragment to be injected into the messages before generation.""" _generator_id: str | None = None _generator: Generator | None = None @@ -437,7 +466,11 @@ def __post_init__(self) -> None: if self.func is None: return - signature = inspect.signature(self.func) + # We include eval_str here to help with __future__ annotations + # usage, but it may cause issues and I haven't had time to fully + # dig into implications. + + signature = inspect.signature(self.func, eval_str=True) undeclared = get_undeclared_variables(self.docstring) for param in signature.parameters.values(): @@ -461,7 +494,7 @@ def docstring(self) -> str: if self._docstring is None: # Guidance is taken from https://github.com/outlines-dev/outlines/blob/main/outlines/prompts.py docstring = self.func.__doc__ or DEFAULT_DOC.format( - func_name=self.func.__name__ if self.func else "function" + func_name=self.func.__name__ if self.func else "function", ) docstring = inspect.cleandoc(docstring) self._docstring = re.sub(r"(?![\r\n])(\b\s+)", " ", docstring) @@ -526,20 +559,20 @@ def _until_parsed(self, message: Message) -> tuple[bool, list[Message]]: Message.from_model( ValidationErrorModel(content=str(e)), suffix="Rewrite your entire message with all the required elements.", - ) + ), ) - except Exception as e: + except Exception as e: # noqa: BLE001 should_continue = True generated.append( Message.from_model( SystemErrorModel(content=str(e)), suffix="Rewrite your entire message with all the required elements.", - ) + ), ) return (should_continue, generated) - def clone(self, *, skip_callbacks: bool = False) -> Prompt[P, R]: + def clone(self, *, skip_callbacks: bool = False) -> "Prompt[P, R]": """ Creates a deep copy of this prompt. @@ -556,12 +589,13 @@ def clone(self, *, skip_callbacks: bool = False) -> Prompt[P, R]: attempt_recovery=self.attempt_recovery, drop_dialog=self.drop_dialog, max_rounds=self.max_rounds, + system_prompt=self.system_prompt, ) if not skip_callbacks: new.watch_callbacks = self.watch_callbacks.copy() return new - def with_(self, params: t.Optional[GenerateParams] = None, **kwargs: t.Any) -> Prompt[P, R]: + def with_(self, params: GenerateParams | None = None, **kwargs: t.Any) -> "Prompt[P, R]": """ Assign specific generation parameter overloads for this prompt. @@ -580,8 +614,11 @@ def with_(self, params: t.Optional[GenerateParams] = None, **kwargs: t.Any) -> P # creating a prompt from other code. def set_( - self, attempt_recovery: bool | None = None, drop_dialog: bool | None = None, max_rounds: int | None = None - ) -> Prompt[P, R]: + self, + attempt_recovery: bool | None = None, + drop_dialog: bool | None = None, + max_rounds: int | None = None, + ) -> "Prompt[P, R]": """ Helper to allow updates to the parsing configuration. @@ -598,7 +635,7 @@ def set_( self.max_rounds = max_rounds or self.max_rounds return self - def watch(self, *callbacks: WatchChatCallback) -> Prompt[P, R]: + def watch(self, *callbacks: "WatchChatCallback") -> "Prompt[P, R]": """ Registers a callback to monitor any chats produced for this prompt @@ -648,7 +685,7 @@ def render(self, *args: P.args, **kwargs: P.kwargs) -> str: Pass the arguments to the jinja2 template and render the full prompt. """ - env = Environment( + env = Environment( # noqa: S701 trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True, @@ -670,9 +707,12 @@ def process(self, chat: Chat) -> R: """ Attempt to parse the output from a chat into the expected return type. """ - return self.output.from_chat(chat) # type: ignore + return self.output.from_chat(chat) # type: ignore [no-any-return] - def bind(self, other: ChatPipeline | Generator | Chat | str) -> t.Callable[P, t.Coroutine[t.Any, t.Any, R]]: + def bind( + self, + other: ChatPipeline | Generator | Chat | str, + ) -> t.Callable[P, t.Coroutine[t.Any, t.Any, R]]: """ Binds the prompt to a pipeline, generator, or chat and returns a scoped run callable. @@ -693,19 +733,20 @@ def say_hello(name: str) -> str: pipeline = self._resolve_to_pipeline(other) if pipeline.on_failed == "skip": raise NotImplementedError( - "pipeline.on_failed='skip' cannot be used for prompt methods that return one object" + "pipeline.on_failed='skip' cannot be used for prompt methods that return one object", ) async def run(*args: P.args, **kwargs: P.kwargs) -> R: results = await self.bind_many(pipeline)(1, *args, **kwargs) return results[0] - run.__rg_prompt__ = self # type: ignore + run.__rg_prompt__ = self # type: ignore [attr-defined] return run def bind_many( - self, other: ChatPipeline | Generator | Chat | str + self, + other: ChatPipeline | Generator | Chat | str, ) -> t.Callable[Concatenate[int, P], t.Coroutine[t.Any, t.Any, list[R]]]: """ Binds the prompt to a pipeline, generator, or chat and returns a scoped run_many callable. @@ -726,7 +767,9 @@ def say_hello(name: str) -> str: """ pipeline = self._resolve_to_pipeline(other) if pipeline.on_failed == "include" and not isinstance(self.output, ChatOutput): - raise NotImplementedError("pipeline.on_failed='include' cannot be used with prompts that process outputs") + raise NotImplementedError( + "pipeline.on_failed='include' cannot be used with prompts that process outputs", + ) async def run_many(count: int, /, *args: P.args, **kwargs: P.kwargs) -> list[R]: name = get_qualified_name(self.func) if self.func else "" @@ -739,7 +782,7 @@ async def run_many(count: int, /, *args: P.args, **kwargs: P.kwargs) -> list[R]: content = self.render(*args, **kwargs) _pipeline = ( pipeline.fork(content) - .using_api_tools(*self.api_tools) + .using(*self.tools) .until( self._until_parsed, attempt_recovery=self.attempt_recovery, @@ -748,12 +791,16 @@ async def run_many(count: int, /, *args: P.args, **kwargs: P.kwargs) -> list[R]: ) .with_(self.params) ) + + if self.system_prompt: + _pipeline.chat.inject_system_content(self.system_prompt) + chats = await _pipeline.run_many(count) # TODO: I can't remember why we don't just pass the watch_callbacks to the pipeline # Maybe it has something to do with uniqueness and merging? - def wrap_watch_callback(callback: WatchChatCallback) -> WatchChatCallback: + def wrap_watch_callback(callback: "WatchChatCallback") -> "WatchChatCallback": async def traced_watch_callback(chats: list[Chat]) -> None: callback_name = get_qualified_name(callback) with tracer.span( @@ -777,13 +824,17 @@ async def traced_watch_callback(chats: list[Chat]) -> None: span.set_attribute("results", results) return results - run_many.__rg_prompt__ = self # type: ignore + run_many.__rg_prompt__ = self # type: ignore [attr-defined] return run_many def bind_over( - self, other: ChatPipeline | Generator | Chat | str | None = None - ) -> t.Callable[Concatenate[t.Sequence[Generator | str], P], t.Coroutine[t.Any, t.Any, list[R]]]: + self, + other: ChatPipeline | Generator | Chat | str | None = None, + ) -> t.Callable[ + Concatenate[t.Sequence[Generator | str], P], + t.Coroutine[t.Any, t.Any, list[R]], + ]: """ Binds the prompt to a pipeline, generator, or chat and returns a scoped run_over callable. @@ -804,18 +855,27 @@ def say_hello(name: str) -> str: include_original = other is not None if other is None: - pipeline = get_generator("base!base").chat().catch(on_failed="skip") # TODO: Clean this up + pipeline = ( + get_generator("base!base").chat().catch(on_failed="skip") + ) # TODO: Clean this up else: pipeline = self._resolve_to_pipeline(other) if pipeline.on_failed == "include" and not isinstance(self.output, ChatOutput): - raise NotImplementedError("pipeline.on_failed='include' cannot be used with prompts that process outputs") + raise NotImplementedError( + "pipeline.on_failed='include' cannot be used with prompts that process outputs", + ) - async def run_over(generators: t.Sequence[Generator | str], /, *args: P.args, **kwargs: P.kwargs) -> list[R]: + async def run_over( + generators: t.Sequence[Generator | str], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> list[R]: content = self.render(*args, **kwargs) _pipeline = ( pipeline.fork(content) - .using_api_tools(*self.api_tools) + .using(*self.tools) .until( self._until_parsed, attempt_recovery=self.attempt_recovery, @@ -824,14 +884,22 @@ async def run_over(generators: t.Sequence[Generator | str], /, *args: P.args, ** ) .with_(self.params) ) + + if self.system_prompt: + _pipeline.chat.inject_system_content(self.system_prompt) + chats = await _pipeline.run_over(*generators, include_original=include_original) - coros = [watch(chats) for watch in self.watch_callbacks if watch not in pipeline.watch_callbacks] + coros = [ + watch(chats) + for watch in self.watch_callbacks + if watch not in pipeline.watch_callbacks + ] await asyncio.gather(*coros) return [self.process(chat) for chat in chats] - run_over.__rg_prompt__ = self # type: ignore + run_over.__rg_prompt__ = self # type: ignore [attr-defined] return run_over @@ -849,7 +917,7 @@ async def run_many(self, count: int, /, *args: P.args, **kwargs: P.kwargs) -> li """ if self.pipeline is None: raise RuntimeError( - "Prompt cannot be executed as a standalone function without being assigned a pipeline or generator" + "Prompt cannot be executed as a standalone function without being assigned a pipeline or generator", ) return await self.bind_many(self.pipeline)(count, *args, **kwargs) @@ -866,14 +934,20 @@ async def run(self, *args: P.args, **kwargs: P.kwargs) -> R: """ if self.pipeline is None: raise RuntimeError( - "Prompt cannot be executed as a standalone function without being assigned a pipeline or generator" + "Prompt cannot be executed as a standalone function without being assigned a pipeline or generator", ) return await self.bind(self.pipeline)(*args, **kwargs) async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: return await self.run(*args, **kwargs) - async def run_over(self, generators: t.Sequence[Generator | str], /, *args: P.args, **kwargs: P.kwargs) -> list[R]: + async def run_over( + self, + generators: t.Sequence[Generator | str], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> list[R]: """ Executes the prompt process across multiple generators. @@ -886,7 +960,7 @@ async def run_over(self, generators: t.Sequence[Generator | str], /, *args: P.ar The implementation currently skips any failed chats and only processes successful chats. This may change in the future. - Parameters: + Args: generators: A sequence of generators to be used for the generation process. Returns: @@ -906,7 +980,8 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[ApiTool | t.Callable[..., t.Any]] | None = None, + tools: list[Tool | t.Callable[..., t.Any]] | None = None, + system_prompt: str | None = None, ) -> t.Callable[[t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | t.Callable[P, R]], Prompt[P, R]]: ... @@ -919,7 +994,8 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[ApiTool | t.Callable[..., t.Any]] | None = None, + tools: list[Tool | t.Callable[..., t.Any]] | None = None, + system_prompt: str | None = None, ) -> Prompt[P, R]: ... @@ -932,7 +1008,8 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[ApiTool | t.Callable[..., t.Any]] | None = None, + tools: list[Tool | t.Callable[..., t.Any]] | None = None, + system_prompt: str | None = None, ) -> Prompt[P, R]: ... @@ -944,8 +1021,12 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[ApiTool | t.Callable[..., t.Any]] | None = None, -) -> t.Callable[[t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | t.Callable[P, R]], Prompt[P, R]] | Prompt[P, R]: + tools: list[Tool | t.Callable[..., t.Any]] | None = None, + system_prompt: str | None = None, +) -> ( + t.Callable[[t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | t.Callable[P, R]], Prompt[P, R]] + | Prompt[P, R] +): """ Convert a hollow function into a Prompt, which can be called directly or passed a chat pipeline to execute the function and parse the outputs. @@ -1001,7 +1082,8 @@ async def write_joke(topic: str) -> ExplainedJoke: pipeline: An optional pipeline to use for the prompt. generator: An optional generator to use for the prompt. generator_id: An optional generator id to use for the prompt. - tools: An optional list of API tools to make available to the prompt (Native tools are not currently supported). + tools: An optional list of tools to make available during generation (can be other prompts). + system_prompt: An optional system prompt fragment to inject into the messages before generation. Returns: A prompt instance or a function that can be used to create a prompt. @@ -1009,13 +1091,18 @@ async def write_joke(topic: str) -> ExplainedJoke: if sum(arg is not None for arg in (pipeline, generator, generator_id)) > 1: raise ValueError("Only one of pipeline, generator, or generator_id can be provided") - def make_prompt(func: t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | t.Callable[P, R]) -> Prompt[P, R]: + def make_prompt( + func: t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | t.Callable[P, R], + ) -> Prompt[P, R]: return Prompt[P, R]( - func=func, # type: ignore + func=func, # type: ignore [arg-type] _generator_id=generator_id, _pipeline=pipeline, _generator=generator, - api_tools=[tool if isinstance(tool, ApiTool) else ApiTool(tool) for tool in tools] if tools else [], + system_prompt=system_prompt, + tools=[tool if isinstance(tool, Tool) else Tool.from_callable(tool) for tool in tools] + if tools + else [], ) if func is not None: @@ -1029,12 +1116,20 @@ def make_prompt(content: str, return_type: type[R], *, ctx: Ctx | None = None) - @t.overload -def make_prompt(content: str, return_type: None = None, *, ctx: Ctx | None = None) -> Prompt[..., str]: +def make_prompt( + content: str, + return_type: None = None, + *, + ctx: Ctx | None = None, +) -> Prompt[..., str]: ... def make_prompt( - content: str, return_type: type[R] | None = None, *, ctx: Ctx | None = None + content: str, + return_type: type[R] | None = None, + *, + ctx: Ctx | None = None, ) -> Prompt[..., R] | Prompt[..., str]: """ Create a prompt at runtime from a basic string and return type (experimental). @@ -1059,6 +1154,9 @@ def make_prompt( Returns: The constructed Prompt """ - return_type = return_type or str # type: ignore - output = parse_output(t.Annotated[return_type, ctx] if ctx is not None else return_type, "make_prompt()") + return_type = return_type or str # type: ignore [assignment] + output = parse_output( + t.Annotated[return_type, ctx] if ctx is not None else return_type, + "make_prompt()", + ) return Prompt(output=output, _docstring=content) diff --git a/rigging/tool/__init__.py b/rigging/tool/__init__.py index 560429a..976145a 100644 --- a/rigging/tool/__init__.py +++ b/rigging/tool/__init__.py @@ -2,15 +2,14 @@ This module defines handles tool interaction with rigging generation. """ -import typing as t -from rigging.tool.api import ApiTool -from rigging.tool.native import Tool - -ToolType = t.Literal["api", "native"] +from rigging.tool.base import Tool, tool +from rigging.tool.mcp import mcp +from rigging.tool.robopages import robopages __all__ = [ "Tool", - "ApiTool", - "ToolType", + "robopages", + "mcp", + "tool", ] diff --git a/rigging/tool/api.py b/rigging/tool/api.py index d6d8e73..6975266 100644 --- a/rigging/tool/api.py +++ b/rigging/tool/api.py @@ -1,44 +1,32 @@ """ -This module handles tools provided externally through APIs (OpenAI, Anthropic, Groq, etc.). +Models and utilities for working with API-based function calling tools. """ -from __future__ import annotations -import functools -import inspect -import json import typing as t -from functools import cached_property -from openai.lib._pydantic import to_strict_json_schema -from pydantic import BaseModel, TypeAdapter, field_validator +from pydantic import BaseModel, field_validator -from rigging.tracing import tracer -from rigging.util import deref_json -if t.TYPE_CHECKING: - from rigging.message import Message - - -class NamedFunction(BaseModel): +class ApiNamedFunction(BaseModel): name: str -class ToolChoiceDefinition(BaseModel): +class ApiToolChoiceDefinition(BaseModel): type: t.Literal["function"] = "function" - function: NamedFunction + function: ApiNamedFunction # I want to avoid making ToolChoice too specific as # different providers interpret it differently. # ToolChoice = t.Union[t.Literal["none"], t.Literal["auto"], ToolChoiceDefinition] -ToolChoice = t.Union[str, dict[str, t.Any]] +ApiToolChoice = str | dict[str, t.Any] -class FunctionDefinition(BaseModel): +class ApiFunctionDefinition(BaseModel): name: str - description: t.Optional[str] = None - parameters: t.Optional[dict[str, t.Any]] = None + description: str | None = None + parameters: dict[str, t.Any] | None = None # Logic here is a bit hacky, but in general # we want to handle cases where pydantic has @@ -53,6 +41,7 @@ class FunctionDefinition(BaseModel): # have special handling, but we'll assume LiteLLM will # take care of things like that for now. @field_validator("parameters", mode="before") + @classmethod def validate_parameters(cls, value: t.Any) -> t.Any: if not isinstance(value, dict): return value @@ -63,160 +52,20 @@ def validate_parameters(cls, value: t.Any) -> t.Any: return value -class ToolDefinition(BaseModel): +class ApiToolDefinition(BaseModel): type: t.Literal["function"] = "function" - function: FunctionDefinition + function: ApiFunctionDefinition -class FunctionCall(BaseModel): +class ApiFunctionCall(BaseModel): name: str arguments: str -class ToolCall(BaseModel): +class ApiToolCall(BaseModel): id: str type: t.Literal["function"] = "function" - function: FunctionCall + function: ApiFunctionCall def __str__(self) -> str: return f"" - - -class ApiTool: - def __init__(self, fn: t.Callable[..., t.Any], *, _fn_to_call: t.Callable[..., t.Any] | None = None) -> None: - from rigging.prompt import Prompt - - # We support _fn_to_call for cases where the incoming function - # for signature analysis and schema generation is different from - # the function we actually want to call (generally internal-only) - # - # TODO: This is likely resolved by our __signature__ assignment - # and __annotations__ override below. - - self.fn = fn - self._fn_to_call = _fn_to_call or fn - - # We need to do some magic here because our Prompt object and - # associated run function lack the context needed to construct - # the schema at runtime - so we pass in the wrapped function for - # attribute access and the top level Prompt.run for actual execution - - if isinstance(fn, Prompt): - self.fn = fn.func # type: ignore - self._fn_to_call = fn.run - - # In the case that we are recieving a bound function which is tracking - # an originating prompt, we can extract it from a private attribute - - elif hasattr(fn, "__rg_prompt__") and isinstance(fn.__rg_prompt__, Prompt): - if fn.__name__ in ["run_many", "run_over"]: - raise ValueError( - "Only the singular Prompt.run (Prompt.bind) is supported when using prompt objects inside API tools" - ) - - self.fn = fn.__rg_prompt__.func # type: ignore - self._fn_to_call = fn - - self.signature = inspect.signature(self.fn) - - # Passing a function to a TypeAdapter results in an internal - # call being made to the function after during validation. - # We want to manage the call ourselves, so we pass in a dummy function - # that does nothing, with a mirrored signature of the original - # to ensure arguments are still validated properly. - - @functools.wraps(self.fn) - def empty_func(*args, **kwargs): # type: ignore - pass - - # We'll also reconstruct __annotations__ from the signature - # manually in case we are working with an object that - # has manually set __signature__ and the __annotations__ - # might not be accurate. This is a bit of a hack, but - # we can't control how pydantic resolves annotations - - annotations: dict[str, t.Any] = {} - for param_name, param in self.signature.parameters.items(): - if param.annotation is not inspect.Parameter.empty: - annotations[param_name] = param.annotation - - if self.signature.return_annotation is not inspect.Parameter.empty: - annotations["return"] = self.signature.return_annotation - - empty_func.__annotations__ = annotations - - self.type_adapter: TypeAdapter[t.Any] = TypeAdapter(empty_func) - - _ = self.schema # Ensure schema is valid - - @cached_property - def name(self) -> str: - return self.fn.__name__ - - @cached_property - def description(self) -> str: - return self.fn.__doc__ or "" - - @cached_property - def schema(self) -> dict[str, t.Any]: - schema = to_strict_json_schema(self.type_adapter) - - # Maintain the behavior of Annotated[, ""] by walking - # adjusting the schema manually using signature annotations. - - for name in schema.get("properties", {}).keys(): - if name not in self.signature.parameters: - continue - - param = self.signature.parameters[name] - if t.get_origin(param.annotation) != t.Annotated: - continue - - annotation_args = t.get_args(param.annotation) - if len(annotation_args) != 2 or not isinstance(annotation_args[1], str): - continue - - schema["properties"][name]["description"] = annotation_args[1] - - return deref_json(schema, is_json_schema=True) - - @cached_property - def definition(self) -> ToolDefinition: - return ToolDefinition( - function=FunctionDefinition(name=self.name, description=self.description, parameters=self.schema) - ) - - async def execute(self, tool_call: ToolCall) -> Message: - """Executes a function call on the tool.""" - - from rigging.message import ContentTypes, Message - - if tool_call.function.name != self.name: - raise ValueError(f"Function name {tool_call.function.name} does not match {self.name}") - - with tracer.span(f"Tool {self.name}()", name=self.name, tool_call_id=tool_call.id) as span: - args = json.loads(tool_call.function.arguments) - span.set_attribute("arguments", args) - - self.type_adapter.validate_python(args) - - result = self._fn_to_call(**args) - if inspect.isawaitable(result): - result = await result - - span.set_attribute("result", result) - - message = Message(role="tool", tool_call_id=tool_call.id) - - if isinstance(result, Message): - message.all_content = result.all_content - elif isinstance(result, ContentTypes): - message.all_content = [result] - elif isinstance(result, list) and all(isinstance(item, ContentTypes) for item in result): - message.all_content = result - else: - message.all_content = str(result) - - return message - - __call__ = execute diff --git a/rigging/tool/base.py b/rigging/tool/base.py new file mode 100644 index 0000000..c06bcbe --- /dev/null +++ b/rigging/tool/base.py @@ -0,0 +1,379 @@ +""" +Core types and functions for defining tools and handling tool calls. +""" + +import functools +import inspect +import json +import typing as t +from dataclasses import dataclass, field +from functools import cached_property + +from pydantic import TypeAdapter + +from rigging.error import ToolDefinitionError +from rigging.model import Model, make_from_schema, make_from_signature +from rigging.tool.api import ApiFunctionDefinition, ApiToolCall, ApiToolDefinition +from rigging.tool.native import ( + JsonInXmlToolCall, + JsonInXmlToolDefinition, + NativeToolResult, + XmlToolCall, + XmlToolDefinition, +) +from rigging.tracing import tracer +from rigging.util import deref_json + +if t.TYPE_CHECKING: + from rigging.message import Message + +ToolMode = t.Literal["auto", "api", "xml", "json-in-xml"] +""" +How tool calls are handled. + +- `auto`: The method is chosed based on support (api > xml). +- `api`: Tool calls are delegated to api-provided function calling. +- `xml`: Tool calls are parsed in nested XML format. +- `json-in-xml`: Tool calls are parsed as raw JSON inside XML tags. +""" + + +@dataclass +class Tool: + """Base class for representing a tool to a generator.""" + + name: str + """The name of the tool.""" + description: str + """A description of the tool.""" + parameters_schema: dict[str, t.Any] + """The JSON schema for the tool's parameters.""" + fn: t.Callable[..., t.Any] + """The function to call.""" + + _signature: inspect.Signature | None = field(default=None, init=False, repr=False) + _type_adapter: TypeAdapter[t.Any] | None = field(default=None, init=False, repr=False) + _model: type[Model] | None = field(default=None, init=False, repr=False) + + # In general we are split between 2 strategies for handling the data translations: + # + # 1. TypeAdapter applied straigh to a callable (`api` and `json-in-xml` modes) + # 2. Dynamic Model class built from the signature (`xml` mode) + # + # TODO: I'd like to unify these and pick which strategy works best for us. I'm inclined + # to trust the TypeAdapter approach as it's less custom code, but we lose some capacity + # to handle complex xml structures. Even with something like `xmltodict`, there is a lot + # of ambiguity about homogeneous lists and nested structures. + + @classmethod + def from_callable( + cls, + fn: t.Callable[..., t.Any], + *, + name: str | None = None, + description: str | None = None, + ) -> "Tool": + from rigging.prompt import Prompt + + fn_for_signature = fn + + # We need to do some magic here because our Prompt object and + # associated run function lack the context needed to construct + # the schema at runtime - so we pass in the wrapped function for + # attribute access and the top level Prompt.run for actual execution + + if isinstance(fn, Prompt): + fn_for_signature = fn.func # type: ignore [assignment] + fn = fn.run + + # In the case that we are recieving a bound function which is tracking + # an originating prompt, unwrap the prompt and use it's function for + # the signature. Be sure to error for cases where we aren't dealing + # with the singular Prompt.run, as we don't currently have logic to + # handle any Concatenate use on the ParamSpec. + + elif hasattr(fn, "__rg_prompt__") and isinstance(fn.__rg_prompt__, Prompt): + if fn.__name__ in ["run_many", "run_over"]: + raise ValueError( + "Only the singular Prompt.run (Prompt.bind) is supported when using prompt objects inside API tools", + ) + fn_for_signature = fn.__rg_prompt__.func # type: ignore [assignment] + + signature = inspect.signature(fn_for_signature, eval_str=True) + + # Passing a function to a TypeAdapter results in an internal + # call being made to the function after during validation. + # We want to manage the call ourselves, so we pass in a dummy function + # that does nothing, with a mirrored signature of the original + # to ensure arguments are still validated properly. + # + # We pull a small trick here by returning the kwargs from the + # dummy function, which will reflect all of the validation + # logic applied from the TypeAdapter. We can use these + # parsed arguments to call the original function. + + @functools.wraps(fn_for_signature) + def empty_func(*args, **kwargs): # type: ignore [no-untyped-def] # noqa: ARG001 + return kwargs + + # We'll also reconstruct __annotations__ from the signature + # manually in case we are working with an object that + # has manually set __signature__ and the __annotations__ + # might not be accurate. This is a bit of a hack, but + # we can't control how pydantic resolves annotations + + annotations: dict[str, t.Any] = {} + for param_name, param in signature.parameters.items(): + if param.annotation is not inspect.Parameter.empty: + annotations[param_name] = param.annotation + + if signature.return_annotation is not inspect.Parameter.empty: + annotations["return"] = signature.return_annotation + + empty_func.__annotations__ = annotations + + type_adapter: TypeAdapter[t.Any] = TypeAdapter(empty_func) + + schema = type_adapter.json_schema() + + # Maintain the behavior of Annotated[, ""] by walking + # adjusting the schema manually using signature annotations. + + for prop_name in schema.get("properties", {}): + if prop_name is None or prop_name not in signature.parameters: + continue + + param = signature.parameters[prop_name] + if t.get_origin(param.annotation) != t.Annotated: + continue + + annotation_args = t.get_args(param.annotation) + if len(annotation_args) != 2 or not isinstance(annotation_args[1], str): # noqa: PLR2004 + continue + + schema["properties"][prop_name]["description"] = annotation_args[1] + + # Deref and flatten the schema for consistency + + schema = deref_json(schema, is_json_schema=True) + + self = cls( + name=name or fn_for_signature.__name__, + description=description or fn_for_signature.__doc__ or "", + parameters_schema=schema, + fn=fn, + ) + + self._signature = signature + + # For handling API calls, we'll use the type adapter to validate + # the arguments before calling the function + + self._type_adapter = type_adapter + + return self + + @cached_property + def api_definition(self) -> ApiToolDefinition: + return ApiToolDefinition( + function=ApiFunctionDefinition( + name=self.name, + description=self.description, + parameters=self.parameters_schema, + ), + ) + + @property + def model(self) -> type[Model]: + # Do this lazily in case our our xml-based model doesn't + # support some of the argument types. We don't want to + # break api-based tools because of this. + + if self._model is None: + try: + self._model = ( + make_from_signature(self._signature, "params") + if self._signature + else make_from_schema(self.parameters_schema, "params") + ) + except Exception as e: # noqa: BLE001 + raise ToolDefinitionError( + f"Failed to create model for tool '{self.name}'. " + "This is likely due to constraints on arguments when the `xml` tool mode is used.", + ) from e + return self._model + + @cached_property + def xml_definition(self) -> XmlToolDefinition: + return XmlToolDefinition.from_parameter_model(self.model, self.name, self.description) + + @cached_property + def json_definition(self) -> JsonInXmlToolDefinition: + return JsonInXmlToolDefinition( + name=self.name, + description=self.description, + parameters=json.dumps(self.parameters_schema), + ) + + async def handle_tool_call( + self, + tool_call: ApiToolCall | XmlToolCall | JsonInXmlToolCall, + ) -> "Message": + """ + Handle an incoming tool call from a generator. + + Args: + tool_call: The tool call to handle. + + Returns: + The message to send back to the generator. + """ + + from rigging.message import ContentText, ContentTypes, Message + + tool_call_name = ( + tool_call.function.name if isinstance(tool_call, ApiToolCall) else tool_call.name + ) + tool_call_parameters = ( + tool_call.function.arguments + if isinstance(tool_call, ApiToolCall) + else tool_call.parameters + ) + + with tracer.span(f"Tool {self.name}()", name=self.name) as span: + if tool_call_name != self.name: + raise ValueError( + f"Requested function name '{tool_call_name}' does not match '{self.name}'", + ) + + if isinstance(tool_call, ApiToolCall): + span.set_attribute("tool_call_id", tool_call.id) + + # Load + validate arguments + + args: dict[str, t.Any] + if isinstance(tool_call, ApiToolCall | JsonInXmlToolCall): + args = json.loads(tool_call_parameters) + + if self._type_adapter is not None: + args = self._type_adapter.validate_python(args) + + elif isinstance(tool_call, XmlToolCall): + parsed = self.model.from_text( + self.model.xml_start_tag() + tool_call_parameters + self.model.xml_end_tag(), + ) + if not parsed: + raise ValueError("Failed to parse parameters") + + parameters = parsed[0][0] + + # As opposed to a model_dump, we want to retain the + # argument object instances. We'll just flatten the + # model into a dictionary for the function call. + + args = { + field_name: getattr(parameters, field_name, None) + for field_name in self.model.model_fields + } + + span.set_attribute("arguments", args) + + # Call the function + + result = self.fn(**args) + if inspect.isawaitable(result): + result = await result + + span.set_attribute("result", result) + + message = ( + Message(role="tool", tool_call_id=tool_call.id) + if isinstance(tool_call, ApiToolCall) + else Message("user") + ) + + # If the tool gave us back anything that looks like a message, we'll + # just pass it along. Otherwise we need to box up the result. + + if isinstance(result, Message): + message.content_parts = result.content_parts + elif isinstance(result, ContentTypes): + message.content_parts = [result] + elif isinstance(result, list) and all(isinstance(item, ContentTypes) for item in result): + message.content_parts = result + else: + message.content_parts = [ContentText(text=str(result))] + + # If this is a native tool call, we should wrap up our + # result in a NativeToolResult object to provide clarity to the + # generator. Otherwise we can rely on the `tool` role and associated + # tool_call_id to provide context. + # + # TODO: It would be great to have some kind of identifier here to let + # the model know what result is associated with what tool call when + # we aren't working with api calls. + + if ( + len(message.content_parts) == 1 + and isinstance(message.content_parts[0], ContentText) + and isinstance(tool_call, XmlToolCall | JsonInXmlToolCall) + ): + message.content_parts[0].text = NativeToolResult( + name=self.name, + result=message.content_parts[0].text, + ).to_pretty_xml() + + return message + + +# Decorator + + +@t.overload +def tool( + func: None = None, + /, + *, + name: str | None = None, + description: str | None = None, +) -> t.Callable[[t.Callable[..., t.Any]], Tool]: + ... + + +@t.overload +def tool( + func: t.Callable[..., t.Any], + /, + *, + name: str | None = None, + description: str | None = None, +) -> Tool: + ... + + +def tool( + func: t.Callable[..., t.Any] | None = None, + /, + *, + name: str | None = None, + description: str | None = None, +) -> t.Callable[[t.Callable[..., t.Any]], Tool] | Tool: + """ + Decorator for creating a Tool, useful for overriding a name or description. + + Args: + func: The function to wrap. + name: The name of the tool. + description: The description of the tool. + + Returns: + The decorated Tool object. + """ + + def make_tool(func: t.Callable[..., t.Any]) -> Tool: + return Tool.from_callable(func, name=name, description=description) + + if func is not None: + return make_tool(func) + + return make_tool diff --git a/rigging/tool/mcp.py b/rigging/tool/mcp.py new file mode 100644 index 0000000..532c9b5 --- /dev/null +++ b/rigging/tool/mcp.py @@ -0,0 +1,225 @@ +""" +Utilities for communicating with MCP servers. +""" + +import asyncio +import typing as t +from contextlib import AsyncExitStack +from dataclasses import dataclass +from pathlib import Path +from types import TracebackType + +import typing_extensions as te +from mcp import ClientSession, StdioServerParameters +from mcp.client.sse import sse_client +from mcp.client.stdio import stdio_client +from mcp.types import CallToolResult, TextResourceContents + +from rigging.tool.base import Tool + +if t.TYPE_CHECKING: + from rigging.message import Content + +INITIALIZE_TIMEOUT = 5 +DEFAULT_HTTP_TIMEOUT = 5 +DEFAULT_SSE_READ_TIMEOUT = 60 * 5 + +Transport = t.Literal["stdio", "sse"] + + +class StdioConnection(te.TypedDict): + command: str + args: list[str] + cwd: str | Path | None + env: dict[str, str] | None + + +class SSEConnection(te.TypedDict): + url: str + headers: dict[str, t.Any] | None + timeout: float + sse_read_timeout: float + + +def _convert_mcp_result_to_message_parts(result: CallToolResult) -> list[t.Any]: + from rigging.message import ContentImageUrl, ContentText + + parts: list[Content] = [] + for content in result.content: + if content.type == "text": + parts.append(ContentText(text=content.text)) + elif content.type == "image": + parts.append(ContentImageUrl.from_url(f"data:{content.mimeType};base64,{content.data}")) + elif content.type == "resource": + resource = content.resource + resource_text = ( + resource.text if isinstance(resource, TextResourceContents) else resource.blob + ) + parts.append(ContentText(text=resource_text)) + else: + raise ValueError(f"Unknown content type: {content.type}") + return parts + + +@dataclass +class MCPClient: + """A client for communicating with MCP servers.""" + + transport: Transport + """The transport to use""" + connection: StdioConnection | SSEConnection + """Connection configuration""" + tools: list[Tool] + """A list of tools available on the server""" + + def __init__(self, transport: Transport, connection: StdioConnection | SSEConnection) -> None: + self.transport = transport + self.connection = connection + self.tools = [] + self._exit_stack = AsyncExitStack() + self._session: ClientSession | None = None + + @property + def session(self) -> ClientSession: + if self._session is None: + raise RuntimeError("Session not initialized") + return self._session + + def _make_execute_on_server(self, tool_name: str) -> t.Callable[..., t.Any]: + async def execute_on_server(**kwargs: t.Any) -> t.Any: + result = await self.session.call_tool(tool_name, kwargs) + return _convert_mcp_result_to_message_parts(result) + + return execute_on_server + + async def _load_tools(self) -> None: + mcp_tool_result = await self.session.list_tools() + + self.tools.clear() + + self.tools = [ + Tool( + name=tool.name, + description=tool.description or "", + parameters_schema=tool.inputSchema, + fn=self._make_execute_on_server(tool.name), + ) + for tool in mcp_tool_result.tools + ] + + async def _connect_via_stdio(self, connection: StdioConnection) -> ClientSession: + server_params = StdioServerParameters(**connection) + read, write = await self._exit_stack.enter_async_context(stdio_client(server_params)) + return await self._exit_stack.enter_async_context(ClientSession(read, write)) + + async def _connect_via_sse(self, connection: SSEConnection) -> ClientSession: + client = sse_client(**connection) + read, write = await self._exit_stack.enter_async_context(client) + return await self._exit_stack.enter_async_context(ClientSession(read, write)) + + async def _shutdown(self) -> None: + await self._exit_stack.aclose() + self._session = None + self.tools.clear() + + async def __aenter__(self) -> "MCPClient": + try: + if self.transport == "stdio": + self._session = await self._connect_via_stdio( + t.cast(StdioConnection, self.connection), + ) + elif self.transport == "sse": + self._session = await self._connect_via_sse(t.cast(SSEConnection, self.connection)) + else: + raise TypeError( # noqa: TRY301 + f"Unsupported transport: {self.transport}. Must be 'stdio' or 'sse'", + ) + + await asyncio.wait_for(self.session.initialize(), timeout=INITIALIZE_TIMEOUT) + await asyncio.wait_for(self._load_tools(), timeout=INITIALIZE_TIMEOUT) + except Exception: + await self._shutdown() + raise + + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self._shutdown() + + +@t.overload +def mcp( + transport: t.Literal["stdio"], + *, + command: str, + args: list[str] | None = None, + cwd: str | Path | None = None, + env: dict[str, str] | None = None, +) -> MCPClient: + """ + Create an MCP client that communicates over stdio. + + Args: + command: The executable to run to start the server. + args: Command line arguments to pass to the executable. + env: The environment to use when spawning the process. + + Returns: + An MCP client context manager. + + Example: + ```python + with mcp("stdio", command="uv", args=["run", "weather-mcp"]) as mcp: + chat = ( + await get_generator("gpt-4o") + .chat("Please tell me the weather") + .using(*mcp.tools) + .run() + ) + ``` + """ + ... + + +@t.overload +def mcp( + transport: t.Literal["sse"], + *, + url: str, + headers: dict[str, str] | None = None, + timeout: float = DEFAULT_HTTP_TIMEOUT, + sse_read_timeout: float = DEFAULT_SSE_READ_TIMEOUT, +) -> MCPClient: + """ + Create an MCP client that communicates over SSE. + + Args: + url: The URL of the SSE endpoint to connect to. + headers: HTTP headers to send to the SSE endpoint. + timeout: HTTP timeout. + sse_read_timeout: SSE read timeout. + + Returns: + An MCP client context manager. + + Example: + ```python + with mcp("sse", url="http://localhost:8000/weather") as mcp: + chat = ( + await get_generator("gpt-4o") + .chat("Please tell me the weather") + .using(*mcp.tools) + .run() + ) + ``` + """ + ... + + +def mcp(transport: Transport, **connection: t.Any) -> MCPClient: + return MCPClient(transport, t.cast(StdioConnection | SSEConnection, connection)) diff --git a/rigging/tool/native.py b/rigging/tool/native.py index f3f48c9..91e93eb 100644 --- a/rigging/tool/native.py +++ b/rigging/tool/native.py @@ -1,316 +1,192 @@ """ -This module handles tools using internal flows, callbacks, and parsing with rigging models. +Models and utilities for defining and working with native-parsed tools. """ -from __future__ import annotations - import inspect import typing as t -from pydantic import Field, computed_field, model_validator -from pydantic_xml import attr, element, wrapped +from pydantic.fields import FieldInfo +from pydantic_xml import attr, element from rigging.model import Model -SUPPORTED_TOOL_ARGUMENT_TYPES = t.Union[int, float, str, bool] -"""Supported types for tool arguments.""" +# xml -SUPPORTED_TOOL_ARGUMENT_TYPES_LIST = [int, float, str, bool] -"""Supported types for tool arguments as a list.""" -ToolArgumentTypesCast = { - "int": int, - "float": float, - "bool": bool, - "str": str, -} +def _get_field_type_name(annotation: type[t.Any]) -> str: + """Extract a clean type name from a type annotation.""" + origin = t.get_origin(annotation) + args = t.get_args(annotation) -# -# 1 - Inbound models from LLM -> functions -# + if origin is list or origin is t.List: # noqa: UP006 + if args: + item_type = _get_field_type_name(args[0]) + return f"list[{item_type}]" + return "list" + if origin is dict or origin is t.Dict: # noqa: UP006 + if len(args) == 2: # noqa: PLR2004 + key_type = _get_field_type_name(args[0]) + value_type = _get_field_type_name(args[1]) + return f"dict[{key_type}, {value_type}]" + return "dict" -class ToolCallParameter(Model): - name: str = attr() - attr_value: SUPPORTED_TOOL_ARGUMENT_TYPES | None = attr("value", default=None, exclude=True) - text_value: SUPPORTED_TOOL_ARGUMENT_TYPES | None = Field(default=None, exclude=True) + if origin is not None: + origin_name = origin.__name__.lower() + args_str = ", ".join(_get_field_type_name(arg) for arg in args) + return f"{origin_name}[{args_str}]" - @computed_field # type: ignore [prop-decorator] - @property - def value(self) -> SUPPORTED_TOOL_ARGUMENT_TYPES: - return self.attr_value or self.text_value or "" + if inspect.isclass(annotation): + return annotation.__name__.lower() - @model_validator(mode="after") - def validate_value(self) -> ToolCallParameter: - if self.value is None: - raise ValueError("Missing parameter value") - return self + return str(annotation).replace("class '", "").replace("'", "").split(".")[-1] -class ToolCall(Model, tag="call"): - tool: str = attr() - function: str = attr() - parameters: list[ToolCallParameter] = element(tag="parameter") +def _make_parameter_xml(field_name: str, field: FieldInfo) -> str: + """Create an XML representation of a parameter.""" + if field.annotation is None: + raise ValueError(f"Field '{field_name}' has no type annotation") -# -# 2 - Inbound function calls from a model -# + type_name = _get_field_type_name(field.annotation) + description = field.description or "" + required = field.is_required() + nested_example = "" + if field.annotation is list or field.annotation is t.List: # noqa: UP006 + list_item_type = t.get_args(field.annotation)[0] + if hasattr(list_item_type, "xml_example"): + nested_example = list_item_type.xml_example() -class ToolCalls(Model, tag="tool_calls"): - calls: list[ToolCall] = element() + if field.annotation is dict or field.annotation is t.Dict: # noqa: UP006 + key_type, value_type = t.get_args(field.annotation) + if hasattr(key_type, "xml_example"): + nested_example = key_type.xml_example() - # This can be used in prompts to teach the model - # the particular XML structure we're looking for - # - # TODO: We should consider building a base model - # interface for both simple tags () - # and full examples will filled in template vars + if hasattr(field.annotation, "xml_example"): + nested_example = field.annotation.xml_example() - @classmethod - def xml_example(cls) -> str: - return cls( - calls=[ - ToolCall( - tool="$TOOL_A", - function="$FUNCTION_A", - parameters=[ToolCallParameter(name="$PARAMETER_NAME", text_value="$PARAMETER_VALUE")], - ), - ToolCall( - tool="$TOOL_B", - function="$FUNCTION_B", - parameters=[ToolCallParameter(name="$PARAMETER_NAME", text_value="$PARAMETER_VALUE")], - ), - ] - ).to_pretty_xml() - - -# -# 3 - Outbound models from functions -> LLM -# - - -# Description of a single tool parameter -class ToolParameter(Model, tag="parameter"): - name: str = attr() - type: str = attr() - description: str = attr() + description_part = f' description="{description}"' if description else "" + required_part = f' required="{str(required).lower()}"' + if nested_example: + return f'{nested_example}' -# Description of a single tool function -class ToolFunction(Model, tag="function"): - name: str = attr() - description: str = attr() - parameters: list[ToolParameter] = wrapped("parameters", element()) + return f'' -# Description of an entire tool -class ToolDescription(Model, tag="tool"): +class XmlToolDefinition(Model, tag="tool-def"): name: str = attr() description: str = attr() - functions: list[ToolFunction] = wrapped("functions", element()) - - -# A list of tools to present to the model -class ToolDescriptionList(Model, tag="tools"): - tools: list[ToolDescription] = element() - - -# A single result from a tool call -class ToolResult(Model, tag="result"): - tool: str = attr() - function: str = attr() - error: bool = attr() - content: str - - -# How we pass back results from a set of calls -class ToolResults(Model, tag="tool_results"): - results: list[ToolResult] = element() - - -# -# 4 - Base class for implementing tools -# - + parameters: str # don't use element() here, we want to keep the raw xml -class Tool: - """ - Base class for implementing internally-managed tools in the Rigging system. - - You should subclass this to define your own tools: - - ```py - def Hammer(Tool): - name = "Hammer" - description = "A tool for hitting things." + @classmethod + def from_parameter_model( + cls, + model_class: type[Model], + name: str, + description: str, + ) -> "XmlToolDefinition": + params_xml = "" + for field_name, field in model_class.model_fields.items(): + params_xml += _make_parameter_xml(field_name, field) + params_xml += "" - def hit(self, target: Annotated[str, "Target of the hit") -> str: - return f"Hit {target} with a hammer." + return cls(name=name, description=description, parameters=params_xml) - chat = await generator.chat(...).using(Hammer()).run() - ``` - Note: - The `name` and `description` attributes are required and can be defined - as class attributes or properties. If you define them as properties, - you must also define a getter for them. +class XmlToolCall(Model, tag="tool"): + name: str = attr() + parameters: str - Note: - All functions on the tool must have type hints for their parameters and - use the `Annotated` type hint to provide a description for each parameter. - """ - name: str - """Name of the tool""" - description: str - """Description of the tool""" +# json-in-xml - def __init_subclass__(cls, *, name: str | None = None, description: str | None = None, **kwargs: t.Any) -> None: - super().__init_subclass__(**kwargs) - if name is not None: - cls.name = name - if description is not None: - cls.description = description - # Ensure name and description are defined - if not (hasattr(cls, "name") or hasattr(cls, "name_property")): - raise TypeError(f"{cls.__name__} must define 'name' attribute or 'name' property.") - if not (hasattr(cls, "description") or hasattr(cls, "description_property")): - raise TypeError(f"{cls.__name__} must define 'description' attribute or 'description' property.") +class JsonInXmlToolDefinition(Model, tag="tool-def"): + name: str = attr() + description: str = attr() + parameters: str = element() - # Check that they aren't empty or unset - if not getattr(cls, "name", None): - raise ValueError(f"{cls.__name__}.name must not be empty.") - if not getattr(cls, "description", None): - raise ValueError(f"{cls.__name__}.description must not be empty.") - # TODO: We could alternatively use the get_description() - # object and check against that (or even cast into it first) - # - # NOTE: We assume some sanity checks have already been performed - def _execute(self, call: ToolCall) -> str: - tool_description = self.get_description() +class JsonInXmlToolCall(Model, tag="tool"): + name: str = attr() + parameters: str - if call.function not in [f.name for f in tool_description.functions]: - raise ValueError(f"Function '{call.function}' does not exist on '{self.name}'") - function_description = next(f for f in tool_description.functions if f.name == call.function) +# results - # TODO: The casting here is terrible, we should probably - # be exposing the raw types on the description, but I - # need to make sure they aren't serialized into the model - arguments: dict[str, SUPPORTED_TOOL_ARGUMENT_TYPES] = {} - for parameter in call.parameters: - if parameter.name not in [p.name for p in function_description.parameters]: - raise ValueError(f"Parameter '{parameter.name}' does not exist on '{self.name}.{call.function}'") - parameter_description = next(p for p in function_description.parameters if p.name == parameter.name) +class NativeToolResult(Model, tag="tool-result"): + name: str = attr() + result: str - arguments[parameter.name] = ToolArgumentTypesCast[parameter_description.type](parameter.value) - function = getattr(self, call.function) - result = function(**arguments) +# prompts - # Cast back to string for simplicity despite us likely - # having more complex types underneath (we want them castable to str) - return str(result) +XML_CALL_FORMAT = """\ +To use a tool, respond with the following format: - def execute(self, call: ToolCall) -> ToolResult: - """Executes a function call on the tool.""" - try: - content = self._execute(call) - return ToolResult(tool=call.tool, function=call.function, error=False, content=content) - except Exception as e: - return ToolResult(tool=call.tool, function=call.function, error=True, content=str(e)) + + argument one + 123 + - __call__ = execute +If a parameter is a primitive list, provide child elements as items: + + 1 + 2 + - # Lots of sanity checks and validation, but we essentially - # want to use the class def, functions, params, etc. and - # build a ToolDescription object that can be serialized - # and passed to a model - def get_description(self) -> ToolDescription: - """Creates a full description of the tool for use in prompting""" - functions: list[ToolFunction] = [] - for method_name, method in inspect.getmembers(self.__class__, predicate=inspect.isfunction): - if not method.__qualname__.startswith(self.__class__.__name__): - continue - - if method_name.startswith("_"): - continue - - signature = inspect.signature(method) - - if signature.return_annotation is inspect.Signature.empty: - raise TypeError(f"Functions must have return type hints ({method_name})") - - if signature.return_annotation is not str: - raise TypeError(f"Functions must return strings ({method_name})") - - parameters: list[ToolParameter] = [] - for param_name, param in signature.parameters.items(): - if param_name == "self": - continue - - formatted_name = f"{method.__name__}#{param_name}" - - if param.kind not in ( - inspect.Parameter.POSITIONAL_OR_KEYWORD, - inspect.Parameter.KEYWORD_ONLY, - ): - raise TypeError(f"Parameters must be positional or keyword ({formatted_name})") - - if param.annotation is inspect.Parameter.empty: - raise TypeError(f"Parameters must have type hints ({formatted_name})") - - if t.get_origin(param.annotation) != t.Annotated: - raise TypeError( - f'Parameters must be annotated like Annotated[, ""] ({formatted_name})' - ) - - annotation_args = t.get_args(param.annotation) - - if len(annotation_args) != 2 or not isinstance(annotation_args[1], str): - raise TypeError( - f'Parameters must be annotated like Annotated[, ""] ({formatted_name})' - ) +If a parameter is a list of objects, provide them as named child elements: + + + bar + + + baz + + - if annotation_args[0] not in SUPPORTED_TOOL_ARGUMENT_TYPES_LIST: - raise TypeError( - f"Parameters must be annotated with one of these types: {SUPPORTED_TOOL_ARGUMENT_TYPES_LIST} ({formatted_name})" - ) - type_name = annotation_args[0].__name__ - description = annotation_args[1] +If a parameter is a dictionary, provide key-value pairs as attributes: +\ +""" - parameters.append(ToolParameter(name=param_name, type=type_name, description=description)) +XML_IN_JSON_CALL_FORMAT = """\ +To use a tool, respond with the following format: - functions.append( - ToolFunction( - name=method_name, - description=method.__doc__ if method.__doc__ else "", - parameters=parameters, - ) - ) + + {"arg1": "argument one", "arg2": 123} + - return ToolDescription(name=self.name, description=self.description, functions=functions) +Arguments should be provided as a valid JSON object between the tags.\ +""" -def system_tool_extension(call_format: str, tool_descriptions: str) -> str: +def tool_description_prompt_part( + tool_descriptions: list[XmlToolDefinition] | list[JsonInXmlToolDefinition], + mode: t.Literal["xml", "json-in-xml"], +) -> str: + call_format = XML_CALL_FORMAT if mode == "xml" else XML_IN_JSON_CALL_FORMAT + tool_definitions = "\n".join([tool.to_pretty_xml() for tool in tool_descriptions]) return f"""\ # Tool Use -In this environment you have access to a set of tools you can use to improve your responses. - -## Tool Call Format -{call_format} +In this environment you have access to a set of tools you can use. ## Available Tools -{tool_descriptions} + +{tool_definitions} + -You can use any of the available tools by responding in the call format above. The XML will be parsed and the tool(s) will be executed with the parameters you provided. The results of each tool call will be provided back to you before you continue the conversation. You can execute multiple tool calls by continuing to respond in the format above until you are finished. Function calls take explicit values and are independent of each other. Tool calls cannot share, re-use, and transfer values between eachother. The use of placeholders is forbidden. +## Tool Call Format +{call_format} -The user will not see the results of your tool calls, only the final message of your conversation. Wait to perform your full response until after you have used any required tools. If you intend to use a tool, please do so before you continue the conversation. +## Tool Use Instructions +You can use any of the available tools by responding in the call format above. The XML will be parsed \ +and the tool(s) will be executed with the parameters you provided. The results of each tool call will \ +be provided back to you before you continue the conversation. You can execute multiple tool calls by \ +continuing to respond in the format above until you are finished. Function calls take explicit values \ +and are independent of each other. Tool calls cannot share, re-use, and transfer values between eachother. """ diff --git a/rigging/tool/robopages.py b/rigging/tool/robopages.py new file mode 100644 index 0000000..c36642c --- /dev/null +++ b/rigging/tool/robopages.py @@ -0,0 +1,104 @@ +""" +Utilities for integrating tools from a Robopages server. +""" + +import re +import typing as t + +import httpx +import requests +from loguru import logger +from pydantic import TypeAdapter + +from rigging.tool.api import ApiToolDefinition +from rigging.tool.base import Tool + +DEFAULT_HTTP_TIMEOUT = 10 + + +def make_execute_on_server(url: str, tool_name: str) -> t.Callable[..., t.Any]: + async def execute_on_server(**kwargs: t.Any) -> t.Any: + async with httpx.AsyncClient() as client: + response = await client.post( + f"{url}/process", + json=[ + { + "type": "function", + "function": { + "name": tool_name, + "arguments": kwargs, + }, + }, + ], + ) + if response.status_code not in [200, 400]: + response.raise_for_status() + + if response.status_code == 400: # noqa: PLR2004 + result = response.content.decode() + else: + result = response.json()[0]["content"] + + return result + + return execute_on_server + + +def robopages(url: str, *, name_filter: str | None = None) -> list[Tool]: + """ + Create a list of tools from a Robopages server. + + Args: + url: The URL of the Robopages server. + name_filter: A regular expression to filter the tools by name. + + Returns: + A list of integrated tools which leverage the Robopages server. + + Example: + + ```python + import rigging as rg + + tools = rg.tool.robopages("http://localhost:8080") + + chat = ( + await rg.get_generator('gpt-4o') + .chat('Please use tools') + .using(*tools) + .run() + ) + + print(chat.conversation) + ``` + """ + + filter_regex = re.compile(name_filter) if name_filter else None + + response = requests.get(url, params={"flavor": "openai"}, timeout=DEFAULT_HTTP_TIMEOUT) + response.raise_for_status() + tools_data = response.json() + + adapter = TypeAdapter(list[ApiToolDefinition]) + tool_definitions = adapter.validate_python(tools_data) + + logger.info(f"Fetched {len(tool_definitions)} functions from Robopages ({url})") + + tools: list[Tool] = [] + for definition in tool_definitions: + function = definition.function + + if filter_regex and not filter_regex.search(function.name): + logger.debug(f"Skipping function {function.name}") + continue + + tools.append( + Tool( + function.name, + function.description or "", + function.parameters or {}, + make_execute_on_server(url, function.name), + ), + ) + + return tools diff --git a/rigging/util.py b/rigging/util.py index 8011352..6c1aeb0 100644 --- a/rigging/util.py +++ b/rigging/util.py @@ -1,4 +1,6 @@ -from __future__ import annotations +""" +Common utilities used throughout the library. +""" import asyncio import functools @@ -29,7 +31,7 @@ def _run_loop(loop: asyncio.AbstractEventLoop) -> None: def _get_event_loop() -> asyncio.AbstractEventLoop: - global g_event_loop + global g_event_loop # noqa: PLW0603 if g_event_loop is None: g_event_loop = asyncio.new_event_loop() @@ -84,9 +86,7 @@ def deref_json(obj: dict[str, t.Any], *, is_json_schema: bool = False) -> dict[s def escape_xml(xml_string: str) -> str: """Escape XML special characters in a string.""" - prepared = re.sub(r"&(?!(?:amp|lt|gt|apos|quot);)", "&", xml_string) - - return prepared + return re.sub(r"&(?!(?:amp|lt|gt|apos|quot);)", "&", xml_string) def unescape_xml(xml_string: str) -> str: @@ -95,9 +95,7 @@ def unescape_xml(xml_string: str) -> str: unescaped = re.sub(r"<", "<", unescaped) unescaped = re.sub(r">", ">", unescaped) unescaped = re.sub(r"'", "'", unescaped) - unescaped = re.sub(r""", '"', unescaped) - - return unescaped + return re.sub(r""", '"', unescaped) def to_snake(text: str) -> str: @@ -143,8 +141,7 @@ def get_qualified_name(obj: t.Callable[..., t.Any]) -> str: if callable(obj): if isinstance(obj, type): return obj.__qualname__ - else: - return f"{obj.__class__.__qualname__}.__call__" + return f"{obj.__class__.__qualname__}.__call__" # Fallback return obj.__class__.__qualname__ diff --git a/rigging/watchers.py b/rigging/watchers.py index 2ee0094..52346ce 100644 --- a/rigging/watchers.py +++ b/rigging/watchers.py @@ -2,23 +2,20 @@ Common watcher callback makers for use with generators, chats, and completions. """ -from __future__ import annotations - import json -import os import typing as t from pathlib import Path +from elasticsearch import AsyncElasticsearch +from mypy_boto3_s3 import S3Client + from rigging.data import chats_to_elastic, flatten_chats, s3_object_exists if t.TYPE_CHECKING: - from elasticsearch import AsyncElasticsearch - from mypy_boto3_s3 import S3Client - from rigging.chat import Chat, WatchChatCallback -def write_chats_to_jsonl(file: str | Path, *, replace: bool = False) -> WatchChatCallback: +def write_chats_to_jsonl(file: str | Path, *, replace: bool = False) -> "WatchChatCallback": """ Create a watcher to write each chat as a single JSON line appended to a file. @@ -38,21 +35,21 @@ def write_chats_to_jsonl(file: str | Path, *, replace: bool = False) -> WatchCha file = Path(file) replaced: bool = False - async def _write_chats_to_jsonl(chats: list[Chat]) -> None: + async def _write_chats_to_jsonl(chats: "list[Chat]") -> None: nonlocal replaced if file.exists() and replace and not replaced: - os.remove(file) + Path.unlink(file) replaced = True with file.open("a") as f: for chat in chats: - f.write(chat.model_dump_json() + "\n") + f.write(chat.model_dump_json(exclude_none=True) + "\n") return _write_chats_to_jsonl -def write_messages_to_jsonl(file: str | Path, *, replace: bool = False) -> WatchChatCallback: +def write_messages_to_jsonl(file: str | Path, *, replace: bool = False) -> "WatchChatCallback": """ Create a watcher to flatten chats to individual messages (like Dataframes) and append to a file. @@ -67,11 +64,11 @@ def write_messages_to_jsonl(file: str | Path, *, replace: bool = False) -> Watch file = Path(file) replaced: bool = False - async def _write_messages_to_jsonl(chats: list[Chat]) -> None: + async def _write_messages_to_jsonl(chats: "list[Chat]") -> None: nonlocal replaced if file.exists() and replace and not replaced: - os.remove(file) + Path.unlink(file) replaced = True with file.open("a") as f: @@ -82,8 +79,12 @@ async def _write_messages_to_jsonl(chats: list[Chat]) -> None: def write_chats_to_elastic( - client: AsyncElasticsearch, index: str, *, create_index: bool = True, **kwargs: t.Any -) -> WatchChatCallback: + client: AsyncElasticsearch, + index: str, + *, + create_index: bool = True, + **kwargs: t.Any, +) -> "WatchChatCallback": """ Create a watcher to write each chat to an ElasticSearch index. @@ -98,13 +99,19 @@ def write_chats_to_elastic( or [rigging.generator.Generator.watch][]. """ - async def _write_chats_to_elastic(chats: list[Chat]) -> None: + async def _write_chats_to_elastic(chats: "list[Chat]") -> None: await chats_to_elastic(chats, index, client, create_index=create_index, **kwargs) return _write_chats_to_elastic -def write_chats_to_s3(client: S3Client, bucket: str, key: str, replace: bool = False) -> WatchChatCallback: +def write_chats_to_s3( + client: S3Client, + bucket: str, + key: str, + *, + replace: bool = False, +) -> "WatchChatCallback": """ Create a watcher to write each chat to an Amazon S3 bucket. @@ -121,7 +128,7 @@ def write_chats_to_s3(client: S3Client, bucket: str, key: str, replace: bool = F replaced: bool = False - async def _write_chats_to_s3(chats: list[Chat]) -> None: + async def _write_chats_to_s3(chats: "list[Chat]") -> None: nonlocal replaced content: str = "" diff --git a/tests/test_chat.py b/tests/test_chat.py index e01b98a..d2df4c4 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import typing as t import pytest @@ -114,7 +112,10 @@ def test_message_from_model() -> None: def test_messages_fit_list() -> None: - messages: t.Any = [{"role": "system", "content": "You are an AI assistant."}, Message("user", "Hello!")] + messages: t.Any = [ + {"role": "system", "content": "You are an AI assistant."}, + Message("user", "Hello!"), + ] fitted = Message.fit_as_list(messages) assert len(fitted) == 2 assert isinstance(fitted[0], Message) @@ -123,7 +124,8 @@ def test_messages_fit_list() -> None: def test_message_parse_multiple_models() -> None: msg = Message( - "user", "30
123 Main StAnytown
" + "user", + "30
123 Main StAnytown
", ) person = msg.parse(Person) address = msg.parse(Address) @@ -153,6 +155,14 @@ def test_message_reparse_modified_content() -> None: assert person.name == "Jane" assert person.age == 25 +def test_message_double_content_part_separation() -> None: + msg = Message("user", ["hello", "world"]) + assert msg.content == "hello\nworld" + + spec = msg.to_openai_spec() + assert len(spec["content"]) == 2 + assert spec["content"][0]["text"] == "hello\n" + assert spec["content"][1]["text"] == "world" def test_chat_generator_id() -> None: generator = get_generator("gpt-3.5") @@ -217,8 +227,11 @@ def test_chat_to_message_dicts() -> None: ) assert len(chat.message_dicts) == 2 - assert chat.message_dicts[0] == {"role": "user", "content": "Hello"} - assert chat.message_dicts[1] == {"role": "assistant", "content": "Hi there!"} + assert chat.message_dicts[0] == {"role": "user", "content": "Hello"}, chat.message_dicts[0] + assert chat.message_dicts[1] == { + "role": "assistant", + "content": "Hi there!", + }, chat.message_dicts[1] def test_chat_to_conversation() -> None: @@ -280,7 +293,9 @@ def test_chat_continue_maintains_parsed_models() -> None: chat = Chat( [ Message("user", "30"), - Message("assistant", "
123 Main StAnytown
"), + Message( + "assistant", "
123 Main StAnytown
" + ), ], generator=get_generator("base"), ) @@ -320,8 +335,10 @@ def test_chat_strip() -> None: chat = Chat( [ Message("user", "30"), - Message("assistant", "
123 Main StAnytown
"), - ] + Message( + "assistant", "
123 Main StAnytown
" + ), + ], ) assert len(chat) == 2 @@ -339,8 +356,10 @@ def test_chat_serialize() -> None: chat = Chat( [ Message("user", "30"), - Message("assistant", "
123 Main StAnytown
"), - ] + Message( + "assistant", "
123 Main StAnytown
" + ), + ], ) assert len(chat) == 2 @@ -420,7 +439,9 @@ def test_chat_pipeline_add_merge_strategy_none() -> None: assert pipeline.chat.all[1].content == "There" # Test that assistant messages also don't merge - pipeline.add([Message("assistant", "Hi!"), Message("assistant", "How are you?")], merge_strategy="none") + pipeline.add( + [Message("assistant", "Hi!"), Message("assistant", "How are you?")], merge_strategy="none" + ) assert len(pipeline.chat) == 4 assert pipeline.chat.all[2].content == "Hi!" assert pipeline.chat.all[3].content == "How are you?" @@ -448,7 +469,8 @@ def test_chat_pipeline_add_merge_strategy_multiple_messages() -> None: # Test adding multiple messages with user first (should merge first message only) pipeline.add( - [Message("user", "There"), Message("assistant", "Hi!"), Message("user", "Another")], merge_strategy="all" + [Message("user", "There"), Message("assistant", "Hi!"), Message("user", "Another")], + merge_strategy="all", ) assert len(pipeline.chat) == 3 diff --git a/tests/test_completion.py b/tests/test_completion.py index 13466f0..d6177d0 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import pytest from rigging.completion import Completion, CompletionPipeline diff --git a/tests/test_tool.py b/tests/test_tool.py new file mode 100644 index 0000000..b796bc6 --- /dev/null +++ b/tests/test_tool.py @@ -0,0 +1,340 @@ +import inspect +import json +import typing as t +from dataclasses import dataclass +from textwrap import dedent + +import pytest +from pydantic import BaseModel + +import rigging as rg +from rigging.error import ToolDefinitionError +from rigging.model import Model, make_from_schema, make_from_signature +from rigging.tool.api import ApiFunctionDefinition, ApiToolDefinition +from rigging.tool.base import Tool +from rigging.tool.native import JsonInXmlToolCall, XmlToolCall, XmlToolDefinition + + +def test_tool_from_simple_callable() -> None: + """Test creating a tool from a simple callable.""" + + def simple_function(name: str, age: int) -> str: + """A simple function that returns a greeting.""" + return f"Hello {name}, you are {age} years old!" + + tool = Tool.from_callable(simple_function) + + assert tool.name == "simple_function" + assert tool.description == "A simple function that returns a greeting." + assert "_signature" in tool.__dict__ + assert "_type_adapter" in tool.__dict__ + + # Check schema + assert "name" in tool.parameters_schema["properties"] + assert tool.parameters_schema["properties"]["name"]["type"] == "string" + assert "age" in tool.parameters_schema["properties"] + assert tool.parameters_schema["properties"]["age"]["type"] == "integer" + + +def test_tool_decorator() -> None: + """Test the @tool decorator functionality.""" + + @rg.tool + def sample_tool(query: str, limit: int = 10) -> list[str]: + """Search for items matching the query.""" + return [f"{query}-{i}" for i in range(limit)] + + assert isinstance(sample_tool, Tool) + assert sample_tool.name == "sample_tool" + assert "Search for items matching the query." in sample_tool.description + + # Test with custom name and description + @rg.tool(name="custom_name", description="Custom description") + def another_tool(x: int) -> int: + return x * 2 + + assert isinstance(another_tool, Tool) + assert another_tool.name == "custom_name" + assert another_tool.description == "Custom description" + + +def test_api_definition_generation() -> None: + """Test that tools correctly generate API definitions.""" + + def complex_function(name: str, age: int, tags: list[str] = ["default"], active: bool = True) -> dict[str, t.Any]: # noqa: B006 + """Process user data with complex parameters.""" + return {"name": name, "age": age, "tags": tags, "active": active} + + tool = Tool.from_callable(complex_function) + api_def = tool.api_definition + + assert isinstance(api_def, ApiToolDefinition) + assert api_def.type == "function" + assert isinstance(api_def.function, ApiFunctionDefinition) + assert api_def.function.name == "complex_function" + assert api_def.function.description is not None + assert "Process user data with complex parameters." in api_def.function.description + + # Check parameters schema + params = api_def.function.parameters + assert isinstance(params, dict) + assert params["type"] == "object" + assert "name" in params["properties"] + assert "age" in params["properties"] + assert "tags" in params["properties"] + assert "active" in params["properties"] + + # Check required parameters + assert "name" in params["required"] + assert "age" in params["required"] + assert "tags" not in params["required"] + assert "active" not in params["required"] + + +def test_xml_definition_generation() -> None: + """Test that tools correctly generate XML definitions.""" + + def profile_function(user_id: str, include_details: bool = False) -> dict[str, t.Any]: + """Get user profile information.""" + return {"id": user_id, "details": include_details} + + tool = Tool.from_callable(profile_function) + xml_def = tool.xml_definition + + assert isinstance(xml_def, XmlToolDefinition) + assert xml_def.name == "profile_function" + assert "Get user profile information." in xml_def.description + + # XML parameters should contain both params + assert ' None: + """Test that tools correctly generate JSON-in-XML definitions.""" + + def data_function(query: str, max_results: int = 10) -> list[str]: + """Query data with pagination.""" + return [f"result-{i}" for i in range(max_results)] + + tool = Tool.from_callable(data_function) + json_def = tool.json_definition + + assert json_def.name == "data_function" + assert "Query data with pagination." in json_def.description + + # Parameters should be JSON schema + params = json.loads(json_def.parameters) + assert params["type"] == "object" + assert "query" in params["properties"] + assert "max_results" in params["properties"] + assert "query" in params["required"] + + +def test_annotated_parameter_descriptions() -> None: + """Test that Annotated types with descriptions are properly handled.""" + + def annotated_function( + simple: str, + described: t.Annotated[int, "Number of items to process"], + optional: t.Annotated[bool, "Enable feature flag"] = False, + ) -> str: + """Function with annotated parameters.""" + return f"{simple} {described} {optional}" + + tool = Tool.from_callable(annotated_function) + + # Check schema descriptions + schema = tool.parameters_schema + assert schema["properties"]["simple"].get("description") is None + assert schema["properties"]["described"]["description"] == "Number of items to process" + assert schema["properties"]["optional"]["description"] == "Enable feature flag" + + # Check API definition + api_def = tool.api_definition + api_params = api_def.function.parameters + assert api_params["properties"]["described"]["description"] == "Number of items to process" # type: ignore [index] + + # Check XML definition + xml_def = tool.xml_definition + assert "Number of items to process" in xml_def.parameters + assert "Enable feature flag" in xml_def.parameters + + +def test_tool_model_creation() -> None: + """Test that the tool correctly creates a Model for XML parsing.""" + + def config_function(name: str, version: int, features: list[str] = []) -> dict[str, t.Any]: # noqa: B006 + """Configure application settings.""" + return {"name": name, "version": version, "features": features} + + tool = Tool.from_callable(config_function) + + # Access model property to create the model + model = tool.model + + # Verify model is properly created + assert issubclass(model, Model) + assert hasattr(model, "model_fields") + assert "name" in model.model_fields + assert "version" in model.model_fields + assert "features" in model.model_fields + + +class TestToolHandleCall: + """Test suite for tool call handling.""" + + @pytest.fixture + def sample_tool(self) -> Tool: + def calculator(a: int, b: int, operation: str = "add") -> int: + """Perform math operations.""" + if operation == "add": + return a + b + elif operation == "multiply": + return a * b + elif operation == "subtract": + return a - b + else: + raise ValueError(f"Unknown operation: {operation}") + + return Tool.from_callable(calculator) + + @pytest.mark.asyncio + async def test_handle_api_tool_call(self, sample_tool: Tool) -> None: + """Test handling API format tool calls.""" + from rigging.tool.api import ApiFunctionCall, ApiToolCall + + tool_call = ApiToolCall( + id="call123", + function=ApiFunctionCall( + name="calculator", arguments=json.dumps({"a": 5, "b": 3, "operation": "multiply"}) + ), + ) + + response = await sample_tool.handle_tool_call(tool_call) + + assert response.role == "tool" + assert response.tool_call_id == "call123" + assert response.content == "15" + + @pytest.mark.asyncio + async def test_handle_xml_tool_call(self, sample_tool: Tool) -> None: + """Test handling XML format tool calls.""" + tool_call = XmlToolCall( + name="calculator", + parameters=dedent( + """ + 10 + 2 + subtract + """ + ).strip(), + ) + + response = await sample_tool.handle_tool_call(tool_call) + + assert response.role == "user" + assert response.content == '8' + + @pytest.mark.asyncio + async def test_handle_json_xml_tool_call(self, sample_tool: Tool) -> None: + """Test handling JSON-in-XML format tool calls.""" + tool_call = JsonInXmlToolCall(name="calculator", parameters=json.dumps({"a": 4, "b": 4, "operation": "add"})) + + response = await sample_tool.handle_tool_call(tool_call) + + assert response.role == "user" + assert response.content == '8' + + +def test_make_from_signature() -> None: + """Test the make_from_signature function directly.""" + + def test_func(name: str, age: int, tags: list[str] = []) -> None: # noqa: B006 + """Test function for signature extraction.""" + pass + + signature = inspect.signature(test_func) + model_class = make_from_signature(signature, "TestParams") + + assert issubclass(model_class, Model) + assert "name" in model_class.model_fields + assert "age" in model_class.model_fields + assert "tags" in model_class.model_fields + + +def test_make_from_schema() -> None: + """Test the make_from_schema function directly.""" + schema = { + "type": "object", + "properties": { + "query": {"type": "string"}, + "limit": {"type": "integer", "description": "Max results to return"}, + "filters": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["query"], + } + + model_class = make_from_schema(schema, "SearchParams") + + assert issubclass(model_class, Model) + assert "query" in model_class.model_fields + assert "limit" in model_class.model_fields + assert "filters" in model_class.model_fields + + # Check that description was preserved + assert model_class.model_fields["limit"].description == "Max results to return" + + +def test_prompt_integration() -> None: + """Test integration between Tool and Prompt objects.""" + + @rg.prompt + async def generate_greeting(name: str, formal: bool = False) -> str: # type: ignore [empty-body] + """Generate a greeting for the user.""" + ... + + tool = Tool.from_callable(generate_greeting) + + assert tool.name == "generate_greeting" + assert "Generate a greeting for the user." in tool.description + + # Verify parameters + schema = tool.parameters_schema + assert "name" in schema["properties"] + assert "formal" in schema["properties"] + assert schema["properties"]["formal"]["default"] is False + + +def test_complex_model_parameters() -> None: + """Test tools with complex model parameters.""" + + class UserProfile(BaseModel): + name: str + age: int + + @dataclass + class UserSettings: + notifications: bool + theme: str + + def process_user(profile: UserProfile, update: bool = False) -> dict[str, t.Any]: + """Process a user profile.""" + return {"profile": profile, "updated": update} + + def process_settings(settings: UserSettings) -> dict[str, t.Any]: + """Process user settings.""" + return {"settings": settings} + + # This should raise an error since pydantic models should be BaseXmlModel + with pytest.raises(ToolDefinitionError): + Tool.from_callable(process_user).xml_definition # noqa: B018 + + # This should raise an error since dataclasses aren't supported + with pytest.raises(ToolDefinitionError): + Tool.from_callable(process_settings).xml_definition # noqa: B018 diff --git a/tests/test_watchers.py b/tests/test_watchers.py index 19c55ab..7c7e233 100644 --- a/tests/test_watchers.py +++ b/tests/test_watchers.py @@ -1,4 +1,6 @@ import json +import typing as t +from pathlib import Path import pytest @@ -8,7 +10,7 @@ @pytest.fixture -def sample_chats(): +def sample_chats() -> list[Chat]: chat1 = Chat(messages=[Message(role="user", content="Hello"), Message(role="assistant", content="Hi there!")]) chat2 = Chat( messages=[Message(role="user", content="How are you?"), Message(role="assistant", content="I'm doing well!")] @@ -17,7 +19,7 @@ def sample_chats(): @pytest.mark.asyncio -async def test_write_chats_to_jsonl(tmp_path, sample_chats): +async def test_write_chats_to_jsonl(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" watcher = rg.watchers.write_chats_to_jsonl(output_file) @@ -40,7 +42,7 @@ async def test_write_chats_to_jsonl(tmp_path, sample_chats): @pytest.mark.asyncio -async def test_write_chats_to_jsonl_append(tmp_path, sample_chats): +async def test_write_chats_to_jsonl_append(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" watcher = rg.watchers.write_chats_to_jsonl(output_file) @@ -56,7 +58,7 @@ async def test_write_chats_to_jsonl_append(tmp_path, sample_chats): @pytest.mark.asyncio -async def test_write_chats_to_jsonl_replace(tmp_path, sample_chats): +async def test_write_chats_to_jsonl_replace(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" # write initial content @@ -104,39 +106,39 @@ class Body: def __init__(self, content: str): self.content = content - def read(self): + def read(self) -> bytes: return self.content.encode() - def __init__(self): - self.buckets = {"test-bucket": {}} + def __init__(self) -> None: + self.buckets: dict[str, t.Any] = {"test-bucket": {}} - def head_object(self, Bucket: str, Key: str): + def head_object(self, Bucket: str, Key: str) -> t.Any: if Bucket not in self.buckets: raise self.exceptions.ClientError("404") if Key not in self.buckets[Bucket]: raise self.exceptions.ClientError("404") return self.buckets[Bucket][Key] - def get_object(self, Bucket: str, Key: str): + def get_object(self, Bucket: str, Key: str) -> t.Any: if Bucket not in self.buckets: raise self.exceptions.ClientError("404") if Key not in self.buckets[Bucket]: raise self.exceptions.ClientError("404") return {"Body": MockS3Client.Body(self.buckets[Bucket][Key])} - def delete_object(self, Bucket: str, Key: str): + def delete_object(self, Bucket: str, Key: str) -> None: if Bucket not in self.buckets: raise self.exceptions.ClientError("404") if Key not in self.buckets[Bucket]: raise self.exceptions.ClientError("404") del self.buckets[Bucket][Key] - def put_object(self, Bucket: str, Key: str, Body: str): + def put_object(self, Bucket: str, Key: str, Body: str) -> None: self.buckets[Bucket][Key] = Body @pytest.mark.asyncio -async def test_write_chats_to_s3(sample_chats): +async def test_write_chats_to_s3(sample_chats: list[Chat]) -> None: s3_mock_client = MockS3Client() bucket = "test-bucket" @@ -146,7 +148,7 @@ async def test_write_chats_to_s3(sample_chats): for chat in sample_chats[:1]: expected_content += chat.model_dump_json() + "\n" - watcher = rg.watchers.write_chats_to_s3(s3_mock_client, bucket, key) + watcher = rg.watchers.write_chats_to_s3(s3_mock_client, bucket, key) # type: ignore [arg-type] # write first batch await watcher(sample_chats[:1]) @@ -165,7 +167,7 @@ async def test_write_chats_to_s3(sample_chats): assert got["Body"].read() == expected_content.encode() # create a new watcher with replace=True - watcher = rg.watchers.write_chats_to_s3(s3_mock_client, bucket, key, replace=True) + watcher = rg.watchers.write_chats_to_s3(s3_mock_client, bucket, key, replace=True) # type: ignore [arg-type] # write a single chat await watcher(sample_chats[:1]) diff --git a/tests/test_xml_parsing.py b/tests/test_xml_parsing.py index d62ba0e..cc95204 100644 --- a/tests/test_xml_parsing.py +++ b/tests/test_xml_parsing.py @@ -87,12 +87,12 @@ class Wrapped(Model): pytest.param( " Should I answer between tags? hello", [Question(content=" Should I answer between tags? "), Answer(content="hello")], - id="question_with_answer_tag", + id="question_with_answer_tag_1", ), pytest.param( " Should I answer between tags? hello", [Question(content=" Should I answer between tags? "), Answer(content="hello")], - id="question_with_answer_tag", + id="question_with_answer_tag_2", ), pytest.param( "helloworld", From 72a7ce0277bddf0301168c8076f226b38633d03e Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 4 Apr 2025 10:55:25 -0600 Subject: [PATCH 02/25] Migrated to PipelineStep mechanics for Chat pipelines and removed internal generation steps --- pyproject.toml | 1 - rigging/__init__.py | 13 +- rigging/chat.py | 1429 ++++++++++------- rigging/completion.py | 78 +- rigging/error.py | 14 + rigging/generator/base.py | 29 +- rigging/generator/http.py | 59 +- rigging/generator/litellm_.py | 50 +- rigging/message.py | 14 +- rigging/model.py | 9 +- rigging/prompt.py | 390 +++-- rigging/tool/api.py | 4 + rigging/tool/base.py | 23 +- rigging/tool/mcp.py | 4 +- rigging/tool/robopages.py | 3 +- rigging/util.py | 13 + tests/generators.py | 87 + tests/test_chat.py | 16 +- tests/test_chat_pipeline.py | 212 +++ ...pletion.py => test_completion_pipeline.py} | 2 + tests/test_generation.py | 188 --- tests/test_generator_ids.py | 28 +- tests/test_http_spec.py | 59 +- tests/test_prompt.py | 19 +- tests/test_tool.py | 58 +- tests/test_watchers.py | 32 +- tests/test_xml_parsing.py | 113 +- 27 files changed, 1782 insertions(+), 1165 deletions(-) create mode 100644 tests/generators.py create mode 100644 tests/test_chat_pipeline.py rename tests/{test_completion.py => test_completion_pipeline.py} (98%) delete mode 100644 tests/test_generation.py diff --git a/pyproject.toml b/pyproject.toml index 1e394d3..72fbbbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,7 +114,6 @@ target-version = "py310" line-length = 100 extend-exclude = [ "*.ipynb", # jupyter notebooks - "tests/*" ] [tool.ruff.lint] diff --git a/rigging/__init__.py b/rigging/__init__.py index 69e2dd3..d9a6975 100644 --- a/rigging/__init__.py +++ b/rigging/__init__.py @@ -1,5 +1,13 @@ from rigging import data, error, generator, logging, model, parsing, watchers -from rigging.chat import Chat, ChatPipeline, MapChatCallback, ThenChatCallback +from rigging.chat import ( + Chat, + ChatPipeline, + MapChatCallback, + PipelineStep, + PipelineStepContextManager, + PipelineStepGenerator, + ThenChatCallback, +) from rigging.completion import ( Completion, CompletionPipeline, @@ -65,6 +73,9 @@ "MapChatCallback", "ThenCompletionCallback", "MapCompletionCallback", + "PipelineStep", + "PipelineStepGenerator", + "PipelineStepContextManager", "generator", "mcp", "robopages", diff --git a/rigging/chat.py b/rigging/chat.py index 1e0983d..b3bab7a 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -5,8 +5,13 @@ """ import asyncio +import contextlib +import inspect import types import typing as t +import warnings +from contextlib import aclosing, asynccontextmanager +from contextvars import ContextVar from copy import deepcopy from dataclasses import dataclass from datetime import datetime @@ -17,12 +22,12 @@ from loguru import logger from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, ValidationError, computed_field -from rigging.error import MessagesExhaustedMaxRoundsError, UnknownToolError +from rigging.error import MaxDepthError, UnknownToolError from rigging.generator import GenerateParams, Generator, get_generator -from rigging.generator.base import GeneratedMessage, StopReason, Usage +from rigging.generator.base import StopReason, Usage from rigging.message import Content, Message, MessageDict, Messages from rigging.model import Model, ModelT, SystemErrorModel, ValidationErrorModel -from rigging.tool.api import ApiToolChoice +from rigging.tool.api import ApiToolCall, ApiToolChoice from rigging.tool.base import Tool, ToolMode from rigging.tool.native import ( JsonInXmlToolCall, @@ -32,7 +37,7 @@ tool_description_prompt_part, ) from rigging.tracing import Span, tracer -from rigging.util import get_qualified_name +from rigging.util import flatten_list, get_qualified_name if t.TYPE_CHECKING: from rigging.data import ElasticOpType @@ -46,6 +51,9 @@ DEFAULT_MAX_ROUNDS = 5 """Maximum number of internal callback rounds to attempt during generation before giving up.""" +DEFAULT_MAX_DEPTH = 20 +"""Maximum depth of nested pipeline generations to attempt before giving up.""" + FailMode = t.Literal["raise", "skip", "include"] """ How to handle failures in pipelines. @@ -65,7 +73,7 @@ class Chat(BaseModel): """ - Represents a completed chat conversation. + A completed chat interaction. """ model_config = ConfigDict(arbitrary_types_allowed=True) @@ -94,7 +102,7 @@ class Chat(BaseModel): """Any additional generation params used for this chat.""" error: t.Annotated[ - Exception, + BaseException, PlainSerializer(lambda x: str(x), return_type=str, when_used="json-unless-none"), ] | None = Field(None, repr=False) """Holds any exception that was caught during the generation pipeline.""" @@ -177,7 +185,10 @@ def last(self) -> Message: @property def conversation(self) -> str: """Returns a string representation of the chat.""" - return "\n\n".join([str(m) for m in self.all]) + conversation = "\n\n".join([str(m) for m in self.all]) + if self.error: + conversation += f"\n\n[error]: {self.error}" + return conversation @property def message_dicts(self) -> list[MessageDict]: @@ -200,7 +211,7 @@ def meta(self, **kwargs: t.Any) -> "Chat": **kwargs: Key-value pairs representing the metadata to be updated. Returns: - The updated chat object. + The updated chat. """ self.metadata.update(kwargs) return self @@ -249,8 +260,7 @@ def fork( include_all: Whether to include the next messages in the restarted chat. Returns: - A new instance of `ChatPipeline` with the specified messages added. - + A new pipeline with specified messages added. """ return self.restart(include_all=include_all).add(messages) @@ -270,7 +280,7 @@ def clone(self, *, only_messages: bool = False) -> "Chat": If False (default), the entire chat object will be cloned. Returns: - A new instance of Chat. + A cloned chat. """ new = Chat( [m.model_copy() for m in self.messages], @@ -295,7 +305,7 @@ def apply(self, **kwargs: str) -> "Chat": **kwargs: The string mapping of replacements. Returns: - The modified Chat object. + The updated chat. """ if self.generated: self.generated[-1] = self.generated[-1].apply(**kwargs) @@ -311,7 +321,7 @@ def apply_to_all(self, **kwargs: str) -> "Chat": **kwargs: The string mapping of replacements. Returns: - The modified chat object. + The updated chat. """ self.messages = Message.apply_to_list(self.messages, **kwargs) self.generated = Message.apply_to_list(self.generated, **kwargs) @@ -326,14 +336,14 @@ def strip(self, model_type: type[Model], fail_on_missing: bool = False) -> "Chat fail_on_missing: Whether to raise an exception if a message of the specified model type is not found. Returns: - A new Chat object with only the messages of the specified model type. + A new chat with only the messages of the specified model type. """ new = self.clone() for message in new.all: message.strip(model_type, fail_on_missing=fail_on_missing) return new - def inject_system_content(self, content: str) -> Message: + def inject_system_content(self, content: str) -> "Chat": """ Injects content into the chat as a system message. @@ -346,23 +356,26 @@ def inject_system_content(self, content: str) -> Message: content: The content to be injected. Returns: - The updated system message. + The updated chat. """ if len(self.messages) == 0 or self.messages[0].role != "system": self.messages.insert(0, Message(role="system", content=content)) elif self.messages[0].role == "system" and content not in self.messages[0].content: self.messages[0].content += "\n\n" + content - return self.messages[0] + return self - def inject_tool_prompt(self, tools: t.Sequence[Tool], mode: ToolMode) -> None: + def inject_tool_prompt(self, tools: t.Sequence[Tool], mode: ToolMode) -> "Chat": """ Injects a default tool use prompt into the system prompt. Args: tools: A sequence of Tool objects. + + Returns: + The updated chat. """ if mode not in ["xml", "json-in-xml"]: - return + return self definitions: list[XmlToolDefinition] | list[JsonInXmlToolDefinition] if mode == "xml": @@ -374,7 +387,7 @@ def inject_tool_prompt(self, tools: t.Sequence[Tool], mode: ToolMode) -> None: definitions, t.cast(t.Literal["xml", "json-in-xml"], mode), ) - self.inject_system_content(tool_system_prompt) + return self.inject_system_content(tool_system_prompt) def to_df(self) -> t.Any: """ @@ -482,34 +495,58 @@ def to_json(self) -> list[dict[str, t.Any]]: @runtime_checkable -class UntilMessageCallback(t.Protocol): - def __call__(self, message: Message, /) -> tuple[bool, list[Message]]: - """ - Passed the next message, returns whether or not to continue and an - optional list of messages to append before continuing. - """ +class _ThenChatCallback(t.Protocol): + def __call__( + self, + chat: Chat, + /, + ) -> t.Awaitable[Chat | None]: ... @runtime_checkable -class ThenChatCallback(t.Protocol): - def __call__(self, chat: Chat, /) -> t.Awaitable[Chat | None]: - """ - Passed a finalized chat to process and can return a new chat to replace it. - """ +class _ThenChatStepCallback(t.Protocol): + def __call__( + self, + chat: Chat, + /, + ) -> "PipelineStepGenerator | PipelineStepContextManager | t.Awaitable[PipelineStepGenerator | PipelineStepContextManager | None]": ... +ThenChatCallback = _ThenChatCallback | _ThenChatStepCallback +""" +Passed a finalized chat to process and can return a new chat to replace it. +""" + + @runtime_checkable -class MapChatCallback(t.Protocol): - def __call__(self, chats: list[Chat], /) -> t.Awaitable[list[Chat]]: - """ - Passed a finalized chats to process. Can replace chats in the pipeline by returning - a new chat object. - """ +class _MapChatCallback(t.Protocol): + def __call__( + self, + chats: list[Chat], + /, + ) -> t.Awaitable[list[Chat]]: ... +@runtime_checkable +class _MapChatStepCallback(t.Protocol): + def __call__( + self, + chats: list[Chat], + /, + ) -> "PipelineStepGenerator | PipelineStepContextManager | t.Awaitable[PipelineStepGenerator | PipelineStepContextManager]": + ... + + +MapChatCallback = _MapChatCallback | _MapChatStepCallback +""" +Passed a finalized chats to process. Can replace chats in the pipeline +by returning any number of new or existing chats. +""" + + @runtime_checkable class WatchChatCallback(t.Protocol): def __call__(self, chats: list[Chat], /) -> t.Awaitable[None]: @@ -519,17 +556,112 @@ def __call__(self, chats: list[Chat], /) -> t.Awaitable[None]: ... -# Helper classes to manage complexity inside the run functions +# Pipeline Step + +g_pipeline_step_ctx: "ContextVar[ChatList | None]" = ContextVar( + "g_pipeline_step_ctx", + default=None, +) + +PipelineState = t.Literal["generated", "callback", "final"] @dataclass -class RunState: - inputs: list[Message] - messages: list[Message] - params: GenerateParams - processor: t.Generator[list[Message], Message, list[Message]] - chat: Chat | None = None - watched: bool = False +class PipelineStep: + """ + An intermediate step during pipeline generation. + """ + + state: PipelineState + """The current state of the generation.""" + chats: ChatList + """The chats associated with this step.""" + pipeline: "ChatPipeline" + """The pipeline associated with this step.""" + parent: "PipelineStep | None" = None + """The parent step of pipelines which are running above this step.""" + callback: "ThenChatCallback | MapChatCallback | None" = None + """The associated callback function if state is 'callback'.""" + + def copy(self) -> "PipelineStep": + """ + Clone the current step. + """ + return PipelineStep( + state=self.state, + chats=self.chats, + pipeline=self.pipeline, + parent=self.parent.copy() if self.parent else None, + callback=self.callback, + ) + + def with_parent(self, parent: "PipelineStep") -> "PipelineStep": + """ + Clone the current step and append a parent to it's heirarchy. + """ + if self is parent: + raise RuntimeError("Cannot set parent to self") + + copy = self.copy() + + if copy.parent is None: + copy.parent = parent + return copy + + next_parent = copy.parent + while next_parent is not None: + if next_parent is next_parent.parent: + raise RuntimeError("Parent is self-referential") + if next_parent.parent is None: + next_parent.parent = parent + return copy + next_parent = next_parent.parent + + raise RuntimeError("Unable to set parent step") + + def __str__(self) -> str: + callback_name = get_qualified_name(self.callback) if self.callback else "None" + self_str = f"PipelineStep(pipeline={id(self.pipeline)}, state={self.state}, chats={len(self.chats)}, callback={callback_name})" + if self.parent is not None: + self_str += f" <- {self.parent!s}" + return self_str + + @property + def depth(self) -> int: + """ + Returns the depth of this pipeline step in the pipeline tree. + + This is useful for setting constraints on recursion depth. + """ + depth = 0 + while self.parent is not None: + depth += 1 + self = self.parent + return depth + + +PipelineStepGenerator = t.AsyncGenerator[PipelineStep, None] +PipelineStepContextManager = t.AsyncContextManager[PipelineStepGenerator] + +# Tracing wrappers + + +def _wrap_watch_callback(callback: WatchChatCallback) -> WatchChatCallback: + callback_name = get_qualified_name(callback) + + async def traced_watch_callback(chats: list[Chat]) -> None: + with tracer.span( + f"Watch with {callback_name}()", + callback=callback_name, + chat_count=len(chats), + chat_ids=[str(c.uuid) for c in chats], + ): + await callback(chats) + + return traced_watch_callback + + +# Pipeline class ChatPipeline: @@ -553,27 +685,22 @@ def __init__( """The parameters for generating messages.""" self.metadata: dict[str, t.Any] = {} """Additional metadata associated with the chat.""" - self.errors_to_fail_on: set[type[Exception]] = set() - """ - The list of exceptions to catch during generation if you are including or skipping failures. - - ExhuastedMaxRounds is implicitly included. - """ + self.errors_to_catch: set[type[Exception]] = {MaxDepthError, ValidationError} + """The list of exceptions to catch during generation if you are including or skipping failures.""" self.errors_to_exclude: set[type[Exception]] = set() """The list of exceptions to exclude from the catch list.""" self.on_failed: FailMode = "raise" """How to handle failures in the pipeline unless overriden in calls.""" self.caching: CacheMode | None = None + """How to handle cache_control entries on messages.""" - # (callback, attempt_recovery, drop_dialog, max_rounds) - self.until_callbacks: list[tuple[UntilMessageCallback, bool, bool, int]] = [] self.until_types: list[type[Model]] = [] self.tools: list[Tool] = [] self.tool_mode: ToolMode = "auto" self.api_tool_choice: ApiToolChoice | None = None self.inject_tool_prompt = True - self.then_callbacks: list[ThenChatCallback] = [] - self.map_callbacks: list[MapChatCallback] = [] + self.then_callbacks: list[tuple[ThenChatCallback, int]] = [] + self.map_callbacks: list[tuple[MapChatCallback, int]] = [] self.watch_callbacks: list[WatchChatCallback] = watch_callbacks or [] def __len__(self) -> int: @@ -591,7 +718,7 @@ def with_(self, params: GenerateParams | None = None, **kwargs: t.Any) -> "ChatP **kwargs: An alternative way to pass parameters as keyword arguments. Returns: - A new instance of ChatPipeline with the updated parameters. + The updated pipeline. """ if params is None: params = GenerateParams(**kwargs) @@ -618,9 +745,9 @@ def catch( on_failed: How to handle failures in the pipeline unless overriden in calls. Returns: - The updated ChatPipeline object. + The updated pipeline. """ - self.errors_to_fail_on.update(errors) + self.errors_to_catch.update(errors) self.errors_to_exclude.update(exclude or []) self.on_failed = on_failed or self.on_failed return self @@ -637,19 +764,24 @@ def watch( *callbacks: The callback functions to be executed. allow_duplicates: Whether to allow (seemingly) duplicate callbacks to be added. - ``` - async def log(chats: list[Chat]) -> None: - ... + Returns: + The updated pipeline. - await pipeline.watch(log).run() - ``` + Example: + ``` + async def log(chats: list[Chat]) -> None: + ... - Returns: - The current instance of the chat. + await pipeline.watch(log).run() + ``` """ for callback in callbacks: - if allow_duplicates or callback not in self.watch_callbacks: - self.watch_callbacks.append(callback) + if not allow_duplicates and callback in self.watch_callbacks: + raise ValueError( + f"Callback '{get_qualified_name(callback)}' is already registered.", + ) + + self.watch_callbacks.extend(callbacks) return self def add( @@ -681,7 +813,7 @@ def add( - "none": Keep messages independent and do not merge any content. Returns: - The updated ChatPipeline object. + The updated pipeline. """ message_list = Message.fit_as_list(messages) @@ -695,7 +827,7 @@ def add( and self.chat.all[-1].role == "user" ) ): - self.chat.all[-1].content += "\n" + message_list[0].content + self.chat.all[-1].content_parts += message_list[0].content_parts message_list = message_list[1:] self.chat.generated += message_list @@ -714,7 +846,7 @@ def fork( messages: A sequence of messages or a single message to be added to the new chat. Returns: - A new instance the pipeline with the specified messages added. + The cloned pipeline with messages added. """ return self.clone().add(messages) @@ -730,7 +862,7 @@ def clone(self, *, only_messages: bool = False, chat: Chat | None = None) -> "Ch internal chat object will be cloned. Returns: - A new instance of `ChatPipeline` that is a clone of the current instance. + The cloned ChatPipeline. """ new = ChatPipeline( self.generator, @@ -740,14 +872,13 @@ def clone(self, *, only_messages: bool = False, chat: Chat | None = None) -> "Ch ) new.chat = (chat or self.chat).clone() if not only_messages: - new.until_callbacks = self.until_callbacks.copy() new.until_types = self.until_types.copy() new.tools = self.tools.copy() new.tool_mode = self.tool_mode new.metadata = deepcopy(self.metadata) new.map_callbacks = self.map_callbacks.copy() new.on_failed = self.on_failed - new.errors_to_fail_on = self.errors_to_fail_on.copy() + new.errors_to_catch = self.errors_to_catch.copy() new.errors_to_exclude = self.errors_to_exclude.copy() new.caching = self.caching @@ -756,18 +887,18 @@ def clone(self, *, only_messages: bool = False, chat: Chat | None = None) -> "Ch # and aren't operating with old state. new.then_callbacks = [ - callback + (callback, max_depth) if not hasattr(callback, "__self__") or not isinstance(callback.__self__, ChatPipeline) - else types.MethodType(callback.__func__, self) # type: ignore [attr-defined] - for callback in self.then_callbacks.copy() + else (types.MethodType(callback.__func__, self), max_depth) # type: ignore [union-attr] + for callback, max_depth in self.then_callbacks.copy() ] new.map_callbacks = [ - callback + (callback, max_depth) if not hasattr(callback, "__self__") or not isinstance(callback.__self__, ChatPipeline) - else types.MethodType(callback.__func__, self) # type: ignore [attr-defined] - for callback in self.map_callbacks.copy() + else (types.MethodType(callback.__func__, self), max_depth) # type: ignore [union-attr] + for callback, max_depth in self.map_callbacks.copy() ] return new @@ -780,65 +911,101 @@ def meta(self, **kwargs: t.Any) -> "ChatPipeline": **kwargs: Key-value pairs representing the metadata to be updated. Returns: - The updated chat object. + The updated pipeline. """ self.metadata.update(kwargs) return self - def then(self, callback: ThenChatCallback) -> "ChatPipeline": + def then( + self, + *callbacks: ThenChatCallback, + max_depth: int = DEFAULT_MAX_DEPTH, + allow_duplicates: bool = False, + ) -> "ChatPipeline": """ - Registers a callback to be executed after the generation process completes. + Registers one or many callbacks to be executed after the generation process completes. Note: - Returning a Chat object from the callback will replace the current chat. - for the remainder of the callbacks + return value of `run()`. This is - optional. - - ``` - async def process(chat: Chat) -> Chat | None: - ... - - await pipeline.then(process).run() - ``` + Returning a Chat object from the callback will replace that chat + for the remainder of the callbacks and the final return value + from the pipeline. Args: - callback: The callback function to be executed. + callbacks: The callback functions to be added. + max_depth: The maximum depth to allow recursive pipeline calls during this callback. Returns: - The current instance of the chat. + The updated pipeline. + + Example: + ``` + async def process(chat: Chat) -> Chat | None: + ... + + await pipeline.then(process).run() + ``` """ - if not asyncio.iscoroutinefunction(callback): - raise TypeError(f"Callback '{get_qualified_name(callback)}' must be an async function") + for callback in callbacks: + if not asyncio.iscoroutinefunction(callback): + raise TypeError( + f"Callback '{get_qualified_name(callback)}' must be an async function", + ) + + if allow_duplicates: + continue - self.then_callbacks.append(callback) + if callback in [c[0] for c in self.then_callbacks]: + raise ValueError( + f"Callback '{get_qualified_name(callback)}' is already registered.", + ) + + self.then_callbacks.extend([(callback, max_depth) for callback in callbacks]) return self - def map(self, callback: MapChatCallback) -> "ChatPipeline": + def map( + self, + *callbacks: MapChatCallback, + max_depth: int = DEFAULT_MAX_DEPTH, + allow_duplicates: bool = False, + ) -> "ChatPipeline": """ Registers a callback to be executed after the generation process completes. Note: You must return a list of Chat objects from the callback which will represent the state of chats for the remainder of the callbacks and - the final return of control. - - ``` - async def process(chats: list[Chat]) -> list[Chat]: - ... - - await pipeline.map(process).run() - ``` + the final return value from the pipeline. Args: callback: The callback function to be executed. + max_depth: The maximum depth to allow recursive pipeline calls during this callback. Returns: - The current instance of the chat. + The updated pipeline. + + Example: + ``` + async def process(chats: list[Chat]) -> list[Chat]: + ... + + await pipeline.map(process).run() + ``` """ - if not asyncio.iscoroutinefunction(callback): - raise TypeError(f"Callback '{get_qualified_name(callback)}' must be an async function") + for callback in callbacks: + if not asyncio.iscoroutinefunction(callback): + raise TypeError( + f"Callback '{get_qualified_name(callback)}' must be an async function", + ) + + if allow_duplicates: + continue + + if callback in [c[0] for c in self.map_callbacks]: + raise ValueError( + f"Callback '{get_qualified_name(callback)}' is already registered.", + ) - self.map_callbacks.append(callback) + self.map_callbacks.extend([(callback, max_depth) for callback in callbacks]) return self def apply(self, **kwargs: str) -> "ChatPipeline": @@ -849,7 +1016,7 @@ def apply(self, **kwargs: str) -> "ChatPipeline": **kwargs: Keyword arguments to be applied to the chat. Returns: - A new instance of ChatPipeline with the applied arguments. + A new pipeline with updated chat. """ new = self.clone() new.chat.apply(**kwargs) @@ -863,7 +1030,7 @@ def apply_to_all(self, **kwargs: str) -> "ChatPipeline": **kwargs: Keyword arguments to be applied to the chat. Returns: - A new instance of ChatPipeline with the applied arguments. + A new pipeline with updated chat. """ new = self.clone() new.chat.apply_to_all(**kwargs) @@ -884,53 +1051,6 @@ def cache(self, mode: CacheMode | None | t.Literal[False] = "latest") -> "ChatPi self.caching = mode return self - def until( - self, - callback: UntilMessageCallback, - *, - attempt_recovery: bool = True, - drop_dialog: bool = True, - max_rounds: int = DEFAULT_MAX_ROUNDS, - ) -> "ChatPipeline": - """ - Registers a callback to participate in validating the generation process. - - ```py - # Takes the next message being generated, and returns whether or not to continue - # generating new messages in addition to a list of messages to append before continuing - - def callback(message: Message) -> tuple[bool, list[Message]]: - if is_valid(message): - return (False, [message]) - else: - return (True, [message, ...]) - - await pipeline.until(callback).run() - ``` - - Note: - Users might prefer the `.then` or `.map` callbacks as they are easier to work with. - - Note: - In general, your callback function should always include the message that was passed to it. - - Whether these messages get used or discarded in the next round depends on `attempt_recovery`. - - Args: - callback: The callback function to be executed. - attempt_recovery: Whether to attempt recovery by continuing to append prior messages - before the next round of generation. - drop_dialog: Whether to drop the intermediate dialog of recovery before returning - the final chat back to the caller. - max_rounds: The maximum number of rounds to attempt generation + callbacks - before giving uop. - - Returns: - The current instance of the chat. - """ - self.until_callbacks.append((callback, attempt_recovery, drop_dialog, max_rounds)) - return self - def wrap(self, func: t.Callable[[CallableT], CallableT]) -> "ChatPipeline": """ Helper for [rigging.generator.base.Generator.wrap][]. @@ -939,16 +1059,17 @@ def wrap(self, func: t.Callable[[CallableT], CallableT]) -> "ChatPipeline": func: The function to wrap the calls with. Returns: - The current instance of the pipeline. + The current pipeline. """ self.generator = self.generator.wrap(func) return self def using( self, - *tools: Tool | t.Callable[..., t.Any], + *tools: Tool | t.Callable[..., t.Any] | t.Sequence[Tool | t.Callable[..., t.Any]], mode: ToolMode | None = None, choice: ApiToolChoice | None = None, + max_depth: int = DEFAULT_MAX_DEPTH, ) -> "ChatPipeline": """ Adds a tool or a sequence of tools to participate in the generation process. @@ -959,14 +1080,15 @@ def using( Args: *tools: The tools to be added to the pipeline. - mode: The tool mode to use (e.g., "xml", "json-in-xml", "api"). + mode: The tool calling mode to use (e.g., "xml", "json-in-xml", "api"). choice: The API tool choice to use. This is only relevant when using the "api" tool mode. + max_depth: The maximum depth for recursive tool calls (this is shared between all tools). Returns: The updated pipeline. Example: - ```python + ``` async def get_weather(city: Annotated[str, "The city name to get weather for"]) -> str: "Get the weather" city = city.replace(" ", "+") @@ -983,7 +1105,10 @@ async def get_weather(city: Annotated[str, "The city name to get weather for"]) if len(tools) == 0: return self - new_tools = [tool if isinstance(tool, Tool) else Tool.from_callable(tool) for tool in tools] + new_tools = [ + tool if isinstance(tool, Tool) else Tool.from_callable(tool) + for tool in flatten_list(list(tools)) # in case the user gave us lists + ] existing_names = {tool.name for tool in self.tools} for tool in new_tools: @@ -992,8 +1117,12 @@ async def get_weather(city: Annotated[str, "The city name to get weather for"]) self.tools += new_tools - if next((c for c in self.then_callbacks if c == self._then_tools), None) is None: - self.then_callbacks.append(self._then_tools) + self.then_callbacks = [ + (callback, max_depth) + for callback, max_depth in self.then_callbacks + if callback != self._then_tools + ] + self.then_callbacks.insert(0, (self._then_tools, max_depth)) # make sure this is first if mode is not None: self.tool_mode = mode @@ -1006,9 +1135,11 @@ async def get_weather(city: Annotated[str, "The city name to get weather for"]) def until_parsed_as( self, *types: type[ModelT], - attempt_recovery: bool = False, - drop_dialog: bool = True, - max_rounds: int = DEFAULT_MAX_ROUNDS, + max_depth: int = DEFAULT_MAX_DEPTH, + # deprecated + attempt_recovery: bool | None = None, + drop_dialog: bool | None = None, + max_rounds: int | None = None, ) -> "ChatPipeline": """ Adds the specified types to the list of types which should successfully parse @@ -1016,274 +1147,114 @@ def until_parsed_as( Args: *types: The type or types of models to wait for. - attempt_recovery: Whether to attempt recovery if parsing fails by providing - validation feedback to the model before the next round. - drop_dialog: Whether to drop the intermediate dialog of recovery efforts - before returning the final chat to the caller. max_rounds: The maximum number of rounds to try to parse successfully. + append: Whether to append the types to the existing list or replace it. + max_depth: The maximum depth to re-attempt parsing using recursive pipelines (this is shared between all types). + attempt_recovery: deprecated, recovery is always attempted. + drop_dialog: deprecated, the full dialog is always returned. + max_rounds: deprecated, use `max_depth` instead. Returns: - The updated ChatPipeline object. + The updated pipeline. """ - self.until_types += types - if ( - next((c for c in self.until_callbacks if c[0] == self._until_parse_callback), None) - is None - ): - self.until_callbacks.append( - (self._until_parse_callback, attempt_recovery, drop_dialog, max_rounds), + if attempt_recovery is not None: + warnings.warn( + "The 'attempt_recovery' argument is deprecated and has no effect.", + DeprecationWarning, + stacklevel=2, + ) + if drop_dialog is not None: + warnings.warn( + "The 'drop_dialog' argument is deprecated and has no effect.", + DeprecationWarning, + stacklevel=2, + ) + if max_rounds is not None: + warnings.warn( + "The 'max_rounds' argument is deprecated, use 'max_depth'.", + DeprecationWarning, + stacklevel=2, ) - return self - - def _has_tool_calls_to_process(self, message: Message) -> bool: - if not self.tools: - return False - - xml_tool_calls = message.try_parse_set(XmlToolCall) - json_tool_calls = message.try_parse_set(JsonInXmlToolCall) - api_tool_calls = message.tool_calls - - return bool(xml_tool_calls or json_tool_calls or api_tool_calls) - - # TODO: There is a lot of duplicated code between these _then_*_tools methods - # and we should clean it up. - - async def _then_api_tools(self, chat: Chat) -> Chat | None: - if not chat.last.tool_calls: - return None + self.until_types = list(types) - next_pipeline = self.clone(chat=chat) - - for tool_call in chat.last.tool_calls: - tool = next((t for t in self.tools if t.name == tool_call.function.name), None) - if tool is None: - raise UnknownToolError(tool_call.function.name) - next_pipeline.add(await tool.handle_tool_call(tool_call)) + max_depth = max_rounds or max_depth + self.then_callbacks = [ + (callback, max_depth) + for callback, max_depth in self.then_callbacks + if callback != self._then_parse + ] + self.then_callbacks.append((self._then_parse, max_depth)) - # Need to prevent infinite loops and treat tool_choice like - # an ephemeral setting which resets after each tool call. - # - # TODO: Seems like this is surfacing a larger architectural issue we should look at + return self - if next_pipeline.params: - next_pipeline.params.tool_choice = None + # Internal callbacks for handling tools and parsing - return await next_pipeline.run() + async def _then_tools(self, chat: Chat) -> PipelineStepContextManager | None: + tool_calls: list[ApiToolCall] | list[XmlToolCall] | list[JsonInXmlToolCall] | None = None + if self.tool_mode == "api": + tool_calls = chat.last.tool_calls + if self.tool_mode == "xml": + tool_calls = chat.last.try_parse_set(XmlToolCall) + elif self.tool_mode == "json-in-xml": + tool_calls = chat.last.try_parse_set(JsonInXmlToolCall) - async def _then_xml_tools(self, chat: Chat) -> Chat | None: - tool_calls = chat.last.try_parse_set(XmlToolCall) if not tool_calls: return None next_pipeline = self.clone(chat=chat) + should_continue = True + for tool_call in tool_calls: tool = next((t for t in self.tools if t.name == tool_call.name), None) if tool is None: raise UnknownToolError(tool_call.name) - next_pipeline.add(await tool.handle_tool_call(tool_call)) - return await next_pipeline.run() + message = await tool.handle_tool_call(tool_call) - async def _then_json_in_xml_tools(self, chat: Chat) -> Chat | None: - tool_calls = chat.last.try_parse_set(JsonInXmlToolCall) - if not tool_calls: - return None - - next_pipeline = self.clone(chat=chat) + if message is None: + should_continue = False + else: + next_pipeline.add(message) - for tool_call in tool_calls: - tool = next((t for t in self.tools if t.name == tool_call.name), None) - if tool is None: - raise UnknownToolError(tool_call.name) - next_pipeline.add(await tool.handle_tool_call(tool_call)) + # Need to prevent infinite loops and treat tool_choice like + # an ephemeral setting which resets after the first tool call. - return await next_pipeline.run() + if self.tool_mode == "api" and next_pipeline.params: + next_pipeline.params.tool_choice = None - async def _then_tools(self, chat: Chat) -> Chat | None: - if self.tool_mode == "api": - return await self._then_api_tools(chat) - if self.tool_mode == "xml": - return await self._then_xml_tools(chat) - if self.tool_mode == "json-in-xml": - return await self._then_json_in_xml_tools(chat) + if not should_continue: + return None - raise RuntimeError( - "tool_mode appears incorrect and must be one of 'api', 'xml', or 'json-in-xml'", - ) + return next_pipeline.step() - def _until_parse_callback(self, message: Message) -> tuple[bool, list[Message]]: - should_continue: bool = False - generated: list[Message] = [message] + async def _then_parse(self, chat: Chat) -> PipelineStepContextManager | None: + next_pipeline = self.clone(chat=chat) try: - message.parse_many(*self.until_types) + chat.last.parse_many(*self.until_types) except ValidationError as e: - should_continue = True - generated.append( + next_pipeline.add( Message.from_model( ValidationErrorModel(content=str(e)), - suffix="Rewrite your entire message with all the required elements.", + suffix="Rewrite your entire message with all the required xml structure.", ), ) except Exception as e: # noqa: BLE001 - should_continue = True - generated.append( + next_pipeline.add( Message.from_model( SystemErrorModel(content=str(e)), - suffix="Rewrite your entire message with all the required elements.", + suffix="Rewrite your entire message with all the required xml structure.", ), ) + else: # parsed successfully + return None - return (should_continue, generated) - - def _until( - self, - message: Message, - callback: UntilMessageCallback, - attempt_recovery: bool, # noqa: FBT001 - drop_dialog: bool, # noqa: FBT001 - max_rounds: int, - ) -> t.Generator[list[Message], Message, list[Message]]: - should_continue, step_messages = callback(message) - if not should_continue: - return step_messages - - running_messages = step_messages if attempt_recovery else [] - next_message: Message - - for _round in range(1, max_rounds + 1): - callback_name = get_qualified_name(callback) - with tracer.span( - f"Until with {callback_name}() ({_round}/{max_rounds})", - callback=callback_name, - round=_round, - max_rounds=max_rounds, - attempt_recovery=attempt_recovery, - drop_dialog=drop_dialog, - ): - logger.trace( - f"_until({callback_name}) round {_round}/{max_rounds} (attempt_recovery={attempt_recovery})", - ) - next_message = yield running_messages - should_continue, step_messages = callback(next_message) - logger.trace( - f" |- returned {should_continue} with {len(step_messages)} new messages)", - ) - - if attempt_recovery: - running_messages += step_messages - - if not should_continue: - return step_messages if drop_dialog else running_messages - - # !attempt_recovery -> Return just the latest generation - # attempt_recovery & drop_dialog -> Return just the latest generation - # attempt_recovery & !drop_dialog -> Return intermediate and the latest - - logger.warning(f"Exhausted max rounds ({max_rounds})") - raise MessagesExhaustedMaxRoundsError( - max_rounds, - [next_message] if not attempt_recovery and next_message else running_messages[:-1], - ) - - # TODO: Much like the CompletionPipeline code, it's opaque exactly how - # multiple callbacks should be blended together when generating. - # - # I think we should look at limiting it to one callback in total, - # but I'll leave the behavior as is for now with the warning that it - # might be a bit unpredictable. - - def _process(self) -> t.Generator[list[Message], Message, list[Message]]: - first_response = yield [] - new_messages = [first_response] - - # If we need to process tool calls, we should do that first - # before proceeding with our until callbacks - if self._has_tool_calls_to_process(first_response): - return new_messages - - for callback, reset_between, drop_internal, max_rounds in self.until_callbacks: - generated = yield from self._until( - new_messages[-1], - callback, - reset_between, - drop_internal, - max_rounds, - ) - new_messages = new_messages[:-1] + generated - - return new_messages - - async def _watch_callback(self, chats: list[Chat]) -> None: - # Given that these watch callbacks don't return a value, - # we should be safe to run them internally. - - def wrap_watch_callback(callback: WatchChatCallback) -> WatchChatCallback: - async def traced_watch_callback(chats: list[Chat]) -> None: - callback_name = get_qualified_name(callback) - with tracer.span( - f"Watch with {callback_name}()", - callback=callback_name, - chat_count=len(chats), - chat_ids=[str(c.uuid) for c in chats], - ): - await callback(chats) - - return traced_watch_callback - - coros = [wrap_watch_callback(callback)(chats) for callback in self.watch_callbacks] - await asyncio.gather(*coros) + return next_pipeline.step() # Run helper methods - async def _post_run(self, chats: list[Chat], on_failed: FailMode) -> ChatList: - if on_failed == "skip": - chats = [c for c in chats if not c.failed] - - # These have to be sequenced to support the concept of - # a pipeline where future then/map calls can depend on - # previous calls being ran. - - for map_callback in self.map_callbacks: - callback_name = get_qualified_name(map_callback) - with tracer.span( - f"Map with {callback_name}()", - callback=callback_name, - chat_count=len(chats), - chat_ids=[str(c.uuid) for c in chats], - ): - chats = await map_callback(chats) - if not all(isinstance(c, Chat) for c in chats): - raise ValueError( - f".map() callback must return a Chat object or None ({callback_name})", - ) - - def wrap_then_callback(callback: ThenChatCallback) -> ThenChatCallback: - async def traced_then_callback(chat: Chat) -> Chat | None: - callback_name = get_qualified_name(callback) - with tracer.span( - f"Then with {callback_name}()", - callback=callback_name, - chat_id=str(chat.uuid), - ): - return await callback(chat) - - return traced_then_callback - - for then_callback in self.then_callbacks: - coros = [wrap_then_callback(then_callback)(chat) for chat in chats] - new_chats = await asyncio.gather(*coros) - if not all(isinstance(c, Chat) or c is None for c in new_chats): - raise ValueError( - f".then() callback must return a Chat object or None ({get_qualified_name(then_callback)})", - ) - - chats = [new or chat for new, chat in zip(new_chats, chats, strict=False)] - - return ChatList(chats) - async def _pre_run(self) -> None: if self.tool_mode == "auto" and self.tools: self.tool_mode = "api" if await self.generator.supports_function_calling() else "xml" @@ -1310,84 +1281,6 @@ def _fit_params( params = [self.params.merge_with(p) for p in params] return [(p or GenerateParams()) for p in params] - # Run methods - - def _initialize_states( - self, - count: int, - params: t.Sequence[GenerateParams | None] | None = None, - ) -> list[RunState]: - states = [RunState([], [], p, self._process()) for p in self._fit_params(count, params)] - for state in states: - next(state.processor) - return states - - def _initialize_batch_states( - self, - many: t.Sequence[t.Sequence[Message]] - | t.Sequence[Message] - | t.Sequence[MessageDict] - | t.Sequence[str] - | MessageDict - | str, - params: t.Sequence[GenerateParams | None] | None = None, - ) -> list[RunState]: - if isinstance(many, dict | str): # Some strange typechecking here - many = t.cast(t.Sequence[str] | t.Sequence[MessageDict], [many]) - - count = max(len(many), len(params) if params is not None else 0) - - many = [Message.fit_as_list(m) for m in many] - if len(many) < count: - if len(many) != 1: - raise ValueError(f"Can't fit many of length {len(many)} to {count}") - many = many * count - - params = self._fit_params(count, params) - - states: list[RunState] = [ - RunState(self.chat.all + m, [], p, self._process()) - for m, p in zip(many, params, strict=False) - ] - for state in states: - next(state.processor) - - return states - - def _create_chat( - self, - state: RunState, - outputs: list[Message], - inbound: GeneratedMessage, - batch_mode: bool, # noqa: FBT001 - failed: bool = False, # noqa: FBT001, FBT002 - error: Exception | None = None, - ) -> Chat: - return Chat( - state.inputs if batch_mode else self.chat.all, - outputs, - generator=self.generator, - pipeline=self, - metadata=self.metadata, - params=state.params, - stop_reason=inbound.stop_reason, - usage=inbound.usage, - extra=inbound.extra, - failed=failed, - error=error, - ) - - def _create_failed_chat(self, state: RunState, error: Exception, batch_mode: bool) -> Chat: # noqa: FBT001 - return Chat( - state.inputs if batch_mode else self.chat.all, - [], - generator=self.generator, - metadata=self.metadata, - params=state.params, - failed=True, - error=error, - ) - def _apply_cache_mode_to_messages(self, messages: list[list[Message]]) -> list[list[Message]]: if self.caching is None: return messages @@ -1407,142 +1300,351 @@ def _apply_cache_mode_to_messages(self, messages: list[list[Message]]) -> list[l return updated - async def _run( # noqa: PLR0912 + @dataclass + class CallbackState: + chat: Chat + ready_event: asyncio.Event + continue_event: asyncio.Event + step: PipelineStep | None = None + completed: bool = False + + async def _process_then_callback( + self, + callback: ThenChatCallback, + state: CallbackState, + ) -> None: + callback_name = get_qualified_name(callback) + + async def complete() -> None: + state.completed = True + state.ready_event.set() + + with tracer.span( + f"Then with {callback_name}()", + callback=callback_name, + chat_id=str(state.chat.uuid), + ): + async with contextlib.AsyncExitStack() as exit_stack: + exit_stack.push_async_callback(complete) + + result = callback(state.chat) + + if inspect.isawaitable(result): + result = await result # type: ignore [assignment] + + if result is None or isinstance(result, Chat): + state.chat = result or state.chat + return + + if isinstance(result, contextlib.AbstractAsyncContextManager): + result = await exit_stack.enter_async_context(result) + + if not inspect.isasyncgen(result): + raise TypeError( + f"Callback '{callback_name}' must return a Chat, PipelineStepGenerator, or None", + ) + + generator = t.cast( + PipelineStepGenerator, + await exit_stack.enter_async_context(aclosing(result)), + ) + async for step in generator: + state.step = step + + state.ready_event.set() + await state.continue_event.wait() + + state.ready_event.clear() + state.continue_event.clear() + state.step = None + + state.chat = step.chats[-1] if step.chats else state.chat + + # Run methods + + async def _step( # noqa: PLR0915, PLR0912 self, span: Span, - states: list[RunState], + messages: list[list[Message]], + params: list[GenerateParams], on_failed: FailMode, - batch_mode: bool = False, # noqa: FBT002, FBT001 - ) -> ChatList: - pending_states: list[RunState] = states - while pending_states: - try: - messages = [ - (s.inputs if batch_mode else self.chat.all) + s.messages for s in pending_states - ] - messages = self._apply_cache_mode_to_messages(messages) - - inbounds = await self.generator.generate_messages( - messages, - [s.params for s in pending_states], - ) + ) -> PipelineStepGenerator: + chats: ChatList = ChatList([]) + + # Pass the messages to the generator + + try: + messages = self._apply_cache_mode_to_messages(messages) + generated = await self.generator.generate_messages(messages, params) + + # If we got a total failure here for generation as a whole, + # we can't distinguish between incoming messages in terms + # of what caused the error, so so we need to set the error + # on all of them. + + except Exception as error: # noqa: BLE001 + span.set_attribute("failed", True) + span.set_attribute("error", error) + + chats = ChatList( + [ + Chat( + messages_, + [], + generator=self.generator, + pipeline=self, + metadata=self.metadata, + params=params_, + failed=True, + error=error, + ) + for messages_, params_ in zip(messages, params, strict=True) + ], + ) - except Exception as error: # noqa: BLE001 - # Handle core generator errors - if ( - on_failed == "raise" - or not any(isinstance(error, t) for t in self.errors_to_fail_on) - or any(isinstance(error, t) for t in self.errors_to_exclude) - ): - raise + # Otherwise we can construct individual chats with + # error states per-generation. - # We will apply the error to all chats in the batch as we can't - # tell which chat caused the error right now. + else: + chats = ChatList( + [ + Chat( + messages_, + [] if isinstance(generated_, BaseException) else [generated_.message], + generator=self.generator, + pipeline=self, + metadata=self.metadata, + params=params_, + stop_reason=None + if isinstance(generated_, BaseException) + else generated_.stop_reason, + usage=None if isinstance(generated_, BaseException) else generated_.usage, + extra=None if isinstance(generated_, BaseException) else generated_.extra, + failed=isinstance(generated_, BaseException), + error=generated_ if isinstance(generated_, BaseException) else None, + ) + for messages_, params_, generated_ in zip( + messages, + params, + generated, + strict=True, + ) + ], + ) + + # Watch callbacks + await asyncio.gather( + *[_wrap_watch_callback(callback)(chats) for callback in self.watch_callbacks], + ) + + # Yield what we generated + + span.set_attribute("chats", chats) + current_step = PipelineStep( + state="generated", + chats=chats, + pipeline=self, + ) + yield current_step + + # Check if we should immediately raise + + # FailMode = t.Literal["raise", "skip", "include"] + # self.on_failed: FailMode = "raise" + # """How to handle failures in the pipeline unless overriden in calls.""" + + # self.errors_to_catch: set[type[Exception]] = {MaxDepthError, ValidationError} + # """The list of exceptions to catch during generation if you are including or skipping failures.""" + # self.errors_to_exclude: set[type[Exception]] = set() + # """The list of exceptions to exclude from the catch list.""" + + for chat in chats: + if chat.error is not None and ( + on_failed == "raise" + or not any(isinstance(chat.error, t) for t in self.errors_to_catch) + or any(isinstance(chat.error, t) for t in self.errors_to_exclude) + ): + span.set_attribute("error", chat.error) span.set_attribute("failed", True) - span.set_attribute("error", error) + raise chat.error - for state in states: - state.chat = self._create_failed_chat(state, error, batch_mode) + # Chat cleanup - else: - # Process each inbound message and individual errors - for inbound, state in zip(inbounds, pending_states, strict=False): - try: - # Process for parsing callbacks, etc. - state.messages = state.processor.send(inbound.message) - except StopIteration as stop: # noqa: PERF203 - # StopIteration implies we are done and the chat is good to go - outputs = t.cast(list[Message], stop.value) - state.chat = self._create_chat(state, outputs, inbound, batch_mode) - except MessagesExhaustedMaxRoundsError as exhausted: - span.set_attribute("failed", True) - span.set_attribute("error", exhausted) - - # exhausted.messages holds the current messages when the error occured, - # so we'll pass them into the chat as the last generated messages. - state.chat = self._create_chat( - state, - exhausted.messages, - inbound, - batch_mode, - failed=True, - error=exhausted, - ) + if on_failed == "skip": + chats = ChatList([chat for chat in chats if not chat.failed]) - if on_failed == "raise": - # Set the attribute for troubleshooting - span.set_attribute( - "chats", - [s.chat for s in states if s.chat is not None], - ) - raise - - except Exception as error: # noqa: BLE001 - span.set_attribute("failed", True) - span.set_attribute("error", error) - state.chat = self._create_chat( - state, - [], - inbound, - batch_mode, - failed=True, - error=error, - ) + span.set_attribute("chats", chats) + + if len(chats) == 0 or all(chat.failed for chat in chats): + yield PipelineStep( + state="final", + chats=chats, + pipeline=self, + ) + return + + # Then callbacks + + for then_callback, max_depth in self.then_callbacks: + callback_name = get_qualified_name(then_callback) - # Check to see if we should be handling any specific errors - # and gracefully marking the chat as failed instead of raising (.catch) - if ( - on_failed == "raise" - or not any(isinstance(error, t) for t in self.errors_to_fail_on) - or any(isinstance(error, t) for t in self.errors_to_exclude) - ): - # Set the attribute for troubleshooting - span.set_attribute( - "chats", - [s.chat for s in states if s.chat is not None], - ) - raise - - pending_states = [s for s in pending_states if s.chat is None] - completed_states: list[RunState] = [ - s for s in states if s.chat is not None and not s.watched + states = [ + self.CallbackState( + chat=chat, + ready_event=asyncio.Event(), + continue_event=asyncio.Event(), + ) + for chat in chats ] - if not completed_states: - continue + tasks = [ + asyncio.create_task(self._process_then_callback(then_callback, state)) + for state in states + ] + + try: + while not all(state.completed for state in states): + await asyncio.gather( + *[state.ready_event.wait() for state in states if not state.completed], + ) - # We want to deliver chats to the watch callback as soon as possible, so we'll - # track whether we've already done so and only deliver new chats. + # TODO(nick): Are we good to throw exceptions here? + for task in tasks: + if task.done() and (exception := task.exception()): + raise exception + + for state in states: + if state.ready_event.is_set() and state.step: + step = state.step.with_parent(current_step) + + if step.depth > max_depth: + max_depth_error = MaxDepthError(max_depth, step, callback_name) + if on_failed == "raise": + raise max_depth_error + + state.chat.error = max_depth_error + state.chat.failed = True + state.completed = True + continue + + yield step + state.continue_event.set() + finally: + with contextlib.suppress(asyncio.CancelledError): + for task in tasks: + if not task.done(): + task.cancel() + await asyncio.gather(*tasks) # TODO(nick): return_exceptions=True ? + + chats = ChatList([state.chat for state in states if state.chat]) + + span.set_attribute("chats", chats) + current_step = PipelineStep( + state="callback", + chats=chats, + pipeline=self, + callback=then_callback, + ) + yield current_step - await self._watch_callback([s.chat for s in completed_states if s.chat is not None]) - for state in completed_states: - state.watched = True + # Chat cleanup + + if on_failed == "skip": + chats = ChatList([chat for chat in chats if not chat.failed]) - chats = [s.chat for s in states if s.chat is not None] - span.set_attribute("chats", chats) - chats = await self._post_run(chats, on_failed) span.set_attribute("chats", chats) - return chats + if len(chats) == 0 or all(chat.failed for chat in chats): + yield PipelineStep( + state="final", + chats=chats, + pipeline=self, + ) + return + + # Map callbacks + + for map_callback, max_depth in self.map_callbacks: + callback_name = get_qualified_name(map_callback) + + with tracer.span( + f"Map with {callback_name}()", + callback=callback_name, + chat_count=len(chats), + chat_ids=[str(c.uuid) for c in chats], + ): + async with contextlib.AsyncExitStack() as exit_stack: + result = map_callback(chats) + chats_or_generator = await result if inspect.isawaitable(result) else result + + if isinstance(result, contextlib.AbstractAsyncContextManager): + result = await exit_stack.enter_async_context(result) + + if inspect.isasyncgen(chats_or_generator): + generator = t.cast( + PipelineStepGenerator, + await exit_stack.enter_async_context(aclosing(chats_or_generator)), + ) + async for step in generator: + _step = step.with_parent(current_step) + if _step.depth > max_depth: + max_depth_error = MaxDepthError(max_depth, _step, callback_name) + if on_failed == "raise": + raise max_depth_error + + chats = ChatList(chats) + for chat in chats: + chat.error = max_depth_error + chat.failed = True + else: + yield _step + chats = step.chats + + chats = step.chats + + elif isinstance(chats_or_generator, list) and all( + isinstance(c, Chat) for c in chats_or_generator + ): + chats = ChatList(chats_or_generator) + + span.set_attribute("chats", chats) + current_step = PipelineStep( + state="callback", + chats=chats, + pipeline=self, + callback=map_callback, + ) + yield current_step + + if on_failed == "skip": + chats = ChatList([chat for chat in chats if not chat.failed]) + + span.set_attribute("chats", chats) + yield PipelineStep( + state="final", + chats=chats, + pipeline=self, + ) # Single messages - async def run(self, *, allow_failed: bool = False, on_failed: FailMode | None = None) -> Chat: + @asynccontextmanager + async def step( + self, + *, + on_failed: FailMode | None = None, + ) -> t.AsyncIterator[PipelineStepGenerator]: """ - Execute the generation process to produce the final chat. + Step through the generation process for a single message. Args: - allow_failed: Ignore any errors and potentially - return the chat in a failed state. on_failed: The behavior when a message fails to generate. (this is used as an alternative to allow_failed) - Returns: - The generated Chat. + Yields: + Pipeline steps. """ - if on_failed is None: - on_failed = "include" if allow_failed else self.on_failed if on_failed == "skip": raise ValueError( @@ -1552,41 +1654,81 @@ async def run(self, *, allow_failed: bool = False, on_failed: FailMode | None = await self._pre_run() on_failed = on_failed or self.on_failed - states = self._initialize_states(1) + + messages = [self.chat.all] + params = self._fit_params(1, [self.params]) with tracer.span( f"Chat with {self.generator.to_identifier()}", generator_id=self.generator.to_identifier(), params=self.params.to_dict() if self.params is not None else {}, ) as span: - return (await self._run(span, states, on_failed))[0] + async with aclosing(self._step(span, messages, params, on_failed)) as generator: + yield generator + + async def run(self, *, on_failed: FailMode | None = None, allow_failed: bool = False) -> Chat: + """ + Execute the generation process for a single message. + + Args: + on_failed: The behavior when a message fails to generate. + allow_failed: Deprecated, use `on_failed="include"`. + + Returns: + The generated Chat. + """ + if allow_failed: + warnings.warn( + "The 'allow_failed' argument is deprecated, use 'on_failed=\"include\"'.", + DeprecationWarning, + stacklevel=2, + ) + + if on_failed is None: + on_failed = "include" if allow_failed else self.on_failed + + last: PipelineStep | None = None + async with self.step(on_failed=on_failed) as steps: + async for step in steps: + last = step + + if last is None or last.state != "final": + raise RuntimeError("The pipeline did not complete successfully") + + if not last.chats: + raise RuntimeError("The pipeline process did not produce any chats") + + return last.chats[-1] __call__ = run # Many messages - async def run_many( + @asynccontextmanager + async def step_many( self, count: int, *, params: t.Sequence[GenerateParams | None] | None = None, on_failed: FailMode | None = None, - ) -> ChatList: + ) -> t.AsyncIterator[PipelineStepGenerator]: """ - Executes the generation process multiple times with the same inputs. + Step through the generation process in parallel over the same input. Args: - count: The number of times to execute the generation process. + count: The number of parallel generations. params: A sequence of parameters to be used for each execution. on_failed: The behavior when a message fails to generate. - Returns: - A list of generatated Chats. + Yields: + Pipeline steps. """ await self._pre_run() on_failed = on_failed or self.on_failed - states = self._initialize_states(count, params) + + messages = [self.chat.all] * count + params = self._fit_params(count, params) with tracer.span( f"Chat with {self.generator.to_identifier()} (x{count})", @@ -1594,11 +1736,42 @@ async def run_many( generator_id=self.generator.to_identifier(), params=self.params.to_dict() if self.params is not None else {}, ) as span: - return await self._run(span, states, on_failed) + async with aclosing(self._step(span, messages, params, on_failed)) as generator: + yield generator + + async def run_many( + self, + count: int, + *, + params: t.Sequence[GenerateParams | None] | None = None, + on_failed: FailMode | None = None, + ) -> ChatList: + """ + Executes the generation process in parallel over the same input. + + Args: + count: The number of times to execute the generation process. + params: A sequence of parameters to be used for each execution. + on_failed: The behavior when a message fails to generate. + + Returns: + A list of generatated Chats. + """ + + last: PipelineStep | None = None + async with self.step_many(count, params=params, on_failed=on_failed) as steps: + async for step in steps: + last = step + + if last is None or last.state != "final": + raise ValueError("The generation process did not complete successfully") + + return last.chats # Batch messages - async def run_batch( + @asynccontextmanager + async def step_batch( self, many: t.Sequence[t.Sequence[Message]] | t.Sequence[Message] @@ -1609,9 +1782,9 @@ async def run_batch( params: t.Sequence[GenerateParams | None] | None = None, *, on_failed: FailMode | None = None, - ) -> ChatList: + ) -> t.AsyncIterator[PipelineStepGenerator]: """ - Executes the generation process accross multiple input messages. + Step through the generation process over multiple inputs. Note: Anything already in this chat pipeline will be prepended to the input messages. @@ -1621,21 +1794,76 @@ async def run_batch( params: A sequence of parameters to be used for each set of messages. on_failed: The behavior when a message fails to generate. - Returns: - A list of generatated Chats. + Yields: + Pipeline steps. """ await self._pre_run() on_failed = on_failed or self.on_failed - states = self._initialize_batch_states(many, params) + + # Get the maximum of either incoming messages or params + + count = max(len(many), len(params) if params is not None else 0) + + # If we have less messages than params, we need to either: + # 1. Error because we have >1 messages that we can't reasonably + # zip with our parameters of a different length + # 2. Duplicate a single message we have len(params) times as the + # user is just batching only over parameters + + messages = [[*self.chat.all, *Message.fit_as_list(m)] for m in many] + if len(messages) < count: + if len(messages) != 1: + raise ValueError(f"Can't fit {len(messages)} messages to {count} params") + messages = messages * count + + params = self._fit_params(count, params) with tracer.span( - f"Chat batch with {self.generator.to_identifier()} ({len(states)})", - count=len(states), + f"Chat batch with {self.generator.to_identifier()} ({count})", + count=count, generator_id=self.generator.to_identifier(), params=self.params.to_dict() if self.params is not None else {}, ) as span: - return await self._run(span, states, on_failed, batch_mode=True) + async with aclosing(self._step(span, messages, params, on_failed)) as generator: + yield generator + + async def run_batch( + self, + many: t.Sequence[t.Sequence[Message]] + | t.Sequence[Message] + | t.Sequence[MessageDict] + | t.Sequence[str] + | MessageDict + | str, + params: t.Sequence[GenerateParams | None] | None = None, + *, + on_failed: FailMode | None = None, + ) -> ChatList: + """ + Executes the generation process over multiple input messages. + + Note: + Anything already in this chat pipeline will be prepended to the input messages. + + Args: + many: A sequence of sequences of messages to be generated. + params: A sequence of parameters to be used for each set of messages. + on_failed: The behavior when a message fails to generate. + + Returns: + A list of generatated Chats. + """ + + last: PipelineStep | None = None + async with self.step_batch(many, params=params, on_failed=on_failed) as steps: + async for step in steps: + last = step + + if last is None or last.state != "final": + raise ValueError("The generation process did not complete successfully") + + return last.chats # Generator iteration @@ -1676,8 +1904,7 @@ async def run_over( coros.append(sub.run(allow_failed=(on_failed != "raise"))) with tracer.span(f"Chat over {len(coros)} generators", count=len(coros)): - chats = await asyncio.gather(*coros) - return await self._post_run(chats, on_failed) + return ChatList(await asyncio.gather(*coros)) # Prompt binding diff --git a/rigging/completion.py b/rigging/completion.py index 2d4d6ae..6122298 100644 --- a/rigging/completion.py +++ b/rigging/completion.py @@ -336,15 +336,16 @@ def watch( *callbacks: The callback functions to be executed. allow_duplicates: Whether to allow (seemingly) duplicate callbacks to be added. - ``` - async def log(completions: list[Completion]) -> None: - ... - - await pipeline.watch(log).run() - ``` - Returns: The current instance. + + Example: + ``` + async def log(completions: list[Completion]) -> None: + ... + + await pipeline.watch(log).run() + ``` """ for callback in callbacks: if allow_duplicates or callback not in self.watch_callbacks: @@ -359,18 +360,19 @@ def then(self, callback: ThenCompletionCallback) -> "CompletionPipeline": Returning a Completion object from the callback will replace the current completion. for the remainder of the callbacks + return value of `run()`. - ``` - async def process(completion: Completion) -> Completion | None: - ... - - await pipeline.then(process).run() - ``` - Args: callback: The callback function to be executed. Returns: The current instance of the pipeline. + + Example: + ``` + async def process(completion: Completion) -> Completion | None: + ... + + await pipeline.then(process).run() + ``` """ self.then_callbacks.append(callback) return self @@ -383,18 +385,19 @@ def map(self, callback: MapCompletionCallback) -> "CompletionPipeline": You must return a list of completion objects from the callback which will represent the state of completions for the remainder of the callbacks and return. - ``` - async def process(completions: list[Completion]) -> list[Completion]: - ... - - await pipeline.map(process).run() - ``` - Args: callback: The callback function to be executed. Returns: The current instance of the completion. + + Example: + ``` + async def process(completions: list[Completion]) -> list[Completion]: + ... + + await pipeline.map(process).run() + ``` """ self.map_callbacks.append(callback) return self @@ -493,18 +496,6 @@ def until( """ Registers a callback to participate in validating the generation process. - ```py - # Takes the generated text, and returns whether or not to retry generation. - - def callback(text: str) -> bool: - if is_valid(text): - return False - else: - return True - - await pipeline.until(callback).run() - ``` - Args: callback: The callback function to be executed. use_all_text: Whether to pass the entire text (including prompt) to the callback. @@ -514,6 +505,19 @@ def callback(text: str) -> bool: Returns: The current instance of the completion. + + Example: + ``` + # Takes the generated text, and returns whether or not to retry generation. + + def callback(text: str) -> bool: + if is_valid(text): + return False + else: + return True + + await pipeline.until(callback).run() + ``` """ self.until_callbacks.append((callback, use_all_text, max_rounds)) return self @@ -733,10 +737,16 @@ async def _run( # noqa: PLR0912 pending_states = states while pending_states: try: - inbounds = await self.generator.generate_texts( + _inbounds = await self.generator.generate_texts( [(self.text + s.text) if batch_mode else s.text for s in pending_states], [s.params for s in pending_states], ) + + for inbound in _inbounds: + if isinstance(inbound, Exception): + raise inbound # noqa: TRY301 + + inbounds = [inbound for inbound in _inbounds if isinstance(inbound, GeneratedText)] except Exception as e: # noqa: BLE001 if on_failed == "raise" or not any( isinstance(e, t) for t in self.errors_to_fail_on diff --git a/rigging/error.py b/rigging/error.py index 398c48d..350c1de 100644 --- a/rigging/error.py +++ b/rigging/error.py @@ -10,6 +10,7 @@ import typing_extensions as te if t.TYPE_CHECKING: + from rigging.chat import PipelineStep from rigging.message import Message @@ -66,6 +67,19 @@ def __init__(self, max_rounds: int, completion: str): """The completion which was being generated when the exception occured.""" +class MaxDepthError(Exception): + """ + Raised when the maximum depth is exceeded while generating. + """ + + def __init__(self, max_depth: int, step: "PipelineStep", reference: str): + super().__init__(f"Exceeded max depth ({max_depth}) while generating ('{reference}')") + self.max_depth = max_depth + """The maximum depth of nested pipeline generations which was exceeded.""" + self.step = step + """The pipeline step which cause the depth error.""" + + class InvalidModelSpecifiedError(Exception): """ Raised when an invalid identifier is specified when getting a generator. diff --git a/rigging/generator/base.py b/rigging/generator/base.py index 9b3b625..ab3523d 100644 --- a/rigging/generator/base.py +++ b/rigging/generator/base.py @@ -328,15 +328,16 @@ def watch( *callbacks: The callback functions to be executed. allow_duplicates: Whether to allow (seemingly) duplicate callbacks to be added. - ``` - async def log(chats: list[Chat]) -> None: - ... - - await pipeline.watch(log).run() - ``` - Returns: The current instance of the chat. + + Example: + ``` + async def log(chats: list[Chat]) -> None: + ... + + await pipeline.watch(log).run() + ``` """ for callback in callbacks: if allow_duplicates or callback not in self._watch_callbacks: @@ -422,7 +423,7 @@ async def generate_messages( self, messages: t.Sequence[t.Sequence[Message]], params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedMessage]: + ) -> t.Sequence[GeneratedMessage | BaseException]: """ Generate a batch of messages using the specified parameters. @@ -445,7 +446,7 @@ async def generate_texts( self, texts: t.Sequence[str], params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedText]: + ) -> t.Sequence[GeneratedText | BaseException]: """ Generate a batch of text completions using the generator. @@ -505,7 +506,7 @@ def chat( params: Optional parameters for generating responses. Returns: - chat pipeline to run. + The chat pipeline to run. """ from rigging.chat import ChatPipeline, WatchChatCallback @@ -596,8 +597,7 @@ def chat( Args: generator: The generator to use for creating the chat. - messages: - The messages to include in the chat. Can be a single message or a sequence of messages. + messages: The messages to include in the chat. Can be a single message or a sequence of messages. params: Additional parameters for generating the chat. Returns: @@ -762,6 +762,9 @@ def register_generator(provider: str, generator_cls: type[Generator] | LazyGener Args: provider: The name of the provider. generator_cls: The generator class to register. + + Returns: + None """ global g_providers # noqa: PLW0602 g_providers[provider] = generator_cls @@ -786,7 +789,7 @@ def trace_messages( logger.trace("---") -def trace_str(content: str | GeneratedText, title: str) -> None: +def trace_str(content: str | GeneratedText | BaseException, title: str) -> None: """ Helper function to trace log a string. diff --git a/rigging/generator/http.py b/rigging/generator/http.py index 388f3f5..4c366f5 100644 --- a/rigging/generator/http.py +++ b/rigging/generator/http.py @@ -251,33 +251,34 @@ class HTTPGenerator(Generator): You can pass this spec as a python dictionary, JSON string, YAML string, or a base64 encoded JSON/YAML string. - ```python - import rigging as rg - - spec = r\""" - request: - url: "https://{{ model }}.crucible.dreadnode.io/submit" - headers: - "X-Api-Key": "{{ api_key }}" - "Content-Type": "application/json" - transforms: - - type: "json" - pattern: { - "data": "$content" - } - response: - transforms: - - type: "jsonpath" - pattern: $.flag,output,message - \""" - - crucible = rg.get_generator("http!test,api_key=") - crucible.spec = spec - - chat = await crucible.chat("How about a flag?").run() - - print(chat.conversation) - ``` + Example: + ``` + import rigging as rg + + spec = r\""" + request: + url: "https://{{ model }}.crucible.dreadnode.io/submit" + headers: + "X-Api-Key": "{{ api_key }}" + "Content-Type": "application/json" + transforms: + - type: "json" + pattern: { + "data": "$content" + } + response: + transforms: + - type: "jsonpath" + pattern: $.flag,output,message + \""" + + crucible = rg.get_generator("http!test,api_key=") + crucible.spec = spec + + chat = await crucible.chat("How about a flag?").run() + + print(chat.conversation) + ``` """ model_config = ConfigDict(validate_assignment=True) @@ -377,11 +378,11 @@ async def generate_messages( ) -> t.Sequence[GeneratedMessage]: coros = [ self._generate_message(_messages, _params) - for _messages, _params in zip(messages, params, strict=False) + for _messages, _params in zip(messages, params, strict=True) ] generated = await asyncio.gather(*coros) - for i, (_messages, response) in enumerate(zip(messages, generated, strict=False)): + for i, (_messages, response) in enumerate(zip(messages, generated, strict=True)): trace_messages(_messages, f"Messages {i + 1}/{len(messages)}") trace_messages([response], f"Response {i + 1}/{len(messages)}") diff --git a/rigging/generator/litellm_.py b/rigging/generator/litellm_.py index 66532c0..9740386 100644 --- a/rigging/generator/litellm_.py +++ b/rigging/generator/litellm_.py @@ -86,7 +86,7 @@ class LiteLLMGenerator(Generator): or [`min_delay_between_requests`][rigging.generator.litellm_.LiteLLMGenerator.min_delay_between_requests if you run into API limits. You can pass this directly in the generator id: - ```py + ``` get_generator("litellm!openai/gpt-4o,max_connections=2,min_delay_between_requests=1000") ``` """ @@ -149,8 +149,15 @@ async def supports_function_calling(self) -> bool | None: ], ) - if generated and generated[0].message.tool_calls: - self._supports_function_calling = True + if generated: + if isinstance(generated[0], BaseException): + raise generated[0] # noqa: TRY301 + + if ( + isinstance(generated[0], GeneratedMessage) + and generated[0].message.tool_calls + ): + self._supports_function_calling = True except Exception as e: # noqa: BLE001 logger.warning(f"Failed to check for function calling support: {e}") span.set_attribute("error", str(e)) @@ -300,16 +307,19 @@ async def generate_messages( self, messages: t.Sequence[t.Sequence[Message]], params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedMessage]: + ) -> t.Sequence[GeneratedMessage | BaseException]: coros = [ self._generate_message(_messages, _params) - for _messages, _params in zip(messages, params, strict=False) + for _messages, _params in zip(messages, params, strict=True) ] - generated = await asyncio.gather(*coros) + generated = await asyncio.gather(*coros, return_exceptions=True) - for i, (_messages, response) in enumerate(zip(messages, generated, strict=False)): + for i, (_messages, response) in enumerate(zip(messages, generated, strict=True)): trace_messages(_messages, f"Messages {i+1}/{len(messages)}") - trace_messages([response], f"Response {i+1}/{len(messages)}") + if isinstance(response, BaseException): + trace_str(str(response), f"Response {i+1}/{len(messages)}") + else: + trace_messages([response], f"Response {i+1}/{len(messages)}") return generated @@ -317,23 +327,15 @@ async def generate_texts( self, texts: t.Sequence[str], params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedText]: - generated: list[GeneratedText] = [] - max_connections = self.max_connections if self.max_connections > 0 else len(texts) - for i in range(0, len(texts), max_connections): - chunk_texts = texts[i : i + max_connections] - chunk_params = params[i : i + max_connections] - chunk_generated = await asyncio.gather( - *[ - self._generate_text(text, _params) - for text, _params in zip(chunk_texts, chunk_params, strict=False) - ], - ) - generated.extend(chunk_generated) + ) -> t.Sequence[GeneratedText | BaseException]: + coros = [ + self._generate_text(text, _params) for text, _params in zip(texts, params, strict=True) + ] + generated = await asyncio.gather(*coros, return_exceptions=True) - for k, (text, response) in enumerate(zip(chunk_texts, chunk_generated, strict=False)): - trace_str(text, f"Text {k+1}/{len(texts)}") - trace_str(response, f"Generated {k+1}/{len(texts)}") + for i, (text, response) in enumerate(zip(texts, generated, strict=True)): + trace_str(text, f"Text {i+1}/{len(texts)}") + trace_str(response, f"Response {i+1}/{len(texts)}") return generated diff --git a/rigging/message.py b/rigging/message.py index f18c3b7..07dab59 100644 --- a/rigging/message.py +++ b/rigging/message.py @@ -7,10 +7,12 @@ import mimetypes import string import typing as t +import warnings from pathlib import Path from textwrap import dedent from uuid import UUID, uuid4 +import typing_extensions as te from pydantic import ( BaseModel, ConfigDict, @@ -302,13 +304,18 @@ def rename_content(self, handler: SerializerFunctionWrapHandler) -> t.Any: return serialized @property + @te.deprecated(".all_content is deprecated, use .content_parts instead", category=None) def all_content(self) -> str | list[Content]: """ Returns all content parts of the message or the single text content part as a string. - Deprecated: - Use `.content_parts` instead + Deprecated - Use `.content_parts` instead """ + warnings.warn( + ".all_content is deprecated, use .content_parts instead.", + DeprecationWarning, + stacklevel=2, + ) if len(self.content_parts) == 1 and isinstance(self.content_parts[0], ContentText): return self.content_parts[0].text return self.content_parts @@ -395,7 +402,8 @@ def to_openai_spec(self) -> dict[str, t.Any]: next_ = obj["content"][i + 1] if ( - current.get("type") == "text" + isinstance(current, dict) + and current.get("type") == "text" and next_.get("type") == "text" and not current.get("text", "").endswith("\n") ): diff --git a/rigging/model.py b/rigging/model.py index e140ae7..4aaae03 100644 --- a/rigging/model.py +++ b/rigging/model.py @@ -338,7 +338,7 @@ def from_text(cls, content: str) -> list[tuple[te.Self, slice]]: if not isinstance(field, XmlEntityInfo) ) if field.annotation in BASIC_TYPES: - model.__dict__[name] = field.annotation(unescape_xml(inner)) + model.__dict__[name] = field.annotation(unescape_xml(inner).strip()) extracted.append((model, slice(match.start(), match.end()))) except Exception as e: # noqa: BLE001 @@ -698,16 +698,21 @@ class DelimitedAnswer(Model): content: str _delimiters: t.ClassVar[list[str]] = [",", "-", "/", "|"] + _items: list[str] | None = None @property def items(self) -> list[str]: """Parsed items from the content.""" + if self._items is not None: + return self._items + split_sizes: dict[str, int] = {} for delimiter in self._delimiters: split_sizes[delimiter] = len(self.content.split(delimiter)) delimiter = max(split_sizes, key=split_sizes.get) # type: ignore [arg-type] split = [i.strip(" \"'\t\r\n") for i in self.content.split(delimiter)] - return [s for s in split if s] + self._items = [s for s in split if s] + return self._items @field_validator("content", mode="before") @classmethod diff --git a/rigging/prompt.py b/rigging/prompt.py index 9bc49cf..80d5f07 100644 --- a/rigging/prompt.py +++ b/rigging/prompt.py @@ -13,7 +13,14 @@ from pydantic import ValidationError from typing_extensions import Concatenate, ParamSpec # noqa: UP035 -from rigging.chat import Chat, ChatPipeline +from rigging.chat import ( + Chat, + ChatPipeline, + MapChatCallback, + PipelineStepContextManager, + ThenChatCallback, + WatchChatCallback, +) from rigging.generator.base import GenerateParams, Generator, get_generator from rigging.message import Message from rigging.model import Model, SystemErrorModel, ValidationErrorModel, make_primitive @@ -21,15 +28,13 @@ from rigging.tracing import tracer from rigging.util import escape_xml, get_qualified_name, to_snake, to_xml_tag -if t.TYPE_CHECKING: - from rigging.chat import WatchChatCallback - DEFAULT_DOC = "Convert the following inputs to outputs ({func_name})." """Default docstring if none is provided to a prompt function.""" -DEFAULT_MAX_ROUNDS = 3 -"""Default maximum number of rounds for a prompt to run until outputs are parsed.""" - +DEFAULT_MAX_PARSE_ROUNDS = 3 +"""Default maximum number of recurive output parsing attempts to allow.""" +DEFAULT_MAX_TOOL_ROUNDS = 20 +"""Default maximum number of recursive tool calls to allow.""" P = ParamSpec("P") R = t.TypeVar("R") @@ -44,9 +49,10 @@ class Ctx: You can use this annotation on inputs and ouputs to prompt functions. - ``` - tag_override = Annotated[str, Ctx(tag="custom_tag", ...)] - ``` + Example: + ``` + tag_override = Annotated[str, Ctx(tag="custom_tag", ...)] + ``` """ tag: str | None = None @@ -390,7 +396,7 @@ def parse_output(annotation: t.Any, error_name: str, *, allow_nested: bool = Tru for field, field_annotation in zip( dataclasses.fields(annotation), interior_annotations, - strict=False, + strict=True, ): interior = parse_output( field_annotation, @@ -435,12 +441,10 @@ class Prompt(t.Generic[P, R]): func: t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | None = None """The function that the prompt was derived from.""" - attempt_recovery: bool = True - """Whether the prompt should attempt to recover from errors in output parsing.""" - drop_dialog: bool = True - """When attempting recovery, whether to drop intermediate dialog while parsing was being resolved.""" - max_rounds: int = DEFAULT_MAX_ROUNDS - """The maximum number of rounds the prompt should try to reparse outputs.""" + max_parsing_rounds: int = DEFAULT_MAX_PARSE_ROUNDS + """The maximum number of recursive output parsing attempts to allow.""" + max_tool_rounds: int = DEFAULT_MAX_TOOL_ROUNDS + """The maximum number of recursive tool calls to allow.""" inputs: list[Input] = dataclasses.field(default_factory=list) """The structured input handlers for the prompt.""" @@ -449,6 +453,11 @@ class Prompt(t.Generic[P, R]): watch_callbacks: "list[WatchChatCallback]" = dataclasses.field(default_factory=list) """Callbacks to be passed any chats produced while executing this prompt.""" + then_callbacks: "list[ThenChatCallback]" = dataclasses.field(default_factory=list) + """Callbacks to be executed for every generated chat (see ChatPipeline.then).""" + map_callbacks: "list[MapChatCallback]" = dataclasses.field(default_factory=list) + """Callbacks to be executed for every generated chat (see ChatPipeline.map).""" + params: GenerateParams | None = None """The parameters to be used when generating chats for this prompt.""" tools: list[Tool] = dataclasses.field(default_factory=list) @@ -542,42 +551,41 @@ def _resolve_to_pipeline(self, other: ChatPipeline | Generator | Chat | str) -> return get_generator(other).chat() raise ValueError(f"Invalid type for binding: {type(other)}") - def _until_parsed(self, message: Message) -> tuple[bool, list[Message]]: - should_continue: bool = False - generated: list[Message] = [message] - + async def _then_parse(self, chat: Chat) -> PipelineStepContextManager | None: if self.output is None or isinstance(self.output, ChatOutput): - return (should_continue, generated) + return None + + next_pipeline = chat.restart(include_all=True) try: # A bit weird, but we need from_chat to properly handle # wrapping Chat output types inside lists/dataclasses - self.output.from_chat(Chat([], generated=[message])) + self.output.from_chat(chat) except ValidationError as e: - should_continue = True - generated.append( + next_pipeline.add( Message.from_model( ValidationErrorModel(content=str(e)), - suffix="Rewrite your entire message with all the required elements.", + suffix="Rewrite your entire message with all the required xml structure.", ), ) except Exception as e: # noqa: BLE001 - should_continue = True - generated.append( + next_pipeline.add( Message.from_model( SystemErrorModel(content=str(e)), - suffix="Rewrite your entire message with all the required elements.", + suffix="Rewrite your entire message with all the required xml structure.", ), ) + else: # parsed successfully + return None - return (should_continue, generated) + return next_pipeline.step() - def clone(self, *, skip_callbacks: bool = False) -> "Prompt[P, R]": + def clone(self, *, include_callbacks: bool = True) -> "Prompt[P, R]": """ Creates a deep copy of this prompt. Args: - skip_callbacks: Whether to skip copying the watch callbacks. + include_callbacks: Whether to skip copying the watch callbacks. Returns: A new instance of the prompt. @@ -586,89 +594,180 @@ def clone(self, *, skip_callbacks: bool = False) -> "Prompt[P, R]": func=self.func, _pipeline=self.pipeline, params=self.params.model_copy() if self.params is not None else None, - attempt_recovery=self.attempt_recovery, - drop_dialog=self.drop_dialog, - max_rounds=self.max_rounds, + max_parsing_rounds=self.max_parsing_rounds, + max_tool_rounds=self.max_tool_rounds, system_prompt=self.system_prompt, ) - if not skip_callbacks: + if not include_callbacks: new.watch_callbacks = self.watch_callbacks.copy() + new.then_callbacks = self.then_callbacks.copy() return new - def with_(self, params: GenerateParams | None = None, **kwargs: t.Any) -> "Prompt[P, R]": + def watch( + self, + *callbacks: WatchChatCallback, + allow_duplicates: bool = False, + ) -> "Prompt[P, R]": """ - Assign specific generation parameter overloads for this prompt. + Registers a callback to monitor any chats produced for this prompt + + See ChatPipeline.watch for more details. Args: - params: The parameters to set for the underlying chat pipeline. - **kwargs: An alternative way to pass parameters as keyword arguments. + *callbacks: The callback functions to be executed. + allow_duplicates: Whether to allow duplicate callbacks. Returns: - Self + The updated prompt instance. + + Example: + ``` + async def log(chats: list[Chat]) -> None: + ... + + @rg.prompt() + async def summarize(text: str) -> str: + ... + + summarize.watch(log)(...) + ``` """ - self.params = params if params is not None else GenerateParams(**kwargs) - return self + for callback in callbacks: + if not allow_duplicates and callback in self.watch_callbacks: + raise ValueError( + f"Callback '{get_qualified_name(callback)}' is already registered.", + ) - # We could put these params into the decorator, but it makes it - # less flexible when we want to build gateway interfaces into - # creating a prompt from other code. + self.watch_callbacks.extend(callbacks) + return self - def set_( + def then( self, - attempt_recovery: bool | None = None, - drop_dialog: bool | None = None, - max_rounds: int | None = None, + *callbacks: ThenChatCallback, + allow_duplicates: bool = False, ) -> "Prompt[P, R]": """ - Helper to allow updates to the parsing configuration. + Registers one or many callbacks to be executed during the prompt run + + See ChatPipeline.then for more details. Args: - attempt_recovery: Whether the prompt should attempt to recover from errors in output parsing. - drop_dialog: When attempting recovery, whether to drop intermediate dialog while parsing was being resolved. - max_rounds: The maximum number of rounds the prompt should try to reparse outputs. + callbacks: The callback functions to be added. + max_depth: The maximum depth to allow recursive pipeline calls during this callback. Returns: - Self + The updated prompt. + + Example: + ``` + async def score_summary(chat: Chat) -> Chat: + ... + + @rg.prompt() + async def summarize(text: str) -> str: + ... + + summarize.then(score_summary)(...) + ``` """ - self.attempt_recovery = attempt_recovery or self.attempt_recovery - self.drop_dialog = drop_dialog or self.drop_dialog - self.max_rounds = max_rounds or self.max_rounds + for callback in callbacks: + if not asyncio.iscoroutinefunction(callback): + raise TypeError( + f"Callback '{get_qualified_name(callback)}' must be an async function", + ) + + if allow_duplicates: + continue + + if callback in self.then_callbacks: + raise ValueError( + f"Callback '{get_qualified_name(callback)}' is already registered.", + ) + + self.then_callbacks.extend(callbacks) return self - def watch(self, *callbacks: "WatchChatCallback") -> "Prompt[P, R]": + def map( + self, + *callbacks: MapChatCallback, + allow_duplicates: bool = False, + ) -> "Prompt[P, R]": """ - Registers a callback to monitor any chats produced for this prompt + Registers a callback to be executed for each chat produced during the prompt run. + + See ChatPipeline.map for more details. Args: - *callbacks: The callback functions to be executed. + callback: The callback function to be executed. + max_depth: The maximum depth to allow recursive pipeline calls during this callback. - ``` - async def log(chats: list[Chat]) -> None: - ... + Returns: + The updated pipeline. - @rg.prompt() - async def summarize(text: str) -> str: - ... + Example: + ``` + async def summarize_chats(chats: list[Chat]) -> list[Chat]: + ... - summarize.watch(log)(...) - ``` - or - ``` - async def log(chats: list[Chat]) -> None: - ... + @rg.prompt() + async def summarize(text: str) -> str: + ... - async def _summarize(text: str) -> str: - ... + summarize.map(summarize_chats).bind_many()(10, ...) + ``` + """ + for callback in callbacks: + if not asyncio.iscoroutinefunction(callback): + raise TypeError( + f"Callback '{get_qualified_name(callback)}' must be an async function", + ) - summarize = rg.prompt(_summarize).watch(log) - ``` + if allow_duplicates: + continue + + if callback in self.map_callbacks: + raise ValueError( + f"Callback '{get_qualified_name(callback)}' is already registered.", + ) + + self.map_callbacks.extend(callbacks) + return self + + def with_(self, params: GenerateParams | None = None, **kwargs: t.Any) -> "Prompt[P, R]": + """ + Assign specific generation parameter overloads for this prompt. + + Args: + params: The parameters to set for the underlying chat pipeline. + **kwargs: An alternative way to pass parameters as keyword arguments. Returns: Self """ - for callback in callbacks: - if callback not in self.watch_callbacks: - self.watch_callbacks.append(callback) + self.params = params if params is not None else GenerateParams(**kwargs) + return self + + # We could put these params into the decorator, but it makes it + # less flexible when we want to build gateway interfaces into + # creating a prompt from other code. + + def set_( + self, + max_parsing_rounds: int | None = None, + max_tool_rounds: int | None = None, + ) -> "Prompt[P, R]": + """ + Helper to allow updates to the parsing configuration. + + Args: + max_parsing_rounds: The maximum number of recursive output parsing attempts to allow. + max_tool_rounds: The maximum number of recursive tool calls to allow. + + Returns: + Self + """ + self.max_parsing_rounds = max_parsing_rounds or self.max_parsing_rounds + self.max_tool_rounds = max_tool_rounds or self.max_tool_rounds return self def _bind_args(self, *args: P.args, **kwargs: P.kwargs) -> t.OrderedDict[str, t.Any]: @@ -716,19 +815,20 @@ def bind( """ Binds the prompt to a pipeline, generator, or chat and returns a scoped run callable. - ``` - @rg.prompt - def say_hello(name: str) -> str: - \"""Say hello to {{ name }}\""" - - await say_hello.bind("gpt-3.5-turbo")("the world") - ``` - Args: other: The pipeline, generator, generator id, or chat to bind to. Returns: A callable for executing this prompt + + Example: + ``` + @rg.prompt + def say_hello(name: str) -> str: + \"""Say hello to {{ name }}\""" + + await say_hello.bind("gpt-3.5-turbo")("the world") + ``` """ pipeline = self._resolve_to_pipeline(other) if pipeline.on_failed == "skip": @@ -751,19 +851,20 @@ def bind_many( """ Binds the prompt to a pipeline, generator, or chat and returns a scoped run_many callable. - ``` - @rg.prompt - def say_hello(name: str) -> str: - \"""Say hello to {{ name }}\""" - - await say_hello.bind("gpt-3.5-turbo")(5, "the world") - ``` - Args: other: The pipeline, generator, generator id, or chat to bind to. Returns: A callable for executing this prompt. + + Example: + ``` + @rg.prompt + def say_hello(name: str) -> str: + \"""Say hello to {{ name }}\""" + + await say_hello.bind("gpt-3.5-turbo")(5, "the world") + ``` """ pipeline = self._resolve_to_pipeline(other) if pipeline.on_failed == "include" and not isinstance(self.output, ChatOutput): @@ -782,13 +883,11 @@ async def run_many(count: int, /, *args: P.args, **kwargs: P.kwargs) -> list[R]: content = self.render(*args, **kwargs) _pipeline = ( pipeline.fork(content) - .using(*self.tools) - .until( - self._until_parsed, - attempt_recovery=self.attempt_recovery, - drop_dialog=self.drop_dialog, - max_rounds=self.max_rounds, - ) + .using(*self.tools, max_depth=self.max_tool_rounds) + .then(self._then_parse, max_depth=self.max_parsing_rounds) + .then(*self.then_callbacks) + .map(*self.map_callbacks) + .watch(*self.watch_callbacks) .with_(self.params) ) @@ -838,19 +937,20 @@ def bind_over( """ Binds the prompt to a pipeline, generator, or chat and returns a scoped run_over callable. - ``` - @rg.prompt - def say_hello(name: str) -> str: - \"""Say hello to {{ name }}\""" - - await say_hello.bind("gpt-3.5-turbo")(["gpt-4o", "gpt-4"], "the world") - ``` - Args: other: The pipeline, generator, generator id, or chat to bind to. Returns: A callable for executing this prompt. + + Example: + ``` + @rg.prompt + def say_hello(name: str) -> str: + \"""Say hello to {{ name }}\""" + + await say_hello.bind("gpt-3.5-turbo")(["gpt-4o", "gpt-4"], "the world") + ``` """ include_original = other is not None @@ -875,13 +975,11 @@ async def run_over( content = self.render(*args, **kwargs) _pipeline = ( pipeline.fork(content) - .using(*self.tools) - .until( - self._until_parsed, - attempt_recovery=self.attempt_recovery, - drop_dialog=self.drop_dialog, - max_rounds=self.max_rounds, - ) + .using(*self.tools, max_depth=self.max_tool_rounds) + .then(self._then_parse, max_depth=self.max_parsing_rounds) + .then(*self.then_callbacks) + .map(*self.map_callbacks) + .watch(*self.watch_callbacks) .with_(self.params) ) @@ -1031,25 +1129,6 @@ def prompt( Convert a hollow function into a Prompt, which can be called directly or passed a chat pipeline to execute the function and parse the outputs. - ``` - from dataclasses import dataclass - import rigging as rg - - @dataclass - class ExplainedJoke: - chat: rg.Chat - setup: str - punchline: str - explanation: str - - @rg.prompt(generator_id="gpt-3.5-turbo") - async def write_joke(topic: str) -> ExplainedJoke: - \"""Write a joke.\""" - ... - - await write_joke("programming") - ``` - Note: A docstring is not required, but this can be used to provide guidance to the model, or even handle any number of input transormations. Any input parameter which is not @@ -1087,6 +1166,26 @@ async def write_joke(topic: str) -> ExplainedJoke: Returns: A prompt instance or a function that can be used to create a prompt. + + Example: + ``` + from dataclasses import dataclass + import rigging as rg + + @dataclass + class ExplainedJoke: + chat: rg.Chat + setup: str + punchline: str + explanation: str + + @rg.prompt(generator_id="gpt-3.5-turbo") + async def write_joke(topic: str) -> ExplainedJoke: + \"""Write a joke.\""" + ... + + await write_joke("programming") + ``` """ if sum(arg is not None for arg in (pipeline, generator, generator_id)) > 1: raise ValueError("Only one of pipeline, generator, or generator_id can be provided") @@ -1134,14 +1233,6 @@ def make_prompt( """ Create a prompt at runtime from a basic string and return type (experimental). - ``` - import rigging as rg - - write_joke = rg.make_prompt("Write a joke.", ctx=rg.Ctx(tag="joke")) - - await write_joke.bind("gpt-4o-mini")() - ``` - Note: Adding input parameters is not currently supported. Instead use the [rigging.prompt.prompt][] decorator. @@ -1153,6 +1244,15 @@ def make_prompt( Returns: The constructed Prompt + + Example: + ``` + import rigging as rg + + write_joke = rg.make_prompt("Write a joke.", ctx=rg.Ctx(tag="joke")) + + await write_joke.bind("gpt-4o-mini")() + ``` """ return_type = return_type or str # type: ignore [assignment] output = parse_output( diff --git a/rigging/tool/api.py b/rigging/tool/api.py index 6975266..0857242 100644 --- a/rigging/tool/api.py +++ b/rigging/tool/api.py @@ -69,3 +69,7 @@ class ApiToolCall(BaseModel): def __str__(self) -> str: return f"" + + @property + def name(self) -> str: + return self.function.name diff --git a/rigging/tool/base.py b/rigging/tool/base.py index c06bcbe..781fe4f 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -57,7 +57,7 @@ class Tool: # In general we are split between 2 strategies for handling the data translations: # - # 1. TypeAdapter applied straigh to a callable (`api` and `json-in-xml` modes) + # 1. TypeAdapter applied straight to a callable (`api` and `json-in-xml` modes) # 2. Dynamic Model class built from the signature (`xml` mode) # # TODO: I'd like to unify these and pick which strategy works best for us. I'm inclined @@ -218,7 +218,7 @@ def json_definition(self) -> JsonInXmlToolDefinition: async def handle_tool_call( self, tool_call: ApiToolCall | XmlToolCall | JsonInXmlToolCall, - ) -> "Message": + ) -> "Message | None": """ Handle an incoming tool call from a generator. @@ -226,14 +226,11 @@ async def handle_tool_call( tool_call: The tool call to handle. Returns: - The message to send back to the generator. + The message to send back to the generator or None if tool calling should not proceed. """ from rigging.message import ContentText, ContentTypes, Message - tool_call_name = ( - tool_call.function.name if isinstance(tool_call, ApiToolCall) else tool_call.name - ) tool_call_parameters = ( tool_call.function.arguments if isinstance(tool_call, ApiToolCall) @@ -241,9 +238,9 @@ async def handle_tool_call( ) with tracer.span(f"Tool {self.name}()", name=self.name) as span: - if tool_call_name != self.name: + if tool_call.name != self.name: raise ValueError( - f"Requested function name '{tool_call_name}' does not match '{self.name}'", + f"Requested function name '{tool_call.name}' does not match '{self.name}'", ) if isinstance(tool_call, ApiToolCall): @@ -292,6 +289,12 @@ async def handle_tool_call( else Message("user") ) + # If the tool returns nothing back to us, we'll assume that + # they do not want to proceed with additional tool calling + + if result is None: + return None + # If the tool gave us back anything that looks like a message, we'll # just pass it along. Otherwise we need to box up the result. @@ -311,7 +314,9 @@ async def handle_tool_call( # # TODO: It would be great to have some kind of identifier here to let # the model know what result is associated with what tool call when - # we aren't working with api calls. + # we aren't working with api calls + # + # (we'd likely have to insert the shared identifier upstream in the call) if ( len(message.content_parts) == 1 diff --git a/rigging/tool/mcp.py b/rigging/tool/mcp.py index 532c9b5..a3a2375 100644 --- a/rigging/tool/mcp.py +++ b/rigging/tool/mcp.py @@ -173,7 +173,7 @@ def mcp( An MCP client context manager. Example: - ```python + ``` with mcp("stdio", command="uv", args=["run", "weather-mcp"]) as mcp: chat = ( await get_generator("gpt-4o") @@ -208,7 +208,7 @@ def mcp( An MCP client context manager. Example: - ```python + ``` with mcp("sse", url="http://localhost:8000/weather") as mcp: chat = ( await get_generator("gpt-4o") diff --git a/rigging/tool/robopages.py b/rigging/tool/robopages.py index c36642c..8a022b6 100644 --- a/rigging/tool/robopages.py +++ b/rigging/tool/robopages.py @@ -56,8 +56,7 @@ def robopages(url: str, *, name_filter: str | None = None) -> list[Tool]: A list of integrated tools which leverage the Robopages server. Example: - - ```python + ``` import rigging as rg tools = rg.tool.robopages("http://localhost:8080") diff --git a/rigging/util.py b/rigging/util.py index 6c1aeb0..200c4cc 100644 --- a/rigging/util.py +++ b/rigging/util.py @@ -158,3 +158,16 @@ def truncate_string(content: str, max_length: int, *, sep: str = "...") -> str: remaining = max_length - len(sep) middle = remaining // 2 return content[:middle] + sep + content[-middle:] + + +# List utilities + + +def flatten_list(nested_list: t.Iterable[t.Iterable[t.Any] | t.Any]) -> list[t.Any]: + flattened = [] + for item in nested_list: + if isinstance(item, list): + flattened.extend(flatten_list(item)) + else: + flattened.append(item) + return flattened diff --git a/tests/generators.py b/tests/generators.py new file mode 100644 index 0000000..62d1915 --- /dev/null +++ b/tests/generators.py @@ -0,0 +1,87 @@ +import typing as t + +from rigging import Message +from rigging.generator import GenerateParams, Generator +from rigging.generator.base import GeneratedMessage, GeneratedText + +# ruff: noqa: S101, ARG002 + + +class FixedGenerator(Generator): + text: str + + async def generate_messages( + self, + messages: t.Sequence[t.Sequence[Message]], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedMessage]: + return [GeneratedMessage.from_text(self.text, stop_reason="stop") for _ in messages] + + async def generate_texts( + self, + texts: t.Sequence[str], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedText]: + return [GeneratedText.from_text(self.text, stop_reason="stop") for _ in texts] + + +class EchoGenerator(Generator): + async def generate_messages( + self, + messages: t.Sequence[t.Sequence[Message]], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedMessage]: + return [GeneratedMessage.from_text(m[-1].content, stop_reason="stop") for m in messages] + + async def generate_texts( + self, + texts: t.Sequence[str], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedText]: + return [GeneratedText.from_text(t, stop_reason="stop") for t in texts] + + +class CallbackGenerator(Generator): + message_callback: t.Callable[["CallbackGenerator", t.Sequence[Message]], str] | None = None + text_callback: t.Callable[["CallbackGenerator", str], str] | None = None + + async def generate_messages( + self, + messages: t.Sequence[t.Sequence[Message]], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedMessage]: + assert self.message_callback is not None + return [ + GeneratedMessage.from_text(self.message_callback(self, m), stop_reason="stop") + for m in messages + ] + + async def generate_texts( + self, + texts: t.Sequence[str], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedText]: + assert len(texts) == 1 + assert self.text_callback is not None + return [ + GeneratedText.from_text(self.text_callback(self, text), stop_reason="stop") + for text in texts + ] + + +class FailingGenerator(Generator): + _exception: Exception = RuntimeError("Intentional failure") + + async def generate_messages( + self, + messages: t.Sequence[t.Sequence[Message]], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedMessage]: + raise self._exception + + async def generate_texts( + self, + texts: t.Sequence[str], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedText]: + raise self._exception diff --git a/tests/test_chat.py b/tests/test_chat.py index d2df4c4..1bd88d7 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -7,6 +7,8 @@ from rigging.error import MissingModelError from rigging.generator import GenerateParams, get_generator +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001 + class Example(Model): content: str @@ -155,6 +157,7 @@ def test_message_reparse_modified_content() -> None: assert person.name == "Jane" assert person.age == 25 + def test_message_double_content_part_separation() -> None: msg = Message("user", ["hello", "world"]) assert msg.content == "hello\nworld" @@ -164,6 +167,7 @@ def test_message_double_content_part_separation() -> None: assert spec["content"][0]["text"] == "hello\n" assert spec["content"][1]["text"] == "world" + def test_chat_generator_id() -> None: generator = get_generator("gpt-3.5") chat = Chat([], generator=generator) @@ -294,7 +298,8 @@ def test_chat_continue_maintains_parsed_models() -> None: [ Message("user", "30"), Message( - "assistant", "
123 Main StAnytown
" + "assistant", + "
123 Main StAnytown
", ), ], generator=get_generator("base"), @@ -336,7 +341,8 @@ def test_chat_strip() -> None: [ Message("user", "30"), Message( - "assistant", "
123 Main StAnytown
" + "assistant", + "
123 Main StAnytown
", ), ], ) @@ -357,7 +363,8 @@ def test_chat_serialize() -> None: [ Message("user", "30"), Message( - "assistant", "
123 Main StAnytown
" + "assistant", + "
123 Main StAnytown
", ), ], ) @@ -440,7 +447,8 @@ def test_chat_pipeline_add_merge_strategy_none() -> None: # Test that assistant messages also don't merge pipeline.add( - [Message("assistant", "Hi!"), Message("assistant", "How are you?")], merge_strategy="none" + [Message("assistant", "Hi!"), Message("assistant", "How are you?")], + merge_strategy="none", ) assert len(pipeline.chat) == 4 assert pipeline.chat.all[2].content == "Hi!" diff --git a/tests/test_chat_pipeline.py b/tests/test_chat_pipeline.py new file mode 100644 index 0000000..3622c65 --- /dev/null +++ b/tests/test_chat_pipeline.py @@ -0,0 +1,212 @@ +import typing as t + +import pytest + +from rigging import Chat, Message +from rigging.chat import PipelineStepContextManager +from rigging.error import MaxDepthError +from rigging.generator import GenerateParams +from rigging.model import YesNoAnswer + +from .generators import ( + CallbackGenerator, + FailingGenerator, + FixedGenerator, +) + +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001 + + +@pytest.mark.asyncio() +async def test_basic_chat_pipeline() -> None: + generator = FixedGenerator( + model="fixed", + text="This is a fixed response", + params=GenerateParams(), + ) + chat = await generator.chat([{"role": "user", "content": "Hello"}]).run() + + assert len(chat) == 2 + assert chat.messages[0].role == "user" + assert chat.messages[0].content == "Hello" + assert chat.generated[0].role == "assistant" + assert chat.generated[0].content == "This is a fixed response" + assert chat.stop_reason == "stop" + assert not chat.failed + + +@pytest.mark.asyncio() +async def test_chat_until_parsed_as_basic() -> None: + generator = FixedGenerator( + model="fixed", + text="yes", + params=GenerateParams(), + ) + + chat = ( + await generator.chat([{"role": "user", "content": "Is the sky blue?"}]) + .until_parsed_as(YesNoAnswer) + .run() + ) + + assert len(chat) == 2 + assert chat.last.content == "yes" + assert chat.last.try_parse(YesNoAnswer) is not None + assert chat.last.parse(YesNoAnswer).boolean is True + assert not chat.failed + + +@pytest.mark.asyncio() +async def test_max_depth_limit() -> None: + max_depth = 3 + call_count = 0 + + async def recursive_callback(chat: Chat) -> PipelineStepContextManager: + nonlocal call_count + call_count += 1 + return chat.restart().step() + + generator = FixedGenerator(model="fixed", text="test response", params=GenerateParams()) + + chat = ( + await generator.chat("Hello") + .then(recursive_callback, max_depth=max_depth) + .run(on_failed="include") + ) + + assert chat.failed + assert isinstance(chat.error, MaxDepthError) + assert call_count == max_depth + 1 # One initial call + max_depth recursive calls + + +@pytest.mark.asyncio() +async def test_pipeline_steps() -> None: + should_continue = True + + async def callback(chat: Chat) -> PipelineStepContextManager | None: + nonlocal should_continue + if not should_continue: + return None + should_continue = False + return chat.restart().step() + + generator = FixedGenerator(model="fixed", text="test response", params=GenerateParams()) + + async with generator.chat("Hello").step() as steps: + all_steps = [step async for step in steps] + + assert len(all_steps) == 2 + assert all_steps[0].state == "generated" + assert all_steps[1].state == "final" + + async with generator.chat("Hello").then(callback).step() as steps: + all_steps = [step async for step in steps] + + assert len(all_steps) == 6 + assert all_steps[0].state == "generated" + assert all_steps[1].state == "generated" + assert all_steps[2].state == "callback" + assert all_steps[3].state == "final" + assert all_steps[4].state == "callback" + assert all_steps[5].state == "final" + assert len({id(step.pipeline) for step in all_steps}) == 2 + assert all_steps[0].pipeline is not all_steps[1].pipeline + assert all_steps[0].chats[0].messages[0].content == "Hello" + assert all_steps[0].chats[0].last.content == "test response" + + +@pytest.mark.asyncio() +async def test_chat_pipeline_error_handling() -> None: + generator = FailingGenerator(model="failing", params=GenerateParams()) + + # Test "raise" mode (default) + with pytest.raises(RuntimeError): + await generator.chat("test").run() + + # Should still raise if the error is not in the default list + with pytest.raises(RuntimeError): + await generator.chat("test").run(on_failed="include") + + # Now add it to the catch list + chat_catch = await generator.chat("test").catch(RuntimeError).run(on_failed="include") + assert chat_catch.failed + assert isinstance(chat_catch.error, RuntimeError) + + # Check for default error handling + generator._exception = MaxDepthError(0, None, "") # type: ignore [arg-type] + chat_catch = await generator.chat("test").run(on_failed="include") + assert chat_catch.failed + assert isinstance(chat_catch.error, MaxDepthError) + + # Skip should fail for single generations + with pytest.raises(ValueError): + await generator.chat("test").run(on_failed="skip") + + # No chats if we skip them all + chat_skip = await generator.chat("test").run_many(5, on_failed="skip") + assert len(chat_skip) == 0 + + +@pytest.mark.asyncio() +async def test_parsing_with_recovery() -> None: + response_sequence = [ + "Invalid response", # First response that fails to parse + "Still invalid", # Second response that fails to parse + "yes", # Finally a valid response + ] + current_response_index = 0 + + def get_next_response(self: CallbackGenerator, messages: t.Sequence[Message]) -> str: + nonlocal current_response_index + response = response_sequence[current_response_index] + current_response_index = min(current_response_index + 1, len(response_sequence) - 1) + return response + + generator = CallbackGenerator(model="callback", params=GenerateParams()) + generator.message_callback = get_next_response + + chat = await generator.chat("test").until_parsed_as(YesNoAnswer).run() + + # Should have gone through recovery and eventually succeeded + assert not chat.failed + assert chat.last.try_parse(YesNoAnswer) is not None + assert chat.last.parse(YesNoAnswer).boolean is True + assert len(chat.all) > 2 # Should have more than just the initial Q&A due to recovery attempts + + +@pytest.mark.asyncio() +async def test_map_callback() -> None: + generator = FixedGenerator(model="fixed", text="Response 1", params=GenerateParams()) + + async def double_chats(chats: list[Chat]) -> list[Chat]: + # Create a copy of each chat with a modified response + new_chats = [] + for chat in chats: + new_chat = chat.clone() + new_chat.generated[0].content = "Modified: " + new_chat.generated[0].content + new_chats.append(new_chat) + return chats + new_chats + + chats = ( + await generator.chat([{"role": "user", "content": "Hello"}]).map(double_chats).run_many(1) + ) + + assert len(chats) == 2 + assert chats[0].last.content == "Response 1" + assert chats[1].last.content == "Modified: Response 1" + + +@pytest.mark.asyncio() +async def test_watch_callback() -> None: + generator = FixedGenerator(model="fixed", text="Response", params=GenerateParams()) + + watch_calls = [] + + async def watch_function(chats: list[Chat]) -> None: + watch_calls.append(len(chats)) + + await generator.chat([{"role": "user", "content": "Hello"}]).watch(watch_function).run() + + # Watch should be called at least once + assert len(watch_calls) >= 1 + assert all(calls >= 1 for calls in watch_calls) diff --git a/tests/test_completion.py b/tests/test_completion_pipeline.py similarity index 98% rename from tests/test_completion.py rename to tests/test_completion_pipeline.py index d6177d0..1530bd6 100644 --- a/tests/test_completion.py +++ b/tests/test_completion_pipeline.py @@ -3,6 +3,8 @@ from rigging.completion import Completion, CompletionPipeline from rigging.generator import GenerateParams, get_generator +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001 + def test_completion_generator_id() -> None: generator = get_generator("gpt-3.5") diff --git a/tests/test_generation.py b/tests/test_generation.py deleted file mode 100644 index 7adea35..0000000 --- a/tests/test_generation.py +++ /dev/null @@ -1,188 +0,0 @@ -from __future__ import annotations - -import typing as t - -import pytest - -from rigging import Message -from rigging.error import ExhaustedMaxRoundsError -from rigging.generator import GenerateParams, Generator -from rigging.generator.base import GeneratedMessage, GeneratedText -from rigging.model import YesNoAnswer -from rigging.parsing import try_parse - - -class FixedGenerator(Generator): - text: str - - async def generate_messages( - self, - messages: t.Sequence[t.Sequence[Message]], - params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedMessage]: - return [GeneratedMessage.from_text(self.text) for _ in messages] - - async def generate_texts( - self, texts: t.Sequence[str], params: t.Sequence[GenerateParams] - ) -> t.Sequence[GeneratedText]: - return [GeneratedText.from_text(self.text) for _ in texts] - - -class EchoGenerator(Generator): - async def generate_messages( - self, - messages: t.Sequence[t.Sequence[Message]], - params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedMessage]: - return [GeneratedMessage.from_text(m[-1].content) for m in messages] - - async def generate_texts( - self, texts: t.Sequence[str], params: t.Sequence[GenerateParams] - ) -> t.Sequence[GeneratedText]: - return [GeneratedText.from_text(t) for t in texts] - - -class CallbackGenerator(Generator): - message_callback: t.Callable[[CallbackGenerator, t.Sequence[Message]], str] | None = None - text_callback: t.Callable[[CallbackGenerator, str], str] | None = None - - async def generate_messages( - self, - messages: t.Sequence[t.Sequence[Message]], - params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedMessage]: - assert self.message_callback is not None - return [GeneratedMessage.from_text(self.message_callback(self, m)) for m in messages] - - async def generate_texts( - self, texts: t.Sequence[str], params: t.Sequence[GenerateParams] - ) -> t.Sequence[GeneratedText]: - assert len(texts) == 1 - assert self.text_callback is not None - return [GeneratedText.from_text(self.text_callback(self, text)) for text in texts] - - -@pytest.mark.asyncio -async def test_chat_until_parsed_as_with_reset() -> None: - generator = CallbackGenerator(model="callback", params=GenerateParams()) - - def valid_cb(self: CallbackGenerator, messages: t.Sequence[Message]) -> str: - assert len(messages) == 1 - assert messages[0].content == "original" - return "yes" - - def invalid_cb(self: CallbackGenerator, messages: t.Sequence[Message]) -> str: - self.message_callback = valid_cb - return "dropped" - - generator.message_callback = invalid_cb - chat = await generator.chat([{"role": "user", "content": "original"}]).until_parsed_as(YesNoAnswer).run() - assert len(chat) == 2 - assert chat.last.try_parse(YesNoAnswer) is not None - - -@pytest.mark.parametrize("drop_dialog", [True, False]) -@pytest.mark.asyncio -async def test_chat_until_parsed_as_with_recovery(drop_dialog: bool) -> None: - generator = CallbackGenerator(model="callback", params=GenerateParams()) - - def valid_cb(self: CallbackGenerator, messages: t.Sequence[Message]) -> str: - assert len(messages) == 5 - assert messages[0].content == "original" - assert messages[1].content == "invalid1" - assert messages[3].content == "invalid2" - return "yes" - - def invalid_cb_2(self: CallbackGenerator, messages: t.Sequence[Message]) -> str: - assert len(messages) == 3, messages - assert messages[0].content == "original" - assert messages[1].content == "invalid1" - assert "" in messages[2].content - self.message_callback = valid_cb - return "invalid2" - - def invalid_cb_1(self: CallbackGenerator, messages: t.Sequence[Message]) -> str: - assert len(messages) == 1 - assert messages[0].content == "original" - self.message_callback = invalid_cb_2 - return "invalid1" - - generator.message_callback = invalid_cb_1 - chat = ( - await generator.chat([{"role": "user", "content": "original"}]) - .until_parsed_as(YesNoAnswer, attempt_recovery=True, drop_dialog=drop_dialog) - .run() - ) - - assert len(chat) == (2 if drop_dialog else 6) - assert chat.last.try_parse(YesNoAnswer) is not None - - -@pytest.mark.asyncio -async def test_completion_until_parsed_as_with_reset() -> None: - generator = CallbackGenerator(model="callback", params=GenerateParams()) - - def valid_cb(self: CallbackGenerator, text: str) -> str: - assert text == "original" - return "yes" - - def invalid_cb(self: CallbackGenerator, text: str) -> str: - self.text_callback = valid_cb - return "dropped" - - generator.text_callback = invalid_cb - completion = await generator.complete("original").until_parsed_as(YesNoAnswer).run() - assert try_parse(completion.generated, YesNoAnswer) is not None - - -@pytest.mark.parametrize("attempt_recovery", [True, False]) -@pytest.mark.asyncio -async def test_chat_run_allowed_failed(attempt_recovery: bool) -> None: - generator = EchoGenerator(model="callback", params=GenerateParams()) - max_rounds = 3 - - chat = ( - await generator.chat([{"role": "user", "content": "test"}]) - .until_parsed_as(YesNoAnswer, attempt_recovery=attempt_recovery, max_rounds=max_rounds) - .run(allow_failed=True) - ) - - assert chat.failed is True - assert isinstance(chat.error, ExhaustedMaxRoundsError) - assert len(chat) == ((max_rounds * 2) + 2 if attempt_recovery else 2) - assert chat.last.role == "assistant" - - -@pytest.mark.parametrize("text", ["test", "yes"]) -@pytest.mark.asyncio -async def test_chat_run_many_include_failed(text: str) -> None: - generator = FixedGenerator(model="callback", params=GenerateParams(), text=text) - - chats = ( - await generator.chat([{"role": "user", "content": "test"}]) - .until_parsed_as(YesNoAnswer) - .run_many(3, on_failed="include") - ) - - assert len(chats) == 3 - for chat in chats: - assert chat.failed is (True if "" not in text else False) - assert len(chat) == 2 - assert chat.last.content == text - - -@pytest.mark.asyncio -async def test_chat_run_batch_include_failed() -> None: - generator = EchoGenerator(model="callback", params=GenerateParams()) - - chats = ( - await generator.chat() - .until_parsed_as(YesNoAnswer) - .run_batch([[Message(role="user", content=f"test-{i}")] for i in range(3)], on_failed="include") - ) - - assert len(chats) == 3 - for i, chat in enumerate(chats): - assert chat.failed is True - assert len(chat) == 2 - assert chat.last.content == f"test-{i}" diff --git a/tests/test_generator_ids.py b/tests/test_generator_ids.py index 0a8e6dd..417485d 100644 --- a/tests/test_generator_ids.py +++ b/tests/test_generator_ids.py @@ -1,8 +1,17 @@ import pytest from rigging.error import InvalidModelSpecifiedError -from rigging.generator import GenerateParams, LiteLLMGenerator, get_generator, get_identifier, register_generator -from tests.test_generation import EchoGenerator +from rigging.generator import ( + GenerateParams, + LiteLLMGenerator, + get_generator, + get_identifier, + register_generator, +) + +from .generators import EchoGenerator + +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001 @pytest.mark.parametrize("identifier", ["test_model", "litellm!test_model"]) @@ -19,11 +28,14 @@ def test_get_generator_invalid_provider(identifier: str) -> None: @pytest.mark.parametrize( - "identifier, valid_params", + ("identifier", "valid_params"), [ ("litellm!test_model,max_tokens=123,top_p=10", GenerateParams(max_tokens=123, top_p=10)), ("litellm!test_model,temperature=0.5", GenerateParams(temperature=0.5)), - ("test_model,temperature=1.0,max_tokens=100", GenerateParams(max_tokens=100, temperature=1.0)), + ( + "test_model,temperature=1.0,max_tokens=100", + GenerateParams(max_tokens=100, temperature=1.0), + ), ], ) def test_get_generator_with_params(identifier: str, valid_params: GenerateParams) -> None: @@ -54,14 +66,18 @@ def test_get_identifier_no_extra() -> None: assert "extra" not in identifier -@pytest.mark.parametrize("identifier", ["litellm:invalid,stuff:test,t1/123", "litellm:invalid,stuff:test,t1/123"]) +@pytest.mark.parametrize( + "identifier", + ["litellm:invalid,stuff:test,t1/123", "bad:invalid,stuff:test,t1//;;123:"], +) def test_get_generator_invalid_structure_format(identifier: str) -> None: with pytest.raises(InvalidModelSpecifiedError): get_generator(identifier) @pytest.mark.parametrize( - "identifier", ["litellm:model,bad_param=123,temperature=1.0", "litellm:model,temperature=True"] + "identifier", + ["litellm:model,bad_param=123,temperature=1.0", "litellm:model,temperature=True"], ) def test_get_generator_invalid_params(identifier: str) -> None: with pytest.raises(InvalidModelSpecifiedError): diff --git a/tests/test_http_spec.py b/tests/test_http_spec.py index 4d78853..806f61a 100644 --- a/tests/test_http_spec.py +++ b/tests/test_http_spec.py @@ -1,10 +1,13 @@ import json + import pytest from pydantic import ValidationError from rigging.error import ProcessingError from rigging.generator.http import HTTPSpec, RequestTransformContext +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001 + def get_basic_context() -> RequestTransformContext: return RequestTransformContext( @@ -24,8 +27,10 @@ def test_basic_json_transform() -> None: "url": "https://api.example.com/v1/chat", "method": "POST", "headers": {"Authorization": "Bearer {{api_key}}"}, - "transforms": [{"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}], - } + "transforms": [ + {"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}, + ], + }, ) ctx = get_basic_context() @@ -59,9 +64,9 @@ def test_complex_jinja_transform() -> None: } } """, - } + }, ], - } + }, ) ctx = get_basic_context() @@ -95,7 +100,7 @@ def test_chained_transforms() -> None: """, }, ], - } + }, ) ctx = get_basic_context() @@ -140,7 +145,10 @@ def test_response_parsing() -> None: def test_regex_response_transform() -> None: spec = HTTPSpec( - request={"url": "https://api.example.com/chat", "transforms": [{"type": "json", "pattern": {}}]}, + request={ + "url": "https://api.example.com/chat", + "transforms": [{"type": "json", "pattern": {}}], + }, response={"transforms": [{"type": "regex", "pattern": r"content:\s*'([^']*)'"}]}, ) @@ -151,11 +159,12 @@ def test_regex_response_transform() -> None: def test_invalid_transform_type() -> None: with pytest.raises(ValidationError): - spec = HTTPSpec( - request={"url": "https://api.example.com/chat", "transforms": [{"type": "invalid", "pattern": {}}]} + HTTPSpec( + request={ + "url": "https://api.example.com/chat", + "transforms": [{"type": "invalid", "pattern": {}}], + }, ) - ctx = get_basic_context() - spec.make_request_body(ctx) def test_missing_variable_in_template() -> None: @@ -163,7 +172,7 @@ def test_missing_variable_in_template() -> None: request={ "url": "https://api.example.com/chat", "transforms": [{"type": "jinja", "pattern": "{{missing_variable}}"}], - } + }, ) ctx = get_basic_context() @@ -179,11 +188,14 @@ def test_nested_json_transforms() -> None: { "type": "json", "pattern": { - "conversation": {"messages": "$messages", "metadata": {"model": "$model", "params": "$params"}} + "conversation": { + "messages": "$messages", + "metadata": {"model": "$model", "params": "$params"}, + }, }, - } + }, ], - } + }, ) ctx = get_basic_context() @@ -204,7 +216,7 @@ def test_custom_header_templates() -> None: "X-Model-Version": "{{model}}-{{params.version|default('v1')}}", }, "transforms": [{"type": "json", "pattern": {}}], - } + }, ) ctx = get_basic_context() @@ -221,7 +233,9 @@ def test_parse_response_body_int() -> None: "url": "https://api.example.com/v1/chat", "method": "POST", "headers": {"Authorization": "Bearer {{api_key}}"}, - "transforms": [{"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}], + "transforms": [ + {"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}, + ], }, response={ "valid_status_codes": [200, 201], @@ -238,7 +252,9 @@ def test_parse_response_body_list() -> None: "url": "https://api.example.com/v1/chat", "method": "POST", "headers": {"Authorization": "Bearer {{api_key}}"}, - "transforms": [{"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}], + "transforms": [ + {"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}, + ], }, response={ "valid_status_codes": [200, 201], @@ -255,7 +271,9 @@ def test_parse_single_value_response_body() -> None: "url": "https://api.example.com/v1/chat", "method": "POST", "headers": {"Authorization": "Bearer {{api_key}}"}, - "transforms": [{"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}], + "transforms": [ + {"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}, + ], }, response={ "valid_status_codes": [200, 201], @@ -266,14 +284,15 @@ def test_parse_single_value_response_body() -> None: assert result == "1" - def test_jsonpath_transform_into_json() -> None: spec = HTTPSpec( request={ "url": "https://api.example.com/v1/chat", "method": "POST", "headers": {"Authorization": "Bearer {{api_key}}"}, - "transforms": [{"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}], + "transforms": [ + {"type": "json", "pattern": {"model": "$model", "messages": "$messages"}}, + ], }, response={ "valid_status_codes": [200, 201], diff --git a/tests/test_prompt.py b/tests/test_prompt.py index cf86372..eb9ef42 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -8,6 +8,7 @@ from rigging.chat import Chat # mypy: disable-error-code=empty-body +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001 def test_prompt_render_docstring_parse() -> None: @@ -54,7 +55,7 @@ async def hello(name: str) -> str: Produce the following output (use xml tags): - """ + """, ) @@ -72,7 +73,7 @@ async def greet(name: str, greeting: str = "Hello") -> str: Produce the following output (use xml tags): - """ + """, ) @@ -101,7 +102,7 @@ async def create_person(name: str, age: int) -> Person: - """ + """, ) @@ -121,7 +122,7 @@ async def generate_numbers(count: int) -> list[int]: Produce the following output for each item (use xml tags): - """ + """, ) @@ -143,7 +144,7 @@ async def create_user(username: str) -> tuple[str, int]: - """ + """, ) @@ -165,7 +166,7 @@ async def create_user(username: str) -> tuple[Annotated[str, rg.Ctx(tag="id")], - """ + """, ) @@ -197,7 +198,7 @@ async def register_user(username: str, email: str, age: int) -> User: - """ + """, ) @@ -213,7 +214,7 @@ async def foo(input_: str) -> Chat: Do something. bar - """ + """, ) @@ -246,7 +247,7 @@ async def register_user(username: str, email: str, age: int) -> User: [test@email.com] - """ + """, ) diff --git a/tests/test_tool.py b/tests/test_tool.py index b796bc6..710773e 100644 --- a/tests/test_tool.py +++ b/tests/test_tool.py @@ -14,6 +14,8 @@ from rigging.tool.base import Tool from rigging.tool.native import JsonInXmlToolCall, XmlToolCall, XmlToolDefinition +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001, FBT001, FBT002 + def test_tool_from_simple_callable() -> None: """Test creating a tool from a simple callable.""" @@ -61,7 +63,12 @@ def another_tool(x: int) -> int: def test_api_definition_generation() -> None: """Test that tools correctly generate API definitions.""" - def complex_function(name: str, age: int, tags: list[str] = ["default"], active: bool = True) -> dict[str, t.Any]: # noqa: B006 + def complex_function( + name: str, + age: int, + tags: list[str] = ["default"], # noqa: B006 + active: bool = True, + ) -> dict[str, t.Any]: """Process user data with complex parameters.""" return {"name": name, "age": age, "tags": tags, "active": active} @@ -189,22 +196,21 @@ def config_function(name: str, version: int, features: list[str] = []) -> dict[s class TestToolHandleCall: """Test suite for tool call handling.""" - @pytest.fixture + @pytest.fixture() def sample_tool(self) -> Tool: def calculator(a: int, b: int, operation: str = "add") -> int: """Perform math operations.""" if operation == "add": return a + b - elif operation == "multiply": + if operation == "multiply": return a * b - elif operation == "subtract": + if operation == "subtract": return a - b - else: - raise ValueError(f"Unknown operation: {operation}") + raise ValueError(f"Unknown operation: {operation}") return Tool.from_callable(calculator) - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_handle_api_tool_call(self, sample_tool: Tool) -> None: """Test handling API format tool calls.""" from rigging.tool.api import ApiFunctionCall, ApiToolCall @@ -212,17 +218,19 @@ async def test_handle_api_tool_call(self, sample_tool: Tool) -> None: tool_call = ApiToolCall( id="call123", function=ApiFunctionCall( - name="calculator", arguments=json.dumps({"a": 5, "b": 3, "operation": "multiply"}) + name="calculator", + arguments=json.dumps({"a": 5, "b": 3, "operation": "multiply"}), ), ) - response = await sample_tool.handle_tool_call(tool_call) + message = await sample_tool.handle_tool_call(tool_call) - assert response.role == "tool" - assert response.tool_call_id == "call123" - assert response.content == "15" + assert message is not None + assert message.role == "tool" + assert message.tool_call_id == "call123" + assert message.content == "15" - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_handle_xml_tool_call(self, sample_tool: Tool) -> None: """Test handling XML format tool calls.""" tool_call = XmlToolCall( @@ -232,24 +240,29 @@ async def test_handle_xml_tool_call(self, sample_tool: Tool) -> None: 10 2 subtract - """ + """, ).strip(), ) - response = await sample_tool.handle_tool_call(tool_call) + message = await sample_tool.handle_tool_call(tool_call) - assert response.role == "user" - assert response.content == '8' + assert message is not None + assert message.role == "user" + assert message.content == '8' - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_handle_json_xml_tool_call(self, sample_tool: Tool) -> None: """Test handling JSON-in-XML format tool calls.""" - tool_call = JsonInXmlToolCall(name="calculator", parameters=json.dumps({"a": 4, "b": 4, "operation": "add"})) + tool_call = JsonInXmlToolCall( + name="calculator", + parameters=json.dumps({"a": 4, "b": 4, "operation": "add"}), + ) - response = await sample_tool.handle_tool_call(tool_call) + message = await sample_tool.handle_tool_call(tool_call) - assert response.role == "user" - assert response.content == '8' + assert message is not None + assert message.role == "user" + assert message.content == '8' def test_make_from_signature() -> None: @@ -257,7 +270,6 @@ def test_make_from_signature() -> None: def test_func(name: str, age: int, tags: list[str] = []) -> None: # noqa: B006 """Test function for signature extraction.""" - pass signature = inspect.signature(test_func) model_class = make_from_signature(signature, "TestParams") diff --git a/tests/test_watchers.py b/tests/test_watchers.py index 7c7e233..f662e1a 100644 --- a/tests/test_watchers.py +++ b/tests/test_watchers.py @@ -8,17 +8,27 @@ from rigging.chat import Chat from rigging.message import Message +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001, FBT001, FBT002, N803 -@pytest.fixture + +@pytest.fixture() def sample_chats() -> list[Chat]: - chat1 = Chat(messages=[Message(role="user", content="Hello"), Message(role="assistant", content="Hi there!")]) + chat1 = Chat( + messages=[ + Message(role="user", content="Hello"), + Message(role="assistant", content="Hi there!"), + ], + ) chat2 = Chat( - messages=[Message(role="user", content="How are you?"), Message(role="assistant", content="I'm doing well!")] + messages=[ + Message(role="user", content="How are you?"), + Message(role="assistant", content="I'm doing well!"), + ], ) return [chat1, chat2] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_write_chats_to_jsonl(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" watcher = rg.watchers.write_chats_to_jsonl(output_file) @@ -41,7 +51,7 @@ async def test_write_chats_to_jsonl(tmp_path: Path, sample_chats: list[Chat]) -> assert message["content"] == original_chat.messages[i].content -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_write_chats_to_jsonl_append(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" watcher = rg.watchers.write_chats_to_jsonl(output_file) @@ -57,7 +67,7 @@ async def test_write_chats_to_jsonl_append(tmp_path: Path, sample_chats: list[Ch assert len(lines) == 2 -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_write_chats_to_jsonl_replace(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" @@ -76,9 +86,9 @@ async def test_write_chats_to_jsonl_replace(tmp_path: Path, sample_chats: list[C assert len(lines) == 1 saved_chat = json.loads(lines[0]) original_chat = sample_chats[0] - for i, message in enumerate(saved_chat["messages"]): - assert message["role"] == original_chat.messages[i].role - assert message["content"] == original_chat.messages[i].content + for k, message in enumerate(saved_chat["messages"]): + assert message["role"] == original_chat.messages[k].role + assert message["content"] == original_chat.messages[k].content # write another chat - should append since already replaced once await watcher2(sample_chats[1:2]) @@ -97,7 +107,7 @@ async def test_write_chats_to_jsonl_replace(tmp_path: Path, sample_chats: list[C class MockS3Client: - class exceptions: + class exceptions: # noqa: N801 class ClientError(Exception): def __init__(self, code: str): self.response = {"Error": {"Code": code}} @@ -137,7 +147,7 @@ def put_object(self, Bucket: str, Key: str, Body: str) -> None: self.buckets[Bucket][Key] = Body -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_write_chats_to_s3(sample_chats: list[Chat]) -> None: s3_mock_client = MockS3Client() diff --git a/tests/test_xml_parsing.py b/tests/test_xml_parsing.py index cc95204..2546e43 100644 --- a/tests/test_xml_parsing.py +++ b/tests/test_xml_parsing.py @@ -16,6 +16,8 @@ ) from rigging.parsing import parse_many +# ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001, FBT001, FBT002 + # Models to use during tests @@ -57,12 +59,18 @@ class Wrapped(Model): @pytest.mark.parametrize( - "content, models", + ("content", "models"), [ pytest.param("hello", [Answer(content="hello")], id="single_answer"), - pytest.param("Random data hello", [Question(content="hello")], id="single_question"), pytest.param( - " hello", [Question(content="hello")], id="single_question_with_unrelated_tag" + "Random data hello", + [Question(content="hello")], + id="single_question", + ), + pytest.param( + " hello", + [Question(content="hello")], + id="single_question_with_unrelated_tag", ), pytest.param( "helloworld", @@ -86,12 +94,12 @@ class Wrapped(Model): ), pytest.param( " Should I answer between tags? hello", - [Question(content=" Should I answer between tags? "), Answer(content="hello")], + [Question(content="Should I answer between tags?"), Answer(content="hello")], id="question_with_answer_tag_1", ), pytest.param( " Should I answer between tags? hello", - [Question(content=" Should I answer between tags? "), Answer(content="hello")], + [Question(content="Should I answer between tags?"), Answer(content="hello")], id="question_with_answer_tag_2", ), pytest.param( @@ -101,12 +109,17 @@ class Wrapped(Model): ), pytest.param( "\n- hello\n - world", - [DelimitedAnswer(content="\n- hello\n - world", _items=["hello", "world"])], + [DelimitedAnswer(content="- hello\n - world", _items=["hello", "world"])], id="newline_delimited_answer", ), pytest.param( "hello, world, foo | bar", - [DelimitedAnswer(content="hello, world, foo | bar", _items=["hello", "world", "foo | bar"])], + [ + DelimitedAnswer( + content="hello, world, foo | bar", + _items=["hello", "world", "foo | bar"], + ), + ], id="comma_delimited_answer", ), pytest.param( @@ -115,7 +128,7 @@ class Wrapped(Model): DelimitedAnswer( content="hello / world / foo / bar, test | value", _items=["hello", "world", "foo", "bar, test | value"], - ) + ), ], id="slash_delimited_answer", ), @@ -126,7 +139,11 @@ class Wrapped(Model): ), pytest.param( 'meowbark', - [Wrapped(inners=[Inner(type="cat", content="meow"), Inner(type="dog", content="bark")])], + [ + Wrapped( + inners=[Inner(type="cat", content="meow"), Inner(type="dog", content="bark")], + ), + ], id="wrapped", ), pytest.param( @@ -141,7 +158,7 @@ class Wrapped(Model): ), pytest.param( "Commentary about the `` tag\n\n\nsomething is and \n", - [Outer(content="\nsomething is and \n")], + [Outer(content="something is and ")], id="comment_before_tag", ), pytest.param( @@ -151,7 +168,11 @@ class Wrapped(Model): ), pytest.param( "level 1 level 2 level 3 still 2 back to 1", - [Outer(content="level 1 level 2 level 3 still 2 back to 1")], + [ + Outer( + content="level 1 level 2 level 3 still 2 back to 1", + ), + ], id="multiple_nested_levels", ), pytest.param( @@ -173,27 +194,39 @@ class Wrapped(Model): ) def test_xml_parsing(content: str, models: list[Model]) -> None: parsed = parse_many(content, *{type(m) for m in models}) - print(models) - print(parsed) - assert len(parsed) == len(models), "Failed to parse set" - for (obj, _), expected in zip(parsed, models): - print(obj) + for (obj, _), expected in zip(parsed, models, strict=False): assert ( obj.model_dump() == expected.model_dump() - ), f"Failed to parse model {expected.__class__.__name__} <- {str(obj)} ({parsed})" - - print("---") + ), f"Failed to parse model {expected.__class__.__name__} <- {obj!s} ({parsed})" @pytest.mark.parametrize( - "content, model, expectation", + ("content", "model", "expectation"), [ - pytest.param("yes", YesNoAnswer, does_not_raise(), id="yes_no_answer_1"), - pytest.param("no", YesNoAnswer, does_not_raise(), id="yes_no_answer_2"), - pytest.param("Yes", YesNoAnswer, does_not_raise(), id="yes_no_answer_3"), pytest.param( - "No, extra stuff", YesNoAnswer, does_not_raise(), id="yes_no_answer_4" + "yes", + YesNoAnswer, + does_not_raise(), + id="yes_no_answer_1", + ), + pytest.param( + "no", + YesNoAnswer, + does_not_raise(), + id="yes_no_answer_2", + ), + pytest.param( + "Yes", + YesNoAnswer, + does_not_raise(), + id="yes_no_answer_3", + ), + pytest.param( + "No, extra stuff", + YesNoAnswer, + does_not_raise(), + id="yes_no_answer_4", ), pytest.param( "No, stuff ", @@ -202,7 +235,10 @@ def test_xml_parsing(content: str, models: list[Model]) -> None: id="yes_no_answer_5", ), pytest.param( - "Invalid", YesNoAnswer, pytest.raises(ValueError), id="yes_no_answer_invalid" + "Invalid", + YesNoAnswer, + pytest.raises(ValueError), + id="yes_no_answer_invalid", ), pytest.param( "hello world", @@ -245,19 +281,29 @@ def test_xml_parsing(content: str, models: list[Model]) -> None: ), ], ) -def test_xml_parsing_with_validation(content: str, model: Model, expectation: t.ContextManager[t.Any]) -> None: +def test_xml_parsing_with_validation( + content: str, + model: Model, + expectation: t.ContextManager[t.Any], +) -> None: with expectation: model.from_text(content) @pytest.mark.parametrize( - "content, count, model", + ("content", "count", "model"), [ pytest.param( - "yesno", 2, YesNoAnswer, id="yes_no_many" + "yesno", + 2, + YesNoAnswer, + id="yes_no_many", ), pytest.param( - "1, 2, 3", 1, DelimitedAnswer, id="delimited_single" + "1, 2, 3", + 1, + DelimitedAnswer, + id="delimited_single", ), ], ) @@ -287,16 +333,7 @@ def test_nested_tag_parsing() -> None: assert inner.content == "nested content" -def test_same_tag_mentioned_in_text() -> None: - """Test that mentions of tags in text don't confuse the parser.""" - content = "Commentary about the `` tag\n\n\nsomething is and \n" - - outer, _ = Outer.one_from_text(content) - assert outer.content == "\nsomething is and \n" - - def test_nested_incomplete_tags() -> None: - """Test handling of content with incomplete nested tags.""" content = "Content with incomplete tag" outer, _ = Outer.one_from_text(content) From 64009eebc341a81fca7bf8e9ffe0fc0ec553ad30 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Tue, 8 Apr 2025 09:57:31 -0600 Subject: [PATCH 03/25] Version to v3.0.0-rc.0 --- pyproject.toml | 2 +- rigging/__init__.py | 4 ++-- rigging/version.py | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 rigging/version.py diff --git a/pyproject.toml b/pyproject.toml index 72fbbbe..ddf6177 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rigging" -version = "2.3.0" +version = "3.0.0-rc.0" description = "LLM Interaction Framework" authors = ["Nick Landers "] license = "MIT" diff --git a/rigging/__init__.py b/rigging/__init__.py index d9a6975..d65865a 100644 --- a/rigging/__init__.py +++ b/rigging/__init__.py @@ -30,9 +30,9 @@ from rigging.prompt import Ctx, Prompt, prompt from rigging.tool import Tool, mcp, robopages, tool from rigging.util import await_ +from rigging.version import VERSION -# TODO: Migrate to importlib for this -__version__ = "2.3.0" +__version__ = VERSION __all__ = [ "get_generator", diff --git a/rigging/version.py b/rigging/version.py new file mode 100644 index 0000000..3194a78 --- /dev/null +++ b/rigging/version.py @@ -0,0 +1,3 @@ +import importlib_metadata + +VERSION = importlib_metadata.version("rigging") From 4aab56616933224e2da2c2c3874aabd6cb8985eb Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 10 Apr 2025 11:00:43 -0600 Subject: [PATCH 04/25] Update CI to reflect dropping 3.9 support and __version__ updates from importlib --- .github/workflows/ci.yml | 12 +----------- .github/workflows/publish.yml | 13 +------------ 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd01b4e..684c599 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12"] runs-on: ubuntu-latest @@ -43,16 +43,6 @@ jobs: - name: Install package run: poetry install --all-extras - - name: Validate version - run: | - POETRY_VERSION=$(poetry version -s) - INIT_VERSION=$(poetry run python -c "import rigging; print(rigging.__version__)") - - if [ "$POETRY_VERSION" != "$INIT_VERSION" ]; then - echo "Version mismatch: pyproject.toml ($POETRY_VERSION) != __init__.py ($INIT_VERSION)" - exit 1 - fi - - name: Lint run: poetry run ruff check --output-format=github rigging diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fa4d21b..9d10e99 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Python uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b with: - python-version: "3.9" + python-version: "3.10" - name: Install Poetry uses: abatilo/actions-poetry@e78f54a89cb052fff327414dd9ff010b5d2b4dbd @@ -36,17 +36,6 @@ jobs: run: | TAG_VERSION=${GITHUB_REF#refs/tags/v} POETRY_VERSION=$(poetry version -s) - INIT_VERSION=$(poetry run python -c "import rigging; print(rigging.__version__)") - - if ! [[ $TAG_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Invalid tag format: $TAG_VERSION. Must be vX.X.X" - exit 1 - fi - - if [ "$POETRY_VERSION" != "$INIT_VERSION" ]; then - echo "Version mismatch: pyproject.toml ($POETRY_VERSION) != __init__.py ($INIT_VERSION)" - exit 1 - fi if [ "$TAG_VERSION" != "$POETRY_VERSION" ]; then echo "Tag ($TAG_VERSION) doesn't match pyproject.toml ($POETRY_VERSION)" From 0cc149fcb18f377713fcaa91b0acae06b2e94293 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 11 Apr 2025 10:00:26 -0600 Subject: [PATCH 05/25] Fix bug with Chat construction on error --- rigging/chat.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/rigging/chat.py b/rigging/chat.py index b3bab7a..e261d23 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -1410,18 +1410,25 @@ async def _step( # noqa: PLR0915, PLR0912 [ Chat( messages_, - [] if isinstance(generated_, BaseException) else [generated_.message], + [], + generator=self.generator, + pipeline=self, + metadata=self.metadata, + params=params_, + failed=True, + error=generated_, + ) + if isinstance(generated_, BaseException) + else Chat( + messages_, + [generated_.message], generator=self.generator, pipeline=self, metadata=self.metadata, params=params_, - stop_reason=None - if isinstance(generated_, BaseException) - else generated_.stop_reason, - usage=None if isinstance(generated_, BaseException) else generated_.usage, - extra=None if isinstance(generated_, BaseException) else generated_.extra, - failed=isinstance(generated_, BaseException), - error=generated_ if isinstance(generated_, BaseException) else None, + stop_reason=generated_.stop_reason, + usage=generated_.usage, + extra=generated_.extra, ) for messages_, params_, generated_ in zip( messages, From 6e1eae06b5be24fe51765ebee2768648ef5020d9 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 11 Apr 2025 13:18:43 -0600 Subject: [PATCH 06/25] Fix some callback clone issues. Fix tool handling when None is returned. Version to 3.0.0-rc.1 --- pyproject.toml | 2 +- rigging/chat.py | 20 ++++++++++++-------- rigging/tool/base.py | 7 ++++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ddf6177..4a7e0e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rigging" -version = "3.0.0-rc.0" +version = "3.0.0-rc.1" description = "LLM Interaction Framework" authors = ["Nick Landers "] license = "MIT" diff --git a/rigging/chat.py b/rigging/chat.py index e261d23..8cc05d9 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -890,14 +890,14 @@ def clone(self, *, only_messages: bool = False, chat: Chat | None = None) -> "Ch (callback, max_depth) if not hasattr(callback, "__self__") or not isinstance(callback.__self__, ChatPipeline) - else (types.MethodType(callback.__func__, self), max_depth) # type: ignore [union-attr] + else (types.MethodType(callback.__func__, new), max_depth) # type: ignore [union-attr] for callback, max_depth in self.then_callbacks.copy() ] new.map_callbacks = [ (callback, max_depth) if not hasattr(callback, "__self__") or not isinstance(callback.__self__, ChatPipeline) - else (types.MethodType(callback.__func__, self), max_depth) # type: ignore [union-attr] + else (types.MethodType(callback.__func__, new), max_depth) # type: ignore [union-attr] for callback, max_depth in self.map_callbacks.copy() ] @@ -1211,12 +1211,14 @@ async def _then_tools(self, chat: Chat) -> PipelineStepContextManager | None: if tool is None: raise UnknownToolError(tool_call.name) - message = await tool.handle_tool_call(tool_call) + message, _should_continue = await tool.handle_tool_call(tool_call) + next_pipeline.add(message) - if message is None: - should_continue = False - else: - next_pipeline.add(message) + # If the tool returns none, we should resolve tool calls, but + # not continue the pipeline. + + if not _should_continue: + should_continue = _should_continue # Need to prevent infinite loops and treat tool_choice like # an ephemeral setting which resets after the first tool call. @@ -1225,7 +1227,9 @@ async def _then_tools(self, chat: Chat) -> PipelineStepContextManager | None: next_pipeline.params.tool_choice = None if not should_continue: - return None + # TODO(nick): Type hints here stop us from mixing step generators + # and basic chat returns. + return next_pipeline.chat # type: ignore [return-value] return next_pipeline.step() diff --git a/rigging/tool/base.py b/rigging/tool/base.py index 781fe4f..fa57f39 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -218,7 +218,7 @@ def json_definition(self) -> JsonInXmlToolDefinition: async def handle_tool_call( self, tool_call: ApiToolCall | XmlToolCall | JsonInXmlToolCall, - ) -> "Message | None": + ) -> tuple["Message", bool]: """ Handle an incoming tool call from a generator. @@ -293,7 +293,8 @@ async def handle_tool_call( # they do not want to proceed with additional tool calling if result is None: - return None + message.content_parts = [ContentText(text="")] + return message, False # If the tool gave us back anything that looks like a message, we'll # just pass it along. Otherwise we need to box up the result. @@ -328,7 +329,7 @@ async def handle_tool_call( result=message.content_parts[0].text, ).to_pretty_xml() - return message + return message, True # Decorator From 311dfe3fca6f8a34115aa418addbd168009b5b05 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Mon, 14 Apr 2025 10:02:46 -0600 Subject: [PATCH 07/25] Add parameter to tools. Make tools callable directly. --- rigging/chat.py | 8 +++-- rigging/prompt.py | 10 +++--- rigging/tool/base.py | 74 ++++++++++++++++++++++++++++----------- rigging/tool/mcp.py | 2 +- rigging/tool/robopages.py | 4 +-- tests/test_tool.py | 49 +++++++++++++++++++++----- 6 files changed, 107 insertions(+), 40 deletions(-) diff --git a/rigging/chat.py b/rigging/chat.py index 8cc05d9..e99f64e 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -364,7 +364,7 @@ def inject_system_content(self, content: str) -> "Chat": self.messages[0].content += "\n\n" + content return self - def inject_tool_prompt(self, tools: t.Sequence[Tool], mode: ToolMode) -> "Chat": + def inject_tool_prompt(self, tools: t.Sequence[Tool[..., t.Any]], mode: ToolMode) -> "Chat": """ Injects a default tool use prompt into the system prompt. @@ -695,7 +695,7 @@ def __init__( """How to handle cache_control entries on messages.""" self.until_types: list[type[Model]] = [] - self.tools: list[Tool] = [] + self.tools: list[Tool[..., t.Any]] = [] self.tool_mode: ToolMode = "auto" self.api_tool_choice: ApiToolChoice | None = None self.inject_tool_prompt = True @@ -1066,7 +1066,9 @@ def wrap(self, func: t.Callable[[CallableT], CallableT]) -> "ChatPipeline": def using( self, - *tools: Tool | t.Callable[..., t.Any] | t.Sequence[Tool | t.Callable[..., t.Any]], + *tools: Tool[..., t.Any] + | t.Callable[..., t.Any] + | t.Sequence[Tool[..., t.Any] | t.Callable[..., t.Any]], mode: ToolMode | None = None, choice: ApiToolChoice | None = None, max_depth: int = DEFAULT_MAX_DEPTH, diff --git a/rigging/prompt.py b/rigging/prompt.py index 80d5f07..4b7db59 100644 --- a/rigging/prompt.py +++ b/rigging/prompt.py @@ -460,7 +460,7 @@ class Prompt(t.Generic[P, R]): params: GenerateParams | None = None """The parameters to be used when generating chats for this prompt.""" - tools: list[Tool] = dataclasses.field(default_factory=list) + tools: list[Tool[..., t.Any]] = dataclasses.field(default_factory=list) """The API tools to be made available when generating chats for this prompt.""" system_prompt: str | None = None """A system prompt fragment to be injected into the messages before generation.""" @@ -1078,7 +1078,7 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[Tool | t.Callable[..., t.Any]] | None = None, + tools: list[Tool[..., t.Any] | t.Callable[..., t.Any]] | None = None, system_prompt: str | None = None, ) -> t.Callable[[t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | t.Callable[P, R]], Prompt[P, R]]: ... @@ -1092,7 +1092,7 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[Tool | t.Callable[..., t.Any]] | None = None, + tools: list[Tool[..., t.Any] | t.Callable[..., t.Any]] | None = None, system_prompt: str | None = None, ) -> Prompt[P, R]: ... @@ -1106,7 +1106,7 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[Tool | t.Callable[..., t.Any]] | None = None, + tools: list[Tool[..., t.Any] | t.Callable[..., t.Any]] | None = None, system_prompt: str | None = None, ) -> Prompt[P, R]: ... @@ -1119,7 +1119,7 @@ def prompt( pipeline: ChatPipeline | None = None, generator: Generator | None = None, generator_id: str | None = None, - tools: list[Tool | t.Callable[..., t.Any]] | None = None, + tools: list[Tool[..., t.Any] | t.Callable[..., t.Any]] | None = None, system_prompt: str | None = None, ) -> ( t.Callable[[t.Callable[P, t.Coroutine[t.Any, t.Any, R]] | t.Callable[P, R]], Prompt[P, R]] diff --git a/rigging/tool/base.py b/rigging/tool/base.py index fa57f39..ab407d1 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -27,6 +27,9 @@ if t.TYPE_CHECKING: from rigging.message import Message +P = t.ParamSpec("P") +R = t.TypeVar("R") + ToolMode = t.Literal["auto", "api", "xml", "json-in-xml"] """ How tool calls are handled. @@ -39,7 +42,7 @@ @dataclass -class Tool: +class Tool(t.Generic[P, R]): """Base class for representing a tool to a generator.""" name: str @@ -48,8 +51,16 @@ class Tool: """A description of the tool.""" parameters_schema: dict[str, t.Any] """The JSON schema for the tool's parameters.""" - fn: t.Callable[..., t.Any] + fn: t.Callable[P, R] """The function to call.""" + catch: bool | set[type[Exception]] = False + """ + Whether to catch exceptions and return them as messages. + + - `False`: Do not catch exceptions. + - `True`: Catch all exceptions. + - `list[type[Exception]]`: Catch only the specified exceptions. + """ _signature: inspect.Signature | None = field(default=None, init=False, repr=False) _type_adapter: TypeAdapter[t.Any] | None = field(default=None, init=False, repr=False) @@ -68,11 +79,12 @@ class Tool: @classmethod def from_callable( cls, - fn: t.Callable[..., t.Any], + fn: t.Callable[P, R], *, name: str | None = None, description: str | None = None, - ) -> "Tool": + catch: bool | t.Iterable[type[Exception]] = False, + ) -> "Tool[P, R]": from rigging.prompt import Prompt fn_for_signature = fn @@ -84,7 +96,7 @@ def from_callable( if isinstance(fn, Prompt): fn_for_signature = fn.func # type: ignore [assignment] - fn = fn.run + fn = fn.run # type: ignore [assignment] # In the case that we are recieving a bound function which is tracking # an originating prompt, unwrap the prompt and use it's function for @@ -162,9 +174,11 @@ def empty_func(*args, **kwargs): # type: ignore [no-untyped-def] # noqa: ARG001 description=description or fn_for_signature.__doc__ or "", parameters_schema=schema, fn=fn, + catch=catch if isinstance(catch, bool) else set(catch), ) self._signature = signature + self.__signature__ = signature # type: ignore [attr-defined] # For handling API calls, we'll use the type adapter to validate # the arguments before calling the function @@ -226,7 +240,7 @@ async def handle_tool_call( tool_call: The tool call to handle. Returns: - The message to send back to the generator or None if tool calling should not proceed. + The message to send back to the generator or `None` if iterative tool calling should not proceed any further. """ from rigging.message import ContentText, ContentTypes, Message @@ -248,12 +262,12 @@ async def handle_tool_call( # Load + validate arguments - args: dict[str, t.Any] + kwargs: dict[str, t.Any] if isinstance(tool_call, ApiToolCall | JsonInXmlToolCall): - args = json.loads(tool_call_parameters) + kwargs = json.loads(tool_call_parameters) if self._type_adapter is not None: - args = self._type_adapter.validate_python(args) + kwargs = self._type_adapter.validate_python(kwargs) elif isinstance(tool_call, XmlToolCall): parsed = self.model.from_text( @@ -268,18 +282,26 @@ async def handle_tool_call( # argument object instances. We'll just flatten the # model into a dictionary for the function call. - args = { + kwargs = { field_name: getattr(parameters, field_name, None) for field_name in self.model.model_fields } - span.set_attribute("arguments", args) + span.set_attribute("arguments", kwargs) # Call the function - result = self.fn(**args) - if inspect.isawaitable(result): - result = await result + try: + result: t.Any = self.fn(**kwargs) # type: ignore [call-arg] + if inspect.isawaitable(result): + result = await result + except Exception as e: # noqa: BLE001 + if self.catch is True or ( + not isinstance(self.catch, bool) and isinstance(e, tuple(self.catch)) + ): + result = f':{e}' + else: + raise span.set_attribute("result", result) @@ -331,6 +353,9 @@ async def handle_tool_call( return message, True + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: + return self.fn(*args, **kwargs) + # Decorator @@ -342,28 +367,31 @@ def tool( *, name: str | None = None, description: str | None = None, -) -> t.Callable[[t.Callable[..., t.Any]], Tool]: + catch: bool | t.Iterable[type[Exception]] = False, +) -> t.Callable[[t.Callable[P, R]], Tool[P, R]]: ... @t.overload def tool( - func: t.Callable[..., t.Any], + func: t.Callable[P, R], /, *, name: str | None = None, description: str | None = None, -) -> Tool: + catch: bool | t.Iterable[type[Exception]] = False, +) -> Tool[P, R]: ... def tool( - func: t.Callable[..., t.Any] | None = None, + func: t.Callable[P, R] | None = None, /, *, name: str | None = None, description: str | None = None, -) -> t.Callable[[t.Callable[..., t.Any]], Tool] | Tool: + catch: bool | t.Iterable[type[Exception]] = False, +) -> t.Callable[[t.Callable[P, R]], Tool[P, R]] | Tool[P, R]: """ Decorator for creating a Tool, useful for overriding a name or description. @@ -371,13 +399,17 @@ def tool( func: The function to wrap. name: The name of the tool. description: The description of the tool. + catch: Whether to catch exceptions and return them as messages. + - `False`: Do not catch exceptions. + - `True`: Catch all exceptions. + - `list[type[Exception]]`: Catch only the specified exceptions. Returns: The decorated Tool object. """ - def make_tool(func: t.Callable[..., t.Any]) -> Tool: - return Tool.from_callable(func, name=name, description=description) + def make_tool(func: t.Callable[..., t.Any]) -> Tool[P, R]: + return Tool.from_callable(func, name=name, description=description, catch=catch) if func is not None: return make_tool(func) diff --git a/rigging/tool/mcp.py b/rigging/tool/mcp.py index a3a2375..7fa286b 100644 --- a/rigging/tool/mcp.py +++ b/rigging/tool/mcp.py @@ -69,7 +69,7 @@ class MCPClient: """The transport to use""" connection: StdioConnection | SSEConnection """Connection configuration""" - tools: list[Tool] + tools: list[Tool[..., t.Any]] """A list of tools available on the server""" def __init__(self, transport: Transport, connection: StdioConnection | SSEConnection) -> None: diff --git a/rigging/tool/robopages.py b/rigging/tool/robopages.py index 8a022b6..b7aeca3 100644 --- a/rigging/tool/robopages.py +++ b/rigging/tool/robopages.py @@ -44,7 +44,7 @@ async def execute_on_server(**kwargs: t.Any) -> t.Any: return execute_on_server -def robopages(url: str, *, name_filter: str | None = None) -> list[Tool]: +def robopages(url: str, *, name_filter: str | None = None) -> list[Tool[..., t.Any]]: """ Create a list of tools from a Robopages server. @@ -83,7 +83,7 @@ def robopages(url: str, *, name_filter: str | None = None) -> list[Tool]: logger.info(f"Fetched {len(tool_definitions)} functions from Robopages ({url})") - tools: list[Tool] = [] + tools: list[Tool[..., t.Any]] = [] for definition in tool_definitions: function = definition.function diff --git a/tests/test_tool.py b/tests/test_tool.py index 710773e..e3bb657 100644 --- a/tests/test_tool.py +++ b/tests/test_tool.py @@ -10,7 +10,7 @@ import rigging as rg from rigging.error import ToolDefinitionError from rigging.model import Model, make_from_schema, make_from_signature -from rigging.tool.api import ApiFunctionDefinition, ApiToolDefinition +from rigging.tool.api import ApiFunctionCall, ApiFunctionDefinition, ApiToolCall, ApiToolDefinition from rigging.tool.base import Tool from rigging.tool.native import JsonInXmlToolCall, XmlToolCall, XmlToolDefinition @@ -197,7 +197,7 @@ class TestToolHandleCall: """Test suite for tool call handling.""" @pytest.fixture() - def sample_tool(self) -> Tool: + def sample_tool(self) -> Tool[..., t.Any]: def calculator(a: int, b: int, operation: str = "add") -> int: """Perform math operations.""" if operation == "add": @@ -211,7 +211,7 @@ def calculator(a: int, b: int, operation: str = "add") -> int: return Tool.from_callable(calculator) @pytest.mark.asyncio() - async def test_handle_api_tool_call(self, sample_tool: Tool) -> None: + async def test_handle_api_tool_call(self, sample_tool: Tool[..., t.Any]) -> None: """Test handling API format tool calls.""" from rigging.tool.api import ApiFunctionCall, ApiToolCall @@ -223,15 +223,16 @@ async def test_handle_api_tool_call(self, sample_tool: Tool) -> None: ), ) - message = await sample_tool.handle_tool_call(tool_call) + message, should_continue = await sample_tool.handle_tool_call(tool_call) + assert should_continue is True assert message is not None assert message.role == "tool" assert message.tool_call_id == "call123" assert message.content == "15" @pytest.mark.asyncio() - async def test_handle_xml_tool_call(self, sample_tool: Tool) -> None: + async def test_handle_xml_tool_call(self, sample_tool: Tool[..., t.Any]) -> None: """Test handling XML format tool calls.""" tool_call = XmlToolCall( name="calculator", @@ -244,22 +245,24 @@ async def test_handle_xml_tool_call(self, sample_tool: Tool) -> None: ).strip(), ) - message = await sample_tool.handle_tool_call(tool_call) + message, should_continue = await sample_tool.handle_tool_call(tool_call) + assert should_continue is True assert message is not None assert message.role == "user" assert message.content == '8' @pytest.mark.asyncio() - async def test_handle_json_xml_tool_call(self, sample_tool: Tool) -> None: + async def test_handle_json_xml_tool_call(self, sample_tool: Tool[..., t.Any]) -> None: """Test handling JSON-in-XML format tool calls.""" tool_call = JsonInXmlToolCall( name="calculator", parameters=json.dumps({"a": 4, "b": 4, "operation": "add"}), ) - message = await sample_tool.handle_tool_call(tool_call) + message, should_continue = await sample_tool.handle_tool_call(tool_call) + assert should_continue is True assert message is not None assert message.role == "user" assert message.content == '8' @@ -350,3 +353,33 @@ def process_settings(settings: UserSettings) -> dict[str, t.Any]: # This should raise an error since dataclasses aren't supported with pytest.raises(ToolDefinitionError): Tool.from_callable(process_settings).xml_definition # noqa: B018 + + +@pytest.mark.asyncio() +async def test_tool_error_catching() -> None: + """Test that errors in tool functions are caught and reported.""" + + def faulty_function(x: int) -> int: + """A function that raises an error.""" + raise ValueError("This is a test error") + + tool = Tool.from_callable(faulty_function) + tool_call = ApiToolCall( + id="call123", + function=ApiFunctionCall(name="faulty_function", arguments='{"x": 5}'), + ) + + with pytest.raises(ValueError, match="This is a test error"): + await tool.handle_tool_call(tool_call) + + tool = Tool.from_callable(faulty_function, catch={RuntimeError}) + + with pytest.raises(ValueError, match="This is a test error"): + await tool.handle_tool_call(tool_call) + + tool = Tool.from_callable(faulty_function, catch={ValueError}) + + message, should_continue = await tool.handle_tool_call(tool_call) + + assert should_continue is True + assert "This is a test error" in message.content From 75421efc3dc9e91c465cec5d4446cca2eebbd525 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Tue, 15 Apr 2025 00:49:27 -0600 Subject: [PATCH 08/25] Bunch of tool mechanic updates and bug fixes. --- rigging/__init__.py | 3 +- rigging/chat.py | 168 +++++++++++++++++++++++++------- rigging/model.py | 46 +++++++-- rigging/tool/__init__.py | 3 +- rigging/tool/base.py | 201 +++++++++++++++++++++++++++++++++++---- rigging/tool/native.py | 34 ++++--- 6 files changed, 374 insertions(+), 81 deletions(-) diff --git a/rigging/__init__.py b/rigging/__init__.py index d65865a..6e21d79 100644 --- a/rigging/__init__.py +++ b/rigging/__init__.py @@ -28,7 +28,7 @@ from rigging.message import ContentImageUrl, ContentText, Message, MessageDict, Messages from rigging.model import Model, attr, element, wrapped from rigging.prompt import Ctx, Prompt, prompt -from rigging.tool import Tool, mcp, robopages, tool +from rigging.tool import Tool, mcp, robopages, tool, tool_method from rigging.util import await_ from rigging.version import VERSION @@ -66,6 +66,7 @@ "error", "parsing", "tool", + "tool_method", "logging", "await_", "interact", diff --git a/rigging/chat.py b/rigging/chat.py index e99f64e..45cec1a 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -20,7 +20,14 @@ from elasticsearch import AsyncElasticsearch from loguru import logger -from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, ValidationError, computed_field +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PlainSerializer, + ValidationError, + computed_field, +) from rigging.error import MaxDepthError, UnknownToolError from rigging.generator import GenerateParams, Generator, get_generator @@ -30,6 +37,7 @@ from rigging.tool.api import ApiToolCall, ApiToolChoice from rigging.tool.base import Tool, ToolMode from rigging.tool.native import ( + TOOL_CALLS_TAG, JsonInXmlToolCall, JsonInXmlToolDefinition, XmlToolCall, @@ -101,10 +109,17 @@ class Chat(BaseModel): params: GenerateParams | None = Field(None, exclude=True, repr=False) """Any additional generation params used for this chat.""" - error: t.Annotated[ - BaseException, - PlainSerializer(lambda x: str(x), return_type=str, when_used="json-unless-none"), - ] | None = Field(None, repr=False) + error: ( + t.Annotated[ + BaseException, + PlainSerializer( + lambda x: str(x), + return_type=str, + when_used="json-unless-none", + ), + ] + | None + ) = Field(None, repr=False) """Holds any exception that was caught during the generation pipeline.""" failed: bool = Field(False, exclude=False, repr=True) """ @@ -199,7 +214,10 @@ def message_dicts(self) -> list[MessageDict]: The MessageDict list """ return [ - t.cast(MessageDict, m.model_dump(include={"role", "content_parts"}, exclude_none=True)) + t.cast( + MessageDict, + m.model_dump(include={"role", "content_parts"}, exclude_none=True), + ) for m in self.all ] @@ -364,7 +382,11 @@ def inject_system_content(self, content: str) -> "Chat": self.messages[0].content += "\n\n" + content return self - def inject_tool_prompt(self, tools: t.Sequence[Tool[..., t.Any]], mode: ToolMode) -> "Chat": + def inject_tool_prompt( + self, + tools: t.Sequence[Tool[..., t.Any]], + mode: ToolMode, + ) -> "Chat": """ Injects a default tool use prompt into the system prompt. @@ -699,6 +721,7 @@ def __init__( self.tool_mode: ToolMode = "auto" self.api_tool_choice: ApiToolChoice | None = None self.inject_tool_prompt = True + self.stop_on_tool_calls = True self.then_callbacks: list[tuple[ThenChatCallback, int]] = [] self.map_callbacks: list[tuple[MapChatCallback, int]] = [] self.watch_callbacks: list[WatchChatCallback] = watch_callbacks or [] @@ -706,7 +729,11 @@ def __init__( def __len__(self) -> int: return len(self.chat) - def with_(self, params: GenerateParams | None = None, **kwargs: t.Any) -> "ChatPipeline": + def with_( + self, + params: GenerateParams | None = None, + **kwargs: t.Any, + ) -> "ChatPipeline": """ Assign specific generation parameter overloads for this chat. @@ -850,7 +877,12 @@ def fork( """ return self.clone().add(messages) - def clone(self, *, only_messages: bool = False, chat: Chat | None = None) -> "ChatPipeline": + def clone( + self, + *, + only_messages: bool = False, + chat: Chat | None = None, + ) -> "ChatPipeline": """ Creates a clone of the current `ChatPipeline` instance. @@ -1036,7 +1068,10 @@ def apply_to_all(self, **kwargs: str) -> "ChatPipeline": new.chat.apply_to_all(**kwargs) return new - def cache(self, mode: CacheMode | None | t.Literal[False] = "latest") -> "ChatPipeline": + def cache( + self, + mode: CacheMode | None | t.Literal[False] = "latest", + ) -> "ChatPipeline": """ Sets the caching mode for the pipeline. @@ -1072,6 +1107,7 @@ def using( mode: ToolMode | None = None, choice: ApiToolChoice | None = None, max_depth: int = DEFAULT_MAX_DEPTH, + stop_on_tool_calls: bool | None = None, ) -> "ChatPipeline": """ Adds a tool or a sequence of tools to participate in the generation process. @@ -1085,6 +1121,7 @@ def using( mode: The tool calling mode to use (e.g., "xml", "json-in-xml", "api"). choice: The API tool choice to use. This is only relevant when using the "api" tool mode. max_depth: The maximum depth for recursive tool calls (this is shared between all tools). + stop_on_tool_calls: When using natively parsed tools, whether to stop generation when a tool call block is observed. Returns: The updated pipeline. @@ -1115,7 +1152,9 @@ async def get_weather(city: Annotated[str, "The city name to get weather for"]) existing_names = {tool.name for tool in self.tools} for tool in new_tools: if tool.name in existing_names: - raise ValueError(f"Tool with name '{tool.name}' already exists in the pipeline.") + raise ValueError( + f"Tool with name '{tool.name}' already exists in the pipeline.", + ) self.tools += new_tools @@ -1124,7 +1163,10 @@ async def get_weather(city: Annotated[str, "The city name to get weather for"]) for callback, max_depth in self.then_callbacks if callback != self._then_tools ] - self.then_callbacks.insert(0, (self._then_tools, max_depth)) # make sure this is first + self.then_callbacks.insert( + 0, + (self._then_tools, max_depth), + ) # make sure this is first if mode is not None: self.tool_mode = mode @@ -1132,6 +1174,9 @@ async def get_weather(city: Annotated[str, "The city name to get weather for"]) if choice is not None: self.api_tool_choice = choice + if stop_on_tool_calls is not None: + self.stop_on_tool_calls = stop_on_tool_calls + return self def until_parsed_as( @@ -1193,7 +1238,30 @@ def until_parsed_as( # Internal callbacks for handling tools and parsing async def _then_tools(self, chat: Chat) -> PipelineStepContextManager | None: - tool_calls: list[ApiToolCall] | list[XmlToolCall] | list[JsonInXmlToolCall] | None = None + if ( + self.stop_on_tool_calls + and self.tool_mode in ["xml", "json-in-xml"] + and chat.stop_reason == "stop" + ): + # If we: + # 1. Are using native tools + # 2. Set a stop token for the tool calls + # 3. Hit that stop token + # + # Then we should re-inject the closing tag for completeness. + + for part in chat.last.content_parts: + if ( + part.type == "text" + and f"<{TOOL_CALLS_TAG}>" in part.text + and f"" not in part.text + ): + part.text += f"" + break + + # Parse the actual tool calls + + tool_calls: (list[ApiToolCall] | list[XmlToolCall] | list[JsonInXmlToolCall] | None) = None if self.tool_mode == "api": tool_calls = chat.last.tool_calls if self.tool_mode == "xml": @@ -1265,9 +1333,15 @@ async def _pre_run(self) -> None: if self.tool_mode == "auto" and self.tools: self.tool_mode = "api" if await self.generator.supports_function_calling() else "xml" - if self.tools and self.tool_mode in ["xml", "json-in-xml"] and self.inject_tool_prompt: - self.chat.inject_tool_prompt(self.tools, self.tool_mode) - self.inject_native_tool_prompt = False + if self.tools and self.tool_mode in ["xml", "json-in-xml"]: + if self.inject_tool_prompt: + self.chat.inject_tool_prompt(self.tools, self.tool_mode) + self.inject_native_tool_prompt = False + + if self.stop_on_tool_calls: + self.params = self.params = GenerateParams() + self.params.stop = self.params.stop or [] + self.params.stop.append(f"") if self.tools and self.tool_mode == "api": if self.params is None: @@ -1287,17 +1361,25 @@ def _fit_params( params = [self.params.merge_with(p) for p in params] return [(p or GenerateParams()) for p in params] - def _apply_cache_mode_to_messages(self, messages: list[list[Message]]) -> list[list[Message]]: + def _apply_cache_mode_to_messages( + self, + messages: list[list[Message]], + ) -> list[list[Message]]: if self.caching is None: return messages if self.caching != "latest": - logger.warning(f"Unknown caching mode '{self.caching}', defaulting to 'latest'") + logger.warning( + f"Unknown caching mode '{self.caching}', defaulting to 'latest'", + ) # first remove existing cache settings updated: list[list[Message]] = [] for _messages in messages: - updated = [*updated, [m.clone().cache(cache_control=False) for m in _messages]] + updated = [ + *updated, + [m.clone().cache(cache_control=False) for m in _messages], + ] # then apply the latest cache settings for _messages in updated: @@ -1463,15 +1545,6 @@ async def _step( # noqa: PLR0915, PLR0912 # Check if we should immediately raise - # FailMode = t.Literal["raise", "skip", "include"] - # self.on_failed: FailMode = "raise" - # """How to handle failures in the pipeline unless overriden in calls.""" - - # self.errors_to_catch: set[type[Exception]] = {MaxDepthError, ValidationError} - # """The list of exceptions to catch during generation if you are including or skipping failures.""" - # self.errors_to_exclude: set[type[Exception]] = set() - # """The list of exceptions to exclude from the catch list.""" - for chat in chats: if chat.error is not None and ( on_failed == "raise" @@ -1532,7 +1605,11 @@ async def _step( # noqa: PLR0915, PLR0912 step = state.step.with_parent(current_step) if step.depth > max_depth: - max_depth_error = MaxDepthError(max_depth, step, callback_name) + max_depth_error = MaxDepthError( + max_depth, + step, + callback_name, + ) if on_failed == "raise": raise max_depth_error @@ -1597,12 +1674,18 @@ async def _step( # noqa: PLR0915, PLR0912 if inspect.isasyncgen(chats_or_generator): generator = t.cast( PipelineStepGenerator, - await exit_stack.enter_async_context(aclosing(chats_or_generator)), + await exit_stack.enter_async_context( + aclosing(chats_or_generator), + ), ) async for step in generator: _step = step.with_parent(current_step) if _step.depth > max_depth: - max_depth_error = MaxDepthError(max_depth, _step, callback_name) + max_depth_error = MaxDepthError( + max_depth, + _step, + callback_name, + ) if on_failed == "raise": raise max_depth_error @@ -1676,10 +1759,17 @@ async def step( generator_id=self.generator.to_identifier(), params=self.params.to_dict() if self.params is not None else {}, ) as span: - async with aclosing(self._step(span, messages, params, on_failed)) as generator: + async with aclosing( + self._step(span, messages, params, on_failed), + ) as generator: yield generator - async def run(self, *, on_failed: FailMode | None = None, allow_failed: bool = False) -> Chat: + async def run( + self, + *, + on_failed: FailMode | None = None, + allow_failed: bool = False, + ) -> Chat: """ Execute the generation process for a single message. @@ -1749,7 +1839,9 @@ async def step_many( generator_id=self.generator.to_identifier(), params=self.params.to_dict() if self.params is not None else {}, ) as span: - async with aclosing(self._step(span, messages, params, on_failed)) as generator: + async with aclosing( + self._step(span, messages, params, on_failed), + ) as generator: yield generator async def run_many( @@ -1827,7 +1919,9 @@ async def step_batch( messages = [[*self.chat.all, *Message.fit_as_list(m)] for m in many] if len(messages) < count: if len(messages) != 1: - raise ValueError(f"Can't fit {len(messages)} messages to {count} params") + raise ValueError( + f"Can't fit {len(messages)} messages to {count} params", + ) messages = messages * count params = self._fit_params(count, params) @@ -1838,7 +1932,9 @@ async def step_batch( generator_id=self.generator.to_identifier(), params=self.params.to_dict() if self.params is not None else {}, ) as span: - async with aclosing(self._step(span, messages, params, on_failed)) as generator: + async with aclosing( + self._step(span, messages, params, on_failed), + ) as generator: yield generator async def run_batch( diff --git a/rigging/model.py b/rigging/model.py index 4aaae03..cc7a198 100644 --- a/rigging/model.py +++ b/rigging/model.py @@ -86,7 +86,15 @@ def __init_subclass__( # Some models appear to do better if the separator is a dash # instead of a underscore, and users are free to override # as needed. - super().__init_subclass__(tag, ns, nsmap, ns_attrs, skip_empty, search_mode, **kwargs) + super().__init_subclass__( + tag, + ns, + nsmap, + ns_attrs, + skip_empty, + search_mode, + **kwargs, + ) cls.__xml_tag__ = tag or XmlTagDescriptor() # type: ignore [assignment] # to_xml() doesn't prettify normally, and extended @@ -313,7 +321,11 @@ def from_text(cls, content: str) -> list[tuple[te.Self, slice]]: inner_match: re.Match[str] | None = match while inner_match is not None: - inner_matches = re.finditer(pattern, inner_with_end_tag, flags=re.DOTALL) + inner_matches = re.finditer( + pattern, + inner_with_end_tag, + flags=re.DOTALL, + ) inner_match = next( (m for m in inner_matches if m.group(2) == cls.__xml_tag__), None, @@ -338,7 +350,9 @@ def from_text(cls, content: str) -> list[tuple[te.Self, slice]]: if not isinstance(field, XmlEntityInfo) ) if field.annotation in BASIC_TYPES: - model.__dict__[name] = field.annotation(unescape_xml(inner).strip()) + model.__dict__[name] = field.annotation( + unescape_xml(inner).strip(), + ) extracted.append((model, slice(match.start(), match.end()))) except Exception as e: # noqa: BLE001 @@ -355,7 +369,12 @@ def from_text(cls, content: str) -> list[tuple[te.Self, slice]]: return sorted(extracted, key=lambda x: x[1].start) @classmethod - def one_from_text(cls, content: str, *, fail_on_many: bool = False) -> tuple[te.Self, slice]: + def one_from_text( + cls, + content: str, + *, + fail_on_many: bool = False, + ) -> tuple[te.Self, slice]: """ Finds and returns a single match from the given text content. @@ -446,7 +465,10 @@ def _validate(value: str) -> str: FieldInfo = t.Any -def _process_field(field_name: str, field_schema: dict[str, t.Any]) -> tuple[FieldType, FieldInfo]: +def _process_field( + field_name: str, + field_schema: dict[str, t.Any], +) -> tuple[FieldType, FieldInfo]: """Process a field schema and return appropriate type and field info.""" field_info: FieldInfo = {} @@ -570,12 +592,15 @@ def _safe_issubclass(cls: t.Any, class_or_tuple: t.Any) -> bool: def _is_complex_type(typ: t.Any) -> bool: """Check if a type is a complex type (class-based, not primitive).""" try: - return _safe_issubclass(typ, (str, int, float, bool, bytes)) and typ is not t.Any + return not _safe_issubclass(typ, (str, int, float, bool, bytes)) and typ is not t.Any except TypeError: return False -def make_from_signature(signature: inspect.Signature, name: str | None = None) -> type[Model]: +def make_from_signature( + signature: inspect.Signature, + name: str | None = None, +) -> type[Model]: fields = {} for param_name, param in signature.parameters.items(): param_type = param.annotation @@ -584,7 +609,10 @@ def make_from_signature(signature: inspect.Signature, name: str | None = None) - # Sanity checks for type_ in (param_type, param_origin, *param_args): - if _safe_issubclass(type_, BaseModel) and not _safe_issubclass(type_, BaseXmlModel): + if _safe_issubclass(type_, BaseModel) and not _safe_issubclass( + type_, + BaseXmlModel, + ): raise ValueError( f"Function arguments which are Pydantic models must inherit from `BaseXmlModel` ({param_name})", ) @@ -598,8 +626,10 @@ def make_from_signature(signature: inspect.Signature, name: str | None = None) - description = "" if param_origin is t.Annotated: param_type = param_args[0] # The actual type + param_origin = t.get_origin(param_type) if len(param_args) > 1 and isinstance(param_args[1], str): description = param_args[1] # The description + param_args = t.get_args(param_type) # Add default value if available default = ... if param.default is inspect.Parameter.empty else param.default diff --git a/rigging/tool/__init__.py b/rigging/tool/__init__.py index 976145a..26877fb 100644 --- a/rigging/tool/__init__.py +++ b/rigging/tool/__init__.py @@ -3,7 +3,7 @@ """ -from rigging.tool.base import Tool, tool +from rigging.tool.base import Tool, tool, tool_method from rigging.tool.mcp import mcp from rigging.tool.robopages import robopages @@ -12,4 +12,5 @@ "robopages", "mcp", "tool", + "tool_method", ] diff --git a/rigging/tool/base.py b/rigging/tool/base.py index ab407d1..b0fe9a8 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -5,10 +5,13 @@ import functools import inspect import json +import re import typing as t +import warnings from dataclasses import dataclass, field from functools import cached_property +import typing_extensions as te from pydantic import TypeAdapter from rigging.error import ToolDefinitionError @@ -41,6 +44,15 @@ """ +def _is_unbound_method(func: t.Any) -> bool: + is_method = ( + (inspect.ismethod(func) or (hasattr(func, "__qualname__") and "." in func.__qualname__)) + and not isinstance(func, staticmethod) + and not isinstance(func, classmethod) + ) + return is_method is not hasattr(func, "__self__") + + @dataclass class Tool(t.Generic[P, R]): """Base class for representing a tool to a generator.""" @@ -63,7 +75,11 @@ class Tool(t.Generic[P, R]): """ _signature: inspect.Signature | None = field(default=None, init=False, repr=False) - _type_adapter: TypeAdapter[t.Any] | None = field(default=None, init=False, repr=False) + _type_adapter: TypeAdapter[t.Any] | None = field( + default=None, + init=False, + repr=False, + ) _model: type[Model] | None = field(default=None, init=False, repr=False) # In general we are split between 2 strategies for handling the data translations: @@ -84,7 +100,7 @@ def from_callable( name: str | None = None, description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, - ) -> "Tool[P, R]": + ) -> te.Self: from rigging.prompt import Prompt fn_for_signature = fn @@ -169,9 +185,12 @@ def empty_func(*args, **kwargs): # type: ignore [no-untyped-def] # noqa: ARG001 schema = deref_json(schema, is_json_schema=True) + description = inspect.cleandoc(description or fn_for_signature.__doc__ or "") + description = re.sub(r"(?![\r\n])(\b\s+)", " ", description) + self = cls( name=name or fn_for_signature.__name__, - description=description or fn_for_signature.__doc__ or "", + description=description, parameters_schema=schema, fn=fn, catch=catch if isinstance(catch, bool) else set(catch), @@ -219,7 +238,11 @@ def model(self) -> type[Model]: @cached_property def xml_definition(self) -> XmlToolDefinition: - return XmlToolDefinition.from_parameter_model(self.model, self.name, self.description) + return XmlToolDefinition.from_parameter_model( + self.model, + self.name, + self.description, + ) @cached_property def json_definition(self) -> JsonInXmlToolDefinition: @@ -270,11 +293,21 @@ async def handle_tool_call( kwargs = self._type_adapter.validate_python(kwargs) elif isinstance(tool_call, XmlToolCall): - parsed = self.model.from_text( - self.model.xml_start_tag() + tool_call_parameters + self.model.xml_end_tag(), - ) + try: + parsed = self.model.from_text( + self.model.xml_start_tag() + + tool_call_parameters + + self.model.xml_end_tag(), + ) + except Exception as e: # noqa: BLE001 + raise ValueError( + f"Failed to parse parameters from:\n{tool_call_parameters}", + ) from e + if not parsed: - raise ValueError("Failed to parse parameters") + raise ValueError( + f"Failed to parse parameters from:\n{tool_call_parameters}", + ) parameters = parsed[0][0] @@ -299,7 +332,7 @@ async def handle_tool_call( if self.catch is True or ( not isinstance(self.catch, bool) and isinstance(e, tuple(self.catch)) ): - result = f':{e}' + result = f'{e}' else: raise @@ -314,9 +347,7 @@ async def handle_tool_call( # If the tool returns nothing back to us, we'll assume that # they do not want to proceed with additional tool calling - if result is None: - message.content_parts = [ContentText(text="")] - return message, False + should_continue = result is not None # If the tool gave us back anything that looks like a message, we'll # just pass it along. Otherwise we need to box up the result. @@ -325,7 +356,11 @@ async def handle_tool_call( message.content_parts = result.content_parts elif isinstance(result, ContentTypes): message.content_parts = [result] - elif isinstance(result, list) and all(isinstance(item, ContentTypes) for item in result): + elif ( + isinstance(result, list) + and result + and all(isinstance(item, ContentTypes) for item in result) + ): message.content_parts = result else: message.content_parts = [ContentText(text=str(result))] @@ -351,15 +386,12 @@ async def handle_tool_call( result=message.content_parts[0].text, ).to_pretty_xml() - return message, True + return message, should_continue def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: return self.fn(*args, **kwargs) -# Decorator - - @t.overload def tool( func: None = None, @@ -376,10 +408,6 @@ def tool( def tool( func: t.Callable[P, R], /, - *, - name: str | None = None, - description: str | None = None, - catch: bool | t.Iterable[type[Exception]] = False, ) -> Tool[P, R]: ... @@ -406,12 +434,143 @@ def tool( Returns: The decorated Tool object. + + Example: + ``` + @tool(name="add_numbers", description="This is my tool") + def add(x: int, y: int) -> int: + return x + y + ``` """ def make_tool(func: t.Callable[..., t.Any]) -> Tool[P, R]: + if _is_unbound_method(func): + warnings.warn( + "Passing a class method to @tool improperly handles the 'self' argument, use @tool_method instead.", + SyntaxWarning, + stacklevel=3, + ) + return Tool.from_callable(func, name=name, description=description, catch=catch) if func is not None: return make_tool(func) return make_tool + + +# Special code for handling tool decorators on class methods + + +class ToolMethod(Tool[P, R]): + """A Tool wrapping a class method.""" + + def __get__(self, instance: t.Any, owner: t.Any) -> "Tool[P, R]": + if instance is None: + return self + + bound_method = self.fn.__get__(instance, owner) + bound_tool = Tool[P, R]( + name=self.name, + description=self.description, + parameters_schema=self.parameters_schema, + fn=bound_method, + catch=self.catch, + ) + + bound_tool.__signature__ = self.__signature__ # type: ignore [attr-defined] + bound_tool._signature = self._signature # noqa: SLF001 + bound_tool._type_adapter = self._type_adapter # noqa: SLF001 + bound_tool._model = self._model # noqa: SLF001 + + return bound_tool + + +@t.overload +def tool_method( + func: None = None, + /, + *, + name: str | None = None, + description: str | None = None, + catch: bool | t.Iterable[type[Exception]] = False, +) -> t.Callable[[t.Callable[t.Concatenate[t.Any, P], R]], ToolMethod[P, R]]: + ... + + +@t.overload +def tool_method( + func: t.Callable[t.Concatenate[t.Any, P], R], + /, +) -> ToolMethod[P, R]: + ... + + +def tool_method( + func: t.Callable[t.Concatenate[t.Any, P], R] | None = None, + /, + *, + name: str | None = None, + description: str | None = None, + catch: bool | t.Iterable[type[Exception]] = False, +) -> t.Callable[[t.Callable[t.Concatenate[t.Any, P], R]], ToolMethod[P, R]] | ToolMethod[P, R]: + """ + Decorator for creating a Tool from a class method. + + The tool produced from this method will be incomplete until it is called + from an instantiated class. If you don't require any active state from + a `self` argument, consider wrapping a method first with `@staticmethod` + and then using `@tool` to create a tool from it. + + See `@tool` for more details. + + Example: + ``` + class Thing: + delta: int = 5 + + @tool_method(name="add_numbers_with_delta", description="This is my tool") + def delta_add(self, x: int, y: int) -> int: + return x + y + self.delta + + @tool(name="add_numbers", description="This is my tool") + @staticmethod + def static_add(x: int, y: int) -> int: + return x + y + ``` + """ + + def make_tool(func: t.Callable[..., t.Any]) -> ToolMethod[P, R]: + if not _is_unbound_method(func): + warnings.warn( + "Passing a regular function to @tool_method improperly handles the 'self' argument, use @tool instead.", + SyntaxWarning, + stacklevel=3, + ) + + # Strip the `self` argument from the function signature so + # our schema generation doesn't include it under the hood. + + @functools.wraps(func) + def wrapper(self: t.Any, *args: P.args, **kwargs: P.kwargs) -> R: + return func(self, *args, **kwargs) # type: ignore [no-any-return] + + wrapper.__signature__ = inspect.signature(func).replace( # type: ignore [attr-defined] + parameters=tuple( + param + for param in inspect.signature(func).parameters.values() + if param.name != "self" + ), + ) + + return ToolMethod.from_callable( + wrapper, # type: ignore [arg-type] + name=name, + description=description, + catch=catch, + ) + + if func is not None: + return make_tool(func) + + return make_tool diff --git a/rigging/tool/native.py b/rigging/tool/native.py index 91e93eb..9368f94 100644 --- a/rigging/tool/native.py +++ b/rigging/tool/native.py @@ -10,6 +10,8 @@ from rigging.model import Model +TOOL_CALLS_TAG = "rg:tool-calls" + # xml @@ -96,7 +98,7 @@ def from_parameter_model( return cls(name=name, description=description, parameters=params_xml) -class XmlToolCall(Model, tag="tool"): +class XmlToolCall(Model, tag="invoke"): name: str = attr() parameters: str @@ -110,7 +112,7 @@ class JsonInXmlToolDefinition(Model, tag="tool-def"): parameters: str = element() -class JsonInXmlToolCall(Model, tag="tool"): +class JsonInXmlToolCall(Model, tag="invoke"): name: str = attr() parameters: str @@ -128,10 +130,12 @@ class NativeToolResult(Model, tag="tool-result"): XML_CALL_FORMAT = """\ To use a tool, respond with the following format: - - argument one - 123 - + + + <$param_name>argument one + <$param_name>123 + + If a parameter is a primitive list, provide child elements as items: @@ -157,9 +161,11 @@ class NativeToolResult(Model, tag="tool-result"): XML_IN_JSON_CALL_FORMAT = """\ To use a tool, respond with the following format: - - {"arg1": "argument one", "arg2": 123} - + + + {"$param_name": "argument one", "$param_name": 123} + + Arguments should be provided as a valid JSON object between the tags.\ """ @@ -184,9 +190,9 @@ def tool_description_prompt_part( {call_format} ## Tool Use Instructions -You can use any of the available tools by responding in the call format above. The XML will be parsed \ -and the tool(s) will be executed with the parameters you provided. The results of each tool call will \ -be provided back to you before you continue the conversation. You can execute multiple tool calls by \ -continuing to respond in the format above until you are finished. Function calls take explicit values \ -and are independent of each other. Tool calls cannot share, re-use, and transfer values between eachother. +- Answer the user's request using the relevant tool(s), if they are available. +- You may issue multiple tools in a single response if needed. +- Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. +- If there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. +- Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted. """ From ff8570f18339d0ba8cb76df400cbf522c50ef8bb Mon Sep 17 00:00:00 2001 From: monoxgas Date: Tue, 15 Apr 2025 01:41:00 -0600 Subject: [PATCH 09/25] Version to v3.0.0-rc.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4a7e0e4..d883905 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rigging" -version = "3.0.0-rc.1" +version = "3.0.0-rc.2" description = "LLM Interaction Framework" authors = ["Nick Landers "] license = "MIT" From 66239ebf668ff0318cf66dfc0a917f1cfd74226a Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 17 Apr 2025 21:34:09 -0600 Subject: [PATCH 10/25] Fixes for core types schema generation --- rigging/chat.py | 2 ++ rigging/message.py | 14 +++++++------- rigging/tool/base.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/rigging/chat.py b/rigging/chat.py index 45cec1a..04252b6 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -26,6 +26,7 @@ Field, PlainSerializer, ValidationError, + WithJsonSchema, computed_field, ) @@ -117,6 +118,7 @@ class Chat(BaseModel): return_type=str, when_used="json-unless-none", ), + WithJsonSchema({"type": "string", "description": "Error message"}), ] | None ) = Field(None, repr=False) diff --git a/rigging/message.py b/rigging/message.py index 07dab59..43cdbbc 100644 --- a/rigging/message.py +++ b/rigging/message.py @@ -17,10 +17,10 @@ BaseModel, ConfigDict, Field, - FieldSerializationInfo, + PlainSerializer, SerializeAsAny, SerializerFunctionWrapHandler, - field_serializer, + WithJsonSchema, field_validator, model_serializer, model_validator, @@ -63,13 +63,13 @@ class ParsedMessagePart(BaseModel): model: SerializeAsAny[Model] """The rigging/pydantic model associated with the message part.""" - slice_: slice + slice_: t.Annotated[ + slice, + PlainSerializer(lambda x: [x.start, x.stop], return_type=list[int]), + WithJsonSchema({"type": "array", "items": {"type": "integer"}}), + ] """The slice representing the range into the message content.""" - @field_serializer("slice_") - def serialize_slice(self, slice_: slice, _info: FieldSerializationInfo) -> list[int]: - return [slice_.start, slice_.stop] - @field_validator("slice_", mode="before") @classmethod def validate_slice(cls, value: t.Any) -> slice: diff --git a/rigging/tool/base.py b/rigging/tool/base.py index b0fe9a8..efafe71 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -46,7 +46,7 @@ def _is_unbound_method(func: t.Any) -> bool: is_method = ( - (inspect.ismethod(func) or (hasattr(func, "__qualname__") and "." in func.__qualname__)) + inspect.ismethod(func) and not isinstance(func, staticmethod) and not isinstance(func, classmethod) ) From d73e49eee89826693ed427fecaf03cf99ce08de0 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Mon, 21 Apr 2025 16:37:38 -0600 Subject: [PATCH 11/25] Fixing some nested decorator properties --- rigging/prompt.py | 4 ++++ rigging/tool/base.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/rigging/prompt.py b/rigging/prompt.py index 4b7db59..4ad7727 100644 --- a/rigging/prompt.py +++ b/rigging/prompt.py @@ -496,6 +496,7 @@ def __post_init__(self) -> None: self.__signature__ = signature self.__name__ = self.func.__name__ + self.__doc__ = self.docstring @property def docstring(self) -> str: @@ -840,6 +841,9 @@ async def run(*args: P.args, **kwargs: P.kwargs) -> R: results = await self.bind_many(pipeline)(1, *args, **kwargs) return results[0] + run.__signature__ = self.__signature__ # type: ignore [attr-defined] + run.__name__ = self.__name__ + run.__doc__ = self.__doc__ run.__rg_prompt__ = self # type: ignore [attr-defined] return run diff --git a/rigging/tool/base.py b/rigging/tool/base.py index efafe71..5ac781c 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -198,6 +198,8 @@ def empty_func(*args, **kwargs): # type: ignore [no-untyped-def] # noqa: ARG001 self._signature = signature self.__signature__ = signature # type: ignore [attr-defined] + self.__name__ = self.name # type: ignore [attr-defined] + self.__doc__ = self.description # For handling API calls, we'll use the type adapter to validate # the arguments before calling the function From 2d69be1651540e2352db6de4e2d6c78b75bf38d0 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Mon, 21 Apr 2025 16:46:04 -0600 Subject: [PATCH 12/25] Version to 3.0.0-rc.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d883905..e83616f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rigging" -version = "3.0.0-rc.2" +version = "3.0.0-rc.3" description = "LLM Interaction Framework" authors = ["Nick Landers "] license = "MIT" From 629332b7c733301146e1d48141f73f75e456bf20 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 25 Apr 2025 01:07:32 -0600 Subject: [PATCH 13/25] Set merge_strategy for .add to . --- rigging/chat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rigging/chat.py b/rigging/chat.py index 04252b6..7b48bba 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -822,7 +822,7 @@ def add( | Content | str, *, - merge_strategy: t.Literal["only-user-role", "all", "none"] = "only-user-role", + merge_strategy: t.Literal["only-user-role", "all", "none"] = "none", ) -> "ChatPipeline": """ Appends new message(s) to the internal chat before generation. @@ -830,9 +830,9 @@ def add( Note: `merge_strategy` configures behavior when the last message in the chat is the same role as the first incoming message. This is useful for appending content - automatically to avoid duplicate messages of the same role. For backwards compatibility, - the default behavior is currently set to `only-user-role`. It can be set to `none` to disable - any merging behavior, which may become the default in the future. + automatically to avoid duplicate messages of the same role - which may cause issues with + some inference models. In version >=3.0, the default has been set to `none` to avoid + unexpected behavior. Args: messages: The messages to be added to the chat. It can be a single message or a sequence of messages. From 06203752817708d2979d47c7fe42944005b99dd2 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 25 Apr 2025 16:51:24 -0600 Subject: [PATCH 14/25] Initial support for audio inputs. Fix for gemini multi-modal output. Update LiteLLM. --- poetry.lock | 23 ++--- pyproject.toml | 2 +- rigging/__init__.py | 10 ++- rigging/generator/base.py | 6 ++ rigging/generator/litellm_.py | 38 +++++++-- rigging/message.py | 155 +++++++++++++++++++++++++++++++++- rigging/util.py | 35 ++++++++ 7 files changed, 246 insertions(+), 23 deletions(-) diff --git a/poetry.lock b/poetry.lock index 62836fd..4992a69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2330,14 +2330,14 @@ regex = ["regex"] [[package]] name = "litellm" -version = "1.63.12" +version = "1.67.2" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] files = [ - {file = "litellm-1.63.12-py3-none-any.whl", hash = "sha256:ae72a9d7099100b4b1172aaa2954bf6d7b205d47ba76beec5cd53f62dd57913e"}, - {file = "litellm-1.63.12.tar.gz", hash = "sha256:db875fb0b5d2bebdcf68805bc0bd4733dcebf3630e9eef4753cfe414a53120fc"}, + {file = "litellm-1.67.2-py3-none-any.whl", hash = "sha256:32df4d17b3ead17d04793311858965e41e83a7bdf9bd661895c0e6bc9c78dc8b"}, + {file = "litellm-1.67.2.tar.gz", hash = "sha256:9e108827bff16af04fd4c35b0c1a1d6c7746c96db3870189a60141d449797487"}, ] [package.dependencies] @@ -2347,15 +2347,15 @@ httpx = ">=0.23.0" importlib-metadata = ">=6.8.0" jinja2 = ">=3.1.2,<4.0.0" jsonschema = ">=4.22.0,<5.0.0" -openai = ">=1.66.1" +openai = ">=1.68.2" pydantic = ">=2.0.0,<3.0.0" python-dotenv = ">=0.2.0" tiktoken = ">=0.7.0" tokenizers = "*" [package.extras] -extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "resend (>=0.8.0,<0.9.0)"] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=22.0.0,<23.0.0)", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"] +extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.11)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"] [[package]] name = "lm-format-enforcer" @@ -3317,14 +3317,14 @@ files = [ [[package]] name = "openai" -version = "1.66.3" +version = "1.76.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-1.66.3-py3-none-any.whl", hash = "sha256:a427c920f727711877ab17c11b95f1230b27767ba7a01e5b66102945141ceca9"}, - {file = "openai-1.66.3.tar.gz", hash = "sha256:8dde3aebe2d081258d4159c4cb27bdc13b5bb3f7ea2201d9bd940b9a89faf0c9"}, + {file = "openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a"}, + {file = "openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2"}, ] [package.dependencies] @@ -3339,7 +3339,8 @@ typing-extensions = ">=4.11,<5" [package.extras] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<15)"] +realtime = ["websockets (>=13,<16)"] +voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "outlines" @@ -6225,4 +6226,4 @@ tracing = [] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "762aeaf734b37469815d6adbc151875abd6a9c078ddd7a9bcc8075ee43d24e0c" +content-hash = "4c7ad8761314553da361ee2d487b858a4fb914a75dcb56178dc093a0551712e0" diff --git a/pyproject.toml b/pyproject.toml index e83616f..325e1da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ python = "^3.10" pydantic = "^2.7.3" pydantic-xml = "^2.11.0" loguru = "^0.7.2" -litellm = "^1.60.0" +litellm = "^1.67.2" pandas = "^2.2.2" eval-type-backport = "^0.2.0" # For 3.9 future annotations elasticsearch = "^8.13.2" diff --git a/rigging/__init__.py b/rigging/__init__.py index 6e21d79..dca33f2 100644 --- a/rigging/__init__.py +++ b/rigging/__init__.py @@ -25,7 +25,14 @@ register_generator, ) from rigging.interact import interact -from rigging.message import ContentImageUrl, ContentText, Message, MessageDict, Messages +from rigging.message import ( + ContentAudioInput, + ContentImageUrl, + ContentText, + Message, + MessageDict, + Messages, +) from rigging.model import Model, attr, element, wrapped from rigging.prompt import Ctx, Prompt, prompt from rigging.tool import Tool, mcp, robopages, tool, tool_method @@ -41,6 +48,7 @@ "Messages", "ContentText", "ContentImageUrl", + "ContentAudioInput", "Tool", "Model", "attr", diff --git a/rigging/generator/base.py b/rigging/generator/base.py index ab3523d..8795421 100644 --- a/rigging/generator/base.py +++ b/rigging/generator/base.py @@ -131,6 +131,12 @@ class GenerateParams(BaseModel): parallel_tool_calls: bool | None = None """Whether to run allow tool calls in parallel.""" + modalities: list[str] | None = None + """The modalities to be used in the generation.""" + + audio: dict[str, str] | None = None + """The audio parameters to be used in the generation.""" + extra: dict[str, t.Any] = Field(default_factory=dict) """Extra parameters to be passed to the API.""" diff --git a/rigging/generator/litellm_.py b/rigging/generator/litellm_.py index 9740386..460d327 100644 --- a/rigging/generator/litellm_.py +++ b/rigging/generator/litellm_.py @@ -1,5 +1,7 @@ import asyncio +import base64 import datetime +import re import typing as t import litellm @@ -16,7 +18,7 @@ trace_messages, trace_str, ) -from rigging.message import ContentText, Message +from rigging.message import ContentAudioInput, ContentImageUrl, ContentText, Message from rigging.tool.api import ApiFunctionDefinition, ApiToolDefinition from rigging.tracing import tracer @@ -223,12 +225,36 @@ def _parse_model_response( ): extra.update(choice.message.provider_specific_fields) + message = Message( + role="assistant", + content=[], + tool_calls=tool_calls, + ) + + if choice.message.content is not None: + # Check for lazy litellm handling + # https://github.com/BerriAI/litellm/blob/0f9ebc23a5c1e386195267dfc8d91ba7169c4508/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py#L578C1-L599C48 + if match := re.match(r"(data:[\w/]+?;base64,[A-Za-z0-9+/=]+)", choice.message.content): + encoded_data = match.group(1) + choice.message.content = choice.message.content.replace(encoded_data, "").strip() + message.content_parts.append(ContentImageUrl.from_url(encoded_data)) + + message.content_parts.append( + ContentText( + text=choice.message.content, + ), + ) + + if hasattr(choice.message, "audio") and choice.message.audio is not None: + message.content_parts.append( + ContentAudioInput.from_bytes( + base64.b64decode(choice.message.audio.data), + transcript=choice.message.audio.transcript, + ), + ) + return GeneratedMessage( - message=Message( - role="assistant", - content=choice.message.content, - tool_calls=tool_calls, - ), + message=message, stop_reason=choice.finish_reason, usage=usage, extra=extra, diff --git a/rigging/message.py b/rigging/message.py index 43cdbbc..7a28a8e 100644 --- a/rigging/message.py +++ b/rigging/message.py @@ -30,7 +30,7 @@ from rigging.model import Model, ModelT from rigging.parsing import try_parse_many from rigging.tool.api import ApiToolCall -from rigging.util import truncate_string +from rigging.util import AudioFormat, identify_audio_format, truncate_string Role = t.Literal["system", "user", "assistant", "tool"] """The role of a message. Can be 'system', 'user', 'assistant', or 'tool'.""" @@ -111,7 +111,7 @@ class ImageUrl(BaseModel): """Cache control entry for prompt caching.""" def __str__(self) -> str: - return f"" + return f"" @classmethod def from_file( @@ -190,10 +190,151 @@ def from_url( """ return cls(image_url=cls.ImageUrl(url=url, detail=detail)) + def to_bytes(self) -> bytes: + """ + Converts the data to bytes (if the URL is base64-encoded). + + Returns: + The decoded image data. + """ + if not self.image_url.url.startswith("data:"): + raise ValueError("Image URL is not base64-encoded") + return base64.b64decode(self.image_url.url.split(",")[1]) + + def save(self, path: Path | str) -> None: + """ + Saves the data to a file. + + Args: + path: The path to save the image to. + """ + data = self.to_bytes() + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + +# https://platform.openai.com/docs/api-reference/chat/create +ContentAudioFormat = AudioFormat +ContentAudioFormatMimeTypes = ["audio/wav", "audio/mp3", "audio/ogg", "audio/flac"] + + +class ContentAudioInput(BaseModel): + """An audio content part of a message.""" + + class Audio(BaseModel): + data: str + """The base64-encoded audio data.""" + format: str + """The format of the audio data.""" + transcript: str | None = None + """The transcript of the audio data (if available).""" + + type: t.Literal["input_audio"] = "input_audio" + """The type of content (always `input_audio`).""" + input_audio: Audio + """The audio URL content.""" + cache_control: dict[str, str] | None = None + """Cache control entry for prompt caching.""" + + def __str__(self) -> str: + return ( + f"" + ) + + @classmethod + def from_file( + cls, + file: Path | str, + *, + format: ContentAudioFormat | None = None, + transcript: str | None = None, + ) -> "ContentAudioInput": + """ + Creates a ContentAudioInput object from a file. + + Args: + file: The file to create the content from. + mimetype: The mimetype of the file. If not provided, it will be guessed. + + Returns: + The created ContentAudioInput object. + """ + + file = Path(file) + if not file.exists(): + raise FileNotFoundError(f"File '{file}' does not exist") -Content = ContentText | ContentImageUrl + if format is None: + mimetype = mimetypes.guess_type(file)[0] + if mimetype is None: + raise ValueError( + f"Could not determine format for file '{file}', please provide one", + ) + format = t.cast(ContentAudioFormat, mimetype.split("/")[-1]) # noqa: A001 + + encoded = base64.b64encode(file.read_bytes()).decode() + return cls(input_audio=cls.Audio(data=encoded, format=format, transcript=transcript)) + + @classmethod + def from_bytes( + cls, + data: bytes, + *, + format: ContentAudioFormat | None = None, + transcript: str | None = None, + ) -> "ContentAudioInput": + """ + Creates a ContentAudioInput object from raw bytes. + + Args: + data: The raw bytes of the audio. + format: The format of the audio. + + Returns: + The created ContentAudioInput + """ + format = format or identify_audio_format(data) or "unknown" # type: ignore [assignment] # noqa: A001 + encoded = base64.b64encode(data).decode() + return cls(input_audio=cls.Audio(data=encoded, format=format, transcript=transcript)) + + @property + def transcript(self) -> str | None: + """ + Returns the transcript of the audio data. + + Returns: + The transcript of the audio data. + """ + return self.input_audio.transcript + + def to_bytes(self) -> bytes: + """ + Converts the audio data to bytes. + + Returns: + The decoded audio data. + """ + return base64.b64decode(self.input_audio.data) + + def save(self, path: Path | str) -> None: + """ + Saves the audio data to a file. + + Args: + path: The path to save the audio to. + """ + data = self.to_bytes() + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + +Content = ContentText | ContentImageUrl | ContentAudioInput """The types of content that can be included in a message.""" -ContentTypes = (ContentText, ContentImageUrl) +ContentTypes = (ContentText, ContentImageUrl, ContentAudioInput) class Message(BaseModel): @@ -409,6 +550,12 @@ def to_openai_spec(self) -> dict[str, t.Any]: ): current["text"] += "\n" + # Strip any transcript parts from audio input + + for part in obj.get("content", []): + if isinstance(part, dict) and part.get("type") == "input_audio": + part.get("input_audio", {}).pop("transcript", None) + return obj # TODO: In general the add/remove/sync_part methods are diff --git a/rigging/util.py b/rigging/util.py index 200c4cc..d938db4 100644 --- a/rigging/util.py +++ b/rigging/util.py @@ -171,3 +171,38 @@ def flatten_list(nested_list: t.Iterable[t.Iterable[t.Any] | t.Any]) -> list[t.A else: flattened.append(item) return flattened + + +# Audio + +AudioFormat = t.Literal["wav", "mp3", "ogg", "flac"] + + +def identify_audio_format(data: bytes) -> AudioFormat | None: + """ + Identify audio format by checking the first few bytes of data + """ + if len(data) < 12: # noqa: PLR2004 + return None # Not enough data to identify format + + header = data[:12] + + signatures: dict[bytes, AudioFormat] = { + b"RIFF": "wav", # WAV files start with 'RIFF' + b"ID3": "mp3", # MP3 files often start with 'ID3' (ID3 tag) + b"\xFF\xFB": "mp3", # MP3 files without ID3 tag + b"\xFF\xF3": "mp3", # MP3 files (MPEG-1 Layer 3) + b"\xFF\xF2": "mp3", # MP3 files (MPEG-2 Layer 3) + b"OggS": "ogg", # Ogg files + b"fLaC": "flac", # FLAC files + } + + for signature, format_name in signatures.items(): + if header.startswith(signature): + return format_name + + # Check for MP3 without ID3 tag (check for MP3 frame sync) + if header[0] == 0xFF and (header[1] & 0xE0) == 0xE0: # noqa: PLR2004 + return "mp3" + + return None From 1b81a997db9f35be0984f6a6d6c37f2ce46a914f Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 25 Apr 2025 16:55:47 -0600 Subject: [PATCH 15/25] Fix tests --- tests/test_chat.py | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/tests/test_chat.py b/tests/test_chat.py index 1bd88d7..df9e3c9 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -196,7 +196,7 @@ def test_chat_restart() -> None: assert len(chat.restart()) == 2 assert len(chat.restart(include_all=True)) == 3 - assert len(chat.continue_(Message("user", "User continue (should append)"))) == 3 + assert len(chat.continue_(Message("user", "User continue"))) == 4 assert len(chat.continue_(Message("assistant", "Assistant continue"))) == 4 chat.generator = None @@ -281,14 +281,20 @@ def test_chat_pipeline_continue() -> None: def test_chat_pipeline_add() -> None: pipeline = ChatPipeline(get_generator("base"), [Message("user", "Hello")]) - added = pipeline.add(Message("user", "There")) - assert added == pipeline - assert len(added.chat) == 1 - assert added.chat.all[0].content == "Hello\nThere" + added = pipeline.fork(Message("user", "There")) + assert added != pipeline + assert len(added.chat) == 2 + assert added.chat.all[0].content == "Hello" + assert added.chat.all[1].content == "There" + + merge_added = pipeline.clone().add(Message("user", "There"), merge_strategy="all") + assert added != pipeline + assert len(merge_added.chat) == 1 + assert merge_added.chat.all[0].content == "Hello\nThere" diff_added = pipeline.add(Message("assistant", "Hi there!")) - assert diff_added == added == pipeline + assert diff_added == pipeline assert len(diff_added.chat) == 2 assert diff_added.chat.all[1].content == "Hi there!" @@ -415,26 +421,6 @@ def test_message_dedent() -> None: assert lines[2] == "" -def test_chat_pipeline_add_merge_strategy_default() -> None: - """Test the default merge strategy (only-user-role) behavior.""" - pipeline = ChatPipeline(get_generator("base"), [Message("user", "Hello")]) - - # Test merging user messages (should merge) - pipeline.add(Message("user", "There")) - assert len(pipeline.chat) == 1 - assert pipeline.chat.all[0].content == "Hello\nThere" - - # Test adding assistant message after merged user messages - pipeline.add(Message("assistant", "Hi there!")) - assert len(pipeline.chat) == 2 - assert pipeline.chat.all[1].content == "Hi there!" - - # Test that assistant messages don't merge by default - pipeline.add(Message("assistant", "How are you?")) - assert len(pipeline.chat) == 3 - assert pipeline.chat.all[2].content == "How are you?" - - def test_chat_pipeline_add_merge_strategy_none() -> None: """Test that merge_strategy='none' prevents any message merging.""" pipeline = ChatPipeline(get_generator("base"), [Message("user", "Hello")]) From 79302653cd21f93ba7112adf011cfb3b9657b5d3 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 25 Apr 2025 23:37:35 -0600 Subject: [PATCH 16/25] Fix logfire fstring weirdness. Add some additional fields to Message.extra from litellm. --- rigging/generator/litellm_.py | 12 +++++++++++- rigging/tracing.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/rigging/generator/litellm_.py b/rigging/generator/litellm_.py index 460d327..73926f7 100644 --- a/rigging/generator/litellm_.py +++ b/rigging/generator/litellm_.py @@ -216,7 +216,7 @@ def _parse_model_response( ): tool_calls = [call.model_dump() for call in choice.message.tool_calls] - extra = {"response_id": response.id} + extra: dict[str, t.Any] = {"response_id": response.id} if hasattr(response, "provider"): extra["provider"] = response.provider if ( @@ -224,6 +224,16 @@ def _parse_model_response( and choice.message.provider_specific_fields is not None ): extra.update(choice.message.provider_specific_fields) + if ( + hasattr(choice.message, "reasoning_content") + and choice.message.reasoning_content is not None + ): + extra["reasoning_content"] = choice.message.reasoning_content + if ( + hasattr(choice.message, "thinking_blocks") + and choice.message.thinking_blocks is not None + ): + extra["thinking_blocks"] = choice.message.thinking_blocks message = Message( role="assistant", diff --git a/rigging/tracing.py b/rigging/tracing.py index 07adf61..ff9e103 100644 --- a/rigging/tracing.py +++ b/rigging/tracing.py @@ -1,5 +1,32 @@ +import typing as t + import logfire_api Span = logfire_api.LogfireSpan -tracer = logfire_api.Logfire(otel_scope="rigging") + +class Tracer(logfire_api.Logfire): + def span( + self, + msg_template: str, + /, + *, + _tags: t.Sequence[str] | None = None, + _span_name: str | None = None, + _level: t.Any | None = None, + _links: t.Any = (), + **attributes: t.Any, + ) -> logfire_api.LogfireSpan: + # Pass msg_template as the span name + # to avoid weird fstring behaviors + return super().span( + msg_template, + _tags=_tags, + _span_name=msg_template, + _level=_level, + _links=_links, + **attributes, + ) + + +tracer = Tracer(otel_scope="rigging") From 0712458221bf6c61b6a2be49936c057ed90dda40 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Fri, 25 Apr 2025 23:44:35 -0600 Subject: [PATCH 17/25] version to v3.0.0-rc.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 325e1da..68b6802 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rigging" -version = "3.0.0-rc.3" +version = "3.0.0-rc.4" description = "LLM Interaction Framework" authors = ["Nick Landers "] license = "MIT" From 016dbc793786608d2f4827b4c48df434fd865674 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Mon, 5 May 2025 15:56:10 -0700 Subject: [PATCH 18/25] Move docs to mintlify spec. Update docs for v3. --- .vscode/settings.json | 2 + docs/api/chat.md | 1 - docs/api/completion.md | 1 - docs/api/data.md | 1 - docs/api/error.md | 1 - docs/api/generator.md | 4 - docs/api/interact.md | 1 - docs/api/logging.md | 1 - docs/api/message.md | 1 - docs/api/model.md | 1 - docs/api/parsing.md | 1 - docs/api/prompt.md | 1 - docs/api/tool.md | 1 - docs/api/util.md | 1 - docs/api/watchers.md | 1 - docs/assets/logo_black.png | Bin 32746 -> 0 bytes docs/assets/logo_white.png | Bin 23700 -> 0 bytes docs/assets/rigging.png | Bin 83168 -> 0 bytes docs/assets/tracing_logfire.png | Bin 272341 -> 0 bytes docs/docs.json | 73 +++ docs/index.md | 561 ------------------ docs/install.mdx | 49 ++ docs/intro.mdx | 454 ++++++++++++++ docs/stylesheets/extra.css | 77 --- docs/topics/callbacks-and-mapping.md | 338 ----------- ...and-messages.md => chats-and-messages.mdx} | 67 +-- docs/topics/completions.md | 80 --- docs/topics/completions.mdx | 75 +++ docs/topics/{models.md => data-models.mdx} | 218 +++---- docs/topics/generators.md | 419 ------------- docs/topics/generators.mdx | 387 ++++++++++++ docs/topics/iterating-and-batching.md | 258 -------- docs/topics/iterating-and-batching.mdx | 239 ++++++++ docs/topics/{logging.md => logging.mdx} | 13 +- docs/topics/migrations.md | 88 --- docs/topics/migrations.mdx | 316 ++++++++++ docs/topics/pipelines.mdx | 232 ++++++++ docs/topics/principles.md | 23 - docs/topics/prompt-functions.md | 507 ---------------- docs/topics/prompt-functions.mdx | 465 +++++++++++++++ docs/topics/serialization.md | 178 ------ docs/topics/serialization.mdx | 171 ++++++ docs/topics/tools.md | 242 -------- docs/topics/tools.mdx | 322 ++++++++++ docs/topics/{tracing.md => tracing.mdx} | 41 +- docs/topics/workflow.md | 38 -- docs/topics/workflow.mdx | 77 +++ mkdocs.yml | 110 ---- 48 files changed, 3003 insertions(+), 3134 deletions(-) delete mode 100644 docs/api/chat.md delete mode 100644 docs/api/completion.md delete mode 100644 docs/api/data.md delete mode 100644 docs/api/error.md delete mode 100644 docs/api/generator.md delete mode 100644 docs/api/interact.md delete mode 100644 docs/api/logging.md delete mode 100644 docs/api/message.md delete mode 100644 docs/api/model.md delete mode 100644 docs/api/parsing.md delete mode 100644 docs/api/prompt.md delete mode 100644 docs/api/tool.md delete mode 100644 docs/api/util.md delete mode 100644 docs/api/watchers.md delete mode 100644 docs/assets/logo_black.png delete mode 100644 docs/assets/logo_white.png delete mode 100644 docs/assets/rigging.png delete mode 100644 docs/assets/tracing_logfire.png create mode 100644 docs/docs.json delete mode 100644 docs/index.md create mode 100644 docs/install.mdx create mode 100644 docs/intro.mdx delete mode 100644 docs/stylesheets/extra.css delete mode 100644 docs/topics/callbacks-and-mapping.md rename docs/topics/{chats-and-messages.md => chats-and-messages.mdx} (58%) delete mode 100644 docs/topics/completions.md create mode 100644 docs/topics/completions.mdx rename docs/topics/{models.md => data-models.mdx} (50%) delete mode 100644 docs/topics/generators.md create mode 100644 docs/topics/generators.mdx delete mode 100644 docs/topics/iterating-and-batching.md create mode 100644 docs/topics/iterating-and-batching.mdx rename docs/topics/{logging.md => logging.mdx} (75%) delete mode 100644 docs/topics/migrations.md create mode 100644 docs/topics/migrations.mdx create mode 100644 docs/topics/pipelines.mdx delete mode 100644 docs/topics/principles.md delete mode 100644 docs/topics/prompt-functions.md create mode 100644 docs/topics/prompt-functions.mdx delete mode 100644 docs/topics/serialization.md create mode 100644 docs/topics/serialization.mdx delete mode 100644 docs/topics/tools.md create mode 100644 docs/topics/tools.mdx rename docs/topics/{tracing.md => tracing.mdx} (53%) delete mode 100644 docs/topics/workflow.md create mode 100644 docs/topics/workflow.mdx delete mode 100644 mkdocs.yml diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a782c4..2980c87 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,4 +13,6 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "mypy.runUsingActiveInterpreter": true, + "debugpy.debugJustMyCode": false, + "jupyter.debugJustMyCode": false } \ No newline at end of file diff --git a/docs/api/chat.md b/docs/api/chat.md deleted file mode 100644 index 6bb886e..0000000 --- a/docs/api/chat.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.chat \ No newline at end of file diff --git a/docs/api/completion.md b/docs/api/completion.md deleted file mode 100644 index a484659..0000000 --- a/docs/api/completion.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.completion \ No newline at end of file diff --git a/docs/api/data.md b/docs/api/data.md deleted file mode 100644 index 44145bf..0000000 --- a/docs/api/data.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.data \ No newline at end of file diff --git a/docs/api/error.md b/docs/api/error.md deleted file mode 100644 index 337c31b..0000000 --- a/docs/api/error.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.error \ No newline at end of file diff --git a/docs/api/generator.md b/docs/api/generator.md deleted file mode 100644 index ec9defa..0000000 --- a/docs/api/generator.md +++ /dev/null @@ -1,4 +0,0 @@ -::: rigging.generator -::: rigging.generator.http -::: rigging.generator.vllm_ -::: rigging.generator.transformers_ \ No newline at end of file diff --git a/docs/api/interact.md b/docs/api/interact.md deleted file mode 100644 index a3773ac..0000000 --- a/docs/api/interact.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.interact \ No newline at end of file diff --git a/docs/api/logging.md b/docs/api/logging.md deleted file mode 100644 index b216e71..0000000 --- a/docs/api/logging.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.logging \ No newline at end of file diff --git a/docs/api/message.md b/docs/api/message.md deleted file mode 100644 index 55d2c77..0000000 --- a/docs/api/message.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.message \ No newline at end of file diff --git a/docs/api/model.md b/docs/api/model.md deleted file mode 100644 index e8ea08a..0000000 --- a/docs/api/model.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.model \ No newline at end of file diff --git a/docs/api/parsing.md b/docs/api/parsing.md deleted file mode 100644 index 53c2c2a..0000000 --- a/docs/api/parsing.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.parsing \ No newline at end of file diff --git a/docs/api/prompt.md b/docs/api/prompt.md deleted file mode 100644 index 614c644..0000000 --- a/docs/api/prompt.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.prompt \ No newline at end of file diff --git a/docs/api/tool.md b/docs/api/tool.md deleted file mode 100644 index 5e73319..0000000 --- a/docs/api/tool.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.tool \ No newline at end of file diff --git a/docs/api/util.md b/docs/api/util.md deleted file mode 100644 index 105dd16..0000000 --- a/docs/api/util.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.util \ No newline at end of file diff --git a/docs/api/watchers.md b/docs/api/watchers.md deleted file mode 100644 index 191f417..0000000 --- a/docs/api/watchers.md +++ /dev/null @@ -1 +0,0 @@ -::: rigging.watchers \ No newline at end of file diff --git a/docs/assets/logo_black.png b/docs/assets/logo_black.png deleted file mode 100644 index 60a5eb6f9067365c7ef1ce100b169009a6b522a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32746 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 diff --git a/docs/assets/logo_white.png b/docs/assets/logo_white.png deleted file mode 100644 index 574f288ad97c7f28d46caf055057b0581151eec4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23700 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 Ckyi}> diff --git a/docs/assets/rigging.png b/docs/assets/rigging.png deleted file mode 100644 index 000776835c863891fb0f6493637ca5d64e2d4fb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83168 zcmeFY^;cV8us#eFiWUh@(Gc9FxDz0_ySqbi*J8ol-JRl2TcAaYyB2qfQ>^{w)82c3 ze*b`Xt&^3Nvy*f7*?Z=hndg~6Rg`4jppl@#!NI+eg@D!I;9f<;!NJp`yoQ}2|Axg4 zdqaH>(RG7^!yNeU1wZ6mX#qP)>@KC_uI_B*?q%v~3FqbI#cJzh?`CfL-jdbX)jH=~ zhy)If5>6H@uHl_~+Uu1_GZb*}@V(ml(c_*E;%ks#Q!@$gia9se*>{d=u-bT@GUeP_)m00eLH&yp@{fhH#lbs77e z2G{Y4l7wu|q<3TjH&%mRKH(%5z_I5qhqNdf162`Zl?(jI87;$sX`TH>X{w(&8Qi{j zY?ZdabzQho0^ntKQ}H89;b=z1y>O1`3Ltpr9OwMZ`kV}YjGJQaj+~~RvOjai9-g58 zSxKgkIU2SXGVpOShsV)3un(Qf*dSfPWwTGa9Q zpM=s^`Q}Wz4^NmSR3Ck0)6wI`J9MwVk@7>~s@(Ip8Bhklb9t*DXjy4h09DKFrS9J6 zNm3{k_Z=K;Z?I8e!~6QAy8#X)R{yzEsX?N|^ZtS&Z=hu#I%kG~R?0Fd-}_&i@*z(u zAb|Sgc^Fz!hq&a=k@B6kTq(t*7!=dd?N!C{p$vMqn8A8MOY|SYRIda> z+O}rEbrKP_r_@;*Ba#`*itDoW!RK4$0?VpvR{y}qGHKhZ-J$c6ZNHnS#A+J-Yd|Ds zulnG&44mCvdP|N-R19QRbA=J>+he3R6%D%_@{0&CnAL?57o;gs0lF+&yf)bGe!XuC~i{(@QNLkY}g?i6!) zgx_r<5!1x@#%@bu32Wda{i3*t20!{?OEUn3mrHdY%t->q^4DMc^Zk%t=XIEo>CCmf z%s1r8m_Qij(Ju$|rO~YG(Xr_Zmha$DjbBWWCLL0U=+)yWbl5%=CjdfzOBE|*l%_HR z{=#mSvRxy_pYx-Ng8B(Bap{p_f@p2S=F1dp?`ALRBWtXUJ<>GD%^<}nFhtCAd{xSG zwP>(tc=4@IXd|y{QqFDy>;}Szz;rq}i(w9#Ha^+j)4@$-CjL==pV(@w9psF(d zN?9kTIbfF9c#MF-=i3*Ae-9iU+*8KUc@vcXQ!O@G>aFkN&8)I^2D5ReG2GizmYI8d zIO!vhfP_opWt8vzcK?2v7H!5BucT7+01E$E5OQR3KMG64>0HXxk6q!2;mb1HdC2i9SFotF6RCQBw! z2Pl}tvFnFa@leLKXL26)O1<2Ku}-#Jl6qzivvirLt1HXf1A*FS*CU@UXZmI`5XDh5q@+*p=A`gKP6(LrI_%jE@~zY zsJNo*8CAtXpWXdrfMAyw#6<0l+QboRCS(~pZ26fJ{YZL(R4R2c2 z2n1YA*X50UGx*NU$I6wBvH-%IK^j1i$V;34HxOF&LAPw;rNA)1X9tFb{nD+`&W_k( zFy+?a^$pu9&$^82BO4wE84CUwps#WNO;MY(p%1diXrBK7H^SztopvT$X<~fPqiYVJ z;RH_0Wu-R`xj>t2o{vc*bMO%LiwK z-8A3u*${Fa5ayfhJQP(4&|P+r%Z1r;>a&rVNImtg6H~WyRVY49e#Qd?qy6UT3y$Q- z2^*-zK*Jm$)pp(mAr4uxG^k4b3UtxfZR5p%2xlA3cP z8C}cSEg4)-9S(S8bpbE*@Wk#Er^_*t+58>%iUUKiMW%lD<6j=K|CVZ7asA$Rr;%P# zxeWUxIoz}}N(n81PK%vCCe4#@lSZ4e`fNGhr#(^ME5LLtsg{x^49<*!LsfSRLI$2 zaOHvZ3%yRNgepX#($qxq=li>~!V2rWngBV(0}os=@+6&9k4F2$K!pWVs`tlpwx{du zgluHqs4_-@#1)gdUrxs{N)<2!Z@U%FO^M8ol4*3hoyeSewqExfmGf>r#=TXo`W0pl zFOo40XdCHQc{P$}jnbBqnb#6QlJon=k8^~Ds6l>Dz*wv-$R~qAVv(cfQNoo)JAdZi zwjq6)d0_qgcY<>KgTA*@JGW|0T>6KY9`XZ0%x~Drwo4D`lbl9~^bw*gV;7D6-b@Nz zGv{wKzM@mg6~p_w%pLZ!s;%`7t8$bh8kj5Wg=syRHvr3E;NjvLIUF{ch}(P5cpTQd z@Z&CBy7NXOaswb@Fi#MTk9*6zElKfA+2*(& zL5X3~nu0m$Q757eB9J`a}d_0ia+Vf(a>o%bA8V9 ztanYO8KWteY79KvXhk=EIM%*gIcbpWeVQPE?j0Yj*W?7=XpGPmfsdESg#3wuejEQG zU2St5Xn1Zm{#HF-qu7li9z1G^KJZH>_Z~Xi>0n>L)8#eAZ_l^-T zYrZBKuA*cX=u>Cm@8gB;au=uTKE{JK$_EuD_|k2x`cgGE4E$FeSt*$oL5MqM4XsS{ zk{p4qwA3!C(G_m%4G=s8zV|<9{LkGA2C8N&pfNTTS}RnfuaQ{IXN2NE(I>xO?*7?} zENHw2-TW1|`8kcLp(Xpf9hdya2QzZvy`(lBd^pN*j)?ADU)dQ3zam*0fExv0s$98O zSDwR+!HXc2#&LMVtnwe8W>Vpdd%+VRBALHLWFU8S$XNOgaHd3DDA2#mE z2Mc~56>QH{%?g|i%HsJls4Gk!CvD|{zrOjPxQG(c`*gSU=Vjdbq92_NF46E!XBYzA zw3|P`i80QEg&H8AHb6>c`z4_H)3{Ve6)q}E?rkq#5nB>UsS=RsUk#br84lFAg zM0o7UEw4&uDRfk=2R7+1vHq)GXL&Px4v(U82Ne!2tSrcU)eMoy%(B%sutjAT7JoY{QYA_6=i@b0`N$!k)krEgtv7LGn|o>iVLPs&rB#xNRwcjEZ6Ln zQTY}7XvAV1xqNKxNlEys`ixGDci+Pe@P%P`7rM%cL$1c>fpQqs+_DuvEmJ>Z z1^#-~vdZ$j((s#vQPZ{asm0HfBPCsqwv>SYnsr4a&(i*EWmbgr1B7r9Cx50%sW;O> zc+W-+7ce!~99Zv8%k?)8i!k-w;lz&)z-SsRgIM6x7fWoZ8?d*>7{ua0&$MuZUKWGm z*8T3Y+abGn4RmXve4!N|c_!rdkyh6AM&Bsl{j9NGy-yi zvEml4wzx=slg~s}G+zUWEg>@g)t|wfR7tD?qTL}MQ5M*qCQj03?WBsL=9-R!9CYA# zpI#1Kws!xFh~3$KmL=hK*Wgdf-}24xDUyVA$Cb#hU<_3KDnx-ZW2Z|(yX~`^OS!c1 z*PoqW#{TCO5nn7pc(dn$qQ_s0O_&RmRy_cIM?L$5@$pjQIr{_IeBq$m@6G|Z%4bWa zb+}?bU+k(8)RLBFU6g#v=#YdDQ0pWT4#pj+sSt#2K15!;M3P@cH#s~}xggw1t1fBD zozRHhNu>+2#Ra|~`FKw^^poQyDZSpUWmU)cgMSY5(4!)<)UgwRDBTcNw*{_$mLbC0 zFg2TWkJD{2<>86-Dfq}zEiqP?KDZ*^mjp}bLU@>uyb{&D!ww48&@t&Xk|~)MtJ4d| zMre$kcJgB9;`*>`)!hB=_%L?krFhO*0X^Ve_}=9E@w+9@>$cp}xj~Ved6G8BO*Dhd zw5OnTeI~QV6#K~TUYc)N$_QvtUbssEWMNG0RN2H3f*);nW(BwiFWpz#AN)%rxe>c% zp?noFc*AD6HrNw@eEQ+e$X$JN(O4A97WBXrRpxT;OO|n%%|64Bc3d4P9$eGV`#Q&| z3-v9JL1az6ULfy-C?EWIjAd#l-8_#)yLiBZK>fs;&M#A5w0Rp6;5O|owQ zy?8>S_8mY}4=6c_stl%@aVfL~`ifHDB`JxO-V`b0brtCH{j)p+0tpwL-k+hDQN*ay z2*mQYyklt`Yz$e~4ADUU%NXOqvZ|^+iF1D?-ExJH&#j9FF%(aYUfV3rK1{I0QF`uG z*01nbI&_?3tr{~kGX#6BhA*odcRXTp1|9F=Cq?c_+nhJ!GmJ%|`8>p4X46uKW=ti1 zJU?9j>L=`rQVuw7SFO=x#+sV;8&S3BCtIvcHVjj;N>MT(IOPVeNxMy|CP;<4TVXFSOq;g1T4)1>}opyS1a(bO=<7fKBqIkHJ-j-On z=-S$fr1;YJg3O$xv!FQl^SW6iXD1AM$$ggA|A!>SU>xsSu-q|`?~rhGE|ya2k0#5p zi*v6}=%OA^XMFCz*!!;Yewy8%3ny|i5&7ZAOaZ7|@;!eB;$Xe}K=%Ig6u7{sEDd68 zEU*h=NS<01axn_Kfev=|uj*(3P*cgBqfa{zeK_-*s^>-n~RZ!F8RWU@ds;pyimW3M0A zE!n(SS%yl8>0*?@9@>U+2?5U;A1!CC5D`$ql1!DDtvWGA&JhsAv42FIGz|UxJ@M0I za-;XxvG%73TskESyLCS=Bfuv1ueC`(TJc6QC;!(4&vx~;puyyvHmS})Ly#ALY9 zDI>O~Y8TQ|u_Di#0yy`GRG)+yGZ0zx?U{4X+gUCy-_52M z*B#KPd%qkiL-M0`DBRCKpOG(+|2%G*T<^2ALFSY&SzMMQ-pgeSq9efZIIe_=J`V)N z+<*ELfD+2Cp0IKA8V402({bWQ_vQ;Gt5J5yO}cvaK+l`yJrb3tsS`_9sp5>^ub8-i zs^9Gd2((}^Py-Eie|ki33Yy&FnXGo){K>DSA(05>z^it4ehyQL55JX~#nkSs z3j#=F;z|2&=4ypQ;wC;sQ)R0YwsYf0>UmGL=3m&ke}XW-g|{{`-Mt0V{}Q} zE_3J&ig&5{=fbmhwuG zuwr68X_qbtE3KL>qe(%jCJFOgT2cj){50n~@ zmK`4b9YV5ch%vG`eiqU{%Xh}sq3^Q$hR?e$;3?Hi;fVvDd001gFJ5k5cG#|}$Uha{ zo%>FT03U4pC(d}NbOq-|`3L5FaYs<`)(*XOQ>9xjNo*pg zv^XiJZ7$;Ks*Zenbf|BC_4eImoUm+sRhOJRp!F!{$G3d(1y4GkHjTd(&j!80zJ%&- z(0D6DZlYYTsH^W5XES)6^>+0DN0rI@ty%Ap5 z-h+=#1^13NDT=9gcbsrV5%jpCM4@MgSF+FI9Dp8pd{=PVq&;CfeHks=*W5h2)NIY0 zI%Ut)h|tejM1m!Yv% zLUX=vgo*nwjCViNo!m4fP>)AEtQ6H+QNF=g%3OwI7b?j}zij)@eT!VO(7&dtUFIp# z>#D|95+Kh!LgL%N+aJBHhPYncFCYbb*lX|XQ2e18j$4LAItE&JtP`= zhY%mlo-Z&m>4zBq3QyRMW~v`1KP;VoLlsPDO?X70YX}OEN6Bb0T(tk#28YUCy~_^eB4r6kj7W)RO0JYC=qe%E;ni!Cd^fh zCR~PDLKZD|k{*Z!xzDFcFj)GZ`G>d=qkr|c9wQ&#C7lf*AWVx&Q>QI{M;9504JUAh zY2<$?MR!_)8H8sZ=Th3BJSXn?HJqLra3JEW(!owQjuZgi^|0{tL39#mwv`9Fap^V>Q+YBe`OkjCB-H+^K{2YJZS=K6djw# zc%`tJQE4SYlU}TMgUXu{KUFam+x@O#eF}jMKY1c*nr8`dYhYsFexmrI)D^;%6DD5 zD8b@M~}F!iyh!sF3Px<2%JMvkSp z8(@+0sTkC<0_7-u&c6C>UK=5bbFxmO@y|PDR}3&-f!STku1Lf>guU(Nm-G)g^l(dA zzaxuW1g>8bB2{!?Q_Xmmnt^n+CW_JCrMiUd@MHu$YS?3JgRH>64EUj0i}J$&QT9*j z=qi<24l|9B{Y}{6xDkw8{EXkr!}FcgcZf*fWk-K#7(v1BJ}?Fp7H**1ebO6vBh2DT z%+H3 zQJLY9rop~);gj#_JiMCnXq(5$cOrWQ&|L|8Q914BcRwM1BP2VPZ|V95u4Y-+DF`YT zPdaWF^jVfw@7{N9F1sDD3v#6h>(P?kD|N?N)nsL5T1;TyP5LVsaG@l3jnf{G53hK; zpBh^c$_p`?qE@=Z>2=BOuvWj^o|dyC%mk3V=HT+ssrI2LLDil&o2_nYX{))p;W0g@ zKXjkBCitJ3r%N&8u_zBG$o$X^onc~JpWH(nDBMt=zU1QK;tSf62s%0rLuzwaN5dzl zqhx8!d_%1GyW~6v^yfyTMcFA=0o<*t(*kuBw7OrnV~X zYPlAk0E&?6Mr@h~g(21%K;}JzM2xs51q*%alMT3}?~TSvA!~`=o%dFjOWeijQ2#$c zXR8SkURp@F{g(hva}*c$g%bB+ns-($&pU0S0?lZoNi=>FFZo`Z?Ko)|TL1%OTas!glD zmWW$uVEpuEN#wXH%@7DvPX-wIsPF~Xs79)gU34TuW0z);1V2uV1U}QjEu3ilmjp=( z+M&5@X$_`e>mb#*uO>|X0kKvJkS|}x*hOdx)G#{2ZzA$Ni%9>CXAxh4_|fYZ=x;WQ z32ShdjGbP#nO45WWMF?yqHFi3I`LelqoZqdU*@E1n&w4Q_D&yNV8``ad|TW>Ewn&1 zko~UaNnYO)F68>aZB2LG272T0-?a7kW#1fA9qdHpWV`7qT@Xt)Jo^xp@~9omJ2UDg zp%9r?e64Sp%Lya`)!oirQ7eiem9jfg2aFePd_ghgE}b9&lgg#^x=@rUNur81vjtF; z%;75zmiZb;elzOoPZt6-ZVu7so&AHNz0RI6Gh)dS1>-ggh3*K4n@#4gLs&u4X+ ziPCXsZD&CG8E zR=39(V~P{Vp}q~lXVU5@oBM-qAL8J_tc_sE&P46SxrD3py`n#R;&m+(caO z*L2KCHqGn|{wW_}N`MffO>fmF=>{%_OV$>FAEyBV==nI#TqX>Pl~k+M60nVm{FcYwlRfohPi z@g5N)2VyJh~@B^S)m9`03D3w1L@(QM{+g>Trl_mLF2fTE#maLq|Udw~Y z18kDk7}P|C0FUCy*{|hf1$-+R)AeNXWX6y{p{kMP$2lqE<8t-FJ4V?%Y1FpW>Hg)L zgm${+xNy^#2{%KAL+s=4i~6VTU+r#Y-0%*tkF)DOOH9jSr6VP-eksp~UeumwgYFQC z2P`sT`Cic^H~t>hX(KzbN&e%8J8=_>K0MD7m~?1$WPiHS zPV{T$)kBCb0!G-!a~nIti`(6D&1X|ZHk26He4U)S_6ER;(+vVokD}HT6;3w;g$PNW zQGTCCTMI;!!_ycOlHp#(NLd<%kgiHL>ns*NIPY4x5$8b^?jp$9qBK%b8QghB3#kPw zEZ>J0?y%~en`TFmj6v=c!PK0lZEfrt_o;WkZnQ`UeZ?wd z42QU71{=Z5=HNUOnEd9j`5R=k^neiG=dTg@anbHwt#QyBr_Kk0o;?<4=`ua3Ry8T2 zh)NAcYzaz~(Al*sPO@IB1{`+(IY+E?+%Z!`jnE%tolwULVVqJ*I&v~WkmdPDNI zw`D0F>#oN|G>@>N7okbTZr5g$s%TIKsJD{G69)O+aevN?e)WmzS62lHT_i3#M9Qq- zye3~mTFrQ~v?@GZK=?gkCT?=IW%)gE3JWDmj((U1dO6?y^UH(V0hbJ{{D<}VU?XFa4wfW{L?;BnxGAS~ zk~zC~5C+vMP!Lat`Y8Jc+##1^pyx;DO%eaS8n2zx<>suHl`|eA0dH(JGNGtQiU16X zKTB-e$2AQSy+*UeI6t0Xz_ryBWt@<-bL;b%vYv43JR%=q)tCq_0k%K#zrE4lw4qyA zZ@15>#uiDlI#Rn8gG%rdKmAF^Jiv13UYG7(zq8DV@#XMqL!2XKgnv^+%25V3!x_an z;Wwr^S2Jaa?XKt-nQt0FPH*F~gs3;Z#+n%~eQ;qTVS45sfkgIH_ssmceIiEcEGSh- zNnbwUVEL$NY`O*);j;FpZgy-EqMZl`Nj_MTOt#Dx)7M78SFW&5|I_%-21XQhx z-@tr8{AV8h@n2GMW&OSsw<@g!Mk{PQoqtWDmSGnb4%ey)SE?BJ&*&vBlhC9honBOG zJ{=QSjuv zk!Q2^xpa892-$=1wwb zGb{h=`u*B$#tysS>Y%9qlVFGY5x~mYddjJN>Zqk|g5FDkxLnApDO-2#rM277*So}q zNB`GC`yY69b@ko8)BhS$t!!YMTv;uZ-$qzX)o~Zs+Ntu^qv%xo_J{G2#`LXADfiIv zA6e^Vddqp@vf?sI;{ma2MMXx+(VhMYA)2k?8qpd4gyk8LUs@M6h?ZV)93Lp5T9-DOSy8)|^(2!?2L1 zQ!0VOp786tS$Tye>hLq$T%eTMvia*r#&*(hDoWc1e5++U#DsS#1%*Maf%qZL`uAqX z`NH2Nby*BCnKmpKPZpASKIFyh6zXLPuja)Q#O$lA%HB{E7}L2uc)D3cN^SzD?TWvE zi&ts&NCk>j72Qkjii%Z@heCzuRnLpNav@Y)8NK?*$E-8R%oD5&xTrMsEi+v8#d=y) zHSTW}rOV`p%x4zu=y@C0&rM)jXwTUS`qMqd=keI1L}}IC+(6CvXD*>#XXXf>fub<8 zirH@ygvO+Ny)RFgCRP!#RObVEFec}+F)AF`b-U_;tRP@G)g+r{Ikyg6={zHRr*7_y ze@3Tnx1On)wb$=@qo*XHn$%s+;2J1r#YnSqhPiSUzeld?XL@$NORuXG@nD{onK6pkNkel-_HUna*6(|`)Ag#ko$+%djsyT(y_2PfNh6Oe+DvhqN?F zn?zZ$SM6_-a`pC|8hK8RG2}svF4i$v<_OuYs?!-b;iX_&gLxyml<=G1n*9?v@t|Z4 z7P_INH+}31b%&*WZ8WN%wQj2OQDn}LuX9B9<)moROeNq{@hW!kk!4VlL=J7J9zu)FOB-wTzan^Ovpl3N`)%T934u`Xevcbg$VDtH!@SHG#> zg-ToJy>Dl1T=G==*uA)+hBxQ2G`!Z82BxQ4aQ+SF0SJLpLla{ZM?QN^TSqhV#P?=8 zOxSb!jcRK+wMX~NTC0hAQJ&RnO&X&0tC=TRxi03qNE$0EP1i}j=E|A){KAEzQN<-G zl_yZW84x=hu6iD=dfrY|J)_vDfhKQ?FH2FgHIiUESz)C_%KXtAyy9s8J+pcu#G}BC zE<-o>Fp?oetk;Nwuh+LO9ZlKIxCsJ7Vj=@_iN+W?w=N?|La2{V9BZV=uwS+AzHLQ) zDzUcZL*>Rt)uvHl__9&TI^zcPM3L2qUv`nu z$n=hvwEV6E*}|Xo)HzP2(pySq$ z>45NGMzaUkyvD-% za?nW>1h0RM*Vk&Hr9~P~?iU43>{U*t={^M&BCLXT`r)45pi`d_xu*h?DBv<;ROT!M zJL}m=TOp)MXg;h9ln;BS>t|J^WpR}`V(TgH$Xhs0GtCZXv#Z1mH#-KGw8^fFokUSY z_~1$u8sh5}T|9`4>~FS^RGYG&eT;a)0D;<%5OpkcM!8yvR2oePtocR`n`v5Hf_dKA zG}bNvL$^&rw$-L-K=o+wou@abF4~3RNJn~yd}Zj^mFPMVWF?_j7kBMS9m>_|W5XP8cV$m2FytKa?FR3!u@6y(bTapehB$$NEeC`Q3oNo+eSx7<(A z_F*(0*sOyC^ics)zTb)au$f`MST9Z#fgJ)VSwTXF(JB!V|Fx#lL0*g_5E|^gbrTrV zia`0EbGZ|>^WEQ@bpyI9jXusdWc+Tb{_w0<>sP+vs6=l-s|bxgG?~{4ds~m);UpX8 zu|rnt0GCn(mVz^v&;K^Kp^by=(PGEMF4#v>ciCza*B~iqLn&=zsAhLq+A_M8lJGkl z1+LB$T@04hqXdWuZAD7iV#Fx?vb=xg0QQ3FXgIm;gD8ChlW2K}{S z8e@;V{1d!6)uUg?#!xEnF(HQ3$`#*+?A*$T_B|D349MOAJ zX!h<)wnkQ`LMRqMnp}DMk4Dn6$HRHXRvd-(x0D!-9w%0?dIxdBm~T^CZ!Del>&iQO zl{zh0Qh#shO3|bPbDZ}C?JB8RNcJfg%n6{Uq>3rOBg0pdu^FQr_iNHunIF={##g6V z#GLTu>1Ro8DhrEoq?i0H!zey=#qk;wo349@MLSSW;W`uX%9x<>1zY`T+?6TwB@hj1N7RBC;QD59fhk(lgluN@jbaX{Qxnjc4V8)} zooD6O)U9KWUqI|_9Dzj3tdjRJc2y$3?Jj&_w$^cr1SIS%rm~f-*u_*-i~Kh-_fW&P zht~F;Q%z?XCWGP{XZ(>jRPNE|B4aSNGq|z{pp|>aG9SrKPNypBts3yEmL)AKX;cGZ1+-nSZh2S;0+dZ-e~tZ6 zNG=gUr=r{`b*xUqZuP}qbX*E9_yzUaz8or`UuUS(U|eeR;HvL1Ln4WD;r=$HQnRR9 zr9lM_4mMh?no>tl)nsKzzT<;JGP-{0y=S6gEhEU}{;+#e(QV^Ci9f&$3PXVzs7lTJ z$KNZrzp7S)09c||MB99f9{yzjv(*Kbqk!elI_Avy(VDm`qyObEubN=_3$?}-Mq@Y= zK%Swx|0Vi3-34z9AFYe`92dHf3C>z}c-O@?U~tbO^B%1WcqS1IM-^<@N+8lx6^G^Q zU~anOHNAsbYyA_h!U44aK)_dLXgtun!c(X~--=e{R*%~-5S zDnQL6SU4qB2bk2MSf_8<1p^3EGk?<6c2OugVa$L^2CHG{_neKeH0|2K-CYnZ z9UVIlPrUE30ivlZ#q#w9MBT~*Q^U^+uSsB`he8cq##Oj_O5=%<3(L>HI8h4NtRhSL z728xtyiQNjyeFhizs;nFBVwpc6h=X_;gNr`7y&qJxKq~~_|T*?>xPbu$a0hHz=%c| zNpcGNCmt6!ET?ksDy#p%ciJ%3NeJyD_y3cs#IOS@w=9Z}U^89$;Dtq)6`o6WW*#e9 zIzxyu-QT^SykqQ`^L6DLb6BiLq$(ox*e+8&P-RQip z5IB8yN?0bomm=ULS0))Fqs_;dlZtUG0Wd`b-LXrqU=aD4)>~|Z zc{?O|TDnHNTgF;F4;m8lHwsEhCP%w!3JMCv92;VFT2f_d;@FvuD#;eg1;j_b$8KgT zhRJA*)+(hx$GErE<1D$KRK#uP(>fAii;0dS_{^k`UkC{7yDlN6%~1B($$Y zK86S3e_D05T;n@v`^dYFJG*MavuQ;uPSeYh+x2~t4r@At!NMX5_pzceX+Jx{#3uUl zbGRWfsv+1o$&HaCNfIkndPn-(QdRu$G%DecQgKS^*jD4OE4^MOBld#t{m0l!_}dsq zWTv=f&AkyRzgMk0y%GZwxape{DHR*`Dr|FF>vsiwobc|0j58&Vii9*A8{jYHm$kY3TLcmh~?aH2(b}W$I<$d_iA2Tj8lmN zLp3}s_C!nwRjd#YQR!F@YY2X0!(%<|)b7D&haFa_ArU)2#J*r-V&;k8#+m8&u6*x|(zeoUcpd zY-tb|cKVK~ImabfKB^{ByzV54LV3!8(fXD+NvFdcv!4w|eiN(84g8f)vB6KE${ZpU z{B~PD9O_DRbi^}4B%|Pndz?!*RAb2yB?xJ9U6<+-!3haHtw5xAp}HMFud-J1s1cVc zQ8J#)WE^jj<3b7Mlg37@EJT=7@Y7(#hnXKY$(t6vIt_Z-dl~GA%80bVGol)Ee}9Bq zUo!J5NUa7VR0usRPz78-r7EKu83&lI*mgV`tkq9(2?~EcAuO!FB zhCllZOwO{OyCpz`XG}voe9@~4tDHZv;kKG`;DNDi*fL2QMzFM{W*!D?>9LBW_9hO@ z#08n--lOKY#1X{gF-G*T=61j3Pud}ev$AFCZ(CPbfDJ$T*fq|?e83QC(7xvICP82C zEBLtzPt)b*poqtD4mcN}i7TC?YMyEWTHCShph+8Fd~VVdka9$`c){n5w& z+_eiMh^iC@<&?xAJ>Zd5wAo~I(XLgshK6;LofZCGIOs*_h^Pk_Ao;C4?Ma@c^EW3j zDZ&_FY7_TmE?OSy+A1phdOrk8;MU(`MvcQ{wM|fEZ)WtR$Sx!6lvZ(xFQH2sd{MSK z5_rscn;kVnpk2NK?4<)S&f7H^c|q)LTJ&t%^`eT2R7v60fEY}&9d@k|cCFDcSNBw@ zL7ADb!4C6$N;Z?7X_H$!Qcr2i$=Z!Dh(F>_zA-3&Vz{(Xw2(9a3%s5Q`>YmXGt*`i zncSIva6OjZX;4U2nI?xXc4g8G^QU<`7?8l&ujwQ3K72gHie1aB(Ws^bCcrD10bEHC zGnP*pVQbA0qej1vobMTF7jEi>|NL?ZKxm|UWcF~*{>}!N|J~J|nbLMq$E-~%Ck3u7 zc*aRT|1GGoYF#+lg9n#~E!7PLx#+o9uzrJAdsJrQvlWO;4gh#H#?3$dlvv8|x95Ep z+Ab`&FW*IM>`A=K6DFC6`pS;N6*oI}YuD>kf)$c!ky@bM2{Px@O7ML2o2_^6$##8d zQM4?RY#01=dwcBic73)!_aolf>*|)-Nb3FRSHzL@!`8sx3VQBA)OokRk9|R*(?+x` zRn-~MGOa8q2r!bnP6PY!V>{1rt3WZm1fG;i!*^^T7#k%l4Gos%n3iStSy8(HUg2Zb z_3O|2%8`jM-Bcx(I%;ysx)!Zgh#e2yG9-UQEZm@L&L8%0-H09LCUeuYYW>8uxqA$@?>UaQh5jG@#~Nm7e8QE=%}qlS+yw_#hD6XKc!+W zd=zH~x2=-=Uc-=lOjcvBxOh527)O<3O`JCs*22BFVa1K1iTahVzECd?dkEk{deWwG zZ;TB(F9Af6I%S9d)E4zO=Im{v4+jPaE`4OX-9l~bNwxWjSKgkcodl0&kwr5z0lM!7 zozXlEHZ#yQ^4UC8)ud!_+f;m~lcqV&YbM(K!>^FJ<7Ohpy($rG z*CT_g9#}8axRa@HSO``!Eh7W=M^kS~RPaF67hjmoG=?)i&Yn{$9$Hm>Efym}%t9a( z*8Ui#^2v-t29&PM`MNH%JLsnrK)Tg7gI*CDzc{;CgJ|Y@Cf8h`#wHg92IAYK2UG27 zP0G!A%Fn!=FCXHP=;w>qVD5U?WUZ9;I9_oA!ii#EjCR7DiF))bX_wTK!9js(T~hN5 z3gC<`CHEmNl<@=i<)zU03#IuMd>*vxV~ncFnAT-Kft1};dD~-pB;qlq8DcePhgRq` zCY2Udc|3MSJU9L&Em(|@J$>0r>G2!Sp3i=oZYOTbmBS3Ru5+}63`$1fH@eCDiAaDo zzXSHy;f#cg?y(a2X}cY3N2r!1z(KW8FyJse6t%Q{jLU)Wwpj)^Vmt#!_2fNrQlt$d4Xw& zowFa~+Dnw1CiuZ{`FY&kD{;J&X`TOQy{aL2sAf`i`mY5CaTAdZz1D( zI1#-qr%v_Lf52K)&y9Cq-Noz{1W8W7TY68!xA8CgY(vSSo>pP_4L6e}%T*%M4L_lv zR9DDxW#Kgt*Xq^jO10;363RMl5~n#Z)(GWX>mA+y3WSZlw;b)gA(I!?9yGHW-`|(B z3v>(+A5xh_TF5^a%*R_btd@w8p#+XOx`E+3Jzt(cJbq!O+Ge1_pt)V?7bd+jstO1t zW(|-@Nw5qyB^-vdEWsQ%$wZ-2hniPu3Z|I9zhFCh4+aJ9KsT0kQt6aha*P5FeysE} z@ze)wkQ}5l;q1G_tRrE~{dtD>Vb1zOs^%DR_n&-<~VZKu~hzDPkz0I_$MlY9BUi z%HzyS`|k&u20tE|#4!R`))t0pd6PMs8Kuxs`_?8E3{#BxT?(R$zl$OJ0qZQh*l!3L zA3ek~OqU7bp!RRUkhV!xmN(G;cBYb&dqnBq+aQan{wQPU4uV99M3Y zBXx`xtljSzl$xqTEaZt@tduzbp0#v1f90#!V+Oow^#YhS4GcgfFfc&WWcq|uq1TA3 zWzc{opTs1I4~GE7IV^)8k{XkQ!))wo6lT!xm%(tdC!frcw`@axEb+%NNY@7isE&7|*((g|;7%5*`db=oRY{3ve-!Z-y1bH9wpNe- z?SV)&D?zw)^?b38=XgRVGV;OA({`y&wL2Vo%U!*mQO9zSREB3KvA?PZH3?2K4 z-v}Sp>IfDc6Ndv);Qpbgce)vtNVQ|@`2+I@R9gb0bL?jon^J)#``ZYng|SMV)@EV@ zZq^!(%rqrjC8%#^c5-`$anj`nJ5YJY%q)@8X&|zk%sMGWq@ks8m@P_oIH}|@>wUfhaE=m| z>Xo;^nUv5rsUGTX?uM_-9Lla6dyZvuW$Mf;ExcP3>YgKUs{WQUOeUg2cJ)JMW}h8M z@)Qu2;5+(^U(6{-lUb@%9G#5zsV7pveP=3`(&Mbp*2nc(%it$9xBYUI9^gA(akT}I zU6Y+exe@_=mNH5lfTPgtOhnGfv7Gr3Kj^>|-C zyy}T$-8q`pQhCK2nZ(hm=t|$XrCIxXW>K-}z}!<4=erpC+zpEpg`DdXu=C<%;fVye zh@%1d|JHu8(Ll3v`)0BjhH-Dm-BOD==Rdo40$_#_9Q%|4L-oZew}BHntQjdWMgIQW z4~!0ZmF>SB*5CXiw+*fS@;eTKWYC4rgTUi)`vba**^d@$Ea1UGtvGU$Z0vnp4e(ZV z95d+q+|2`>(19@!U__1G4n^mxXOpR(vi6dk!h1&-bX3sZ12H^$2Ki&TU?NY0M7a{f zgis-FqDI41B@V61$!@HPi+mPCQ`TVgRvOM47hH~JI!~|pRQMrFEi8MPosd#i)jys6 z?w(S~B>RCtKHe*%w4!>iOqrk6g9s>AcbRmRl(G>7202PoX;ay={I>bl90w2K8NZ+b zvZ9ak-yg~v<-;$134g+eo?jl+jYLt)|ijt0$=hPElb>M zHGCjM(&6NGe%kN9P3kMeP4&OmJpDDSs)IuYNrddF0ZpGz_+>4Ee(=?0+-3 zR>Cd4ENhwmY}KPKmJI2H8lOb50$LtFAVt-NbR$8}=$cI_bPN!q#B#tkRn!rKV7 znJ+kWHiPRv%PEgBYF+Q%X!p8$ygys&{D9!koKU%jj`Mn9w(r1c{Fpn_{@IUX{Z_WX zNT%_oG5EOoVf4YnlE`b{`Ow-KSYF+%n;p1z(uAx*l_l&0_pz<~UeGy$HbmwSpK*Xh`tky z=Vp>3RR6hLYkSc5bkOmnvhm1$P_rC|k>5DwL0e>RJvmb;2=JAgyzH*l+J6YwulBaG z_OkV*Z+bHVwv*Lbr+u8~x0a3?``%VnCREqYSaF;>_9vZuy(e|`RvZb4bGu# z+|EcQYjSmnWacRmecH&-Ci}E9gY!u+5w!~^+MauuT=uUyjnQE`V0%3^9{K>c*7#R3 zEC!UsXI-J4CR20O7)9&`Ow`(&lwtTTAYa0Y3(yiGQGLtEcu55l0I?2%9=ElF!v&uM zonIdjz0YhPOm_z?FLTj5(91Qe#q`T_b<4$|5Tf%IEp=P97yjKY2RYqVJRmX<{R)$! zzf)g+GPUu9!{V&7lF8AmE?$&g$fV+yYY}E7PQ*?sD)UUILgxj=7FW4>3Z-3T5G+o& zpUq(J+=i>}jLAu4_C*5`92gxRZ=}W#Swdfau#NyyrpqURi@t&A#qC4kM{;1NCT!4E z6j@TOCJEog&s37`)NUuLv_J;5QTRmiA#f#^3npUgNYq1s5H8DONA^FPn8(}R$(PZ{ z89*d9H!E;?>O8r9*0>&${gj$L@c77UU8mwsfrZzkzS5AMl{zps#xyWcKrQ<0iXHDp zu>PRWQ&b%P3_ud)sG8X(`4h6(l41P&RbaXZw$Gn~rOZ&eQd||w|8=-nBW*qo@YL+4 zT817hr~wiXst(7GH;>zC>8k+rHd3Aq4@#Q`Lf})7aP!_V()Mx^!mZ| zV2&-hR0{-=Jggx%o&B)y1tz5(09kun$ln=g{)7Kd@&g7Fa1nC25|kq!@LPa@UJhpC zQ7LcH`pXP`Sr|ez)^9WKL(6G?N2uB+n*wu3WL!xSVqtImYGNxnlYq@48Z>bNd=XWR9>TJ*Q?t6 zNxd7?XL3v!u?N=KU0KD@6rYnI!oou{8NLdwy-k!>1yfyoPY5F$JLP+B{G=7EaIYB- z4ap?*>XXrVUOZh9rAMM?Bd1n-l%kfZ0KA*Bd4?c_=gxO{oN!0@OeOeCx8?~kBbjh{ zJXUcbw6bA-C~}8`Q$8Tk!hL6NIG%F!c`CQ}u~p!_Z}M#ozSexyf-4W9!IOj|MzvHE zt08^@l&0X4uc1JZQgNh^ANbu&AqDYk9NP;1nVao5*W-b?IqIt8Q4)s4OOTWYubloQ0lj!S{7--v$E<@URR5Jx}Igu;>${VKfFny zk5?J~bYV0S{;1BU*3N$A&JzQVL4nPH0d44cT~z-d49J2gK=X44!@mw#X-xqw*O>x* zs!F@BfKHWOE^)@&%l+=Vy)V|Fa>fzkx*^zwLa^fv@pRP>g7oQf(qz|lF;|$o$N+^L z1^~Uv=NWv^SPjw^eiRYs8Lp0?`h9O$dDC<>W>Hi$@$@7VoAUFK3Lrw#S#XTWLn~6V z)DB)q`+A<#CO8y4gue`hFSe(0jLNc|Hm#EbSUcMY>Ycfa#&| zOp+AzGSO%|y2PZL%S(r>NChPeZ>6AQ;=O2)!h1^+WJ;JgrNBUL?s=N?kbDd?)0T)V zjS=uA_(gEc4T8S9xK>7Yahtj0QGuCN5A+n&$-L0uHBosk0f3f0LVfHRuc z#gWOBOjFu67lSeNTkHr*kfk#!7?_U!Bd?x}p$T=ClO^PZ=DzXN-!Wi%D+hRRd>(wD zfyS^`VDrhXPc6@|98x?dPRS+zi+duQhDo9>X{rxX==8EQ1txDV%Ed`yX{q(t#6*r- zWsYV!d5js1i9dw6-=X%R<}vSzvUF7WkpYceUXKkWH5p`O(VeGQ9-`Vd9ZVczLV<#$ z_Kb?VEOdEApJm;Hu?!m`ZN^E&esg%s4tBVXRRUnGk3{`n_cL0 zG(djq{s&4gyydVo?T+wgq7@YvU)@#vkthqbn{C~`Yy*i_k2}flpGbk7|MFjBJF1!4 zor{877~OHsIt%NZMN#}olV*9Ip>p+Zm(Qhk4D2?YQtI>3%iqao6rx>E$-)m@3Duxd zu(d{6BG>C&oAfo9M8M%;{UM=4BqSgK1WXqwLm^+N?8n02W`OS&-G- zwrW&v*&Z4*$*Va@;Xr0*@hRTdlWcAlv=W3d;cWic>~ZR;s#b+p8*>w7QQ8Xf-axZ1 zcz3I=?RVy>@AoF9(IYo)3&jagIM<-0Pi@e<)xOA(E8y7z3|-OEb2>+YxCs*!fxWp( z>5K$Dqzp{0o$aG`4}*#?r@H)_4Ta}J5x3zc(sKG0l~Om%oxkt5FHDy=s&g3hhFcGV zR0jzLS#gG1d}ObnFwK^yr5)bO8Ywh$RrWlLUR2pFGk@IGKn#M) zNnOc&p)O1Sl&@k!BsZT|Gy3s5E(`fE!xKV%FDZeJ?)6dmwTZ8@LH8QuMeQ`RELO-V zrOanF~a?JLEc6 zqcXpU)(K$bWFl2#gh4wM z_!D8%!y@v?3sgPJOCbdPg%02A-;?f_62$F<$E!i9RUiqQ3wOYj*{m#(W1#*u_sK>! zV3FsY_?yaL#Co>m*8%JId#{VXSvezv$Ex{mZX(Cy$SDMRkR*XeVz5igKfvxd@x?Ki z42e_Ed3XGgS!eC!uI;-nD5VOPU}9=OF+Q$|oVW~>bM1boKc!|($O`*6T8tVc9HPKfDvi5QU$V~dBjD?f$QjibUZxvJiP$xc0J-$f*UpM%_4zui7W9Ot zb4MI7S`{0NqZ#D7=DB;k0*P#@@N2j^3c=u!1WAUl4{r9`DG9IdPJ>vQNlBacEgc|V zKY8q<6^^N`h3jDxEyhX|p0xQM#~(7`yToY6YhFCt9og!(4NxUs^}08B*9x&}JJRSP zziQOFSM6I-?>te(!?K9`-CCoS1aQ7yZRIgoS3k9~h8kq6Vmw?niWZSa4nHUoow=c7)W(wT0no=fEWJ*_dU?zmDJJy1Cxe_FS<*pD$p^9Z5jl>~zd zf4@pIgTB%cVdQ|2G=9bGmfcN^okoS9O>on@+Ttx+H$+~5h*EOsuGI!@#LuUaTW7Dp9>2DD>4cF zaMtgnlv6mrm>VsIe}a3bpfNf<*BTD9FLG-IWa8llt!}$p@}W}cI*`Dl@P5H+x|v{Q!jF}C03J=!|Nj5 zJ+`EY>vQl+$Ga;~{h-y=aRMgIu)G*obX4SQ8SiIs@zwIApKo`V#7Hl?NCZ%gfnWY6 z*Y>JDS4ltD1nK~h)>LC}H^|v3DkM|^y7*?Mn_6s7w??@$va}adJM=OSm?iL-F#3a~ z@GzGjbq@#ZN#=(>OBWffZSI?zFAE0p7qhcMz1x2%&kGdQkg3W4qUuHE)BVN4N7#aq zg5pImB@?@L_wQ1H=tW}EA)@B^OQu)wErQUIXrgBu#j+?lH4E%hd47DpQoiAK!S}R# z`mnC$YUFyyv3`+W>U*)i|NSAuxpT9zs-gm6fHiu!1vkx6yy}A2Zg&nm%j@_4tN*Aa zSuHN7>Glgs*0Tua0xOcA#E|jlpyT&tKSCF`62o}{NKi0ZZ2q|SYtx(gYmgryDoE}J zwBvIk>^C2gBM8-dR~VEZXlp{x^_tzQ~yM%c8h#uiFX$&VP9J}pA48-FAOgu$u3oya8(#2TRDC! zW$=bSOteKtN2AW2jK7LvmaR(VcdGyW91dI?&HL%(`}ai!21Ig%UILz;v?Ny|ZI5cakpjl*SMc`E1W~lv^b0kC}I>>30slZ=X8y8$W;EW?ST~*xt0x z@S>DO904gyV7wPG%haSb_;C!kqVWYrQqb_7v>xTcwF+9L6AZ%s^lo_yj=d< zxncmc1{1)V4<=xnu^ZGjG#uEuM#8;dIp=+tdmY!~^?4Qo8e!=zct}BpS1jnw#b2cG zOi(OzYMnTbNt=i`0U1MIP}FiMLZM7GL39pQC$aq*Usoh4F4Q;0$b5p=sY?_!n2oR93D)j%DB{w^JM&uh1;hYib_@uU`{4mPLf^&I_c=*Oj{ zP_kM4#m^Gd1vV8NGu%XqM+VD+&!GfnOKWR=*VhcV$+)#`ZE-|Yizlva*T=tsWyo9i zp3-R5EgtbJ&B23n_G-*0o}x5of7!}=AP&&s?D_7;1!6D8mq6%}`7-?R(bVVv(*l%y zRi%@<+|#d9&tV&=sqKgk`)@rFLJv(*jl@eDS*^j4O(T4%nQy>0P+ zXn(kj7I~ZX^4!QeG;zoYv8jv*ut6nTU&~cV-ReL*O%mP z?DMLFl60R_?}*cPRV^3Lf4hzwf68;-B~K`0)xQQ_IufXGUbNtAD1G%NWTF8vx&2UM z*-$8eFy$4jui1(-Kkw%YC*@FiMaL0H2Y2!s>Sc6QJrI2_Tj`b^w`pqpu&Ru+i z{qo7JS?IkGv$I}o?7}oyH1Pb6Ja-jbP=A>zDSJ~QgXvG^%NHH@kMBrQ*WYf(8yy}m z#Z`JZ;zD zYS(o(48cACo73U}mO~$dhYT%`Ry9_IDTzH{!uc*YH)hU2Kqmo zS}nqCTv@+bi*kJ(>USyK@T(b0?@0y!`^$j---M_g90+Hu&M{Ddy_IrQ5)lPnwi z#8iw~-x;?Z^IA`$El*8Gue+O>GqipC8*Y}*0YM)hv>(2cX0|Jr0LNcpdu!1h-T@#~ z_7dnC%?2%e6Xgihv9Rz-NT!?Ty^W%hX$jP9*w2Z3CY_v|K(fU7hSj?(cSKFTZ*a%a z1OM(a1>8S5J9FZHA{@|sIejDBjZ{!&(YhS%yp2?&)R(TC7S)5>A8Po=RHupt2PpsV zOH%ZNwL#9chW6iy%p18?`m=`VQP9y-g*F}}d)yLXJ~Grln#O5r~_V$`3eMwBI_uJ1JRgG)g{X)$dwcW0I1V{v;qa5U=2q^C(KMr(PhM zr|fU_B&4zNxOjQeNG-FL$E36s3)3qZ;e=^rr~8M(HriqrRSf3;8x{{Gs3x<#=HDJi zjyX17hfh52kI2wiULo_M8^vUpq<;}oCx4@9CNWG**;VSk=IkA*|x{qMNhH73Od#}9O6<3dgFkD zO~tY7vvI8+r5dVBJuCOacmshA#^b83d~00P_p&!>0b2?VyqT>nj2KdpkcPX@-|fEV zY*^*2+Xd=a@R#9H9LfB;YiQ6Hbz~&`MA*083;*9nN{5XpDVfUF46*qTWsx{!RiH2P zW0zN~d|skbv-kb&#bKw9eogm?d1d!_g;4l$i%IzUl+y|q384qWFfOc%J12%;5!OxB z)+inop8#tY{F5$Gp7(vsv7)ll)W?h@+LNSGQ}4M{EIo%HUeiT^2nUu+R$?klKDqk- zm4jAgI;7tRTVv7;7UAs4{Dt-STu)ch^*13rB-uO^PnQ}V8T@Z&I|EvR3~iY8r~Agv zKt`?VyE#@-ne~VB)SkIHm^_)33LK;!D#j^=q_G=GM7mrR%s2w|=5bR1Khd!L48Sp_ zCfjHenF68xKOlBfIZ3zHnr{RUVs>I0_|izJ7EaP1cSW9_V}dAEu}$pU;DGr~hrQ>4 zgT$hK{3Ios4}+#JDUCZ>F>hnyV1*EhM$*?%zUXlO`ouiG$b0y>@!$&~*B%8^BNqvv^Mq+n;tiU*TDg; z_sbW$AXw~(%$xBWZ;z}wx{)UuE*E{cVe5RgN>dLHeD`Er$Ll?7;HP(U3yFzefliEY zQshHWT<4FS5&!H8Ju+45w8rx-DLm+j z+G1=oy6>iroJ&fv4b;r6%-ai<>A(czfNz8<)1l4m)n_D-UV2$_2TLY65Rrw?c^8SQ z$VJlPkGg~=Fo^eYQX4}+o9^tM-P1a9Jn`xxOb;`}&RHI+V2152?#t;IiMHHfNrBq< zJh1~U>0i-umUIH2xVA$PEH8;hMSt+eH(z*P(N$`|4IRnBF%fdkwS9X@E&7ayOIl6g z`^>RvNDoEkq$H_|=(q-FSrz(cW}trcjHFjf(yNHZVXI3RJ5n-1jv6~3p^y$VCV_8b z&6(KD$)`}`PbE8L^ACvgQe~J267GDUEQNU^A2vl+u~Ixg#^8Cqk1!%su`0>@Nnwn) z$T)!PvOlRBnB3{;UH)Xbaxk(WkA$8FXzWN>Hp76!pPqs`ajXy!0Yi)_R49%!QsvWM zL}5zbfas0F%D)Yf2Tsn+v0|PH1UApawZWom75Jza{c52x&y0}Vv4;uR8M36U51Stl z07jet>J!h!rqbID{Q|f#fC-8ziJ6y|B0$lp1u3?5(sw}Pmo{U=(N`Pxcg3#ZKg;y@ zdKHF5c0TqP`DE7J^#@^5VoOeC{WP%$IcuU2yTtisTZA2#r8)$qaTXj;^n3xDX}N-*Yq=%sd^ttb?bcE* zO4~0o1jF;Cv&C!af__0U&3Mf)tejj|zuy?`4!lNZozn&xPtL(uSXy8t=2e}{JOua4 zv!49bXZQ~;x17l6E*#Zr`6re-eh)Zd4oFrdL}toI$ji~QAVYcJ~DN#g2)j`cS0U7FRkkU)gYCRpQ}=_yKEVgtAcPfAyDkp zxO-3u%f!q|9zlWK3k5JXbkvunY_2#fo)irxYl*nwV(d3DMA4=gFk28M(wK0Nprj!e zoHznk7&sYn7OTp9Ikk(sknVBe3AQ!q%;C63Z1kA_h_(&I)_5)y(!43m>6J)g1{o(P&pCT3<7}W^-$uJH=5 z2TjO&?=>F=70?se;}iW=m>V+xFcmcNg`Etp_K(BmExDCWz*G$H?{iR38mT^lVu7uI z2~=!GD69qAOSKzIM`*8aQ-r7jjj-X|dzxqXgr7!W3^OUspEt!3K1qqzrJihzaZ~Bp z&Y@^42w@C=KFpjPLE$0QRtW6lA_UWskU1D8WzqWQvGJL!nOQtjDTrWk5xIW1yT{#D zQf*cn*!}_!EOsiT8h}krC5ctGTOue2Scko>=Veh94k>bl%!p;xM1dnakjZ<*!@3?E zPr>=xdIr70k;%@Dhco?*z^j!Mu*>g)LD>ImS4pBSI>eEWtt;i8fk=Ld0O?IiPA2Bj z9fU>8HYr_}lBtu|LjSqhu{Bz=m03Z`ivk)HE!8w@q}nKeO2~X>Qsg!bm8j`TcnMNx z3xf^fGt*6f%qQ^%rRJI5r4dAT1q^}P6$FpBfvq%6lt$cxJo2xh~MbKi^?7Q)`@ z^HkEe4Upu=j!nzg#@4;Is11|K4-?M~GtKpPDhx{YyJbj9x)8dsERGUGnhN_C$5{aX z&l(8)=`->^(G#*B15^*Q%B0h3K{o7}~FmZyD;%ob~V@@HMkc=04csiddv+A8p@VAbpd+`ZANjB?C73z!W zAhrKpC`9O>d@3Y9`D?(GM;~k%EDkB%gE22hP~G-yR{ts2b-DQEi!zhS8o?bDYipnc zT30?y;j?*y;fo&zb2dCVhY$=rZ*Go^JxC3?kjo$m-6#M=OiaaFx7M@r7aa|ArU&Xsg#`_-6o4>p_smD zo{(J`sc|R+H%nodf;4BoK$DvC-C9zLsKQAQ{2)p{FJ{KCn**r}CGOA0C|R&6(J0@6r5DS)_?h!JJ3%eF0)df} zl$)DIC)i~Oe+k)D9&`oEFxWW~&{8<{lAJSZY&S3vxiC?V{%Rf>kTX%g z!7fDCsqRZ;dV@`n984z0CJ5EjL!<_rkrj7o3v?^lHuO!ptXjJzqhwQFY12VEP$1Zv zW?(}q>fMsT?_GH&?D*(S_B15QoC!qxUQj##; zsmt7G)Z!DUC_FSUg6KDMAdFsj@Gjdg-W;^L@=El)SH$7qz2#4^S+KGhQBQ%RRswAo zuWePlMY^ucq}c_zg5(+^0HcjfVMN?q7j6#}ROxbUzNlS4oAU&)F5;UZK!uRuT6DmI zcZpzjCU1m*gHuFS8g3yT#O0dI>NUm522!dAd0Fo@(^zWZ)u@EvIX&B~zq`P|qn}Eb zYKQ|tQ-!)>4Z4!pi5NJM0%`Vgbw{IHJQOO45X#vvrQ>{+X@XGJy*99_}Jv&C*~RGCbR!yOL%{GGI)k; z>l#G&y}TCEfxZJz7t^BAoyX8%4k5!at@|fcynK>&-F8hd{Aj#PAH{gWO#G=y80WH3 z{uy02Ui9*m_j2G2j-?7a*1-&9!R2U>wp2(&tJDUKX1ux9acawtl*ktAi~{3+>2Bi} zjd+n)8Z`PEwszmh1^OvN)@|-5!KK0SnFc0^%>Wg;fB8`dfPo3-FuN`EAd^jwrOrzt z+<=fNiGHimm%lAavHpFE4^h6tYOp(2`*W@e$?$2Ec&nOeYs@rmJj2|fmp50$^xTj+ z3WSmu|E#0Fv=EDnBEsXSVd4!j&g!9+suqN;LPAwPU}Nl>QQ{L5ivn2 zFBzl(nNm`J#0?P)vKymBge?r0`62dg$pMOO!0~ix*5ju5$%%U-5sV2GA3JVDj!Y{i z1JQC*W(!64^7YNW!%;}%zkS=EE&JhC+w zB5tcdW@xykKuenR>WWNOiT(XZrC$InM7%-si(*Wwwk`D;r4;=c zTjy#FInF*Z#QD!p-Xhb&8UY+eg)rIgd$gC=<<)luknKj_0ww#8h&sQDwZr|Djf3&M z?5q7|P^!(eHMIRDEDvE`4yPf2undPBMk9&h9|rA(w|ZCBjKqfWJ|W)ZEhXtsTzfk# z*RRtD2Q=p!?U8^k0ASU`yPKeI^Hl47u>7WaTd0DzN-0CxI0U1B=@qa7QPxn#HO`!? z%_I}QwlRn(Q!S5JR!BDg5KMN*nf(l7==qc=ZEA22exJgsV>~;@KL2Va!3C{#X__$c z@tJ}41A(h_v<_yg59z$r@MfldjIV4YUI7;#VLPQvWlV$TN(_7ZJ|sCC@iY%&H)D5C z;KJDF2g%@X(#Z+DLZTvk;gH&%uilPZdL?lJai#fFVzw=UFom*?$gmm|UKp{Ym{Ala zkiIMLR7zB=WJ&K~XvCAt_Psr6tw4w;=b&k8F$on>&Xs1j*mno3P5MqpDBPr39`nWG z2CmGrB?3o0rfUg~jI#AIus*a0WvL90sJ}1j95ruU!^{M70wLjbw6MJcToU!(~d)2%u93RCv80lBvR)KM)U zImN`>6}Cb5>!XESHgy>+H4T0ww@_lGszs>{6W<{X68kQNF-D=O^fppS;n=*IF_!<( zCH=VenZI!RpCrhkx2Tcu5;w*PlfoSEoT+={h&AbAPF(p{GuR1LXNCK8S!S?B`$l}W zL800J&OqJ5TeVSS&BLwU6M1g08OU*=IXG+8UM1+D2MB* zWO_l0W16(_W6)GCaFt2FI^`Pj#7s3JsANvLK96%A*i_MDDfmR*j(gpw?s{CTKh)?< z9wIsJ4xlP1Dw5j`%7AJYy$m}JQx<5lgxql#o%eog0KLJdS{6nd*4EAwwJ1DTY7Jem z3Nfz-g#73z@6ThGbGHX@ucR`JN}Kzil*XX%iWFoXffUv`*9V=_E?|4npA_Si^bqJ7 z4l2mRh$NO7WRB2$HBREIF++ zi1F3`R$2iPIc|OO{F&xY^w|M8qf$e$5*2P#?qJt<*Oc#Bwn`)X=&*o znv9!m_+CUKN6oXtP5)5SdI5iq7tBZRt=Fs|^LD>#f4DHFD(AR&DYiYSTb`$uNK?Ge zk!LC-|DHCcOx|? zz6B6?hr|J6TBU;;hBsG-A`$5~H>*WNKmR1mFSfmDk%_@#t6KCZ^P{4ontu1zf63Hh zz)2XPh-&~r*=RxCl95fM*mkh)@;i{3h?hUisfS)ItkOCqHrLljxwSIiSPq~)PBJnX zJF$8_CBFWLD{Oal=x7v-22Wi^x?1O@pP#q5KosLXI(eU(P3}Iw3Ic@I0ZXSc>8IoM zg_AGhYtO9@ao>;4v9u!NDSPv-%K`b8m6N z_0Z83Q&azh!rWzl>)?ge>A6i=(w9##Z9b+XCq{ zOVJ{rC-R{rDgqSuwc#uN`fpQYs^#mv43WjIsi}8Yc9s&>?5I;LXl|)G2A*9%55Scc zmUty)pRuAG1E)#;q}!nJfKdB0`2D9`)0{@ZR$CKVM}3cUNWs5kELE_(#l;DJD%n!- z8^$Hwk4*1jRK}_fcbC$7<01z`A(@0Nr-Ijh=;;}KY5jo@KhR&pK5z$kRk;PE^dKR^ z`3vA?9;3=Sto-=$AQJWdq|$13IzRMK$4zT{m1$$hy-#K$|1eHD1Co7|5z(7k78&9Z zFcxA~Jq|OyQAi*Zl%h-=98>~(4=Q2*?Kwp23p zxKN<5dsN`}>5>^e-k%OtO84q$hA61#Fr63SZg!eD$StG5lZE}%`fU#M?j-Q{7s=C# z!J5)ASm7M0(`Vk<|MFZ27|KfIwA2`qRf+JsXoa7`NfxJz(c#C(Cv4;~F;gm{OsD<( z+B)3UOPs5oOsjAtx{~sxe;=^enwv{D5%fvrmjqkuX(Ja!^J?yN04H=hXA_+>|l z>rM-Y+ZZsIfCF`tfdwS{yn1vn>63i)PP?VUG9u$VqA&y`DAF4%G5Ld40-08{usYMk zn=2y-?Xb8jk7bA<9XbkxA>*t7fhhPLt<7Hm{sZxK2f-Jd>`T!3w%Un`^}4qq!|vK> zx8B#&qX1Z51yxU9;G}3Ny2Oz`Ic@T->R>S;-LC6m#7}^h=Cu zz3#l$*HtXmQAHAj>e6idLVKiN+b?x?oWgHwmJ!I&y=JG^x^TUuRmi2+{55m=Y&im< z5TArdQtVeBfc-h?$mXPcwO^3evyD)z306VJ_PHv( z+jc&ZOJk<4;Dh4~I`&_HajIJU2lwZiST@>}mSwX$%NSk~s?r~yN8P*uqE<7Mz$(mIDsf`Jp+x2&nGqnw&0RJU zlGeDQ(tqE6zULhdl`uD^*nH@Qq>9SbSNce+!4xjs&J5&XTf;P58}t3|E<^t5Ue3i|H(&OPDZhC9^0Gwr>GMBSG*Ldn(B z+dhG(#r?}EjDZ1Z$vKnEnZxZ$`2moEva*@oCNYaaTRr0)(1+v>g6PgOu4~Gy-B#OK zQ9)noRes)mNP!m+A;Oj9stUT$R*NG4VMrfyC5ss%qo!%nvVsTf-Oo$5)OyVCyNVL({K7G^D(Ut(o$XxEbgJL<^ zy(UI%B;8+xLqUV}p?C$5zb&N7i~zkhKD{KyBR&z)=z51|e(sljPDE_-Q4P4gK2I{w z6gBO>g{9@Q&j#vg%-THVgeV@<{kkhPYu_j(>>9+r(*NU>f1xS=S~onv)(0VOijxM5 zTbK7rSf`Gb)F<@RAEgY80ET=G7Fp)JFa-P%infkx#m_HwpnX>&^cnWwKc=iiJ7jdk zgkt4q9|VKM5O8HV8v(z={p!VShS4+-AfMH_q@4r(3-f5GL)@X;p#`3_GZmBeQ}q97 z0aDxPbkj0RG_Xs}&Y@?|Z^t=Y)iJ4hYy9ct00!XlRXY0wx_>Z%hGsw#O+EEv(v7y} zb9k$)O&0yQDLyV+gS#?I&XP^Tluct7i00mVq9&1sU6FqU40dVh>o=vkp&n_k zhz#-LDP3*R0Q(EyPairw&Fp7AZx%~uKZ~l)D$%-e%fOH$$o?8h|G=GoRfeQegOp-s zndJEKi#k9n&$P?Dt6MBwhHDan>K)_!)1&Je$qP2fBaIxI&Uaty{nJFYKR}jO~obbHRjWf zZ@7pP)QAN=FvKu;E!SC7MIfQD33=FEo>s~9gSmLDW!1i#|AWG?FgY_KH14Fz}Ui*us_aNIA7-RDhL*{DP$-#k1L$PAUd^&-oeVR?B?H8cZ=ae5OfZ9 zf-_72`asZ=X0!gltn}}lyX>~ua@L4GwOtEYoxxJy_o?rVe6m(!Gj1&x&R}$3j^)~^}Rk#zNdaY zJxi7_G6|I`-(^cRdbB=9t9x#&q%T_T|7mq(Hu*`g#}bf@zMZ^hJEMb{0;$;#>?d~b z7X6~|j9!T{etw_;`VR{Uz6#b-cB)c);INLEVe7^ppM5egFu^W6>mm|Mx(j3HNp$mY zf8%2JwyffT8U~vFDC0wHi6gk;_+K%LzoKJGU%SMEijZX%BylD#okTjW_hk&DSFTMV zMXxJ7KWvEQQ^n~mHMl}d2aQ^T17jyRC3J@uE*2AoPs>(-Bt{OcA`5FlK85itmB1!? zu?dn5Nrj<1P&ZKDCN|2Nncaybc+E4n!fR$SLA$QYG=bw&^STx4*gD_{iYRl9^7nUy z8%RatcVxD7$hDDtq{Kyr?bgOY0w`nv3UU6<&}BZ_etKbd)BIHbVt?VME83NL5*~$r z^pblIWPyW=1!frca&OJZ`}`eT^(EH#!**eSeQ)H)2DdN=1O7doc&+wKGisuyw6)vm zmWqXk8w-rn%OY4>lMWjLhf>gW@3Lxcx>!0iXwo4_9A7Rn7DEi5_dVi>TLmqBR0+Sz3hp$$k?BV_0* zMFhb{?O9xT?q6cHdIXvQ?UVQOy@WK0Mki$EYU(Ub6x;RZU6)~fp83r>v7Z_ z(^IbhO#7{IR{+HVyn0z!%fkkb?la>WJL~>Su*kTTOEqCwxV3;rqmio%BO}8aU=#{P zC&*E)Dj3cthZzV=PD`>4KKW0NKoC6G|6qt$x8C(YbK4AiR5ew2sIn5U{&0t4$i zV}gmjbSZx#?}>>q9W1I~2Z~DNJ9Sr`3mUfzSd@o!fGL@yF$e;Lg#r#AKD;Av-KR&F z1_uWb1fdm>C2-PU+;7q*sJ@4h(c#>6w%ct95;?T>2(}lPD6n|(^5xuZ93LN-Yx2Qp zT)R={x)jgt(4j*edqkf;-1LN-p7#0@Q>?OkpW@quKOjedHi~cjF8p}Ia7dhVsD3&G z^v(%c7mW?$!3b8Te-797|6?S+NHouxV7IB(9H_Dc2llgH`Z5+4ZsF~BPRbGuAh{n~ zO4f+x)g6&sSEcSDKpeMm=FEGza^(u1efC*QOiXCtWh*@tGc`6ghS|BVc=VN%pfc_vy=JpRn`A zueD}Y*_?x?O)&vy(xmIwK-OCHM9*P$y;?8U-X<9<#u;wj%AW^tXmCiHnhNQA5Crgi zPXi5VgHmpdvpx51(5QtVY7JUX!bWYjR#BaXNo4K&(tNs5z})N%Zrr$$yH7$UV6{Ol za`i4E0N2Vxh9pjK=kDEHWfuw|D%A?YFqD6R4irVChFw2f?&e#wTi7YOxwkiuO8gy$ zA!@ZM#zseRt8ae#*ifxj)pJf+i%?4LyQa)FKNUGVgFv*S+1x5evLKZ&mouh?Bl&{a z2#w-h=~}ZLg+YLUff@>7L1v!_pi85T=YAge2lk&j}J5V)5XAg`3kq4@N}V2`D|5 z0}^>h9bf%N{|M`A8#sUA;sXJTJEeP-J#TJq;PuyE$L#DJjvqe`-}j(;sQNiJHirHC z_iwqGX0bLD9HshoOrH9G5p7)85ocAN$V`yS%|{mSYo9}kH_efzNqEvdy1kRE;@9K+Eo7Z>t{CTXeuVZs# zLjkLD5zc@-C*R4uG`4g*v9TKpRZjCY83&s2C5ykS#7QW@u=F_dlNb|hSA!31L*2@^qp@{U=hQb7=eI@ph=wXcd1wLqZyu(ajkeP@1~HJNoR zZ$Fg}nE{KFQ!mh`wT z1F?@uF#RAku4&SHXlh+fO-=TJ;$uWlC{Vl?R@J@NUYdjeg?I&pcmE?Om?efLaKn#U z8Ys;G8Ma89Npg|btta14DJ` z1Sa17JrrBZfEqz=CpPE0h|XtgN>o)a4y@089<9=_F)7TDzjvq3E4RFNhtHLRgKG7l zVeIv{w0t71;!3?RvsYg4>oE)iX~uVN`Lwp8bdUq(AH_L`?|F!#2#NIfGI(tYjoBB*(9u7jO z3j($2ahK;{BR~t#ipInIwqz1EU=#tLtbBk=`IxI>QApoC-$&pZpH|~z2QkMrkWM`t zZq0iOgsF5L&TwiCuAKX6lV9t>5=ucClVSNoOifMY4whtw)M852{2;nEbrryckK)S8 zO0IG%KK{trj}Mfox%pwWDkSAdS{iF{J^fCZ~GJ zquHlNo66;~*h76E9`%HP?E!-zqnV>p*t|W7Kw)YH8;rSO4HWp!Ce)jzt zD)8D}UeY2Sw9-jH* z!$awjU0zwntFOF_m1P0!W@e@_GczrtG;RU*o$tboc=Y!$U(D92&yV&=3X&22ib5Q7V;0e!d`p=LedB zR?=}LdA1p3XAj7jqfUwq>%03nEaXBA(1e+O`Y$)B`D-B}JS=J@eQx#N zG?%;W<#tLEdn~LE3AQPv@F>-0TRoAQ`^2==`IOH;VGONSE4NQ)REo4&o98g2QuC_U zo9eDHiMCDe#3;IJv(s*Z4&Vq0I2mP_F*;>CvYk@DT(de9sqV;hqZ;A1qiD zS~Abit4rb5o^HMGw;>VCCdOucGuQP|DUupjUq||W7~_*BdtG{-kKy6rht`%p?LIwW z8pU2gV)}dr!m@4)k~#|KUUW;clEWL_6F2ue*HVDAK)}H`$L7!{aO3HJhI%j{r1#*) zLQyv-&xRyk;KeAy=RW&6EUm0zX=yQ~ze<^+W-w}xGG&dzW_=Ssdif>%gTMa|q=d_5 z?lh$UmelDWWNR2d{|yXXe-#u7I2fQL>4aur97&f7Sl)j0aVCleDP%n+_Xozhq zKE-lxtBbssmk0-3(^&3ymfN2sb0D@GrCuhR(#TOloScdG(|?5$BFq&b{G`Gl08^T^ zR2%H5kexs_4bp5gQwvNe&|+;B>N*W#6bRJXDh1TkwYmoYF=mNroS$O=-0KxM1faBp zDkmtHbsOy=cRQ^b&~aY&QfV4(@TfF+vYU(xKgD*tm8<#1VhG<0Bv1^5ra}#Z7WSAO z(Ee>lTs6g& z7mj8t-*^E)6h%Tqt^zT6M#w?r02uA1=_uE8PtI$#|0y|w9N$jA#td_IP4bL&y)q3P z9_cfrj|+*lNkKgcI37?0Pn+Dm96bk1;+9Z&E3jxR0XUk?R&HQfDn`-_Tv(|n|ILR< zS?h9WXwVU8_UXe-PY6)lZnExdBeM4|LV#fY9fTVT0B0ifsS)#vYq&YYEr!7`4s8HH z!hqGWFXGnI{|%ac36yx?V3Jq2G*hWA*b?s!q6DYkJ%d+Xe-pLAVfdbpyNh?gQ)W&d zX0X^v1WA(MFTVK~s8lL=>ZzxY?od|u2!K*+9#ilAKFZ7Q14M1=6pFia{Bs=eZWu~e zAix2h036t??8oAPpLGBj(HG_!aN>YPryZ`8!aA^h+1<{%EL;cDNi_gS&%o_LV(;X< z(08rW?0S@$C1ngG2tXMF_kHIjmR>zoR?xG2*jWEcO}F$AmnsT{rn~jWNQ5W<+`esCoPnyWQ7p<`>`up#amque7ZX z9!zJEy)G&BP$(2ZDMgaxOZm~KN1GmV(^E@IpZj`y!P#%SE|<$M9w8`R_#Ql+eBFM* zWd%EN>r#G+oN=s8>Jg+^~Id$QV0{b z<6O=6JRiQ&P*6K!r)4Io#?Jwa_Vcj+Gdp_vI!8dHdSdMd>bKorLWu^jVG!i%lPS%N zuLjtvZ}K>kkr?v=Ye0;dGSB72Zlij?7!zIfJoJdjeMxJ9VVG#4JT}kY%tQAYAPo*| zMqIo*g_)bkZ`!91Kj}S_5=&F|Z~@T$BxN1a>Os@*OOspy5Fbteo2^M%)DQ$ge_cK< z^cVxhov^63RwS#OY|fp4AZ)LL-a08Sw;1;f;l0gBsduKxlgP1AJ%pP_|69Z~5DW`r z8blKJ?yb9FN$1az zb!ZGVQ$rn4sLXG6Vh-yBJaE9I1+r4WMrm`@dR3rNF!XAU6Rk-kurOWjhC$;IZR)7o zB>qu=)ZXv(93WQcC}ke0krOeK)>f0J&9{g>J{+7u2NK%0`IJb|Y5Vh;dNushG9UvO zZQ|B{&UGXWEQx#t*z4IE80+<5Ks$lwQFuKuSN8LV8B3+L)bmeN7$^CFH2`=%(ZJfH zRHhG6&r!}pg-Zqc>)d86_*Jnr{2QtsJbL~Bx_PFo) z`$Y-Mzb+~D9F6+@_RsH?Oq+V~@9fi~PLDBA%uCxXt}MVroDUFy-hLNB+=TH&w6%z~ z3>Cr*iQA=#A1$whg`>ZTwqF8=2hJHlxMoN?g8*c;*04C7bDTSO9(V7G;5q~xf$!ts zf&HjfDtbXzD;4aWp2pD75GeIzo8_O)scL{?Up@%68N zBSW&z5%NupUil`b-v2%L@w#js1z4c-W4hCTq`n*G$uP|#C=obA!hywozlepIFJ$pg zdAniVWTf?W6C7VRyLGwh+6sYYoF^S$%%%649@ZgYsdgM z0~!=yggoaK$9sE7+ya*lnAHt~^64?>t!SSqKQz;RyI`1o09H8`kmNF$jj26vuahM4DX+!K1=NcB9U z;+LbW?5N+>Sf_!p?T2XQqnZ!yciRjbNC`(ABx*qaBY-LTv`dr*K92(Ux$o4=(j+3C z&!apx8t^~thV*II=m`gkSyMUB^mbP^B?Q6kGoZ>pXqzJJOc?@zZ~zHbCO(gi${aW+ z5FR<|n*1P9b%Aq^ix)59&f=nJvV3=Y9>sxubC?((gQv|r2nGiSF*i4hv9U1}io#b$ zX$fo|ZhLZH=vi-b$=0SYA{mq&OuI==1Gk<#oq(@%T)W?XAGU4o ze&NoyJhXEz??Hgkhe<9VvOq@$(`*!RmZnCItp+>Xp1(ax$}Mo2rIpmsirc?apXtp* zX(nxrqU;Q}H=7LgrxL`_y!qAKr3(U>h^Ge5YH(+cnekH~&jycahXS7JIoDvRbG9mT zO{BV?H#LTu6%J)-XD##NUV>gQ1{nV*-5aQBS6Xw{_k();=n(}$yGhEg(d%Jdrri}DRj>#EN!(wTj|)BFy5STD;m}qKkigcn zQNrQhIR}lzc$yJQG4nkA;^b(N2ussn27?MgF5blIHM<=VyBZE$zi|T#x9=D!wh_#V z7qUk&H8qK9rGjhMuc6gy=@)&uTtc}VAxRRfudQKoV-s-_BVnwA-^-A%r#?Hcc6)A@ zk)f?f*TFLmZuJ?y521St|DhUB9py7Mj%ke}*9(zb zg}d|J04Jc;Z072dCi}gTHsh7&6{swPVeVq%BoVF;y18QfDK*coL)mL%dsrA>Pk^*w zz~0*>TsF45M;ynwwXnLGPZ$vC)1yhsd^G4GfYpa3ifED~ADXuIX?uEt zf#SWqEG*`Y0JOCM+FFx8U7tSUUM=pHZ%5mtr#^55349PvKu7`(PiVNd>p|+J*J`$K z;o?OLBz6I>>KFm2=V5Ac62rqIn7?%gi;K6gx_AeR3pYdwgn+RYT1m^nW#XT-4`aGk z*;lvo7#SVM3!nNtN|h?KFO8N?b2J-GEG*o@<;$0`D9vXb1&Qj<4)6s5!Sx&0apJ|V zV{Y~?2uWmV)VXHs!qd`x<&P)>ZJ>a~qrZw<`+rVriB;R}mSh~vZZk2`bi@2Xrd+Yw z>TtW+b0o9;9*^6U_56FHK1j?>9g$M?Z^dYv`KYuq;w%A^`$_K_6*r=#0!x$e_m)J}Gi68C0iiArdqDg7147C1g{x(E#V-cFvgI zV>ky0wZSqB8nnkCC8)Bp2AT1-gnue!i`FJuU>uz5>%>o03c=FS68hAq4>R?mQEW6C z;Lcw5=)F5R2Ub^BAIY}$X=n6=0>#~vC=vj)T?b(zK#C&`M;Ep^T|Fn*tjvOFhAgLQ zECpvc2hLx(fOb31glq22sdI#&SP1d40|k8I$QfL|ynr7?zHsg1MSIcofh5%x6F;waVZYV?j`iikO|7!`!|(++MhiQ>Ra3WqAdP zMKkQFJ&yL+bm=#dj2)%;^LIJEus6bx*U)CEg734g)2%bw4l@9zGJ+W342%;b{s3-$ z{s@7;{E7f75``nYDyy?Uo(rA}W<~ zHNDYjNHcIIO)3>qWI`ZkI-)_KS$hiPm^~+RY+zQHeN|e!X!|<~@NT5;Ny4zUww9|; zx_k`~^((<`7|a?j6bcA}AXl1<&3YZ2GsBE3VIBcEvq7Re0QU1&??QWCxX<6hm^Kh4 z+?fDwARIHN7)#_kp$D=aV3yFOuWjtpgC@`SQ79D9%zp+r;s5|307*naRAEg}uh+pC z17ijx+Mf@5{;K_AX{9f~d7S781d6+{kZeYafW!^Uzs)(7P0B0_1_ZRjs>6C|Q^<@m z2o&SuR<&^FHkMabQ;m@4J2;$lV;N?G%lKUMB1%}q^kLw$hbii<1h-b3xVQvdTLNw@ zd$_Ynu~KJPZvxFWkT5E>X5uC9Q-;vzs1Hcg%3eNetQ#fPrFD@08qSNo4+ZSu-Fo#Z&zj3!Y2BSN$A$?6-*d$ zesc()lB-v)LHi?ZRb8r$9%390-^V9|?<3$Fx;X$xxe#OjILH1m0Gxvn5!X{>2Luw% z!I&^nB1HJGcogsngO3;<7#JAV`7mBx|3%!6pOV!Z->U7N5FsM0{jp=mFf=rTpZ@fx zh_{l8Z;6X6utNyGb1B4E_63M&TWS>nVlhGbdYj?3RVI>oVw_-e^f_F8?!QMXs2J&Y zx*>@C(+CaVj)(R9^>{n;!SvIZ zr4pGtQ<(R2&T!-Ujof3V1%Hlt_V$7k+ZQ#7IJ$w{`69j%`6vIQXyN0$%x2&JtF5khzEEcN8m9O`~;;S zL8X|W7A2@g3ChI;#V|$?Ch!7)_ympeas0&lO| zskIz-gjLxh%|x59^{&_;tFW!=w=_*#Hp(8EF6HN?K1h5h~qG) z>WacEp4X+anvKI4G!cgcVlA{ z&1OpnrQl3HI}6=)YD~?1P%x3hdbWy@Ox5KG5|myt_52|jP&JG((=5X_cRKQU5CRnHi`a^TChEeBMkGmsn>TM62*VA7qT8s>a`P8q z6<;j=HJ+hwz~?R9HX_j^=M-AQL&&F&!0!ItPrV$aoSv+HY_J3V5mvb+7y(1Y@F56*z|yBK8_eF*z}w zJ2&91g#{#Wj5v-FCyAEIgIG~xZ3jDT6RG;e%@VTOtA107xt~o=PUXJm`uq*VabkoxI}TE|d!#gqj3u~u@dE1g`a^3=pLU=6K=Gc5 z0Dc5m=?`ocA@hw&(o8o;Y3;Iems^9<>~&s12*I7ZcU)~^#!JnSG;@w2?-qU``b*4_ zvmj6b6{rM)Zo9?)qXOAb`CSCJfdr0HO)1<;mTw`PA13?4F{wJNX&V3gKM$l6nktd_m&vDD0WW?fl&LvFQ=J6B~9cfRca zB&L7&!Qw-tUIt&y4Qmw=`wUV*P(EB@T_`WsV1X!L01FUUmntw}eG+QY)=)}N2n!g_ zsa3qaa0ks+(`XeLbA)Cc08xFnrFm)Di}gFbmw|L@pX^{Z+Ru@Rh^ObzT|~3l$ki87 zJke#@ZxdoY7-C;bDJZ4byLV6SI_Br+(T>}Q~^iUb#?uKhJ8+Vo_vG3E^r+Xn`c1q!S z6w^Jd%NsZ5(Qda8$8BvI&eH4hArNB;;yA&{Q>PzV8~SvAs1Fo(NH)`)1SK%P*j-tY z^+K^PCP1|E9(LArczu4iIn*4%P6?@#Bc1vTlF&*0$YC znBe&F<0uphslm!ecAcwj;Dbe(%ZL*Ri0yuJ01TA30nWh~LA`PiS6=v^u{d%(8>p%- z^u$XLOG}y#Uqb=TK?!xF;_mJ4u{4{iKQwckF2~5ww-Pnv?aQ{OQbqOU^AD1G8*QUu zG1?dXI>`tAKxgCk47q}pg>57ie1;cSye1(IdmjEnrSuf6``T>Ws$afs;S zS}R5s2oru#)FT)g8p<6lcK+r~tgfx2)oP&~w~@qc69~u#hK|n!(all5i#qS89=BBF zY;Me2^&GN<1mZdsgoq)I8J3qRki?IxF^Wf*8(Ihe*wfXev9SCsx^hi@5DBdG+ z4)CyyxHxEmitWgN8pV_so3~M1dLIPLz6u~xa;!mzE#(9n^*Ywp*E6eDvQZ8QO&^Kz zO!!lLIrwXo_>#Ovn2jnKwdq)PlZh8j||f`3ndFA5R^5$}M3BV389KCvOXlsXk#v(H9_CY{Ur%)+RoM zD=+*@Y?ddppLWjxQQl^0A)IF?|0u~y_U)zeDB(8m!3eQBSWPp8C-Zb!HUW zXR{t{N(TV3H80#+Ckj9sg_8>?l_)}K>CK$ydXF&xL4hi>^>GjoO5jln&-bx!-)!za zoIQ6Q%|-*Qb_?w|MjR(#NdnH8N!o5~XQ4q*ngO$Y8i^hS^RJPe$?J7j#5K~dC`l3| zae^z?uHnk%E4lk&lPx<`X1-2-N8JPhkNWU^53@7VxynuwhWFopAFWm!?N(clm+o46 zTx390{;i5iJ?3oP1q`oD@0~?sh)GT#p&s{V@3CGB_@51B4gw%1p(;&@)PxAJ~#U-mH zSxml=Fo)R0XZ#=G6ZBz!#P$|a*+{PQm9x!D zC@&W6(}$MaR0!&~4Iaf=vwta*d4-NDCsLr$ znfvB?eZG$x`;?QE>8T(1cRyhut&ab zl{S%p5U%@S0~|ttxtVD^ee@{8Fw8MtonX=TeN64iZ{Odo+^cI8ExjB#7$E?X|DU~g z4U+7*&cwdV`>aRz^t>=Ln86Hy8N3MML4XtlQY1x7mgrqFq|n3R4N>b2JLGW0t~cz} z%0C>AwIdw%AOEo<*5N;1?OGBQqCyl&kq|^KwH8T`AOsQsLE=RY1{geMFoSvaOm|gR z-FwqNGEe5odv8_Us_vQY>8S(sRMoxtzBf;v@0^n-A(Y+&Fhm>geem~i>gaDH7l9G= zd;mKO7!uGC9Q}B!4lD*jy8T%sP#Gb*3Jck{2AFyL9vfeaKC-mBSEZp9vB9l5Z835} zNY$#s85MUowSjOffK;A9WB2QU0Z*F_mgz}p86Sc$KoA5-(ga5iH`Bxez>6>b1XovA zv9{L7(72)IxjB;zoQ?W0Ow@rO&jr=Dpz-VcyD*cyf_>kpY`X2dFSte4DLZ%o9Dgpj_k9#-EDP{+KzVbM zb3Y9b1R;(bIgECz)hKV4Wq9uS=d_2!+8Tx?=#)FA;@ZAjyq-Ka^YY2BV=x$8(|YH$ zIWz}~jcC-QQ+C13ZuuaJvIe|~2+RbNKo!_?;)`gnoT`%mn3+khctu`dWo4z>J^@?% zE_oWi8a;`F;tdEC5bWp+ZlTm)%z`fTcOLc4HdX)vIRpwo6(|tn{k^|~clZBO41$g= zPYNgWc`!)hOs{)OON)5ufd{Z>w{}GxA3?Ty@4$f(5yM7-mKwS|l(@P^7LFVOLjw|l zezFIrKJ+hee&5F!II9EL z?6W^Wzt_`XG0RXC#J;Q_j<@$tXk}9v21wHu?z;P~#?>5u{RB>)d<(r^AH%^KhQk5!EC&I8vX<7l zo9eoKYb)5h!O)p=O=Z?drC?lxwJ`d_=bp#QFTc{b?X{$_sb1!Udk2+y=3Hsv6dn91 z4cA_GGyp(r9p~a6e;54TBt_kvwllfC+(KIv!w^vvZVQ~ewzVxrr zxq1@99;9xLtPsBTzSr*~&ogJ)VgkJcaC`VO{3pQ^*rm=I`;$2g(QAXVXqNlr-KQoZ zhk$l99R|B`YX9%!^se8=P(%)6mR)j6)s?MX0zedpxcly7xcA<>(Mr<^7|$*|iUGGJ00vX&le zfpkGm=$)2AA_zi6afCyM4r1TFTN=0a)mMLt*Iz$@UayaSzmMTi2e;An78Ri50T{9v zT0U*3G;>@U_4MxK;*eDoc1wAMtENE)l63<5+^gbzLZ5P~3R zR+Ccr&QssR<;$0_+UsL59Gc)ZMkp+%qg6fC{}eo|giY*1$NyTmZdF2i%WImrdJ(Sf<5D8h$6 z^l;;O2>?$$^<7-NcpkleA8TuC#zP`!3XF|?zqq?;VH94cPoKh9zVcTaTF;!e7R`a; zRK&I6?5Y9=kSK0@%($O&Rj)r$g(tmNMPz{jasNFWeDVLp;`!$xh2F4V2tU1mK(E)+ z6IYl&WBv)n5D&%A;8Wt8Xko=vD&Q=MTwxh*Q5;4?bJsMs{>++-!SdT z6|gc8X1`^DypUMFRV)AQTTkKi>C@=mTSRkZ7{%Je(_6q>+QFvYO_xSLg0@WSt=Y9x8DL8APho;QHWhTcj4%rcQ&qV zWn~55`1gN{m8)0LTUo_mZGg-~ThyRVd)9D8AU~hdg2y_mqrI+NTEM!tW?6>w7tZ64 z|M>G*>o-3_q(ba*Sr2CcXR-Dyh42&=uw8LHnYb;1M(au~djoNsDeoi+A zxg#<{L{WsL9XqD7Dz2{f@bzze6IZS*qu2An;zsXTq;fb&ym;Xv{^*ZBkN$e*<(#%W z&4J=rD#Ht3f6LS53doLr0s{nkA<^ney$qBlY6WZ%cvRYQD1<_kUBH2t|1}Q1@^8@T zorJ{DXsMN5xB6>qR;w%k#G;2gf}i5E!B_B+;u%Ef>-~WUARtRZ7|N}%ZLxrk+1-Q* z16mXju6BMNZyx@ivD~@~k}HdC{*t>{L7-9CwBUd2hk!sBNF2O%KR)#E!?@#)+tF&J zHoph<%x|R>I-L%45vz2`zot-=PGV(Ep->W;?Bd+9&*9Yl|0M=t*Cwuv)kJKg;J-so zi^#Jpdw!#~GJ;tWy{eeTyY7vm@)i5OJ`#Q(eZ{z&4%KdsB}(cZ{~RQ4AF93tTwvzu zxEm@DlH)AmZvU9~>Iq#^AOnP9fH;bA$L+Uc|AA(Q$pgS(Fu)T}egmgYzlYxHD%J)A zZ7CaSS5y-Ii-IY=ytW7i3<0MlQx=I3Kvd=o!Yt3w@AvVgzx*;@{P9m3x3!KG<41~J zvqzG^I`kUwCN;P=;}s_{e(vWUMHGeg71Yq_)9>NyU;hS{uP&qC@1x&md!tOhRY7CM zS%<33a|;|jH&bPU+M-WBL_7DTW znKNhcb8OGgmi@gB`d4p@z`sI|x8YKo~2hRg2O;qjpge*f6#{l>;cC zRD?@AKZDczKZk)Zkp|5k4+6wxS5w+sS8GP(-LJVFh7z|P*pCDI_F;K>8E4O($K^|x zFc=KDT@4BfAqhM+<&45Q9w~u77O}GXew;h{SzPJf!P0V&D7=qpIz{_J?Hj;^50W*N z(r#j;<)${56%L|Q{xpDVB!CoSevXPaCQd6?uj0#J{wsX?)4zga$Bt=*78IZ&1VI3S zz<@mlV3o5N*>*GJQIJwh6;MiB#R&AQtoHE5zxX14veD;^mDsuhAj#4t+)bf`1=mRu zW7p1Ic<_M-@WbbxZ(P+IZ=S@Lzx-8v<{$n8?AfyiPz91CMWGbJAkb@plnj6gw_`%l z-ao^?*!xKL2q@-7fowR$JMX@W&wu_;apA(n^^cQq;{(99g#&*3WceCq+U_2+gW{wF zRjPmxLh~T=o4};qKEOV8krbrI1?=HKbrNxR<}x5?ItPFomoIJyqIW{d0AUy+j$`cH zxeJdx{2-orws~aVlW)C+FMjDu`0QstgS~t9LMeqLNuZR{yiEAY`zFtqyRY)VHCdM7 z)aldslRy4E&YeFueW&Mi!_gcl*3vjetzb6mK?^a`2vNYA2oOK;TgbopBM9UWl3ZFe zO`4jbrrMxTkgA99(u-(c{4pRys33%r0d7|Xgc@jfQ0U+SdT*qFHWo|Q9jm!j_L3h} zhE=YV$*KXOP(Y3@&hG!mIJfIpk*mP;hk-;mkmxLB%AWly4X=w-Y zyuj+}DlRWy!OF^2^!hr~eIT0|$1Ctn8K^0Nb9eqa-hSxkF+>O_q}DdD0hK_aCC{P%;q_N5yo|p1d-aJQP_R%yQH%~~mg1PYD z`;y{Av3y*5*8o+&TY7I#j3|zA%f5a1zWjX|$OBkzSh&W1+wpw`jLl5Ej@z-&AdAV`Dr%s*5=l}H2@aa$g3hp|3 z6opbq;uu93Aq)aY0#m}M#b}W*4ORuU-u3cp!cXWT~ zu=706A*G8p-h|40v7C1+^2O~&{?xr^p&4J?7 zL`bNx)*-+xc*FeL86qmY>=*3WIjkOa*oYvo#&p}W9C{~7{ z2`CGL0E-Jd80=%}oHBq144D3rU;+h<+OOnB`qn?gt`oYi4C93l)u zsG@*0+S8H!GH#1PN^jw$ds~I9tB|n-y5Jtu0YqM&Ki16;D_6!MrVpvl?9l`?hSQJ> z;Gl&gE{${dJOVZzxOYY4Q)kK@Nr z;Fo{-Q#f+_?Z~qXQ5>Tv0v!k@2&`MUb&z17w_u==tYTT7V|8T(-~P^1_|A8}i#%`s zem5engdyHrvRGM#rqKI3QD_T;9yC4At%d-02( z`bF&CwG(liAP)nCVWb1+(EO4@JM3~2RsaAX07*naRJhc4!9+DRN}oK#nfK1(Yk&RM z_}Nc?HsipIAm{p0dXL;&M{B6B+@h9h3o$)s0LV?mT#YZ4wWRX>wFC}5S62g2wKY-C z%fVoPY&gZdR0XnJ&q+5g74kfoVO|1&@Socjg)rJ$K`mKJ_W=-n|=fk{}F2gkf0wd`A&kx{D%5mgP8m_I*6@*H7Tp zS6^FKpXYSr(HtmxG(iC4mPG3|$Zjj3R{-IMe;4uBet@{>LkJ0h!nngZNHk9VcdLG3 z78L_FjXSC4>0HY$yli9pp(dqM+D57rpd|X~9XNI9pW|wJ2MPsq2IFXuNCPnrBm!*l zAz6lXfi&YbH(OjyW_Cl@PlwX;WL_T`Xs)KATV-gt0f4IxB$~3NsY0m{?xuAO^&9Af zLb@~oB#pi~;zt8a;y_}7qEI+-;spBrH7i7stD{v)kk!LTYZ|-^aIvBGR4IisXU}4F zbp;nMUP7zYLKGQyK`BQ*HNGe0wnGQeU06U61YVT6tsoR}SD$m|&g0D4v(^eoE@jh# zMFN<4o+BF$(eL*gmvR2W1tcxI_4tD=-j@4Jf=q^%4K3k&$vCqIs_KJg@mgXzxNd;i=y{Mi@&0!NM<#$%6t z1b5tV2hub_7=#GIzzJC}T!-{*LMtvHfTB<+@(dR*UB(Z8_#B>o`WdXQu1>6bq1(aU zy?b!(`~@s8Uuo800v!Aj=W+ZyfgPdUPop?OoNI7>@Zha@^wEd$_kaJxrbA`v0nl(tmA_4d1X`g`BQi$DHJ zBiG3T`))z2mEz>v?=}ay*4k;tO1ORQe1Y(QBmFjRo=^(s&Y#DbGiP+FEx!S4!d4S6{>N6AF%HAArzx@uLe)?(r=*1s5nrHj=-GWZLg_Cc+v+>HYIc*Bf zfuf~J%vr$hAr#uu##dqsn-s9Ja2ukB{tX%@X3BkW-mNfmm(A-SAb zC?yrkTd=aK{qv4Xpd&Z@HPR#vu-yF!-aGVPV>R4KG6s~;VR1<4_dp_9jT(SNYM~T0 zIIxmo(hV2dp%x_C@8cIcA!UP9&8Z$F>YhqAW`ZAuVFhJg5d5x?_E!jh%*F!2=Dp&; zy$MhRU=|i_5%|Iv{v7AdpKn~>w4{{6bI-l7t};INJO3H(y8AA~u?`wQ`#2`K|NfR|o+b;Ha2gMazoVsXa~#Bq!u2oMHA)f#5V8SdO!i{!rN z+_nNFNrGXkgK_5iBh& zAt10Qg{Suz4l|rNa~5x$cmu~@e;seU@n++?gFxcYZMUM8rdVFSI#qii+*lDHEGr?c zK2aH``-d9c)0TH95j-Q-_<@x@I)5@6luN!HN6{QZA{ojZ3{_~M+d9r;$p z5K&YhO%n`<9enWq`*Gp?1-v$cbv?^6JpaP;c>aawv2Wi#+;!|IZomBq4jnv%cBc&? zoOO{%DTRK&k9XgB2ggsGz-zC)hBH&R%Xc~*+y;$-BWX*xJ_sd6dOsx%zDOz zmP#rGD4|dk!0Nrfj_A~LNYDNVq9}ksyIB<=i$@teDhJxi#R~$03JO|t3EK{i)08W# zQpSjWThohZr#Z3nid6RsI>=SE74~FbWJWpbZ2Gz} zGbXG{I1`6VxW3yR-Ip5nInCBn)Hkeyk|N>J)@qS2Cnk^%#Nmf!lVQSB5-CRex=(OG z`?B<-P-H6FkX7M832z{EJ*?Z8V3qf8VCwpCxk~Hi6+2Y1^TuU#K7Z1L&Cg0}j8^`-v^XN~M5?tQ(#<4IO-+Xs7pNz_aArp@^4aLQ_JtFj zcm|?_o3TJr;D@~Lj`Y~m1Aa{r0Gk|6DfO2{)@Y&#L`hIf3#WdED=a-dJ^M0QSU-su zF%Y^T$#U79vzS)OtDGb4STVO33xlm*;uPbJ{fs~+{0!gE{SI++Zr)=1?>^y~_ua_CiQvNs!+0RsS(1nc7b3d!lAN7SAT4lk7VO7m zXZxg=MvcQmBtk(_rbH)F!m7`UsL*ctUH6RN63@Q<3C|QEE@PMk0D@87iFD83{rf}T z5{u~uK{@B`NJhvd_(tv@JTV#d`O+CR1t!>{h#(oYX7-wt{{jaioB}TRy~`?69ii@h z1OECF5&b;?TnUF}N~nfiLRfMJ503vK5%o%E<4kiN-`Fd8SUqA{4qfsyYt2ZzPFkvtyd9HffW)hST!TuGrVwDc^37-`}B z`x1b+CZWN&k}W~GWko%B>RAk6KhM9qnR_2y+5R!ppvQ|@C5_00Xm*8%JHmf{?T7z! zW8_Wjg6-f>vmyhy*+1_1;cZ-^b>^bs;&vho(QW9AJwAOk&Q@G&jwotmMNS+>+_=QW zF~NaA|3aZfOw$MhQqwWB!V(-~0_r6DEM{K5H*NKn4L1Sm`AkhCT)wH)ZR{ zy*%!QP<(R|E883y9E9TAz9Ob|r0D#~7;PplW)^%%gT$4ZFLT5Ia<*9A#^`$zh6sG@ zBPTWFh%bXRei&M}T+*R=V}6!rpz21cg`X0vQqQRR)?CI^qYR4;l#T}8C-H$1%SXl~ zhCwR_svWp{r;V4TZ>mzZ|H4nc(#d~C59yp0KkDnPR#Yssomn^j9C2=OX=MH)|!KjLcLmp7hW9k(IiS8#!355X}t)Jgc$G9dg zEf@0yyPO{l#^wx83m30I3X7Fqlaz(5WN}P7uCjVGzx6CfTgKh^i+xnq%F8z6es@o~ zE{35hxsIaw zYhgF6iPovn3a~bCCMiPOe{QeP^PdvlY4$ch_>>7 z#Oy-KJOa(hu#uD{f{shZT+YV>gGJhhbUV7||A6D)zo8Lwz4O(=mRPdRZXof}6E2fg zbJu=a`&r3fiOqW(uZ7;?B;P4nH;Ql5>aJR-!1S|cwK;W$=dQY;h!E62I^ZNmbwC&J zqj`0~&QxK8&H@@Xh#YE%c2L4Zpa1L>(fCe;JP97x31&$UI*E#P_O4=PmRMpq28xcX zFfuM15=!+z`2AREKOE0+_=09}Q(>61qt?zuL?DHflwoMw6-q z$(F9)A(bg1vRHQh7@~W?i)-L&yX(J^U;oPq*JJ++H2lMUt=U+8eSOeKIBYY&0T7_; z^Njk;aH3w4jX|pjnoXuHg^r?p915_tgRSp&$BQ<4H%bh+{s6xmB#>!1VyNU7r7h@Q znF#KNP^W}^fn{~!mW!+3O}$8G05yLAOe1`3OhpENxRKlsI<_Rj)#3=yJSjC?RRNP9 zf1IQ`m0-%I3s6?zrS_}d$HKa}| z1@mPo;a8w|;*0$JaYU6Py!%=`5?2Tqlk8n}U5R^{Kq7B5;o{-}EHTV6*4yrk^H=_> zZsSx>%bo9o(z_=s67X2}-e>kE*gktnTWX;!!^~fA4}ne(6^QFos(T^Z1mX$C_cxt4 z9f0%9v%b9^IyanYqlRk`)X$PVSUFf7@k`D^US8DsxaPRb9lv-Gb?Bs&imx%U*GZha zOiwI4;E^l4v`G_LnrxcBrMLC&b!!L=&N#IE+k2%HaQ>a={rSTCK6SO;7OKgB83{^| z8U1rFD)M&iFDqnZ>y{0KA&jAYXHWPNUI*UJpFs!swNe9yFe5w^wh&l7rr};99}8nP z`Zm{Nf7CvZ{wehaJ>1lhPr(Pd>+-8W7F!N9el8*$ge+i0ah%Ox0I3|ew#(@te>^sI z)t}7s-?W3YAieDdDXbR zIyLGri<*#5D>>)Cb=J-bNfXbPcEU;xCyN6)V_x{S%3{+4u!~u}WGgPp- z$MK73E0)6umjmU`g?1-%rSKF!6fNvv@(nmu@ufLxV^?TiY zpzZ3)C}o^aNTdMcO+z5Ii-Cc`*rXsvu`H%Jl{}H~FNa_{R?xm(Gf#PPVLV;FcP14q z*5?lv1O}xfRXq+`tcw7dkWC<%%T8D@{y%{BYoNnQfwjDx9KZA5Qxffs2Iy@?Ej=SV zT)-_ciL&BqheKeS(e!GuvoNi{`M~fq)U&)$JBut2;wrOia%=7T|>BNwoyrcu?1lR@J3hs?jp9*Zgn!d#3oWeb=$ z$4Q@ORkog47EV4G59po9X^{;2u>Z)m#AtPJk&eHPtYjbg2IJ~ z!F}LUE9EeevbgMLVT|%}hG>7>-8@IzXN-&`Sk*+}l&!BmL3m?kV6UGl)DlEbcFEt} zLWdw0reZ$i?L=j4xfyzoOstnWm{|Blwn~33GEeti+?W;R(t82~#<$7O7-mhT92n3; z-yR}Rex2a27wW0 zN8I0_DRFCP{4y#C8s4RYiwsLaKtPc}v?-jIG>N1IybLi({ZAo@`8|OhbO97;9IXLHZxrPh!#V$;# zbX4@_zIoc{W>at$`vZ@L!5z)Zmb4k9ov5g+AFV&exWeJjG$VPJVKMv&grRZCsO`ws z^Z{~oaLKIRk{g>0mcU}tb!|=Ak z2LS~w>M)lndyS%xiAJf(hra)BcT+E9%Z~)3(zCaeK@03)S4zG(rgLc6$=>$*wWIJ% zpXsY&p{v=2pxyvO*YYiGt{fY=f&zWjZfV&q!Wp$30;1cJNaXIn+`zEO^mLcw2{uhl zt%jBBiyrIBdJ!|O5Zj#ki$%;+wh-wJTa(opDoU5?aB{lK40|q?!)Sa0n5T{efoNbh zbTDztR%_T3ZCLkh-x)yz8|QJlM%MrJJEcfgDXMVb~)g#mI^&UAX6Ni?>QaZj|CSdlu zk;}C}DUnQ#5CWl-CSvLOWnUIzh7;>5gYfnV^mRH_jr79;mxHXNrC7z<&*ojaBDolG zVdo35LY_d#y%+Il>M=b44)}S04EpHn7)qv1!!>W#aCu&B1qZbK9yvG&A!$m3j0Rxa zOHCq~s&LA9G8h)(#?w_)c=_NPgZ^7x{wx1o@1M7oQ}-Z*Kl?!ZG(P~y-KNre8Uo{T zfk_kpNNUcK>Sy8ts9!!gb!s~Adwqh}II~?*ez%GAvEmrg!z!_wU2!EOa?%GdK-UA; zyHGblL=!p;N^C#L>$)*|+qpLJ`S<^~^J`rh^e^o2-0v>Lb}{P7pLE6<8j0X>p|jFeKf+Cd zSYk28Sd)1~HWuz~;{iqO@q|XV-!t}1PaxXlq!vpKtE^Mk_8^d@INc2_+OV`5D%Pe0 zJM*SPvo2jB?Fv$g2^?MM&+yr^h=Obc+Lc6)dj{FGVK|+!otnni+cqe$l4rf=Y8@Hx zt&984lio0})tdteMSo?*;nmY;kSkvxM$BxKw`bEK(Tn3H@(7mzSY1+4ZfR=dcjDCK}QWvPJWZm_>efxtAEQ&nvmQikX9Cf`TJXR z`g%ZynhS4Lm%R#NEWBhS!c|7;PgP(VMV)-r8GheLDgjRUwaf*Y>C} zPkpc%7uFiWd^N*)6URytkIdA;iYWG&E{mnHOH&kB0|Q3an;uDAC9*lyv}SjVW%73e zf!=58tap5R4QvVN>8CPDXa~+mB3g_MUJ9z>ObTLJNnF}e>r9u+$CpzkAkiaf2p{E4GAn30*RW1ON|o zZL{_1MIIuk;M-^yK(N;E7iB(Z(n;*%_>`T-gj~zPFti`7Nb_65vA~If#gkeXB7WJo zal|K4({0$>Y0-iUE`fI!z0EesT9kxKbCwTuW|NQj^>f0GF6M??@ncw}jJ5X?X9 z4*6;Ho(8(W%nA9}15zfOQ+s_rcCf#p&LmE&FZ8xer-RWXb+GPD?H^ZseAaRHCo0s< z;XMDoPyakhkSO_wicD*C`=Z|ZeilOkKG0^YrLRK5$_!EsD11++!;G-{`?r#1p2Zk9 zn?*c}T3MD10lL@*S%)j^OYDdlbU_^WuG}9+TzZcxF_98c6k1bRH<3FoR+1m(UPrfb zJ0cOw1>0oJ(XL>V_`|sf15$$UV>mXzK8UZQ!{P6m-<4h|Db|CH|98rP`sKk5;b%F~+*2p7?J%*3= z%{sFP8=$$MtULoyGSE36^uMF5J6n@motxph!zfga!MwBhbq^wbYnku3Pn({oz|7frLl~JuDNenGBNA!|5;DmI}CMGg{pCdeZ?8 zXDjS-88C?JS~2gKR(@EQjxvOWWmZTciyGHkf7Suge~Tk5sbTX;EtB`j6;dj(? z_!4BgHCs%wM5y*zy4VK3`@t|9hnO(IsI`qakua)Q5MgsyfjU_mhm>3k;g7x;Uc#uM z{UH^!(IC3W2~NU5ZYBReVreNn@ktg03;RG@g|Y4&8T@amQU2rjS+EsEQjgVR45eob z@fCXnfdWshN<#RZhIVjF9S136s#b!}RL(s7pp`dE3XUU%h;wV^}oyqZNJ4<{u z93zpMMY;ShDR2xV71II7X@pM1zHy`p*HVyghDpZBh6FSBo(xWY5q$XmN$@&!1!JtG zPy{A9MbLRe3`Rs!D+{b+-=;EZE=XuP$Y(`d!w}~Z-BB5P;>Ok_N6>HXf-PPV_ATBBpw)u_^^@~RwW?eeQSoTqp24j-y_nxxk@WL6=q zJOLk+3;yXH-%k%URssry;%*^kh`w;6aL(b(VR(U7mO~gJXDTd){I}2_pBF0dGHy+8q%*_W(tf354 z0%d|*HCE<%9f%#w^0k|eG{NLG_lJy8!`4z5?>oMK@qKL&VVTOJE79Ek3+vrfp z=%AnwakL6Jga~?sTOp)^KTD5!qpN*4f`8q@3$SQYIMQ0!yj_B-{Ob&Na^{myYxM1D zVA}MNp3;UJV#9&ZE}PVoGSS8uHhUn6d2@O`B{Ip$XuhSq9^~LZi94?NCFC`}GpHtttP|hQLRQ zK*aEY^auO3wj7T{*-l6Kh=*V#!g6=-M#Hv{?INr8uKQH@=ZSsh;8qW2K`F>Lz(IC{ zks-Ak5N7XZnY5mY7aixeoZ}N(3l#}I07o#JKnEROz<3xGui5sV*~IGC6_qoxQXFBOHwQC8+4pwtbxQI0SA)K`tECJON#C7ar^pbf^iZj+6OTV zr1di+1r9b_IOJEc`2@6$&&d7J&tt)0h^eprb6*4|625OZ{9ETlOVnigKFWabx1(7F z`>Z45lsD6=gIgGw`RAz_z21wr8Dy1@6~N}3;Ol^iVG15|Cb~ToHxpV}mCc9yW8k8$ zTs77nY#zePv#OA2|2mcJFUAC94Fi=B%M?<-wW$Xl7DyfhW?XiWz#h2+`ok zTU#LxBE{l}UKR#pIT{-`qhPTjQl|A!@w{rEg{!8*-ef6vKv4(X(AX1lHVAseYQPs6 z!y=}y#1qJ{Ot8bu|MX%@AQE;nyF*psr{ryHRml=YZQz_?lm3M6BPcVMre`+Mvr6-5fg9tx> zbOTJ&a~e_;xfmAMC=Nkg1aLH>2yH(MZM~+*nR7PQ(=?#5rTkAO$mZOP<7)cFYcc9C zn#E!terhZP#}z@r16a^KVk#?67u17IMb^1dkOc>%hU$?!xgX$gM=kQ|>QCGiD z7$GGbg?Y2Eh7NtK2;MqMG}t7e4KG=o~R8kO)XrvZX?L_>96B*vuO^N zXnR1c;v_xTN_LQ8hKuntyU~FDGi$CzVSHBbux%i1OUgY`WL&Y$1MM?GKz+Y(8`AoT zL&$G!)HhG2s3=iZcL}56@*0sTm$T9#zPj(iGn$UV#)e1ss1e2*wx#%bfXU+1g}|dC z;c=n%FZk@A-zPy+4pkXBd4Yy@U)nx7AI=8*==|hFtB1z_IVnl^_zUFXw>S-n%_}|3 z2^FPDT076v5SX1J7UNx&TG+NBm1ae8BsOsFU?@={`d|_shY8hNm*gcH&!~{uP#ZLS zexR}%>q-*mX9AmTH$StIl;1DJ1V(g2tHYB*_D-?FpAX+cACQ1Z`|#x>dm zJKB&;2lbNlq_4PO^?vj zU6m0$;{u_$HmV=dkW3z=WUmP`;lhg_l^;WH`T7y;3~((Q$l#p@f2cG1JZUrgAMdCk zcn(MZbl4@Cqak8`*gUX%%1=rSThMEvdV*3uR{FltvVO_rCkGL)bi>a2Rs*^RH=Ld1 zIHGkT1{DSgF^Q^ellFEJ$Kn2PElko)wGE&c!d-(P(XHBWECG7JFYqFIf~Qk=E#$1~ zvL{*cy4~T^Uw-Rq(`7P82RRW8cvG`~g2wt9V?~lIsiCzHxnR0lPW@5`8-&9$-n^#S zo}+xu=kqx3)vJ1Sv_4Q1v$(v|{fT?g)Pw!rgT0)=yn>A-OVms;D6#hi7iX-(pB@4% z&_d_3sv;V-K?P7s*N9&s;>Y^;@!KqU6A(@Y4*H9Zt8fLrK>u!jDfyy9kfKU>^rxpR zj|$bXlHS|u^cg%4DaHaG4c_56QY89iwfPL5Gc-Gt(v-k6-wLM8uv~X3X@Fbf3L%^u z7le@v!cyhZ#-QEuiB~?_IJ?!}vu2!__+^5xc0H6*+oMU>zwqZo2^K-bI9?Anm2q6l zH;Wb8BmMKF#O_l$tB(otO;^oQ8nybW;V!wrCZS(Z+)xfzd@HZ=lKePipN&YNsg~q% zc75}mL=yIL+%Tl+6I8AzGJO>ct`7eSugit&%OJdx{*eDu`mL|kv?*00Pp#mErFb0K zS57^(TRxQ;o(@p01C_{-785;kH7l%IAFev8k8Lm3mYTigq$Wb55ys)G_>qnXRN#l*bElT<}7nq34xmB z0Bz0CBfk?s$nLo9*%xj=w6!DY^%qA_|JcB*Ah10h3Fec^8ej2sLqll;w?&sn%nq!@ zSolK6*mX$VJkj1o>4iw6-MznHMylnpnTOAb``rQ;d~~>;JGD3$tmHr}=J$z;?NMx6 z{ejjHneR(_)FQfVo>k$?hO@+{au#1k*J-$|u;Z*o<<)+Fh)_4N;Y&iOI9Eh4f=-PQ|ilf5B zBWHboeb_b5)WE2MNa31ysS!F>oH>`C43({J+Tc?&5u)x1N7;oE>7O8U!z`7b(PCPe zT?M_r83fF6-0cv$ch$eZ$9XDXlw3PMGP0PuR5%(+32yIUbVvtWDs<(V>{=C_w?pXY={1c%MkWs~P@aV1PtD{gdU<%;93HlvP-2&Sio!Ab= z$7YcDd+e@#^Oo}^kj+dA(YdHx1*1AaiZbO5G5L(2*!LnMGQ&Pn&+FVn%kE%EjSkO4 zhhgZOQKuke2mRxGrg3-?E56ildT zExYkuw7g^8fx}b4p|#9ObKCH3W(+PAFUVE2YN3qI>MDc;!$`9H{z;H+N<>Em&>Ek6 z48whNJ-e9dttZs%lggV@-b`!7EfRSu5Q*ThRuzbpk&LSBV(7y$AM>_TKq}RrDOYeO zA*y#fZ7N>PxlGC2k`sb%VbXc0ZLm_qD{4UcA|2!TvS;i>b+y%sY7iIS8bPw3#|B_M z(U5X1#=fW(G>fJ}OF`cvOzOxbLn!mS5cWG8id10)!%su24?JakVYpSUoLfbPMHv0e zi+`YwZ)`nUJ&ueL0sZ&IPAxVQ`e!|O6hcg`51PEhs}zRZz-O>e@v2PVvzazpMpMr@ z$k`_F&uF94=hF_^Eib2QQ&{n2(GVen*@^2|^n3?R>V(hmI@bc4J6U0 zi=xY}pK7QyhLuv#kfCXyI`ZvWi~5?AplE|Sgg z=*ZZ)w^Ma++$}}N>cDUbV2mpAZFd2=P~5JhmvGKXPGk)Y^Kw#J1%$xQJiSfZYXL2C zBZ^Pn;p@LUwX}mfY^IF#o3ucH0DJ(>5rcf@5qH2@uG(x1A1FBkZOFKlas$}i0w81ybDRwsAsXy{62ZKv4SnP>AvSh?vO zi8j)}E?626I*dQ|vluybm<(v7iPu}Ra532v3d>Ul&wNG@(l>RV%Gj4mab)4~_7dl6 zZw!l41yg(&^l;fBNah*f@4_->?HZlwW61GJp;ZVTWjB7&9O@xgM;C7G`tQ80z z&K!oixM1K@C~77WmAPKhH=@XbrM5NWKUN8P5X$GgtVC}092frx7(Rc2fP%2dGs&}R z?`({A3OuPO9jdv2gl=v&{9+_?`tUxai@&`wu@JW>jEj=xE8uKhOJ4%L%VT`pRjI2i zf$BbVHrhH`cpBF;ebUd0Zg2LloGse4DV{!U_sYXU(5-(5Qif0g8S9eI$oHN2FY>{q z#a!8@g+WopN;0>ij>=^ZKW*7M>99{`d`R7PFZX6+`DO|Qk)^hd&dBcDc3ZX;uIr{? z*`*{ml`=78lr0ullDQqkIvC7jy-Y^qa} zI!gJVzd%YueftxbZKo-t*O=(`XnN=Duurjjm#neCbH76%{+-TaLfiEQ_d8 zt7vrCpH$`sPUw)nI~fSdzYlF(9S@cAIfn3986%3}0lxxDh=|o)9+MPm%UMU(&FZcE zQIQ0nU}O$vW+TTOK_qAhUo@U@KxK%vodRwU^f((L07Q4UWDf zY8Xy0uj2awiBxRo4ABqI6;05jYxmz<`u`LpU370qc)m*8${9$-Uo#B;{il3mL58ph=l26&r1~K#emB+Xes&^0K&k`S z;6O7zJ5=K?U17Cp?ubGgATSoS{+PI0|}wNXu*a)^&vq<&73ML{-^K-!J|O z{yl!hZih{eDcSkHwTO&QRZu!y;=OSX_#XTY;wE8WS$53@9d!jl)2J<2^-MeSoUvN-|?jE@| z>gj0EF?ifP(v3MF!CGmvKK4`ujAZwiIFn zc7K=J?6M6+}UN3RYdHE6x z=uj-4TdO}=FC>SM!sNXK`R*wR7ki`JtOp&E?PLcpbAByltQqFn^KAWIvBxj52?6MW zAOuh-ga}5;ujbNQOeqhh$duB23cmbAD}v~p2e1SBMvEPuIX+l63WUN?zw#6|DpjTt zHkBe===da#2yP6>6bOtOy|h_6)`ApTpE!-DZCQqt);`XHHv0OZ3^hu-%O2>@kHkHd z6jNSrakIxg^=!q)vs`Xd1evT)N)|-d{m!26m_71H$48Zj4%&y*dJh-QZoEy~bm4V) z=oJP^q`$W9HNTDlnj2;q8Q@|9Le=50WQSC#};j+}oSU1ELDD~Bi-tw>gSMVS>t7?#-*5HvcQ3a@NwL7}?% zy4-fJtBoaCU}Ac1Cyfd&#Gd#qQ$2Y8@0PW!)V%1%fnxu>brX#|nmkOUc!0bkKRJ0i zB&u;F40)6|2LRpX5uSbW$Y$Gsk)UkUCfa9ld`aN66Tz@`W-RIRj9Jjy?LV$I#dXHS zc14`PmGs-)@kkrv)6yX2iidY;s0zQW$tJ}xJ^@qq{0PIAAs;SFd247?i))h5M{Th!Tm7b~p)BmawfC|&i zx-k94j^)CpC$^D5n%Kh1&`RNgL6a61G5^n&!kZJ({4K9T-~t@Vr$~$Jc9m8=I!AtY z41m8MPcCi}2|^*d-DGoy`Ba?CVhjB~<(v41TNK(x+8;kcBAu*_Typdk3BvycP6rl; zhz#0Qpq$YkIRn~d=0OYYn^3F^xDky-H%J=1q`NP>y6#3%f6kDgPHNXS5;3D|gPD6v z_8p$`9}WAu&SVkY7MvnEa?$*a3q7sVk0zwsQH_SGOJNPGnrOv&69Z*L5aE)Ji^j28 z>FXWaKaY2TRwSJ8MwP%e=>K#DX=!NyB?90!7^}cZo^is*0OUkQo^>RSzqAAhTSBCiSr2Uytl%ByUIPrx@Bqk9gnR65{Ir%{ZaZHo1*Gx4;7HJEO zi@3o2!rEuyuUnm2QxjsFmt49v>kQGF29qw$ZMB4Rr?BrG@u2y$ZmHr82wgmBm|~s# z`-0VF82wPA27y@{#m$^T+F|I&-I(nzFB`*UN4t9bF*I7P;>P}qr})OcwosPNb~8+L zA?MEk)z^K)p?Q;$5>8*OS-VQS3R-$$|jPDOxd~oq?#^Wb7Z#SNq-A6X%6X>DPSNm`O8g!cf`gZiB(?g?%GTe?Y}dB{IEPwf-dK?JTq7zSYc$W8ih zE_AIe@N*}?!R<6?-qdYJl0kM_m>HIml~0W)Z)cT4K?sFt``m&0pPt_HxAvf@7v#31 z7ILvmEDHigSivz%-ulF5*U6aX!F>&vcLe$rRH$@G6JmO7fVBl;{I5{X~=oweTu&+R4O>M)>0Z`=qB_-wOT z96n7rec~UT3pPbNe`5c9|JuQs+!9l(_o!m&@i!hWZ@Y)e3}dR9Wh$ zLMQDIGH+$v^z-(<(DhKDsHfo!lS}9ii>n;%bi3czR8^uD zv0xw!A=qS@p3G4FXBLqgpO>z?UxLG9Q)MAYp)Y)5_o`W#jYq31sli@l3t9MPB{xsf zwi^%V8f^?L%0>5Ic0YuKUe;$hqz!Z_tx9wgwHJzSd!ULnjy&kYodPd5sYINfBM>3J zp?_YHP*e-S|5qftb1^z_HEM@Ybydoqx*)KQqM=}CM|YAhA_j_dsw-#0)rLl!Mi2f3Pqi4cL8qQzB0FJ*Zj$Um83g~O(S0p9bQEXICl zPo#`bqND>rqm7!@R^8@J=|g-KVlsrGXwiiZBiCcfs`glA{HF?9_VDt036p5GK^z+? zDc-zk12z&YS3jIdri4@o-aNlb=uFM)i6=Vx9VoGp-c63Jtg*T~)2Cl^lUG1Wezjnw6fG?vYp;6(uRqM(wE4$s2so zTnnX#O$pU62)sDz1aDxv7P@n@8f=#jnr4M!l>8440oBl0n0^E&JuAO9v5kzoDAe*1 zF8ZovA@`0zRJxH5&P4#13I2ZTkd(57;G)-@4KKks38JSZbkw1*pMyvt;ftHSCRb~S zO-mJM{;HNh?I%zv2chgCg+Qoa7o?WbMnkE}{-!YCtRaL8I6iH#FN zXgmvD5uVtq^2Eer=z>y3B6;G03PZOh4KXE&T){NV1cnYe1n1csr7JI!Q*ucgkAS7V zMUUPTwZHgbk#jJOq3a+a=89<8@`c$O%X10_I`&f=N52lYf^^FNxBz%0)us^Nf*DnJ z*8l2qIBz$Rgu==T31O4+6!+>tOhuhr9F{%tS3fOM=gUE;dGG2p*{43d?e8UJ(PR6C zO@t`fsZd^!|IPb%d#B~R+c9bDgQi(GjfMrOZ76x|se%ot6lw+lmWY_)QR~h$C26cA z5+Z?gm`EgTGzYVs$6miCy#(kj>7e=H&*}zZu-Mw8c;*SC zKJFqZ{Di}D?WBFc0>YXvLemMl%GY4!Rb;do){%JItV1L0mM$aV^Sz!B%Lk)Cn-%_ zUHSX6Khk*@`T?Mtf8n_q%_a&VxaztVf(|Mp(eeiVx%WdC+0y9!|$e+!w?tF2mk^ONa|umc}i4fuRiJ^`>cLcL2eaH;mJD| zVnI;7a4nER`v;{0dkhF~1)h9sKxc`hctPoNVA%TI8irsHa>IJqSb6uF)r; z%oFs(1)_{rN3|iXRTzvaG(W>YUcL3h3&fe29q3G)D#um?K#Pzg@~_4d#>+W;+UlZ( zvG*_LOXM*k|J6Ki&giZxDvlY}kaDxfAfD}@@;6)&Kdu{2*xaP}x?M6PwdFSed8L-3 z5wdGwlX7c+WbhVro)a8|r1ytF!6*NA0ikqT+tJ2lHk*}-aK@!OHMr+TtPruko7+SL zlZ4oMX8Ub=EMw9J#cf-c9&{D)T;g6&PTM~ZzlRA>%geahr=`WlmIj0?vbyt zCvHh+7U|6Ra~*WIA=Kg2gtSGM|6%A3kA<{(aN(v|52v?I%Uisb%Q4>cF0XVq>nSw= z+fYx(p<+Sq<@sF=)Ee*uRh-0VePuv-ZZr~z29FG8=7(^C@XH&` zOKnAiq<@aWRN7H#ue*i^QF8N{S!6qQKfStNAfsEWDbk>PDeqLfeFa|HwfqcWoWT{`I*n(9gQXzLM2!_g-&61k!%qfO#4(jhB-m@JD?bT4;YxdZ&g@fQ?}~ zY_*+HkI@gBNXNO18`p~m9wd?esmqc>gM3TS z0u;Y6D83$*DX#7UtGXpARPtOH`dbP75SZePdQN`!Ct4otcaF3P0laPWt}cw`A4wi# zu~bAPgigppCNI;QT005WCRL18#Ehx4tj7Uf&Rw!Al{PPDaQ;;xBW!)0JKgq*L}HtG zw2D}s7lsRreCxzFTrUo9BT8Sf0$SSxEud{_hWUsciqZn};X?WQ%|D>GnPQ+9}CT z?)T^IO$Qqo=@IrGn)a(k?%k{fyR|CABqvXFb|@?K2~Dmv$}ACk6TOyrC;!}?sDl5@ z{#Mjl7|Nu`T*|Ym&*;%|Tqju~hv>#2f=s*mjFQM*XxvlRF|4}7j0)>xNC*?k^;Z-o zs<=l&SQvXlHPHzP`I(7gNUd*}6~QtCEjFCueNc%YD7$rLP%Q{g&VV1<$M5ls$HhgT z3yONaLe%FUb87VZfyw&nWN{m}VmCzm+bf$0DY1Q-19ON&X1fZxYid8|(!cB=22Av| zrD@^`_L91dqhCOZbtQ!4JFaWhn7HNiB&M2G18Tk2rZQ&4#MtjCU9TXh7N>CDiIP7b zY)4W#9?3hM!5^4}<0~N%{qVwX54#TwkPhJRLHeu>^y*R1@5sB73z3NV4aUG5z91BZ zM1G@#6ttk}hBP|%2#($HMfA_OAFaEt7f^8Eizv0jmUMFJk(o+G(kdo~x6!87ro06W>YSm-(`vCesz0lBYf@nIDGRV6+t!FXMNT|2iV^kv_<5*n z%O_gA82C8cd&l!f@vMNf`m}J=*ofAj=`yF=WGQkE8TW>XRqF3)vyXb{8WUMXY9)Tek(Xd%irc`URQHko7&bZm-iee|UG?WZ8$U(OwHYeae3CIk@Z&x@CG;8WfGpKZt9+ z%)6~Nmwj90s|=e(Y|a|B?9A<096YuOA~{15jS4WIjIP-{+1{54B90zx@l{*mZ;faZ zJd&t7CX7)h848VFTHI3q$d;|?zp1C3i<6XkwrMk6o_n{WbUwFcccpdw18aWCRYX?o z=F1Mir+B*jel`3N%r62i{9KRDmFSbG3saF4dB?dI8loUcW%J0{z7MAUd(;%&yYV6$ zrBSeGQKyni{yV9L&RG>voP;(?pwy<{VT|dUD0`;qa_DvO2CrjhyrU5O&#v}qN-Mbc zM?om?>3pH|4%ro1nK&gdT{P}hz@`$d;}`l$?nA!TZFe+*xoi116cZ)o3JSOFqx20! zd!OYz1Fv%5qI!eQZil=jNpb9t<4-{GacqNLX&RZYlt-0llbT8lO1iWP$l_DV&OStz z#<180-Ff?)CiDiR;Jr1j7*Gde0lG-BuE_>hmt|)TNmW0%BxYBD$=P-EwrXGM{4p>c zy4HrQjDiV@>7=VLb?jKw*Pk-R^pz{BW&}l!2QU&1bq^yjs-aW2;pVkY+ZD*U zk7p4u+im({_6&MNRq%cpx9*HWd_R=%KQ*W5&5suq96I=`X|DbozUjC}`(c~n5sQ@k z!5SxxQeDNJx}CJ4Wrf#u;(+~pbffEHdr-~r-XM71#9EWw`0Q7vL9;Lpwq^;FGC8(A zznqjmOz?IMHwB(}{V`JgP%yb^Pnm`>y_N}At_o*!X)Cy~4h&s!$HQ5p&WO#*P5 z1n`9ucQ8fH8P^%hb982Ld8~eGUviHL<6X$t3YW+pvI#hS#oJaV!yC)ouHMJ4t}M7a zC_T+lw6)9u2qLU=NPGSfDq|xp0m&}H_-Nbn{^**O{! z1P(PSpu?InQxoCa-11}oyN%Z*8HrM*6qsJpnpJsn?^ia zSBi~c@no~y9OTr?hb(ppyBRM>5r7-?GDPihXE81{sj+}8di!to!8rQ?F7O^FTb$vB zj)QOA1INg97u57-$+vY}#x`-mQiiwr^0)3`fUhH@D)6@KVFyQbi^=#gy5r^B>&#CH zNx(PWqE4uXiN$twXTRsxIqOdszYL;#fwA`~^+z*4&Uh16SwMO2P8p%c7NjTx;iAw! zEyfh$VO4}{x4B2VwhjMJ+}Ogry2!CG>@YDf_IAIMyTxz_^z%kYe|llmiK(WntfI8l z5A#|7q*H}6NMQ@YGk(0*<%_>!fIWfsGDX0P(ZA@xh5G1kK=yX`9EeJQ z&SoNcJMJ45=r?R(lAZYTU>xn#fO$g+wjE;%fYUPJrb~)3x{s?U{}rqu6rGme0xm++ z*zu+kzK>lJred>FhK&HT9XjTF1-xq5*ZoVB2s2(lx<@rKM^xlXW}Ta{nA`p0=*!M_ znbX#*_1QNJ3{;tdD;oX9L6Q;^0;y(vP!F4t+CJ1g4ps~frT?}>?!OTY1oqx$c0*K}y_b%P+U9wByHgWLg2$sGmK#SF!`#ntLkp5xJ|s=9FarNtUnZutS$K7v?uVD= zCqJ`!ZpV|diQVc(PR+q@t2~r}p>%<5KPaAC#X?A&ny|nUc_+qFY8=bICMJFr&xVT* z9|sDo{k;3Az$op-K%IKn_6&NSO0gAT_rT-S($jL3JI;6h4%WPLlNDG_}9@g zy?0!q9>_?4GZ~8*S(G1&E(YAsmc5jo=+;s@wxZ=S(eYo-f(XEvSNt#kD8)#lXb|w>{15d=c_eP_LMkm0$BA0c(_K zsonpPH5ge2NK;fViY{r$`wdi&{M7fGGuTc0bliF)`w6GoW<7EBRzt^8eo}LIrx+(0 z6$#rY))oqMN(WPrj7COMF0ZqWYbWH``b&^0FOu(&snjGFBHRKlMQ@^IO%oEDOxli! z-xrA$jR#g7qPkt}?=i>Mdf@4NaVu(iA{eSNjZ8Pyr(+OLLUeiS-ZvbJkIcCT=6=GM zurkJYBfm1w^h{zdVbVMx!JXu!;S^Q~!zFQXwZRd4`+g!j&XiMmc;JM!=6aCz(1j=( z9HR>a>oQ5MHn0X5hLGC=YpPIm4Te~P6@birv?x-hS=PS`V?ClH4cbTndkeCFBeaKm!>M5{J*An#SVLgJ`IhN8SfY~rnCsxc3<=` z=Ndo`@$fOBN>*4-mM2^*nZUF#y8WLM zo=U6~cTtYqm&4EaA@w~6G^ob!iQmqjBNnN9S7qgMo^3nqloUuD2v+wOzDuGq-#cgK zUT*Psyld(Z?N^M-Pun_u_E0r%lQtcKsl%GX2KHKva`wRaOH27 zRo?~qG;5+pE>n`U`OxkEJ5Q+uexH<2<|BhEFK)12`FowyinjUa8jAfoKG-%l%?q^M zW+!bZ3IION%JxDfH20@N|#6|sT zXN-TG@xaGgyKzp|^Bjg*fPLS?fS391xb5+qnalY@LU*%)Z*iwt%D7tT6=qw5WmQYF zMuW#nT~6}5OLn^DG$12ncCiz-hcWPhPYsuJn@+TekFEEc0#IONdikbehKzf@TVS&* zj#kVcCemk#_|Hd&5)=&KODLzN?Z|#Ll>*N#9|5gg5*QC!o#3z|%!{|o&G--V2yvtc zIveGadHrKY3g_H|{3{&IpIPB}UzONt`L)d31GM8^bN6hdQL&8do9|LSiu0D&**^$w zRMKZmQOuHk^&_l!HH|@*%3qx;Y*^(DEbMM>sRIX_HJDOX1C?_}HRp*7&JBM3m0zD3 zDJAeBWHmU((X7g32i_t$M$|MACbvj*@d=sH}b0P%`$YKhc3 zxMO24>xKhyrOYv>gYOH{7~vDO!?&uwDnKGdHUEwR@!GiJHDL{*hV4g$ay9&3l-!ii z#g4m%hsW$^U;hL9P8{*~nb6A3RmQCYb(ACQvQ2GWVBhAG&_!?N=2JTtAvHc-87;EY8F4?aOZO|BYe8gUl+PTQ_f;# zqvW!DbZq+v!y@G=zOT!_T%M~H9Vcm0L$&o+@G6|5q6GJB$&8>&p?37!RzCv#h{YKN zMyg{1pcs@n34Q&cb^m>C5ue)zyVI(#-0BcVm4$JiH#$>7Qd7Ur4E~2DM^Cv&v6E&t zI7$gZnv$a)DtT!b;JnSOiR|h&E|s?z+K4sdKAl&*jq!x(sk_8Rj~;CdmD&wKnG)9= zgu=?nr(LNa%6h%B7n)nqLuUC|F>;FfI#wFeAOe^XXk?BC*EW*Su-4&2bf9?8w=GKo zqpD=apfaX>-aPi(=_csmN3sSk}|8nB*BGu*oZY)P#o3_q;O4;c@^lI4sd(^t|i8uz*8yI&3rO_gKzj zA;I-_&$c5gXU3@?H;KNv2(BMeT2um%9mfJ;s8-@n+y)V7BvFnhf{-tZ#bCZRNWyer z#4PZ{tWg$|k+d~*B+oD`h>-btBmnU0o1uPdYs{$N3p?l5dy|DqC`J(=ugG8zvTuyk z$`i3OyEXHpR5tJ0YPSdBmOt0d!gMvgk%{9QqXFI>_H{;cpK)_iIZaO*!fhuLW!Z73 zcu-oPzX_|fKP2b)2sFs+<5gC~1TFx0>FtI6(A#Wpx@~~$ zKmuxX9r5cA&`OPdA4SR3*Sk;*Opbr{bD?o54*c4cU8hRVZTA(~4|fWiP?);J&Br>QfIWPzB8=RF-Un@K<^TW_#K${uJ@S zaHM@Zd?`I&`6Yu5X}6t>mrpFumSrvpBL32KYUuyV1sIPp@XJbjd{d8ciD?X0>|Yd% z?(}AW$?DpwUAVIROlaI9CcZD>lJU)zUgGLwQ`5Uz+Of+@zMP3T??!mZhY;9|W$>da znF!xrq{k7m9+@4m1^v=Z;g3x5nzh-Tq$+HRzW( z=ty2AYLdvQ4YvUDSNNsUY<1kfEL+LX{Ugy8vXKT4KO@ch2kK$4qzLD#mA}Cg_dgx- zO3yIXkmR`GM&yCNIoH7$K-vI2fyYh7K7@PwGpPt9qmGc5dWz9PBRyLBb3?GurNQi= zhrjk|jIljMl*46jUKE4}P)fiC;D`I;hrZwQdE;Lp-v{=4-c2F$Kq>lm6~F5qbZ4zs zq0^h(V&y2EXuq?`Ji53YA+`3OIagOVA~TKaU@0rx57iARr*CyF*XnN%98$->p`C`< z@{aTWM#Tos6y!63Jr_ROHc_Dg3e)5|o>g zxLh)Sm@9h2 zAostVeR#@#Z+3PC&_5?}t5o^#Lv}kl-6+mm??u;qc0s%`3dFqBBrEuS$aUYPnd;D7 zS;Pz6wR`R>8iprJoEXV$aF3M+MwKgemv{&eqWmcTn&p#(tbE`af+yEqW~pgS3~^Z~ zsnV0^J>-~51(}x~`Ch9{{a!+7%0=P&EV!J=>fa7_dmPzm>72Y~SrkC{gAWY*P(tIe z!cw^b&dc7Gc8^tWza^8pjpwNN@xy6LzFLy2`QEP#=ovH}$81eYc-HfJrOFCczO@J+ zFyfnxieypec|ed5$+}eSTeQYse=|IurWF{}5+Z?}UTd|xHmj&9zsd`fD5|X$$*QqK zmGZk7A-ZtF?k_IhaL)`GqE+swn6AAoeNADO3OGMR!t3J312nlnL)NZ|+n%(mdS350 z;|D4HNHGHba{282l)Kz#3nG%66oI6N36A*br+3>63iEeCI7 zO=m35O6Hx<6@8(RdH(%N{-b;yr4AG$W3PT`a-Zk%-cg|&`ZrH7A$G<(l9ba!xu*h> z!W`lVHdVoL@u-eiAW!l{!W^Jny0)T2pOV7rRtJAN9C>9d4@&%}L*PH}8ljI%ThTUK z8XNNz7?xJ2Vas=TGbxPEVjM_QNKZ;UWRHn3E^YNU&L(TLT#2UJT=(5S9uGKy65`xZ z2rm${g1yjHqG7e?)G;(kZO4g<&a@cB^P`^2V-pWV&1$clMfTy@QedI#6sPum{I-L)=c_s6H` zN^rUKfqk-xUcr4N-BsHq&M)w9n|z&7^H*x2WgWvF+5lF4=?pq5m%CW=-34v*I3ALA7ya= zy0V-oM7?CT();|^1GSCHm#^CYtx@dJlRUC61t0uAB!kG(_nQ5YRPlxFrqE{&9i}#)&S;0{3@K+D1Z=_h}6bd;RC&Qrq%NpzQr8Q+(O!OaV};uQwvxWCvs`cW+c3d zD|+)$IKQQZOCmi3M7|U!;FZ#qB>N_1M$|;q4ap|#DL68AI^mEGL;j}yVel|dG3&l; z*R61%b&MX;IuKNGd{3deb@sirWIMOoj2IlimMYE%gg*5!N?g^?7!MV@y0U0{I||AF zBTq|J(eoE|Y`9a8rz;EPBPA!_Iy}@P$+Lw~6zIFU;yAW*O0DWV7X-9CI{{iwSfRQ~ zD_#Drx%=E1ZuW4gih->E_#$jREkHhS{cvsIwuxX@e~j+-ue3Hewtyog*Q*^FK+`cJ z=~qP2HyO+uThgHAkwIDcdX*{Tq-9n|D0`WV=tlQ7fhoKA5ZRP!R%%1772?hnj;Yw~ zr$ytT>ANJ;LRMH0S(-+Oa|(MFbVkAeXC_p4k5hsF8bq@Zm>x z={rp(bC!7S9QRc7Pd0U&*l=2*w2i_8`^Byif0B(ng9r8EC9ufI z;jcxBLZZHGFQ|tA(S$2KB~R(gBas5Ni;qvRd1yaO4k_tQ6Y2A~<4Qx=5fmcgf0Opd zNmZ*8CcD1@0S?4j{`A!uK8m_?0nwg+0k&L;{&_{awa4IVq(mOO&!pkPwxNIPz-Z>; zZ`SdykpL!ImZ%Tlp{>-J4GnQ%6a}96R-1Uv;f7PeTEXnSkKp_j7Y=}0alO6-Vo?x& zNSjvD?j)Y>sy*Cp{4VM2%KX+=AlFLP>7b2mVp{}?01&}58R{b5Wv%;`9H z)Cod5nxM@#X>VdB;5ebBOEIl|t$gZXwye!>a|>a{8)$T$K4ty3`P;4czN3SXiTbmx zs_S<$LV#Xz5R4BKEt8l)Be;A!a=%)`Z~n;0$qPKP`ThnoITaP%*?}6xgd3o0hWed& zDm|4fam5g4P>eAoCoC+C2yWjQn^IU0s?iMO^OB@viB9^D_JySwwXFhAs9weNS79Yb z#%d!c^UjuyD6^&Gw7bCbgw@esBAGUy{Ide2sIna?mUH*C2l{c(&~~P)dfUp}1te@+ z@q~{g=|Evuj&2YEG{dBBBT!52~b-NP*ZtbDB`L1~>Ihvni`BdQ6;bZBqTJCF5`vj1z!2v?CP? z-tq0QfnD!NlLBT62dtNTiEowQADo!Vw#o>WP7QDH{P4^Oca%{T4&YjZ=02^UJ2*>R zW29=t&A!g(hSPDi`UeWSN5!P~v4^6Xa8y0`6W{*M!=}^qZ2~`WM@$uM0wI9$ytI=W zF4t1;cVTERLIh_Fm-5YKclXQbj?)dFA=btpe^+WP_7s72R)E1yo;FXkJjrzj^+itI zL!6C3HTc3_BwKz3(OW&sK~D2ZNL6UWZ8jjd;++gPMEa8f0>yuY;Y6R3#-k6n?`gJc zy*1HbnVGQOF%)1jrolGOzIp~jk>&8&d|&jO0(iWa2(NP#Y8YKcR(z4&4#uL44rU!J zCl_WIc8XK)Qa9BIbwG5OcN_gm@E!0P8FDNK?TPy9H=csr&o;HYaq5FiE*VNP#BX-T zOlWW{sPI*0N}Axx@+Y(yo@wdUzN{K3N<6F+cGF*dJMNro zE}sm)9t?*i*QZWJr7YNZjjG8-=;qeBC}o~p60puYspKkcuS}_cVGaj)rW5J2wiHGo z3!%u7eY1=(CpOKtyw@L&HhErfr;@bjVVYYN=a;jC5ks^ai zu;4ad-L^I6e#_Z}Aja?gz{bJaK8%#6XEJvFiG!W{IVZui96dXc73bg=qQm$Q$AX4M z#!}H389f?JK&wrK`{q{VmqjBO!%8BRZFX~Aqo)&Io+ zN);+-&NI+12doKty&rJ0BCwg*kgP%(qIy)eCF*EP&yvAh`xxDPHJQvXoAIp3QeP8} z7S(ep-~DwP)@nWZ!Qj0lvr4{)Y{`a781u+CO&><1iPb(FJ)-hz;s6U34S|;5&x^w; zf$Q7EbZTznNy-WOc?+ny>8+=xi!5F>)(QAR<`d&rYa**e9bl6#h^SuHDrWJcr0tw} zSL?m{Da}u{e7OZW!G&dLqy&piACHKXAt)E0HAcs>^VC(=gH^&+dt}xR&Ba5tf&_v- zFxl=-PWnA~jndz{d5SmqR0p3n0umY)#v8miAs6i0)6E^hYfD5dgziDvu=i~*Nee8I zVHM0G>J8J@DE+06XjI|(({V8X5zw}~vS?}C!sBm*Ug1ag(^;i)%Q2fgtS@OZKlN8> zP-@4pEb)gUrWi1-6G|7sR^dz=(lxK|MCqt4ktaAi)u-siG>!xCEAehxwW^0`Dh+K zw)XzKGiN6D{dSj%?0RM!EW4ubs596);9v88QJOs_`nSJhOHj}I{lGR3gSw_|K4Va} z2}ZVKGMPNKJU4Z5BBZrEz>!5uVnC4}iM_wMF?1GfmXexuiE*2Mh;vi!p)Rws=S5f{ zu#wE==5?$>e^T+Ri+I7)f#6}-bje=WR!WxVaS&$5WK1{dBY)`Tr& zV$y32nJlIyqF%XDcb67JWbyhsL!xxi%KTAf87M;Yfv|nF zS_toK=hsp(Gh!o>Rhr(>cj*;_h63gN9Q!dVYP*#m&b<^ti^x*uH^#XfO`nw;xOG^% zrbyhE8!w_eEK)povn$I>*~)YzuvIFc&$Aj9dn~XY|x;Aij#o>#~*!KC~d`eerODpU1Uc{)M1rGp1svTbjWMZGv^)LU7 zJ|X&K&afo83I3f=Vw~#sCl1(gB&xv_GM}lE3aPDsf^=i0CBr-4PEOJ9D&E*+vW_b_ zfn{zdcU5Za4DMcLjH}4$yr-`sYANpc%^8arlDW#SFDZiV6reT(uQ4A?KGaF?Q%LGU z?BLh9qBvfO5`zU;k!;rr(uIcRt)7?Bpr0y43yIacAieK z@$xP-)`ZFKEaMw?+Pz3!*%#D?hq2~82;k{wEW$!_;y=wPa!9v1B029)63_QMPfwY^ zrN_KY+HMUDAOaxQK!4!+WIz@mi(B0KeJRnf1PYgM@bIDt zDz(n$%xwBZ^buAbE|@WazhRguq;FWl&6q<*sJPS#VS+X311UpE@G-N%JbG|J!VA|?!5;Ipm*nj6|u8EiPlpgUAyVBTISGhW6rF|i77J!+R z5rjee?->rBH48@0L7(P6V(mvt(yMD*N-Kwg5((lXd1a8OI;C2Y%==y|gb{x{u%5TY zT8k-+-Xidine73;Sib8B66WJyHTi#kVw&=Y;oB*qlS!O|%~NHJ9KI~RE=};>*IY+} zxNK*7y1&r6+j136H>Dd(%x<0lb6>q()U&Cxbk*PrpGdx3>*9AX>vT3CDb=Z%D^U${ zJfvJVEpr&1Wif+lT8)u~c4*Pj^>5;eV6@M~pat|rbGimiql6wh@3OxSN!-cG z$ktiC^j=I0hVK$f+H%{-)-T04$K2_y4QZe%aiylICwVl1Q8$w4vj$c)2<^N?7=*Kp zTyF|PZ`R=c3wQcyx5j9!Yqi3#n1KkBbkg8aE}LTH&v)hK4d_~FXq>2mN=axKE7cJ? zbf3QB))C)|kMs!${n)a3TtGc4`id!}VfsOr@X9C@M`MjiMua9+rS-<|y{uw=se7ua z<`L!Nowl%}QffkoY*NEN|DCB;#BaG(cWbbrA>rn!qlh2m7TD1C z*_>}xeIB`sgkwiTtNCP|zqaq!UFmK3*cH`OGZ|Rql_2QK82V*1OE|@+Iu2@kjai6D zf+~Nyt4O8&(V|>6@q3mfYr7<;ARi z!MlY%aI&Shv&coCUneyFZn;WV94~k0V#oC5K*_9k2z{19hLv2b!j?Ynw3}qS7W-nk zXhX&oTwc@0@a(2mT+G<)db^aO#ACvDL#d5_|FEa?LG9B5B|HsjkgURGN~pzrr^9*3 z&}W4Lx+w?xv-^uZUnzjEP!QhiE8;~T_jWtiX)k^swu$B=LHRsvi@nOx-x@pb`~2%& za!YR8D_EHCQG4Kq-6Kxqk>92I>?qs2JulcnyiQ)FqgO$UtJGUMDoj&xlS#cO39#vU z`~mG@$kNZlcj(%Pw371(axG5Tk*)dGS;?UPE9Cfj7suA_NYB&MCCn#u(;Ghclc8{P zi=#=dVY0IQ-;*`qX@S4&$G!*nu3Wv<`2hJg{w;;XCCn9(Rq)3bJ`06ov;4|gj7fpt zUB0NW{T;2kHd@Ed0I|&pw~w8&G!LLsK~~*`Xic&*6M>)@t7B-pi>PSl5U~hsi65(t za6>CORn61Pbzv>aeo0{0CSq5Gkt`)Sav2(g^iLlr*lp1SFs0F%Owf{n3`|SXA)mK& ztslnu(vkDh+j{;~8T8$~)ZJbkE|@Lw%Ke?ZID6%=I!yYHh+Y+k;5@v1vVL?#^}!MD zb8^yrWySutKXA(gG?mRG(|EX~q@)R!HRFLbDOI>RXBv!?^eWpIIb)L|C>G~y(J&#tIFB6$&F)}0Gwb4so1t|7M_k==8csy7ZduVg~ zr8Atw3UI3?@zR*z{~7V*$BN#_PK*ROT}Hf0RJRb??9gH8Wi)53WBw3ta14d6Mqj_1 z6gMy)on$+9+c5)6D`=TW@w`tXi5H4LshHCn2pQ}PKD$;gM7O$SEenY0o9v%MRf}I# z@RNI0Z9zF#@A8Rr@+V)>5k#+_+&ms(Wc%#Yya&6NyE}f^@jJ!a?1gLz1)|IRR5ubf zggJ48h{x&VgS4lYUh-M&Lpl1lJ!Z`xM!rZ%g4Et=tr@o0!v{TW;51zY$SzxiJ$ipXpAKu2h zae{r@S`zQ=1?dwVhzrKh*u$mfy8}4qj&8xU+qIdOW1qjpn61%#NwtPNGq>@~-1f>0 zIP{Y#EwiDav|P_!BH-aw4c8=Rq@u6$p&_T8Wam`r1s1$neyuE1JQ8O1@nNFYr@Xk|nhoL+AE+ywQPPH6{b=}Ll@ zO|Urb4Zml{I10YjYpIhQb`e%e;vM!ssJNgt7hfqUGneB%%@`j1l|{L+8~s385H3WT zMZjxdloSE&d%tYl0KvkCu$=}qjcmN9z&YB_1rf=emrB{NzUD_uC9_zO4~NvSR_dV8 z+n}ANlX>p`_ab%?H=NlsXRtZuqJPwi`f%Hd?mKp@xjsef@UCH#Q68)GL+~4H zB6NZN;7~RxTbBER9La(&OG+Tn-=xRPc#BD?B7e5KZ`U>jHR*>#jozq5sECY(| z3D-&GSN!MoXLMG#`L~EIiABI6B9b!%i)SBam^9Q`$C z2g0rSZ9dPCIsRHCuEJ8FG?gR`W7?!FsKka(151gz_^muye1jCg8A$Sea}$dIYE#g} zs+Q?5t$bSsyT1UaPS)5&EJa?1tb?HO-f_jR#CJ&8@ir~b4@eO98uH8<*wu^)+t@-a z$w)y}_ZzTl4upwpk)7i#9B?h7Vse9n#Y~I;)c3B(_5;r)BGsE#sGtru%)QoF$18rY zsn`MO*VmD8<7D0qn~LypI^Q&Gk1i92vN(B84dwJmXg=gNprc-6xbNE5l2epa|Em?o z7@d52$@1C*6`|ZcvL=&g<-0lBhJXNU$`Nl$kAsvded7zG4kN^mgUjYF;bE$AOZu!O zki9tdNCp7$HmNa*#E#AIG3nR$b6LJ)^{qQ6)U=nY7R!F!KDR&4GTk>uP;QlNe*R9Z(c%+;mbY^afvv2~_`>TNoe5*IwA|9^=J*o3qHe ze)8hp-1ci++mk}QO`Bexgk`>3Msu`k782^D#@4l94^@%SDzeXT?VN;ch))*<|I!a>iAC2Wpbhz=+rXTJB>O(?neICgalT`i{#V zrw{LidU@-$Rs6%GH1cX#P|rf0eO=xKD!!ef&i~pe%Rg&S8jJPWnr!)d6H35cuugNi z5M?Z}KEdwebj^+K50bLD&A+A>Pya1+bUa1%YA2()B#O(!u zyJbZYeW`IF=;#<~g?Tb%tv=9BI?B^|xbKTzB4-n$%+pX(1$t|&B`wL_9rk)B*CURa z+c;@N*jS%khkIEM`x`nt*Om91rBQ|rv7yU81X(|r5~?7|X~!r03h5jD_tUh@Nwlpm zY7A;{L~+_=bGk?linv_OOLcL7Uik$A)4PtsG^8u81@4Q$Q{D(Yo=LNI*X6)w4_bNl zqrY>e(sW9UXGC&gVxs_zC=TN0fy)Y4iIMUZ>zJ6y8yB$R#`?qDfDQ2*eu7?sSra*p za>XgU-uhqJ1aBFGSNoOXHn1Szb&_1SVMnYYjup@{Ly6UuVyzy2y!!wJ1$(O?Eum#0 z@^5*vW7{MmNW=b7J?De9+~*-;PoP9q?Mm2|TTT#CKX;k6gW~7}Q3=ZAZ&`4Yu>Z_4 zpwYr5CpQ>?yoZSXWLl|&iYVkJTPORpOEI?Nn1aU#uiLVZ=d?VaKEdJSq&ms=D#esW ziO`7;&t@3mzS`;Me7D*euWoJx`RZAaKwlTFeWBak@T_RseA;?_<}G#>F9gw2#kC2l zM46oV8(qm}!l$Bh2_zP(+fNvUCrt&pFZJ-325n1b;&vJ5?TEq7vKh5_JcsQ{{|*99 z=g>d>D-sJh+urW?o)>H<19xB*dqXvKz$~JP%pnii@7`K%!bN4Oda2kTjM^ea*C?#f zellExHI&+MTWioErJ!Kr&xy<7rZ^FI!3}X=l$tuvY?oI98?x2~$rq^9!Dwb&R5Myi zjF&sRc)z~l=cKkvFWVq0rL*S0BcFC7}!Jb|Eyyo`UW~ze60=2vd)K7%#VK zVQxTwWoA{52uNF7lJFV8B9!`^bXZ7R3i~dMKp2v1&JHih2Y~A&H@mAt{h7ci(r@74P^#hJ5be-VfHO|N zJ~Y9>!RuN`NvX(5Nl~dd*qT~cnZUs@#yCgU%S##%c2M?4D{y2}XD=$f(Wf?*pYE6R zmG4sAG}>~%#TeBsuEI^ouM~KEg;-ESoe+rjKG~Nl>zR20k6qWwnRd}=HsZQS( zC{xuUZCyWDzUn?6pU2xXF32zU;*hH4P-w!VlC4rSIHqtLTzIo0mBOtkS}riO(i_N- z8`$l-;wAEfu3OpIJ6jP}VJ}+s-FsKxc3+NYDXdVc505ceTr!mhg^0`B@#8OVcgcu! z4qhr1Ums`V=zwl?E+U^NZ&ut0GmQ|u96<1EpFOwd`K&CiE%c{kC#)?nwVK))7d)*k z8eH#SKHFqjq*E{x@^@yOBBWLhdZZfyKMSkqns`t~nd9FvYpLBLZFnIS7i3g67^@cu zTkZk_u4bYkXR4?O#{`to;NXKS;E;e4Jn(r8e1Lh04TM7lzVU&NbSA>TuA)?DBL1t4 zX!mfVgsPOB9Pq7bnMz+>$2FA9ACTw8q-&Np* z!Gb{1+Qi9#3T$m<<0uFgq4|A>AW(id%}zu0`xYll5gH9e6)GuP2NNn@HV!rp8c_@? zDk@gvko%FSl$V8+fVARxfb!NtzS#R}ZP>gZLx z=pg&U6?RTG4)%YZ4OA6=I4h`P0XDJHl(w)2h6i|uC_g`k@bCNod8f`S)31phYo++5ed| zQH)(-dJ8x>aX2|?iML?*oix;sPuedVQPt%jRyAEeStjG_A~;E~a!E=zv;6U6ZxH<9 z?g`&eQGbn5!XGA(6#NP=$B2%O&g#TLQVP#!*IZ9L>0lds?VW+=s9SWVmLA zJ7x0xT0!ldTgZP5UBo_*uc_IuR+?lHE6;&A>9ZkHwKCg2)Vf%kHmy}@; zVy$(()BUtp)blD49uXz6(sDf6d3($RTIj?Dyi4%J>Ce#NU&Jzr`(-A?YXl-rXz^Udb^B)WEDDhK@MhG!+39t1O3H0kLdHMd~ zZ}R;EX+l~R&h9oguWy*N|5$^W0cds^k)~~y$7}t$rKN*&Ic7(qBC9;r?k5`tFCreB zFstk6u-g9!5DW<=`j5HemtJV{K|F(ZAIVibOZs9q9?vDhS4HUd<qK-NnXn)kDScRq*lC(|6_%T zH-Pl}wfBAvC6+qvjl`WT&_G+Fsih2GX*Gzeiu~uO;IJcr9gDPUhDBSCSf;ge*Voo) ziX8r2;72i^j@KkDU#*LZSonk?dsu$>uu}6Ss{V!ZqyN=ypVn|Vn( z<2sp1RqW}}`g4AGKz8SSdlZ6`ymWMSUlJ0?)b)QNKdb-ab^lu86@>8VCT_X?kj)x_ zY5y;cf7~K&NfSNb>2B=v!*aZMZK2r@LvbS6J#vcXf3Mddba+Xky@|5c1FIC)5l2Tz z7n9zfVlejVlS+WO^7olM*+e|qd!-mc|X!% z`kNZ%C>A>+LPC;9YhV6T*n`Cz@E87T8(em$k^uRwG7bEY^2eb4HLl{!3{B++{`S z7=~`RF$!FD$By2L?#Hx8O<{!?X$(KX2l-3ZdNkg?_ZM;p; zl>5n0XCe`Ee2VjS0WEplcWLWs2u~-aw=YrEhYuh6fIW!&i?h<&NV%D^<4O#zwfFma z_eufqfqwZI_hiFR;TQR`QXfRrB;9)Vq;K*G$%v>pUlZl`D8}kNa6LSgQh0w^O+#3V zLLJi1H-=hwX6wfLQux@5pbn=CYqcHKk+>AX`qw(eKUL}?Jn7bBt8Z><*YrG+HP>bvMYXQRZQoH#$h%VckA<0O0_fEb zqGV1J&^dTIqdidiJf&{D0fXsm~oSovnsEW>E3%GNCX<572N z!CYA8!4tZM%HN^ricoSfX7Fp^=EF$9{g`oc!9rSDsUk&y!edAyT? zepxVxe#Gp?O?vT#07vk!?y#Uub8R_0>Gn*GV|jw6wsz|1X_%}5R8;2LV7E4SdGAU+ z8u>a-Q|t$z2z8egPn-1xJ}YY$Qxxmo9j^{w9E*;oUx~u(4H|Ku$P0yrF6mj`3Gz7W zT#eMiMtkQ`s?uE5mG5~)gUF>Q?p)#q4i~uN`*>bV8{nU%yw1>CZXCtbJuF@{o?3Q@ zat)s@aedfX&5MxSu^wArg^O~t+B0{dWQ7ZNYXN)79~kXYTAV`r(h2fb9nahV0c?#& zzn^Fn$=ONH*P)QaCEyT2(!~HZu(0?}zG7TYH`ZZTlnF?j^gxh503x_=uTIOeG3H(O z=d3l#w~DGvlk*qxW~ZJPHn)Ca-6mpvUops{1qo-7ir{_XJjz(tfrnV>PiS0UR^zx% zBI$Fv+s6%i9uOe2mSmRBo0smMOg__|vJ@kGx2-(3alDxqRpr--D|TaBw?E?utGqn; zR#@1l?a=+rXP8JwsToH@5r4nEA*aQpZP{x(xNLzI)+-~k&9q)r0(AQ$$` zZ>Pcx=pe6uYVS;0P6R{7$50)H?hp{OT45Abf@$j=d7pNq-*r}USdL9cj8*qaCk4Y73aIJo+FZxg-SOw8o@f5b6YDtH zn-!whcCblc*IWB#uU4o`?mA;XS2-c(QoWs_n%6K%rFXPg>pV_2&a$3n84r^nEP%Mr<$@%ueGZR;6xwa+$e)PNQ?s`t7V2>^zd;Rkv-A*h2A#q#sa$;TZ+=K1_ zl)+R(f2F9$8Ot<#^?*iWZ^+Hf+JP1c#8h{5dsvO}0QL1D^mxw5J5Ix~&vrf4v6zTF z!+q;nx#N1|1lyL7%T9=CNBHL1#r_+<{`54hpRfIo2>%2)KH>P$O5P_w)v^mUq$>ydv9hBB4F071f;Y){Zf(x;R>A3jeuc%~$Y zd8dCbDk}FSIBn(*Xx_?g!-cXb32qt-@xRy0I8~d`&lF9UCGneF?nv7TK@2ecRq!r7 zvvP>+Bl{tW$#t@-5~-Q)$3tY>OgA&XT-qM0R1W?I{pOFh^%-s13Vwd`w_IiePlTc) zsD1+4Ez!<@VgZZ#laJdNiKNwZKj~8kJUbeVY4>LrHobhYzEwzW~41b;f&Zlj`K!di9JcdS$W`niB z@RM~a0eM9wNysGBpc&PYEFJMiy_4td>ptsN=uxW!{_4HnA zNCa>1{8HDe+^o3+G?mqh$R`%{{@QxSmyB zLKG{ks{JME4@@WS@?cuysIM|16eYvWk(y|lo-St5d9|$w5Y%Q2@~>uaUKa;=F43MR8%AXPC%9{Jo*>=> z;RBB-U_+f)5Fs>uz)GhlZnvk;)D|2p)Qa9)vQBeC*uHDlI0T4fqzMssLcSekrO!Uv zE?SMu0C0`tVgNBI)C1T>@s4R6u5b4h?Hw)fH1fp^DFQQx%h%s~f#>Vb-q*iu6BT~DQ8$XpX))j6c^0kkIfNKb63Lf#CUx=K3-5LJCdZDD zT(q2Etsm+oAE+Q@qEEEl&)KRZQ7Pn4P<2}ae}#Q-zUy`qUj6JIbN($s_eeTr7Xse; z>1=t+;{gE4B3D8o&`Cb-EqM5ALXml|%K+y3avOp2ie+4fEjoKKE)tuxMR832S{gkw zl={LD%`-2=zM!!I48)(!wQguD1oWu}FfD6q&kSeer^j`A zIy&rKukSOP0_^s^Y!9y5Uj{YH{?^e&X>dzwm-!VcpRij=%8ef?wo~beKk5rgP+cPEp}uQo#NxrxY%Fnbt@P&)W@vDQJN1LrlL)G` z=Wf{a@3v`ynOxnR!$lVhf>s|)Wh=DNPaKkk4P_iSIdxVGOYHH-|MY4u3-Ild6wT1IlM(%FANSA$b%pm-3hD4we)7l-SQ z`GUvmdWR4pYvfp&TaW& zoq|_(sW$Qr0qmy_BFY#T7-YeGcTS_Ii*T?44$SGub2wilnkS1_z8wqfZm|Q6IJkc2 zcg{b3J582^W;lc&GtS$Nk~&)4TtjTd2*>)YT@grIg67Y5e-Lw<=Wq|e`;uhCYSz=u z0>q@@Q08<**{qrNSA5nr26=7gj05sGI5`t}d(L;KtN75OIytdYM?~8YNgODEnQ9h{ zKIejIMMk?uYAN=+lbx|vKYEwju^eg9c@H5l@E)CU|TRCL?bvG>>^Vc z8+&y}x<9ojOv67?rN2Df+1SCyhoMef$=3ZfNADt{_7Fpt?5ln1&3I7VT5LVrt;tbR zgHI`n81!ZPNmEHNFw^Z!Tx^ z=2`=>&s)sK?!9QusS`(d`Kd++&Rd2JwNpNmK}~O*2hDy%WNai2M@-v-{ILd4RPNFb z`I{qk!%obb78sbJJ6V;^b$x5BWPQn7?@YUYBG26yOJx-ktSU~98l0Y(5zvi@;+38| z%;ee4<3rg&6nHP=;b8igg9+8jYce`z3+KkuMJIou?eBO1WO>X-PKQ9VfTzDEidf=L zVubyrlqVN0AZT}{rndq0o*zD#OC}@wLhC*t2mf57-JgrCK_1sZ)6HHeOJ@D< zLabTLbAI;FU2LX=HsW&*eBq}WO7z!FTDJWaa|!_LfF3v^?uH#5V6_Mhp?f4T66`-S z3%RyoNC`gyo6G zOj&AUCZWMlQ&*n`g@Z+@k*E=B+OTe7w#MVGqB~)1rJX*(l+;yOPA)0W5CoHA~a!v%tCGW}^!w^8DV8cJ5Y}y!wcH z>q4m>5j^Tx&2>EG54tyvLNnN+c5)rnc}GDUN^28uN7C&(NY?_%Y&gVj7IEwC4sCOj zBO-gH5TsA40iCy7KeJmkpYDR0nCiNo=9(-^={X7*B_bB<2}M{Wh!@ScUiu8+yPI{f zs`ga(t?z5x8JFQThaF_J^KoWByn56hoCP=xfS(*Eue=BnDjtZwP}AhDotNfPOI%=A zNL*{>kb@7g{-LMdi>-}HP`xV=6{0WL5Etm9YLh+d{`}5~t;BlzvAf>INVS8YYcb{E z`TnxwgbVTe2s_=o(_G(9NSo^xN?a#}$MZG0VeQj}8^D~IZXmIvXCXr*gz^)YG+e!M zd`GvvYFvxUvC)KS`$f0@6}F+en;Rt=(*a6AJbD@3y^Rmc&53-xdin){`3e;J<+Pon z1#fBcIbdlp`&u7B8MzlFHf1~f5w28(&w`akiy&u%PXph2oeRvPxd*#pnV%g918&}w z4?ljoSXWlP(D6)E;me{b$V+Zyy<&MVST7|rGsD)C^f;ftah8n|Y)*AK62SH^a^|sA z><|1v){vqQ8Y-AHoL;Y}X;Gu-deX(IhL!UCQj^-IjJdCXK3eefYK6uTOV@_PO~mY7Z3yy56e$y4WW{B>OT zY)UEwPHM)U+8>iPq;ati-nXl>T}y@@`*|=w$@cM2-U@;$^W(2_aebHT8MfZjUT5VQ z&W`+2DGA3MSUo6lpI)~6uG7D6tgXXaQ=Tnk&0 zF39I~(Z)8nc?(|muEQLDF-E=!#D3}iYDPkSCx_uz3#$t~*ty3gGAn01&f)S2yRLkt z+w7?Hr!6n59_d7(ynt%JwnX*?=5gAV(LZD2@&k$)E9c{Y8YR^VC50_iN%-)os;1^9 z_RjeU%EkEm^pL9$pWWjEOH)?5#orJf`iZvtF=fjPYT-*1o53g`HXMgqBxbHUj<>RS z)!OEg!>v!v4aUtl6erG0YXiRZjzSc46M{w1wEGr1v@pv$NpHV;-`|bpgha>NHUh%a zvmvydMa}OF_<}Gp@A@uhu^)TVPR@H?2~uc*?KsU_1su0S1YSp-QSz70-xZe@=O{dg z%zI{^Sg%8o<6CRd<%(mGl0r@wg2pISGX-A9wf+;~@)jvS*Jo^;v_Vn4os=Fw8JsG4 zmOInTq(~e1H|4w`SsC9|Pj}?9u48Arq?6gbFMH?it8(xu+yR%v)V<^U_1yVRy=+Tc zk<&V0K1=N%vNzs1T;H@)xgU555lcs@xeGdEYXU-=Or+9qJ-hFY_wMJiW|D#wdSfI5 z!FgD7L!yG{<>&bIuRlr}rl?x#PQg02_m?p5WLwW|Ez^~*m@=NSS<#1Gtc}5L)_FWB zPD!x)1#U=2!KR2paw?ZeXwCIrU5e67-!o8U!^t;xP5WM=e%|n1vD;1Mer2yCf-3oa zIS`;dkXYydA2k{ShuMIY5EX?sV(6%Ug5ls#fG;Z6t27^K(Vz!hDQ6u~J}Sbv$v;oG z$K4h|Q74k`$d}z*t;{(F1szsikCj_a{``hsZUFj{qJpB5WD&@}9)3mOz;14{K%^YP z1RLqlLn&KR1`Oft!=qt&0pXm4vX?1Zttut_P_!g00fwc_DRtiu6&Jj2>bl`BQs&=(K#f;r90q$!DUQW| zcR^r<)?_h~%af0Ss7m5@sgc_w{ZzlG$S6u=z+HQ)Z~ped^;-{$^Xv~N<+jqm<0W{E z=7U+%r>>hss3i;Eytopl46!stu5Z@J!-l;QzPxEk&|(Ow+*T{mwH{Y=SS5%*036Yp zuczFusXG=Wc#V(prmd3JW!ukMZbG|1X z^k6}OJz%&EAldo^o%_gCt13h#{&?of|0rZmmcO!yQtMC*tAzv{pDB=)f)5Dykf z-PtLJL15Aki#x}Zz#-i%^ZvT=zFbm02Wjd7uLd`Hp3j=ykMm8%ECrs|?|TMrpA`Hk ze#IHoN7>OvA{PE>z@1_sR4GUuQfPJ?|cmGV7e#8bckhL8t$A_fkE zu|5(kG4-(UF0~x1bY0taG|&|Ghe^A zx158z{^Tv~x#BI<^rN!tAT^r`x<|%b>x7bMj9381Gb@3xpvHNwRu#S$U!+dMN6nnG zuXR4nrqVn;GGHk?f7ESzJQP%kelfT-8@KOIGJ>_3E={s=`Elv|-$1kY_4+sr?b1sr zz|vR9*$SWK^duIVM}q9)gAs#!_G+{GPSeqLtqzxDAoo=Hqk3kRJI;#acs}Yy?|+Hs zFY&hpFDXSC0s4r0#ct>&KE{imCM!XxxcV3dIn?^ny&dbr3Kkd|`9-OFvq#;qT)(w1 z#HL6;w+_oNyIhv&-50)v=+vE!<;2^C-&M|OyM4de6N+M;BG5L%gjG^YZM8aIGSI6~ z%tZxj-@Iti`E6xhDr8o>zKi?Cz>;-5V<=LzuIHZJZFGaK;jul$aBnd1*gFgR-Amn+ zDl2m?ZE9dmHp!T?oAoBJ^Xc7gsgr?|XkBI z)fr{`45@~h<)>VGQWqJ1sP%Q7i`8HXm6hG_Yl6FdF8KVO6#v#XfgV~UXUH98-A`4w zy!07V{9=(w4=3ThM5|U$9Z2{UUbcH@B)J@Mvzu}DJEqRzs10^#_T$cjndLFU2=^xh zv<9Rl-lZ9p%6>7gvezitbqY=+67&S8Q*zi}H@=-x5Tikm&Njh~3=ue^D0ut|03>UdezXISEs9rFJi!rWEsg^MoojM)H4ELV#8n{3 z!RQgDc180yiJhf#;Tm5v-a}V(?T}0J5*yuyw@L9eF`DYn6e@JZ7OQX zdb-CLh8Gspk*ySPeNH$##=B)PIW*(j%R{`9wh+Nr_Gxe5zMYaF+VaWk0lYvvJS!#Y zxjp`fmQU?>u_<*a&w@pxE&`%KPd1wB!h?jaYUE)846I8ggQoq-)|6c}yddfe5j#T) zLtGkSUG3m5C2>=sTWsQ%i)rx&Asrf(c3EiZPqUkD>c! zPMVd0MTfpbUO2@ZvCy~CKG+5A)8x@yw)F|Fzbrj62dK-IVfrA^P^Sgh=}Pc?r9Anr z_PsnATdtgu{dOg!^I%+=gfpnbDD6P_!9+gA!^Q;w!xx#V7EAYzI-zw_JXPkz=~~^P zT3!@SroZQxpRO2^m(cY-dU}G8W5a_vd_1bZkR*pba4?5)XS?j(2W*|=D1M!HgWY^H zc772E5)|d8{alR~?d(|7H@fXBhds;%J>;ntKdl*d$^;GaYAUr+<~ucw@q{ ziHIw_Fng?2PVxBe%H@Ex5wM9h7p_^{bb7DcGdJ7rz zQR4mGdE?xz+Rf>Bb=7?ns_23ui}2Xoa&w&B2$rbm*@RKV9=U3fD?nk?9Q81Zupe@& zeHQ;}*g}ICTe@0` zODmlQ*ju?h1OVTf3b#PbtkYm{8DzcO93I3oU;%XxARkYiPF{+CrNcFZy5W(^;LQk;cJ7u=>ca|< z9o-H*A@M^|B=z(W-Dn%vzcRkY80Q;z8?8m@J@#Jr`Er-k=e#E-ng(OF*U43#&y8$H zOb9_}hVPc{G}&?$(mFJ1 zBO8n#8JLPM&6hnWFLWd&w(9pUGs(p}wV2bX6LlQh^XfhPAZxnZ7*{zA8xL|E8n&2|~v!*{|M71rq^*T;L zc^%^%34G6A;8r4Zu8ZBfk@+Ss^Xr|!Um>@ws5cxD2_+4CgVy&?$Mw$X!K@sBcRh=! z4rF3ZcDk+`&~SM&qI`{8o*{PGt^D3TjK}a<)U_dfVKV`z|7q#Xxy-1aR}U5QI=mm9 zvlIz64Z?Z#{N;eFRsDB@IM&Iii6B|37_^B-ht*de!mBat)Tfui7Q;`O1IywBx!w9S zf1Wybe!BwX{C+mnA=uSR{u{f!=pOV~>3Ab2D%s;>3;j4w_VX6J>B;@7w^QLSgJqv( zbNlkZFtjOC5!3T7vBeLL$QE0xy}sw@=^y7AyxGdQzcD)=mS>+7-I>pW0XrWyx?K^6 z&q9nW1?P?TfIna^c>jzOx=34a&7s4NVxj-?6u;``)REv5UdvBbyPE6AW7sR+nuJET zZ|~L({C)cZ=^QimdJ<3*XtMye*vFI>l)4!Ip;=h^tI$FJCq`Y_?qn^7XOqjriprh@ z*h>zo6ucuWvknC0Fe!J(grs~?gX`p_M=lDg(|{}-6uj!-dTmOW)uWeZJUl|Y6r#0~ zWTn^r^;2rT?%kQ`vZK6q!lZdnFL&j9-^x`jGxL^5_I!K38)Mo7g;Md{$G2F7KrPGj z8i)5QRY>kYVyeo_)?85@sm43XsY+Q__Rr_J2X>zR%uh?ojFc+f8o5^8_PyP1(6vW7 zb$l?R?5c|Cv*6Kx`7u124>W4JR>ZM~oD^zd0^KjnHBa z@-ZXE2)PAwE^f9vB2!4%kYgQ5G$n7iZTOQ~dQc5;HuXOZQ&krErNl+cig<{1b#u`v z7D*ne(&JKr(iiyYWL_0-U|&}3D&`bUQd&a8K5)aoR9eTj;d1Ro zx=P0N%zB>;v5v7wm@k4~FKv$y!BNBAY02~kVX$GUM`M`{#wl(&;If z0Q+5cQ zgc<8)i?t+fvngo2`qmGQr?wF}9aUY!^}diQ{GQ4#3Bng7+80EbzwNM33>#aRUl(=M zq2{lxu&J8^qLpyO5Q5+Cr8B(RmV?y?A1{SEDAZbj3bTW?z3iE1xwt()rYM-@-~1T~ z_fI*NcCgG*t_2ZP2vRK-IDU+4b&We$;pT^}chS;&AI5A`bHc`cPCzKK@`d|Dh90tDE z=Ey{2$23k)3_x;W3|N+H3kH=2t$`X-Mv(&aX95@S_DIyh%`TF2oZB<(G&RwEd)1jB zACd$`+CBmr?4_7S-Ot@f5&$P8BDsEv4WpN&ToxdDG?oORNv0N;5#YOn-ew#|8ysXD zcuClYIe|sXbTxqex1VQhB!rjWzW1AyQxoP7Aj~q5%4vF5Xz?i?#qH9gYyN#SGXKJ- zZ!@`RNVPprf2qC9ZbZU{JrnNJn6HtIqG+CV>EGb65A!eNk-w+?YBG3W-YUeIZf3|) zf1PgQsUw;MhP~BWat)bBgzkW-1(dzyEBpO%+p5p}I&3j?V{dqouwPRw-_UVMD%Mtn z3|nM8NZ19s|1JIHx!Do-xxxUb22{GFsWNj!Nw%N3HpAxgL%`w(5Y6=`%l^BJMm^Y2 z%5S`#rReECjtedMp}Fm5Yb{VgJCHZI-^WqDrW93vK4^MGqulq!-rTlTS@Bny!PbFo zFnX{Q*!nVnF(FySzA)rC!uDgU`^JGty8TUw_w6>yB?td5?z3*e_yG+V@#_F$YQGzU z)dQ5XAR;GA)*67SMtkd@?5Vq->Y#&+LUh2Kpk|%jvDB#bwM{pzgPB?~HVPP%#(QHs7q~txEB0=k`SV;qj8P~AF2`+f~CwN zIosUgmScyLEI6<$+w_vBk7nlZspD$kFK!m3C)QF2&Ap*saa}D)4C?2l%i3Z`n6H$c z%D+wD<}NjgU~gSfV6dzt|07PI|k+MVx#3rT1c+@xA)txVrUQB@2oa30cC2Bz)A#JqD8)tFp zFgiLqUg?Z!3S2)S+~QR5Ccd@VAY}|ypC62To%rc2FA^J8iLkH-KB*_Yd3QSP`Zt$m zWMUHC5Ee~}6Pd9M!!!o4exWC30(j0Mq{mE;cYFHnC@$BrC!U|+B72h-xW z#0>@!Ksj;~bltz-TI&lLaNh&aL{nM9yY3?V;E_p$_&FIKy$kB`=mQkEVfe%!5WE^* zS&=F*)UzAK;81h>gp56+mpg9%P@1jBu7x<`@T`Xi#<~?i*Wg#IoYVL}#`0hBWp0vi zSp}YT?modVMRe_&L6$Zgs0*00>&6H7Bb6HK7>z^AE32GyYK1oa#^VBei!^gTyR$`H z?+I%N1zBNK097C=%uMu{bfn(neB$oo&BD|xaGmtpS=|XZX`yBb(kV2yYZj69HbNz4 zxihNDH~BafV@-y!C9|e-T4Q_*h&sjI8@O;)pL+nNQfb^4<9gop0_QW1{eY2S4un$5 zksZZ4kED9;IcAiH&zv*vPZrvKKGI26IHeP=J{UBNs86wNn4EE$wvLbrh)2V}7^AtCI?lu~UDr5>j9+==u#U^$eYNf^R ztHjmjY4U!-?F|cFk|aXQ7i$oXm|ndg?MGGlc}Q&clf%F1$p5tio5LfZp6zp6PmP7W zF=!tj+x?jm7P=*c8~9DmIi+{x3m!Y=R$?T%V;|>QD06YJ$>9&ZTc)?wG;b627DCBE z7oSK15?7@kSEURF4rn-b7bUbdRdP$FFE%gojhXkj`dGGy61hZVGTGkdO9v7Y`i5$S zq4~r_``z*MwnVyK1_pqzMi94L0^x23A|iNHw-A%wN8376Wk#~w_UrTO_u=}1otlKU?DzH$6i~DtQmUOy z5nj;&3O4t8%<9<0QJ%z%gRZW@QlgkOuNbNfo4Q(YnAT`{#E-%Ubn*|>_^0qbu<1@D z2+G&;q13GxrLLyJY`=8Y!rIZTmTSpm>@E2+`8tHwY9Wc+X+!0@cCU7O$6d-sY`?IT z=n0kNM6elnoZl9`rNMqD|8Mjdbg@5VU9ucFl110 zsS>dybBGs7cuQRc)L=^P*%VNTMUk=N+G*C8$Z3yjvdh~;%cA#IO5C8R1fY^w-_B%P z7h)2b?tMfa|3PaF*& z`~J^h{<}bBiF7t*qnvmSu;eb@0R6W4=7}eVh(Ye0(r;W~(CB|-Q4mQT^nAjffrtWs z0EBa(FY&cSX!$tf9(s9N;%yx%w8x*&1_Wut_}L>SI$XsT$bIxX;4W~z4iJwYoGKqz zXemSJTDjK_J1L_)&r8VdkGdJiTAI>a=iJtEC~xzLGKMu1nAKi(p-#E=^Ned41iD;_ zbW6tO)o%hOE2ITa+t2W^B^u{Kp!gDQh;)|!2U=sQ&H%Tc&5vMAEr=+{EyhgsAAT_% z*UjG!l=4nhHQrj+;oz_0T}AMGe}LhEyBptFOqxN8)qI&0$y2i=g&e< zS6zke4xS1gwh{DS&)?s@xLgR4$+!iGL_z{hGCGV&@$2qu!0@vfMLD|JF%u(mcza9F zknCT{u5}urm4qb3HJNZQbv+65GCe4L8qSE2U;cLbO-Z&XFoc_ND0@D*3s@1*H12qSct*$kBKc+F8g1rOxDKJfbv5}g ztsA|=lA6`69!TlEfQ>gBlv&z1t12u~YhONjtGwQyjIwcBjm&Re4>-xh0pRW8!@KXzhZJdv$_IW{W@c*|H* z`3|5UU~L<0bT0 zZe5_-s4vKBBMf{z6!=UvS7V(zVhZ*T0#UF~=y6q}L{clnkV=Bm;5I@0B?i zKAPk-L@?nQNabGRTWV52#aBCt*Rm-??A@%m1VZ`C8<8`vM)L|g*HZ-?`v4x?Dp(1n z0Piu-BX7PB&&H7go7yWurm3=Cdceg;5UpuI`aU9E=mjj20|5v3lT{3-Q9w)}RAgc` z&0Xm*%4zA*vz9j2J9X;!F$48C=~_w?OBdRNRcP|T0?lFP+BokReR|W`Zt?yD@PmBf zX;rE!MUf1kkAcLA&OR@*Kmqp^633e|h7^Hg(?sj(szCbr>Kb}_wL^h)=VyB#jt#}! zRA10o7DR?bI$x7Wn@0QdKn4Iydl2OO)nGRTaMm?Spot^|-KW8q^{4PnN4v14lIXq! z+uQwOLI7?#bQ9l39?{u){;^K?^ zMnHA*L&BUh9-Jx6rER=dqEwk5@2}_i)ODP{5ZQt3IfPBR2<&?Ux{t}@hh?E<`cg0D zkeIe!Tyi+9bFml=Fw@}f9#vTb3eDxy$@jeUp9X}jk%}&4E$lRebCzbQ_AmT2chD?P z8Z%W$zJ)j5?%D?0xsJ{k@l^pd__Qsh`$m?EebmQ8LqpGez*25SVV7=OygXj-Y*{q< z(eQeTP$6!!L0UsWSon~mjn;P_?+VOhty|;ptp;$F_H~M$arT0mK|hL$Dt$K}EZK=G zr#lzxnd%`GT!z8x^BNc%^eYmZtn@w&C`0^L-L?#uMbo_p#eMLGhxXi1RB(LkJSBq) zwNm?o8f@cm4VT}S=4d=vJ>yyT7!mHnNxTkQQD8Zn6y8< zyf*_#N<-A(eb0p|dt~k-!BX!M&9e_u&5W-|6~7tmW&@0!rZS1JI*D*c98YS3%C0&$sqUTFc8Tha;JPz$cp4@|6uUU$bCT8l%Jro-M(tXs)I?BW9gEvCHK z5`Ai7xWWNr$mSi?y9KC9uekRoJT)JSu?}_5t-oU4>2SDRgb&Vsk z(a}>MK1yol!@F!sBy4zmDe*WU-Rmj+K^JY5Mhs~Q>@7W}|7s*Eu`lUdpTV|3AbDIL z0L-PC{l9bU7nMSN4KM^C-vU^-l3va@>;}hneCu!X;5y$d7Iw1}!2iwu!sGEf zU%O$+hsy)yEu)&t?9XA1C(<(~Aue_-OOIZpA3~e8DMnezUL=PHZ)-$U}nq?Vq4K%ySd-0j@v=5-jqfv zRthA}dCF2neDF_b8}SFlf~PNwfY1-MQnLtw2ZDG6X{PdgEyebIR8MY{xgc`ZdpwBt zoFZk6HU_a8DLiX|fNgR9W(uv&+r`jRdB@o}c?5OG6Ou2~zcW>$htSLgrl@J#T9(s#;elcdNZUoco@lfW#&7>H2lcwPGSi zb6~Ou5YF7^i;TI`RtT8K%oL!W5S~a4GR1gu1mW|qO96qwqs2Nx+*T7DWbScO_IEhM zMuRtiCMwanjw(&_3fu=)e^G*I?${?G?ZkpKn&>E`)WI)N&UpiDCsY}?2I!X*6&x|! zz+HMAnqi&^O}`8`-j)6iDi@c>v&&XCFv#+xTBH_AvZG>sx1YaC(|72gtNkQM_I(fz z`3{Rbu_^sfI4s}f*>5-MgXche4Q-Idlk+lC9Ti_iyvGUvQI)Qf3K z{EPl5lQfya2Th0(P@~6BhZ8E$7gnPSF3s+M)R|e~+mn&+re6{|%W!5MLJ-*McDRlg z{BWmuL26;nQw^8>woDE$bkpCxdykZjr$5U|Z-6vD8tVGom*3*jI|0)G-S5=u)r`ZY zA5Fq^I_5rpwZkGyKF(kB+5h4b$?dmq-^Rkw4Yut#+t`Gwz|=csV={`05t9(>=|x}h z-MV~umgf!Op~f`Qs@nNOylLX(Ac{lTl`Qf;sRK5*tY^6d7XnP!+y?e zAATcz>ZLItLu{7*At30v#3x2Uk}M@|6E^k)*W4rQ-k{1#`~XTodw0?}tj0Ed2}ONG zqt4VBP}aM`i>{sY^*i)0*zJ%E*P@oEl(y5d+HYST_k#m`VK~^tY9292_fINt7d8U; zm9&yaZTE56iW_ zq&xae$8-Wa%hLMWvuU@v*w!Ccg=F#ah?T4o83##spycL#*4GSQo&nsee$VG;Y8(PU zs!-qU`Mm8oj&dG$u+~aIj4N;WyePE#A)slJ*aS(2t&QbOh@E{Hb;LNwCde{f7K|-M z^6iiz%_&y1!u<8;uditT&Px-J%>=T%oB-XhGgG5VO* z2yz^d9Y0+fakTO(AUpDJyyde4%1^q_-Q>Q)8p*Z3eu+$_-LJ<#-9SS=D4h+YoIc*7 z`ezzJK2~_*!4K9bR$S;*rMz+h)f?Qn*u949J*#(o@$8l#@UlV5tbObs4rkrmF9X;i zys)q!7S3F0qsLkTf$x9kKXc>d&uUwb z0Tks!l8v!)vD1&h^+!vOOnU!qUWy|v5r}uiCK6>RJMLJFJPF9h`ny}^=m;Y2Cmeu6 zK;d)NG(_O=CF=Wkn`@-tvMl=hh_SZ~Or{zAem=D^NvSw`2*vu>(S-yAF(qZ@3;!)t zk>U~(KF?TKo`auuXXAY$>eFZVXBxW55&SWz8`i0vL^L|_!53| z(+^FcszS5U7XJU(d+V^OyRB_hdLiA7gaV3mHxiU8FT<5y$l2uzJmZ9Nt^xPE9mv+ z1W50h;&=!ofon$I9uGe3^wn z81iT|x58#*)1h>5d@woA)Z>^PRg1|MQ|05oK7FvLDGUIGF17l{BBR6+LqkIg?l=KT zhG&&x*Z_Ij{A8UtYi3~ z;C;95gp)!bA@L-jGsh|jUj8Vrxtg!UJ7cQ()8JO&+)m+gW#>1mxlFFAGN+fZpb_V< z(+CzV$9=_{JW(L|H)Q4uCR|67=jDmy;AquUC*6W> z>*>?`@rj@i;Le{QnAZi) zg$D1v*T-&$$D!{qTSL?)*$)LN$sV4)If6U0rNsq`Q8z*R`yMhE4rEQ8)Iq4WQRB{oaNYI2`zdk!9BikU|)3 zJTHIs^}T-Cq>Ds8Xvj=CII#D77Cu+p1yKFfO=Fm&mCz6pZp%b! z3B$yO;R30JMB56G z{i(Mm+H}F7;Q!X2CNNP|2sCBw3$2u9WPTEdSt5mi^W-)}r_ntw=^d>Ux1ngTs9*(P zH2B)m2lA+^YcSRWN+EYmoKQ~ik+~`>E7Q+Zeg?3nPGoKe>#92)Pd_#3n7LE``8)dZ zYS#AOvFblx09&i+$C%<6SN&`EMZ>VaREa9R92uwz+w*-={CTx6VQMcJ5DY|CHrCXH z=^&uN^@nI#uU{N*7P!P;?##pB*y5PT;>06W$;qUQ*5A`s(c#dj3q4N7SIe+`!Wx#` zLTZ<+D1JeDPn z;zvZNs;$i;u|0{CF03mI;Oh*G`a5dO z{;LnN)lEd_U;a(ze^1&{O+XkV42sfl#-x5}9F-M3FkqFGN>jMzzsyPnkfY(c@v*U~ z?E=q$Q)Lh-U++|*VCCiKV_26h;FPJa=L3~H5ui~SHtMH%Hm_A>X^ovT9HR5$%HBN1 z{gWW^|Ehh!l#OwGaQ{OGKFZ!v%ms!+1J4{VdYEI^?hU^$$Rr>noRs_6?!28xR04om z+wL09A9f@0T3)9V(2oyiwWH7L0I)b~3Xe@_rPUuAz$hP!jh%v;$5g(>dN#lz{5YK;#W;&;KibXL3xUnXc#=Z02jog%I%? ze~Ef3HVvqI3e6^RNxSsd;9w2s;%b?&F6vuE6P;llU<;Kxb2Mn z5WN}$@wXUC>LAOuSpzV_B_HjoThX0yYnEt6F#n5Q`;b#*Vh8Zq;wmHkq1OGlqOU;# zA!uVb%P*vQM}_}^^-R^kWA&02bTaOKkat%vw_8RYGJ-}D6a|J=5i`;~{*0VS_m(BCyTXAbZ&;+nJLUsj{p=v<*Idtj+!?gV$ zK4#ps0z0@HqC-aN>cz!CHAs}G$n(ETma&}nLj}CECpl~-NnW*Z9EIvIbIFxi+^qf-*Mf9T3JWM zvsjAb3)t#6A77F3JANjpoD6Is9O4};%!reA6-#V=@vGtYKMGc@ETDm?eeJBw^T$TU zf=*Z!EiEWyzuTj%%D4v{(Yr!`so%4hsVc-!`Qe}K+FcS)$TD8DMC|Q;fJ&;X-LRKu zXvLD?1?;xz*Y}dGE2Xq-dzK zPS;hxP`B|BeAdN%*B}A{0u5Ex|IHub0e`q@_U!l%#d#KXXH%uLNEuww`awuA(#x2D z^)%CBp(!o5sOUtDk(t{yM3QR~TyQwPPwOfkzU2R}i++U(JXf+`S?`~YOcB1oNChYf zzsrtgKq@SM_sX+O@eJfBq6Z0wm^zr+9<)9C?+1M@CJMD(?Mr(3Cy~_%O^Nj!zY;iV zP}VGOqEpi(u*&h%a6ltu^?CbuWcs^d{`LAG!G>jS(H{q8j?*H8YLh)O^0NK~ z!P4OOR{;S5IhmO)?p8Q$ivP=Mo&pckl*Tsp3mqpx1!@@l5G`0x4-1@qVX~U z<(a^;9?f|R(`%i`@RMTo(l(Ht=9$PZy!h`y3Iho5v(zgTfBBD0EY}#uhVx(Z@KNvG6}gm2p}NNU)XZ2HyQgAOOmZ z&r!AAUwtWz`855XFA2XJ_m=O^M5*s$b(a@$|9)Zq&p#+i3A3i%^ztgd-`4-JfIt7~ z)BX;SM7Hl22BCH2Om5ykR&k$7LHSz`Ix&0y5pywXT3CG|ZNw{2 zhwqqxDa_e&!8C88h+BqekO$mrWJM+F|ED(DCxK;X3k&e+Ry$LK$tDQ<$tj-Nk2Zbg zWkGpDyfRr2t)HqiFPy3}eK%F@p%$T}cKol0P#q%XA!D+2hm%0#nKWLRHggSFE>sPW z^V(7!B30T(SWQ>3b?VfCEc>GYJ|bO<`Q(#8D+e?+na+QF46vUcMS)4ZJM0+D*4c=X zoGu>GC!^TseUq3po28_7`BRhco25GMoMo}@lKgvE7xABehVxLVtT8C;Vv`Pf!%R2h znF{L)gPkkhsU0V)tR*hLsMwaAg!#V;be}BopkzFG!LNd#^lI8y-~0rS4Du{@z2yia zV&QgJAGAse%~k%*pZTQHgOG7uUU5eS+b-Yn$9}C^DJRr_a&`K_J4U%g{=X?BsaXDC zO=y2Idp8|hqNqBj+4=?lF?xBE``b1GWVj8Fd7PTncrDpTN+Hs39su18yCj-Um6@0> zb%dCLC19Mawej^lghe#jW?AQKbO2zYDsg|dq3em*)361i$k1+WJa zb2|8MLZ`Dv)^!^>6^HE(P{6+l(h(2M*TWC$6?sJ6-!Bka5;QZ@G}0mlvvaZ~I@$;qw`BrIjEqc0>cdpmvOui2O7|7c82G z$#bSzBjaCvF_~jHKXh~rCT1<1Fl638@qZ-vGm*`7P-`@XSNgdm6%XpxLi57wpGp~_ zDiKcW$e84>U_?K{0Oy`O^m>gW2RJ&}bV0s&x;U(rnNF>tBBf6+a>?fxiGN00dy*PY z`syuugZ5nR&rblC3#=|~%?!YYhjY7L6R(dx+~fM##|15FQc_+iT-`oS14d?z6#mbv z9ta zkTQD}+XU#$kXg#~e@J+X^-^O=lz)C&>w4#SYl`D1HB-FxYH!>c$hdL<2z?%bGcpz> zxn_by0r|S~HtCS!8t)krM!IFB3#H$N--6PHK&siGi+WNKMuXAO2Z>{uShbR(Kgh0N zl4E;@`|7oe<5`(PA1(GPJ!Ntp6%)z0+@AsrJ2#6FI+X$gN7d12;$H!NX{r_~#hUNG zP+PjbCP*UlZP55Yae7?xO%#RR<@7{9;IHflU3{M9;9}fhy$G`KM$C;zFAqV7ZC}IL zQkZ$Q$(JbVH2`9&fIa}-4z$}>(LHZ$1+j#VqTDVvK5g?EHX@kyCtm{zGXRYRrtLpC z*TbjKSQ>@_#FfHX2cbI0KVQwEq1qTA={z5agLCnOcP*m#$cXjIt&Kz2w%NwHYado) zG5Qh_AjC{y><1};l4agD5;X44BC9-PVd1%VvlleL)=h4-S7v}gHzIhp=;w^_FyTzC z`9eijom9xZg1}}{s)>t>i<^WReg8fgAn0=O97EHa5o9|DpodTApa!#ER<`Re5BpF9 zpn=Q@;P!SoCyB1a8o#)JHas>Q695ma!kk4}D`V1am<$YCNq4)sIxo$8YA}|s#&S5b za5LVp*M_#n(|j(sr7nDRkkHJzvJVgw)5>aU-6v`Gy$@CU8vrh3+-}~C<|+9IMI0Mn%k-YltZ>jZ7pHF<;PSj947!LJJfVJgU`j{k$LmC)d+qgl_`nH zY(1zhK{&J2+30<_{VHp*&S}#$g8wH&xy!C@`=WUcazgC}-L7lb!0g&U%C@yBi5dZYm3d%+XnrCVd0rBUx}^`nV_ zY+2}v)>`Lg9?Qe2fa}I*Rl<#skT6NO@iMtP!;3+YU1`34gxq_JS4mgZ0I-@g9O{~l z7wQ@5dN{oI#1cq!q>rSqaX-!D6sk*b_cV>vXwI5`tjj;tz~x~vNsUp}2{rSRkb8ap zMM~K4g}`DeXHw)NS*&A3{j$AHQhwm0Gfru85Q6bupTuxtj{uzs4VHlyv~W<)rXI zX{2ZWNfrQBv1b#~getJ4XA?{5TlzS4VoxMaCR#K-RH5tfgW!dYOC$JxYaD< zJiw`D?dSruh`rmP*=c+Zrj}a#_m+;+fX-jT`aM|zHnj4zLBM6F!ijIEX1QR3Y6vAb zd$#-r1_fFV>oJEl5Df%kkoD)Ob#7EJ8h;(S%4zB7ZJ5vVnD>ZsmZcH^MY)rUhZYa%9TFjc>hKdL4@;b8OpCVza*FBO_8*-}RGR6!-rp>btIg^bKwfjb(y#Cf!rlVZ zb5-TD*p^!5t#7=hND-L{o4W9Vq2WF$Igcl1A|~6#@B87OTR=i8j6f8zma=eVI%`Vx zxV(nPkD8_NC^{{KoW~B?nf0O1R0XWRjDya4pX?W?i&=S)>-b``c&7DzA9H>G-o+A8 z^DEFEHvhEJW~YiL?+vV&QK1R%7Z|~+BVCu2kRigEnH}(1M@=E6+))0k1TKlMP2o0zagqpA!c0gd5|4FBG>`E93`6n$@Y?{m6=rPb^idioZPjg(~ zvEjI!Y)z?t5$j(PFd{u-YM4%a5R(8pNq>u;3gC%&KK9>P4!i)Swi+Q{@dX@~>EqsB zIdZJ$NrZ^l-ZfthhdJ`7djkUGi9vw?5PIT~`6CA>e-fdnG}R^i||Q?6|f`-d>9$1bC}^c-!Z+F95`(8xxhA*>fp5t{v9KxDm;orT^$y?BurxK`s*Ann5u%^=SoQBbOY`duP0Xik`Y{U9iUw=s}?Z8&; z=s+G<-*Rl~)PSod_UXI^;3D*V*6`zf*w!Kf7JS(iRm+__eq4o}iCjI58QaOwqS7oI zVg|eBnEmPY%$6aq6OKbz|Kbgc9RkAs$fxpa!0_O6Jbtww>7Dw58zZ8VHhjSz*^$`R z@fPh|ueE;z=05{((fn^wH65yi)T@5kIbHbOey_d?XvTSWh~?^x`%p(#JcZ}c*I)gU zpzpH!-1~O-mSXzd3caAAb^SK?VVK@k8|;@<3>ly+FUN@|nyzCEIN(Fk_Fuc~)UIn{ zStGJY;O^M%|FnO{8eQNUZT9Ytu3E4b$o8u)8b;F@pt?03hiNIp8Ng#v(e1KL(anEU z@ODaq5OR}*+q-pY`SoT+{c??tG?eQ1NYX^eG)gLOZ*{< zvZ@C`0dj$Hcr|-VhGqA=JgMg68WOd;{iS=qU(w{T)iTBqZV80LvF0-N|B$(|9(KM^ zREqCuNPZ-9`FHrLiRIHT@~+FEU2dy~KEqOH7^ix+MLTt+bikF4Ewj6YoulbJB~c$$ z8wzEATSF-g`7{G6mgCAQmcTHBqCdtgqWyB8Tvol^IxDhqE#Li*h;11nUCs@b`wSq) z&Y-ZwAUg|p(U!3(-V>~-d9qOEq$E50{skynaAQ+{=s2?-dLZk_s|TwT|q4(bMFaRIvu(@t!*jtOFATG*L?*^ zNN`R#iM?Py|1SH81CxYMd{AZ1$cPk{n#`wdMHH;$6f1{sj7jW2+F__99r5s>ZHA`| zBBl&_05j%nRlv}FKP7NM3z--qm)LWw0cdhd(tN_`vu2PTE~-k5&Bs?Bj9c(hr$ zpxlIt<|NRlq1;XB9bHW)OT=+}tR}l%pN?eh0`*v@#5O>+c_fCKVZOkGr*Bf}cTQ{KvFp`oF_2@! zq==W1m}PV8ncI71O|(jO&kh#_Va4B2XeyNMq0u)EjXq=Dp7_5Q+kl-qm!vuuc#A*N zjX;z&P3Qg{kyv59izSr8`)c2LI1S}P4)Unq`S<6Y81aleH4C2dPZ!@~c0^#NgnU8J zO<>m9!pQdC`x@6S;0s?)mZaE;VKLcGZ(g&%PRG0DgXM$h6NZ9LZ_5Uab`Qv2rz4v0 z5ez@uoPC*b>bhy#cybYDdKiDPhj!y`Ib+9=?yXY(Y2*T$w>mCjztn9yAGZ5_AhjpXIklggFLYVgJyUg#AHm`W(igd zduyOy4Dc+36Zoy)eb3p&<)bKq%zNd0dwn_+5Pd!K-Yci@v2nOm7{~M*Dom*&lyH)6 z>*0W55-g!_wv9agzAUf5qg7e2NHx0ExL_Hr*Z~$bO)P3*0V4`n;63R1w(--b^94M@ z(<|%J`ObsYKC2&ugG!T56pWlpC>IFLNCr=v!`54S+0YHYo}SGuWu->3S8ydQ7$f%r z@Ps_E1nEWU!b>Xw&rmp$X~HoGnt0;#3*Ku!GUxNvgqf1EknuS~As&BgvGUjFeR|dk z7&nou3ay!VXpNUc-VII*7SK)a-NaV%q2v$S`@Q4NFd!5i>(!SJjkkp<{m#3$&|TrU zK#vLn8KRV4FO{6(`UG`e8|wm}G=8F#R`H;e!A_mVgW6)QC^Ryl9zDvC(xMSo%GQpwDn`A}$#a<>c@S=~@vS}Q9aX%ScbiSvD5uM5&$%%29Su9z6<3}28 zjWZ)KD{(fcXs0uN&>rd`hG<-kNkl`~My#vv|8ecL{ZK@UBx%t?$v(1Lwedpx1*RKo z53K*QDhp31eKw_xQojt?^Jpfmjlb>rL5yfQs+1Ol|npJlb`)z~V(`O8lHeta& zVtto2WH#(s*Uxp}LTS}(7*9pLqkg;omS?`CXX94zvw=U3L92oH%ogS@7W|96t_$&C zGW6s<5^4;)2oz~kN8H_ew3c%Kc*b*?gd*0RfB`}Azmr`UYz1+@8sx?KM!9nR9qua& zDOGp}o--I5e7<4B;v`lg%R~1-t00T?6~#HYPj7S+H4xU)Kg>eMLJydh#X;;m)$hxw*Fk| zSLONW(rtR|35UzLYzm8aAm%;uM;FJ{ZWCiW2Z%LrIW^Fbr0Ww$8{fGb^}6vhpC^tx zBuR(_yCgJ8&JK~%uh@1ru4occIy#KQCAzK&h8S%ck5wOQ6zevk&v(qa;IJo#xI^fI z;b2IAbSOhlnS6_!>k{R}+$lRXy2sUiH(7yIoP||QGj)M5oI>i9;(BaTiHdA~w$vaFnaHYQzF3r1wx&CjR*pFN zyGT~S3_n0;o9#PLJGNXZ;VMq~hIn{wEfM>TYuQlaod+dB?eaKVVd%7O)LzA2_Z z$xNQ&BV<(I5Du8Dc`YvINL8n2NeW*s8JW(vqj=OThw+ya?j)0$uw z@$Az@vCQ`(`>Z(l^q)zwf`U=71ouP{ikT^y_X!Zx9WwhOwiFzlpxKdy4Kxh~0&N^c}@$?IQ=QfmlZDQ`ooQx+0KNQYjU3tZoMf`F6U zFej@in1)S@-IO>MPOrvKNJy69uy(3?xE{$nh;HjO`UCG~;R$8`exUe|yL}U@;xoj? zEe>c{PuhsindYhvFXN?o)rm+0pJPv9*L)`0l;k$i<`FzwCg|a6s|--@cQc?sZKm2B zLW4`?yeby+zmH|QV=s(*Eq_1KvU*NW-V;m~dZmL{=PLHKZ%BlI3>yoole9k9+1RmF zoinvSo1;tM_Fj4Q)@)aBvEcJyv$rUmhPHb#+K4hX(nB2l5`&Nj@vp+!EMy0%0>G2sZm&RFq1PJ8AB$6x+v0y$_0(@-r(t?gV5<#f1|_9PGkL0yIE2# zhuMqsfutmd^o2vgPg+=q8{IRF4*V|ptog!v=IuDq-JvEj2nTtE8@R51g_sdiJ(#U7 z=3O&N{p0%N;foaGm4-xzH{tr>_16jtFI+Y?PBa#E0tIBT5P*vKR{*pQ+A)?+b6NEfovHexag64@HAMb0t1?z*rq%SuN`Ra zB6Zw-${LQUd^hrr#OHzx42qBq`p>ojC7v`*7DuNvgcOV~&}{2|-H z1AnKc`+2(xDB%tvdo7-}0kxWszVoZu_w)@&of5!FisC3B;=@U1T2m{0LnM+GjBP@9 zoP6&u#W$pnaS(QRL)F{pseb&ae2d68?27pHhpmq3;1Uu7IKDsv;@(*XU)f$fF)Hcj z%p1hP^&bPtU8==w-w{4RMf>iSdOPgw|X~1=U^9h6<~58VNOq(tJyJ#E~POr4|=x+n9QtMV!AL3+*WVIcb2) zf+s-d6s0;Va6wwStb|I*?N5)tDJ{Z8=*@7q#Q!LO)mFV@326maTTsSuHOLH;#+cFb zURZre57SDDjI|vF-eU-JEQMgC>zQ)Uvq`daMF{gVCc#*$H`MjmNeZZ~1gb1WM*9w> zniw)lKfo}L4ZJ9IG3)0^veML_86!wXZ-uKx3ZFL?JF{^OYxDb4|i=JwhOKAet6Bd9L~{SJ|qkk zTA_k>i>K!Hl;=6s41WkQd+2+Bz1z!cBAqN&q4V>D7C4>HX^_Vn4OI4mG)zVJqVTm4 zcHM6z_KMb1s9qLRLIdc6&}PX$P8kH*j6BnH36qMC(jxDcDR5cvdLrRxUoH$8BE`4* ziFC^#k2vBzGj3%XL{aS#-?u+q)hQZ?{lm3rU1smj2Don?7qqd4vEB9V4@*pu9AIL# zrn!3uS<~OgIiQw&S)mKXHJHQ3Ftv+UV@=4~eQxL;d_NIuVuwsbFb%Q7cRA)ZZ3GtE zwNzK#+7+T9776-8BlI6NLBUba!ilIE<~+U#naE0bN4a57u$t+ocBImhWm)^vN};rz zJ@>zIokvyLiKkzOy7#4q@f)?AfdUOrj|{U?0juz`hW{7CrscqoX7eqL_c?h_{cAOg zqKkH)iR?|>vmVQfOY07dW0@Fs(Kh1XOMk9waK6d*mrR4r(}H|Ve?Cq{i2eoQ>7#-* z9$Yq#uOgOJyW4cBxk~`KGr`U}`e^wCaeSoadoUnG&K;TJ#c$GBSkqnzI*b zb|raio;_`oI7lay3`$*3`?7T4A4aXLM_em8tvwSFq|3d_?G`E4yQXy?(Q4KmhY~Wx z-LUF9??$h$ktV$_-n@FV=P5DeKS8vM*YK0-zS48*ne;qup-&J7cqas(RUaPJU7gD$ z>sEdZG~pDZUU>P@7dmxGCNfn)A_xJi;NGHhxoh57rCO|Mu32df)vU6xn#X7;XMe|} z=dovW<*!dv^?EA01+So9qtdW;>P#{__XlOT5Bw5z_25C0vCd0#60;@La36nSwv`WX z;TqIjtltcU{c&&{SQ|JY@)X>Gt0^7)3E@+y6Psi^th%DD@<%?UM%|j?VofftY`m`hr9s95`yJ6kk8Q`eXdpHDH6%A-n>d{BTcpM9XqKf?Sh$n7Bg7?~K?z>5WHShO-rz z4~Z{`mz8%ou926YWfUxVL7u$0k3kc?=oUCgpVAPzdSf93to2_;dkvdG(>H7wo?(}m zvwkIBG~i`aD>lNEHfqu5Jx}zK#9_s!j||%0n54M2ev~F#PxYt6-(_drYkzAyn5OT) z%GkT?sB2;{LAYG%_mslG{djWB5+YbiRsATfv$y?c*pksrTkXlWhVzW%=QeVzm%A>X zH=1Xt#C1=FkE%=h4!pvjtK&b9Dq_`RTl^H%(J{hgLDS7=I7;+VqV>sS=udpQr(%X| z22G$;wtYXMhk0|~cF6R!3VlRqjnF*xAC#+3_> z^rK*W!QsT$9~MQwFUk~It>1L*8z1+@LEFZejz&!{QuwJ~K26=+sE^Xbu8Pvx_=&W^ z@Ju3QIbW@K|g2?ghUrb~Ne8t(Pg2@;rl4n^#QpG*{HF>6yG>ScV=$hK-QgEkO88f*mz5gVg!R^rBE88rB!@ULi9ArfuBp)%=5 z$bA59G{BZtw+jylRUu}9QLJ#mAavWBEx10kWCi|+d#*5+uxa?REaW9`o`Q7*+v>G>@7(gCW~IW^cW{t`W(x)9N?xY}X| z5=_yLyEXhiD)`c)9lx;*!$tPNevbaw#46@6Sp z5TXn%uR70Ky8c79xyp^f<(|Hey1ma5(hp%XSmcH({e29tD_Pqkl-;t1pG^6^q#nD_ zds!@2A*6DBluDBc`G$c=%57onelkBG>%RJ+oXwT!z53}}TTPTrt2iDjQ@Vydk;VtaY0Hq^7MBE|;q(o@^q8`<}^!G9|WjK?v!C#oYyj=Y7fQu4UaxjG_{ygGBzw zlr%+)U*!@xo6e*36i;LM2+{||5}){Hee2pxagrq&9jh>FoGE%an{ZrNRw!V^fhsa^ zmV9oScLRWgrLD8ErCI&*8x92m0iTG^MYX?N&FSE&{d7B>b0NFYrrrf?WL-kWr_^$+ zkoU3&rn3)MRiA4Qpp1OK1!7KmquqngSoEObBbnjIK&0J$?4-bt1glK{tX=U1nnZlho8cXBi!ZZyY^-CHwT*HCME`bZ_h?5c{ek#@yzD4Nww= zoEHS1SQlzi$bCd7XYG&jx@qHk;@WnZr(FI##@-{m@?O(yYQv3qT*m|3JG-3#DM4zO zdWnd4O?1%rHY-CW#(#M4uq6i9fc<3}e5>Ru?uKgDrvZTPk5$067LSn|~ zsoERqAV+8ZMXmRl3k90Lp3|5L_tp^5K$k{EZ7 zJ3-#Ib(*6d2*rO=!pk8ni`}k>Gcx6yEw>2VCd0inxBD!I%!Xg80FJo4mr%YGy^^bkJu4bT(@_)yVp$xJUc9t@<1s$K;` z%VGWtGF&)ftwNyWxOuaq$I`5LLgBGhnq>efweZ)M=CU(>8PZ5(^mUd6zfbY);i z_<&(8gr0dj){%jymU5T9P1YY(TA{5X>-CEO`N_&DN?L9GIJT~TjO2PO=5nz2Pb_{@Dwuyc9{qCUY@Op8dFiD&)p#1dN&6N+>UR__(M*>U7~G&-;+xFC7l=p{zXW~?a<0JuX!%fnS@*O zb4p?M<#pAQ=PalSP~DnmQk+JR=+XqdZSIXt4zQ zBOW+BvDl$6m)+#vmd%(uEpa=Yafl+}HuSzU`?(tJuw74RL)tXqy8e+g-$1tGqBKf9 z9Cg-4@H6)NWIi+~Hybn6Z1Sbcj8*l6mSr-jV16a{NU}vG%I!w*yAKT~#Bp(E_+^7Bm<_n!KaRZGROkxrege>I zg_TVifeTR{9ocj%C>tfO#Wn6yx^9*bbKF-;{F1Ei#amKRan8?eb#Z^R)RTzA{w#4) zqwMUoV%U+ zyxp!GofJ%;Q*nfH=Q#@wzGq67VzU<@2|J`J(}syj!rsCB{;|^obh-||FJKx(A-j+I z33kq7^>D6bK>1`Tc(uRWodVpEX7qZ+s+%~;Ex2-Ov4?p*<>iV!61Jm8+SkT*4>Y?C z=NOD?ckUx~Ia;<1SdTM<_f1ABl%$%0#I=>L;c7QP)zPF8v3SPm9X2Ih(M9@)aHPl$ z;&|*SHa+%r?TS4BMMa(~U#FEo{6K+5KoH)%bM?G?zNg-Py8ZzCyp!T4qIBBbQpx=Q z+)!37hDv}9W|nb%LMhg?w_~gm$!iaq2GmE8i|=0`xKYAY>@ezav2&!S`4lk+e}k0W2?wu|+M3wdsL^EkoW|2?q4xIX62n^&o+KcMYZLzb zW1I&D6nUTMf&X(OVSF`pmuQi-z;K;If+ZpA^W+zQNR@- zXqf#nr4%(2A`!J5j1Z2aR}rH17Z(7ab=Z6dx9@M8DygJ+l8p;t3w}grt!R*<@CX<8 zE)D8~23Of9xS66sqWU7T_tB~LTbJ)j8^zW!thI}IJQt2qEBMgn{s`MK3KcT(;VAq- z@D1-r4B4#@BhN^tEUS^AT4VXu*TQELBIjG}@uHzns+3-bu!g=SOnQb@PK(KB&M{R2 zJ5jT57g8@mHA?Msr_8>*YS+{nNUkc`(6E6HHbf5e;oX`Ebw{)Bdkqi%j0gQE^`Rpa zom}2DQ8G>NWYHlWZ&9$r+-dzGwaXC#TVh}Bt%|e+NlF_eAHtCYWjcxg4>7xX~4EZrgzIOM`>Xa-6dBiqRM43Ahn~!l(q>iu`pu5I78cbTg5c zsRO%-Pvb6Aq)W=ynue0O`l63=M@&7KXR4OjIxB-H-Cs%ftA`=lG{d^>#%CVRxOde? z>_xlk860~}nIv@9-ghQ#3W}@v@=<>mV z(1K@+=xC{uTDg5Ed%3}!rVMW!O1jEGay2VQjy#795er1H<|zu)cH3C>v6clllFsh_VVLW<uQM#dbruRX+Yng&!>~&?;p0@bz^T;L<_huzf`O-X-(PZG_ZZO#Eb|) z-5vA86zC22sIl$BGFbgtnk;??%c7xo8FHw9Kjw&w&n9A&PVNIN-1*I+;nTt=g^y<6 z^c1n!S3s&6zLuA=hs#)hHID5ue5m&fq9D=rp!ze)M!C5M^|ZqI`BF4ZP05l@ouy)z z!tmfryo={f8g00wg$ig-6G(T?i*|pG{e54h+AIZR?!Z$=1%@NbtH3wbZ z={EOJ*L98gKIaOX{R1xJEXvQrKHZ~yaXRl|)7r!I^NRr4u1IhD<|CEF&n4X1{9UI} zPBLd6IBuuJR$8 zdf`J&k`)Z5_nHv)rx>AD>nGwrthh(6{MS|U?_>weY)Yf~L{ryDqm55%%~Xa7a}V&J zMxZOHlWD#nCY#QE55Z@Imx+H|3PsAvUjQ9PrtvI#*5Grm7r$%e_m0x4EeMD&!W^@s ze7_E8l`4#Jsj*pzIzH#3?4*aCyBqLYZST~cq z3PM-W6G(}v>SaXQp_nZWk)cXS6`{9c*>QfM=|*(R+2=cb1_lLH`t!4&-_u5pdf6)c zN<92#_kVpV1}4PV#YcsgD$A8~Ww|2-i9!Pv8;ve+C$bd?GCeDlBykv25h0Ak-C=#l z_?>;IWt{}$3PuCP%A0U?8)0tuqM=7WzGkVUig-6odEZ`(4-s5YZN|Z7y96-PvEL?J zdKhOP`NK4V0N%mW+ZdDo>Q%Y%SKq5BZOO5^qd0Ry|C+)dU;6}azZq@dLGS*RPWpYF zKJUduQH$Q!QDiG7ImrlD5?IYa^$QPH+8ogAWv10gT|Vqp-tV9DL7`0Kw$%L&eg6Og z4+j6n%}vh0ruuMz{x26Aq0_kI3iTOz$v^yG<_?W$aLb0$0w)48CfDdExgw?eL@V)D zF2nziJH=TUfH+-a5n?`F`spEbs+E(0QU1B}m>%cAxF@vRM{*ST5qxfCt$muo08<-o z-n#19c9O8C`x&Z`COc%Yr#&O|uc&g7mi2UW>SVYC=L5#2t3#nyJ-4Hr`0ez|OPyvf z&-yiDz`HZ+I~#M7ps7{S^t;ynsl(r#!0F{}3Q+}}h$DLnuj^bUFsA^J;Pm~B7gsKI zAKi8T&D&=t-TR3NDY3pUp>c#yea3TEB z4o{PP{@Oy=?X42gCsO35=7-jMM76IAG=-VJ|L0%3CTb;>iH{Gdyrz70`R~66DDp+N zicVqSfBLcCEakuKrl^h-5XicDgo^9G9{(TT_uDsPiQ%Pf<0dgie?Iy*^ZT!d{`Tfy z-&B~uQSi+#I=y4n{C~T!2Z3*JwI4XQSJeIAE(-{dbHV1FSC{`d6Of@yEGwhS@hBF~ z@eF!lx_*$8quV%bhE{Dgqgw7Yr&(^iFUDbZB9&8S?>F!MkHi1tSaBu6CgC>PX>Qvh zI$J86COdDL_i8$`V)XXPJZ1AMGjUkuS2{m8EO-9eZQ7U9yNCQ!@%IPaeIqIvscYJj zNo~6GseB-OLumkX2MvrDYN08-95B@YDxx%Bk!!wCTF&=l@58`RuFW*}gk@oc#Th&u@!t7$-W&Wzn92t@WdLen`nuquGGdZjk2Enm~;&tudUW#^GMdHUQyx+*#Xh~ z-v^?AjYw7PzB@f6>waufZhHLFH&*d+lGyaur#JDBHT99v$u07cOFzB;hiCoc6g45= znrdjX1DeOVSBV`7AeDY(>cGdK{N|ne}JhW!J>X$72m5O<;2hL zZ_D%dbHKwz&8dO@na{BlT0sCtN`;K*$tSrAxjzl%A7?2Bc<;sO$?}R7&J)C%{2w8r z5d6nY9)Vfvkvcui8Eiyw)(}2T0nD+rs<8-xkA=T#yRgSelli3er6fB+C*qxH?F)pq zQU2@G^t(s$ipEuOK8K3>l^x`vGz*W^Dh1^JD3e)nyOmf4v?$X@&u_keSpd`hcm%jC zENhCeqw}l+7$OCOFvv{1!byy7K$9;RYPU|5ml*wph&)2@;Mw6`J5~y~yRK&s!shsz zrMu<)aq59iGhCsPPT@ysi-9T(%HSl$*(~4OIz+&eoLH!q7S3+B^^=FRZcp+KSh9BRe4( z2E_t7^iR{tV3gK+A`HXQe6`8~288pe^jb}pWYun!^}*O)2f*Y%)zt<{1;i2RH{EAI z>_;EukFA?~TZJPl8{Ne0qJ>P7_u@gCy$lGF-bd3#uz*P(ug|^*g)B_% zIBtw|?!mY#4nd<`BA{DI(I_`f0y|D`5;8)Lq|Totz7QAylLUp=KxLy5Hb4ij+hM9FXS+5wAreYm^J4}U)xO2n z7T7BB;{!iwm9YVs6>IIoP?_i7&L-$Kx=-y6pxk?{Zw)%H+9w~$zKRr70t}WZ2D1mC zv2gnG3onB#J5&1*sO}FwBSzZWxx6|Yy1pNH%CK<)mNc5HL=VG^sf#1o7WciRbQR*V z{Pv|lyHv2475~vfgKAkY$FgVtp`X5ERM_z8=`@r@K-B|uwhr_sanN>;_3tNEzdjyQ zJ0a=G)^1e&w;f?F-Y&17&pIv-xhyQ6$Rh20!{3JnlD->Kb~~2&!kvwpRp4d@5MGG za@*iTjz8tLo@Uj^Z8@n=A7`&!#UNALRP|14q^X1{T`NkfdhU9?WHhV$WJ_c+oLsmo zS{0lfrW`CUHW2mXa@ql4mDpgH-3sfm@uQ8gp$eOc9>J4u2&Tu$#;cQqa=insm-kHS z5I?3oYe&W~sm|1b_<%VbdZvK2W)0{w8;5B>tDw9qO(wqJ)n0o(b`DZuRKhucL_+x; zf9Qw4TZFY+^#d+rD2yGkcwEIBWTlGMKm~(aP1nuD7rQt)&piSRR`zDS3K0Yi965mD z;Q)+OBc6_!An}j;0;1Vhl&*_#=OlFlQZwQlFYIgqZ`jGa_{Wv7#onClhk5Fo^8234 zLOk{>&)QvV!7#IU|Fc1tCZn@@fb1Ip=#Qc3b|0@j@7q)F@i!*3HFghFZrzW6l7utB z=*0z?-2faF-AI(0+8dx_$TG++@Ub7b%-RJVcU%FP-a5UzOd%+ZC48puF!G$9gcYYJ z2fbFcDm))XTQ7iY{1_?g2cC8~-aWS!oXc&AR;-P#>aZ#Ue0y+dk$QBMojP4Nmwj54 zV}3|zx)e6FQW+A^%TpZpc&WdrX6f!B7|X`X8k=YKe<*v)uqxZFe^-#0^rTC=rgV3Q zfHaDLbc1wvPa5g&mhKdsbb}xuAkryaA`ScId7kyIwf_6v``G)F-#Fyvj&Y4K&hy6} z6znka>V%(xZCh+i=zY@%GkvdJI%mN3t?8-EF4?z1W}I^d&^{ST@m?SQmqLh`O0^2u z6>`Ocx1O!!V1r#1XrT9%vEVj5wJ`x3$`p=}hr`wzO8+OenO-psETkhRP#Nb7@OrUL z@S&}vGOAZncJg0LjYs-$=#GJewecrNno$P)XH0V}4pdBW#ip0BU{d-zl+KT`dMAY& zxOon9^e;98S`KIOE9hE)%MsB&}z0ZuG#Kn4S9Ve1gX$t zCE|#vV6vpgIX(Amx)Ok?&~OHj*!EAjw;ov%U;maC>ts<%a~RQ3VV`WD4S(+IZ}Qm* zCY~OT!8jAkhP;I}-MP(g9|HG$D95s%`x5?-PaCUUy1^XOYagEy{(gEAEssQ#`CqoL zXeTI~QGG^4f)G(jDsLQ?{%6_pPuV5zN~zi@h?*Y_9B1g|7?YIzv>Hpj43*FXg1=Kn zg4N#36i;mPBWR}az2o&<>zB}Pdw+^M5_a)SA24UrRw&4c3OfLHr79WoEWvfPQ2Z?* z-%&xmjquaQ@5v?BspQS2Sms|+DO>mh80K{BJMAW}t^LSqSJq05f?sW446|j*=3b>L zz0rHb#ykda*=D*VJ*TKMU{}XZVYvwP6T<~F6I0?+qfXB{*qAXUPj_sl0iq1!iASOq z)fq-uu^EMIy8dV;L*zJKF~*rQN%g|7_w%U0Q@`>%i^f;4x00edx&k`Y<@#xJu>wVl=|czVvzwf2skD$QWw{S zmpwf`uF|1}Qm3a-%?QEz5dR(>Lzxvu>COnW4>hWwURhppQ0)~~o{xY{9%~k#2upb?y z7VIhV$z~k68~3QhV??p8*G}uv+GeJ-u3x8P5Twcf&}1<_ zWfsM$4&8okkQ4}UqwqQDo;=V}p?~dRetILR4VVAg|8C#l@xz=%eDiPa^=z_|(ZND>bYQXX!Ayavdw)*^)%5O^>KWI}#8*#jFX zao`m~O*cIyYURxJNPAGJp^hDEp`-_}TZ*+8K;IYtG7bZuasMPQD_&K&G4)ToQx_aE zR5TMBPWytu+DDBcNouCwz+_^)LiP6hk3ZNnK-^9`)M)E|v>h#k*#A3A*bArd>+dyw zW+GCp6N)oW&3-8g>JE~fHVTi(j*D5H{O?k`VMDy!-{z1#TTHNHn}qHu5NVftJ%Tnj zVCQJ`}LqO%)>@M#{rPN z>Vj%c2<-SlNP2-9iKo&#m5r*B?3hou&%@s+8lK*Hd#p@dy5tfXX9#A4flJt?MSDI+ zLE5!jVlKX!M#qi&UhXFzT>@+L)<2JX2JAXiJlP_HvN}|aPj7)*27Nr^l7{@?w=GDC zcA9fBn&Vl1?71(k5A{kYJ5?Xu*}O@thJez~tL7|^^@!PD5Y6LzMnt@Ca6@H0-5%z8 zCsjev8>jPB^W`iR#hq$k<=gU4*fGP0pQbxb(+%zh4bH1e%B7YApa!v$O7BUtFg;K*M zshKmo`SN%{=k6}CU>)jTmj%Be39nlIISTg&iZ@~FLb|R-r6~hdNg9>|!LK1wy-9RN z*PI+CP735|b4JPChP@Z7k*^hqi%e`|*}J{b`o%^TAVyVeNc2yG3g@DT>z&HR}nkC_%J#C*04dRw>zI#72_b^UtGhjMJ@I>&( zp6MW%flzVdpGGo&-dqyxq~%#KDH2iIq@6TOtm4VAAt2;?Lo1E-I=zp-@TsBGChA}< zbW_gx&eC#B z4Fb&3MDWgkBj1pXINXdsSu0kiKV`eUpK7Pk&^OvirJd5&v<=k^a9xfPc||!hkGwWoUY7e<&~D)bBvB&QiZGlzf|d-nM>9s z+?4pkYK)HAK`LF}#sG|)6}(!+(K&NYkAsu2o9H@-ia1OXqF6seB<$iv zGjGW?l*>2NHh4h#2*G3kqXPXCh7!pl9gRY(L~l04V^af&TyAa2h1@E=TSxpBy&{a% zlhHp62fnjiK%#wXGVOi|2WQo|%&gsbPrB4-#np~9XVc*81MMCJ+MZ)0Tcga^GeyH+ zOJt6h4m>2Qy9=!+Cw<2l8br|tE-8>hSq~L~C4j?>lH}x{_ra`S|yBIj% z6P0{%Sg;LFZ_GR;p+dP9ZKWlQ=K-GQ2Bwp`Rnr&r)N9^7L!#f1K}aVkA3u!l3gYvh$hFqH`>LPowr!{(NGON=o4z6j8dE~1KFd= zbh*sFDp8LAHOl$g0dfA$`U{(-^6VehU8?+1oOTKCJ4VB?9|y|0X?YNdqp+$qA%K@2DJ&OjrFvcL8MJkw>tO=Ps z1h&S%w5Wwl5qhwF(%Aq@#=1a|`=^5^^W$W+s!e{!RrFspT1{8FXL;aAIDlX80r8-j z-6qqA4Qhw-t{s3dW*f*Uf~9j?C0xuWxeGvJJ>};}`(lj` zr2U?={vJajT$A!xe$Eio&)36`0%y@tu6km~PdV5xH$)$K#a50k|MheAZ`|-_+N6qK zuXQ^0!Tmn69tP~nV@3LY%f)}%t;F$^d`{E*+J!0~4&jekhvtc=KP^~bc23p@B<(F$ zg`+7*dqL=W-}{E`G>_zneMr>GZcqsEIpX|IZp zr~=$MvH7JZt@CgpbOO6Yi3!3XL=)1x%CF0SmD!QY#8WK)F|jBAQ&58LSeSO_EdBtW z9|FOpkL|LB;*j@K5LLA;{kg<8SQ`Cbvn+@mLS)9Oq*{m>e-5S%$`*ol@CEB8;SLH0 zE}e&Lh^JiJZpbRNSU7W1ht`N;8>T?0ad2!FB!9_z$rp)APOzAJWZb3t;t3P!cG8(G z1J}nmJI;zqsC~`j&6F=?5uSMD5O`HLk?{et*rlUb0 zw=O0bnaU$Z^gy~n>YfbUY|Mb4Z`SpDyc}MjB(16XHsF0pPI`f+M^EShIXO5b?bcZX zj@QbsN3m`g$(7@#VHftveV(F}`i#Q8arh9L72iueg#vLms`VvcH^#I2)yTm~qvHxw zhr3p8+%P(7p38AUQaa(T{Oq~-uB6thP8FO2EKWR?vJA@BUkspoAfrRMfU3III2H%Q zN-uo`BvG7w48ZH&kWcbTKyvzcLRcNrku_?HCJ%bKk2b+M@p2_t7g6CA{R(jO)5Dhv zc@jnG`dx2nL=OFdomYjao`<+c32!_Ipes@0$gqdD0kt8nY0`^SmdNhfrnauAOTDyJ z6#C7SEPvp{$kN^4u>ab15w}J74~iPnTIdCvexNb)#;4J3zGHRaZ@z2S*eb6cvu;Uc z;PQ=#ck-g$bBR$rX^xS4nk4BZoFv{w;x`mIpSxbZNtO^C@b!qsNnux>Cw{52hiIjE zFW}dYui00fsxzp*WKi!Rk?h{Gl5%sgmtJMyYxcPLVhz2GjrwBmJJyk}Q)}0_L`l(v4yjy|u;LqZSj)J!1a#zD2QjPR~d7(4vXR;MMM%*}=}?86p8!E?#r zV?`L$9@hj1HXhdxA&-?zw|*@oyWuql~lZ_J(-K(FR zgRa6O!-9#tXZr3MSyzy4+y82Dq*KRv(WhJ4c`8l}rc4t07}mXV!4HC{pOhP(1owOM zuFVydL;O^Iq?%hJ?ZP?a=tM0cF5wsybm*w1$VPb6+9$GGYvh;7S(_1ONvitQoRDlD zL8Bji04FldQX_boK2yM7q_1_sWjXrY*x7mvse{k?!&uU3Vlt1NG0gc&K5z=Q=p*Q! z;(48HFPyf%qu!cSzpcni6!uc4t~qQ~vW@{ERmWSnuC)ed3hNj%$LOKF0ZWp9RsLmu zC1sQ9JKUQ{2QxV(TwS#*bw0JydV@@apb&Q=T0Vc+31+`e6FnkgrdY(LUgm27asOZ! zc{(CSN;N8*^Lrm|`{hN^DC53vZ_Ph%zH==Jpv)1x>QaA|X6n6?uB-1fOs7##>oXKW zsIRrfk9_4e9pJGY`ibY&zsNU(9F-r4Y`-cV1vX0H^WC>3XlCy_y zXU-T&y*-)bqA)y~#?l%a1r4^A>?U7-&()9Lz32Q&bZV7ngC@4q>bpww-=x}^|46W3 zjvq~BlSV9 z3Z~w=V8LF(oi;gL<7AfbJ|M4+kZ6HhM6Yucymya1?AL7jeLy`q0z~8`!l~=snexPA z&$WW1ES~|h?xs*^+#C?FUE=F55m44A6~NIFZPi=N|0^%}K$m1p120frGQcs2G$s9- zIcKp+o_z-A9>><~Xiztwn*FN_;&Y(^z&5X1MvgG~g=4}I<2eVVT2O`8$(;IM^E1E{ zTtL@K)L57&8F7@LGo>n7Ea9+bl+4tPj}OCIk3z|Yeu}TZXBpvUexn!VFQ!D_DMLj! zBKH^G!MX&M5gjp#98%m%JZ^+Uq3qHa`8thv$_@s$na6*>_(*bS3OhecRE*lL8takC zP5+epsKRWf{Vv4JGhSE-@@_+G?i161=AD8U{X9fM%cN8bVbqwaj`>=ViT0DSW!>WdFAr#zL5Fh{MURp+pIn-sO#LS-m4Us-AH| zFzG7W%})v!r0}G}MPxsg-G#-~7H)7Z#_7o>z~o4v6`}#u-)#+F1xKsnLw1Uopko+J zG#p z=2q>~Md7|7dUh}9P>lQVeH;4+3K@_*`dJA#RtX$4WjsMta+ZA(fN8<^6}jbkHF~t( zayqAg+0}`3|7(}93t^65M&r^wGEw11h<%(ZrD8z4*@>}`Wo^z3Ll_;4{n@6#$U6@M zxhrn;o24Uh5>9wQxq@mV33Y7b4Z zd;j%3hE}QkINw`I>3}P=dpfhtaRvt=4kHeI48MyzTcT_Q(}jpA{dFVS0I{0c_1vrb zlCPV?ixgjEVIK?boOU%mir(07zezV<-ANs`kCr#hR&I-NlwIU?7Qt3L;kEF|ITVh) z#oCTWHhV#NF+51n%S6?>cz2>@!6KQu+AEdHli6S!mFb^<*`0Fp;_P4(vc-AwwQKm1y<(R;=a^tMRERP5fsHD(*KUW(Jpc@%2J94p{5BVRcjiNOvvw%CP9ec(^jg`nsF7y z2(}6)9!vc@sp>6LFA;Jg{BDF~I2KwSqkhUL&v#Vo{F=b*rpj7RK33+%F^$~70xe`Q zJ-P%(ZGq4pB2oCb&N;%S>qxh>_QtBWn!iR;KW^>gd~%aJ-4odGK!8ql$% zmgFom8m{LnsPx73kfZ!gPOJ`+H6%}XXDOC2g6T}3nwk3kMUBBtb&~A0QK(GW-qyB( z<{wfdHH?3ACMSE+FL%91nlNX7GTJYUDSE8=yB*LTm-!gZRC!NO>aHyW%w(93eyqS7 zm;J$$jJ!GB3FL_mt#_|UHGt;UgTX(Hy zkEVURDJ_!i!crN^u=1d7x9(?WRk^_Ex}D%72?T9re+)!0S_jDvcbo$w6gkceL%Z(z z9Uz^mam4b&7!j$x@LoV5ijkAAicJsvsBkWLpt5rt^&B-_wsnw7V=3+z(9Rf?A6$q8 z;yc^DBlLptIQOR%*mz6^qU%f#4(GhkFs5n z;BIKLb7xgjNc{_`c3|Z`rl1@|K2Ed|p~|;_`VL5_ts>^OjLWJ&o&lyn{j=GgFwlTB zR8ZFE9%RBhx0>u2+o=zM@*fVi64Dq`X?cj5fP1_Ogdca&1AZ~J#bpr{h&!|k`SH0GWD7Z?S{uOLw$6PYAl&uL{z7*lb%@(I%o_`7 zMhbd6=*(f%gYYx#-XfAibS<|RTKDl*nGz#Zm8b{1tn0ACN#q!VnC7&SGgO&jtZMhs zkRga1J|WHBo6#5V&?Gv1hg>>=&BvaobNYe(z|-vn)y)hrPW}<<6>JpFJX1^zJ;pq4 zDBX$Ye(;Xp33T<4+13ZztQmu>Yw{s0_L$KRsK113cTOnJ9mK?7x zo8@{DMdj}^Jl;?VqF_UMS9Db0C;cBYM}L5bj?pBDzA}{)aQ7A2d{D1L=$7%|MEWA! z3t_}YT2KtAb)*0I`hqGzJv~kU0)UpS#ax~cnNi#2Z@ZOv{mAkNJ=D*^AQ@KUlB(CXI_= z3v(?MEaGhu;QB5*L+Vt~ugTw~FJCI|Q}3#&=j--YJTqGYPPL4m%5Ai9qfW6#x`BCp z`-o=@&Bk+?e5UlI?pD;0BbJuMb@TYfi5sV(r**(n=^xUdS0h+lk*oZ#U)|@}gwn0;cu5YuvM_xnetVQdaKX`#g#oKct<_gDjk#Z~bK88@y9tXTRH}{Y7U^)`ashTCV#s>-1ztgo2y_x+UdXWD>#1#eR6^L9dSX#GY-Cgm!wN)s%9^4Y1 zsEdQvCp!Zl-kG>UzfR+^eu^E>+!MdE5bt0MAEG)stG2A=^>l?A7+!cZxWLO}K z>X~N{S;j z9pEgoY~~!Sb!F5ej<-ca@%Z)%s;83~qhYrQMsFM$%ehC+d;;WV4d%Ws9cEytLGky6 zU-+Gzmb@9XGk47kC)VuSB)Cj~V;)hW*L&!~RuDn@^&_-09!3pCxc`oSLxno)k}!e` z5RheG&(TjYyKdqg3aPb{3dSsOGF3uM^B1TtShsB#n@w24;6I|&vMF|{zJMy(+&A|B z$&9#A;#)FhxKMMIg6npNA2oc`UeoRi68SLxsZ{uWLpTbe?o$wP625M>ZXorG7@;_X zNw`yS-B%(o6sqlfGs4@}Jb9Tt%&-ZVP41fCAgc+6yfiI}30W|AR;jOws;HHSk3P%b zKu@{SDrryH^>wfL*gheY(&WX+$cR4D@OG2HvmS|JEsbO7q?0V#YUnwd7gban{WRx^ zO_4#^eZHKuDx;%*v?OIKkwX5=`CuV9EqeOb#!(A6gwcpd4V}YIDnG(03^pRNM#j%u z%lhZM0UvVu_>S4YoN=y(tNr(pwcBCKr+XUH)uHE@Jcoidn&N)EJ0=w%lPcD!1K6Ox z`H<*sdOR|@`Jio^IQwOyc8P1ITgk+c_k73gp}-J2X*|oEANX zHsP31E#GV1pX)oW@D$0$ha*Scan!7nQg{B~B$py`)(3ohD0Bjw?D;LKh8wQsI9PBU zOpC{4{G2dE+0?hI%P(AK)J{3?1@Sk`XLX`+wre~hi@p5SSoKvN7HJqKB*{LhTyLi6 z5cJ{P$NTLj>evZ_FvGD^-^QJ{EV=0zXHg9nG{-^I^v8~Peuj6fMl{N zR%P4)80Z~U3p>%-3L4Rm7*f9K5wA9%IlGS2R|*}0_-o<4lGuY~75i1Myo3h|hG>U$ z*u$w??#@aHkJ$}Q-farxlT|~Bl}^Vyw==A9$nETId=A5!G{XswA}EprE!39l{K(P_ z+JdOyzM-Abbzrs5iMDNRPY4fQ)~*ZBIXtez7$yVbbB+}RjhKg9Aj#qbw*Wr>MWr$! zg(LevDoa~?OXeWTXlc`D-Gd7n1*a0JwfN{WY9DG#Y4i!ROxP!$C5eG@-XdVq;_On@ z6|9GHSsLdBIK{>i&5}Ssb-ZgP=<ZeV_n8k2HWP3%xG4akL$J3l> z@$>bnZG=<2#W#l5+7uVUgA{k%V@gRsYA`n>+Bcs}F)mHQ(*lU)ceJ&j9#R?MP>_ehNsRk33N8_d3Y+QfuDc;8dCk>IrmH^kr48=LIJx7f5A>;kiIexUdAy2}V!&bDVH{G_A8maRjJmu|&r7T%A)0SbR8y4@}U$ zuE%VfVq0>lR~^+ZBQWY629pHgCQ15mbqAra9t+`d+WE(3x>xT40~MdkC6pG$JI_z3 z>0@8;>p99}qV-e-4Tr?#IJD10e*+)?QciU;*|y;;G<1+quR&TK1ZM(!FR}+tMsS7c zi0M%5V&=`^{Fvsp+51O~x_abi2m82;( zJO_u}Q;xBY`PJ^~av2-5X7Y)eYm_{w8cB{P(sEpUVgrN66Z+A{KQ)NGej!UWV=?zo zB0C{!emzB(Vw1sn&C_smo*cmqetaHwVDj1R*htjNKlJqc!ryrq7z5f;O3Yxie-bL~MW#BC9vGpiG8ImTAXXY$+yDVooWob>{Sxjzam)&gCN zSWJ8-BClcf{eU<%m>y?DDQ3<0FIVtI@`+bZRNsa9CW)l{?Y{Nsg-uA3_*`H-;yLP` zayYjp(GL5AF)Tz`3KkzN17Kd&!S+?*`==3MpO~V_eu=FHU

)6G?&y*ag<0xHx8x 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

Rigging is a very powerful library ``` -1. Notice how our message content got updated to reflect fixing the the extra whitespace - in our start tag and our string stripping annotation. +*1. Notice how our message content got updated to reflect fixing the the extra whitespace in our start tag and our string stripping annotation.* ## Stripping Parts -Because we track exactly where a parsed model is inside a message, we can cleanly remove just that portion from -the content and re-sync the other parts to align with the new content. This is helpful for removing context -from a conversation that you might not want there for future generations. - -This is a very powerful primitive, that allows you to operate on messages more like a collection of structured -models than raw text. +Because we track exactly where a parsed model is inside a message, we can cleanly remove just that portion from the content and re-sync the other parts to align with the new content. This is helpful for removing context from a conversation that you might not want there for future generations. This is a very powerful primitive, that allows you to operate on messages more like a collection of structured models than raw text. -```py +```python import rigging as rg class Reasoning(rg.Model): @@ -156,13 +141,13 @@ follow_up = await without_reasons.continue_(...).run() Both Chats and ChatPipelines support the concept of arbitrary metadata that you can use to store things like tags, metrics, and supporting data for storage, sorting, and filtering. -- [`ChatPipeline.meta()`][rigging.chat.ChatPipeline.meta] adds to [`ChatPipeline.metadata`][rigging.chat.ChatPipeline.metadata] -- [`Chat.meta()`][rigging.chat.Chat.meta] adds to [`Chat.metadata`][rigging.chat.Chat.metadata] +- `ChatPipeline.meta()` adds to `ChatPipeline.metadata` +- `Chat.meta()` adds to `Chat.metadata` Metadata will carry forward from a ChatPipeline to a Chat object when generation completes. This metadata is also maintained in the [serialization process](serialization.md). -```py +```python import rigging as rg chat = ( @@ -184,19 +169,15 @@ print(chat.metadata) Chats maintain some additional data to understand more about the generation process: -- [`Chat.stop_reason`][rigging.chat.Chat.stop_reason] -- [`Chat.usage`][rigging.chat.Chat.usage] -- [`Chat.extra`][rigging.chat.Chat.extra] +- `Chat.stop_reason` +- `Chat.usage` +- `Chat.extra` -It's the responsibility of the generator to populate these fields, and their content -will vary dependent on the underlying implementation. For instance, the `transformers` generator -doesn't provide any usage information and the `vllm` generator will add metrics information -to the `extra` field. +It's the responsibility of the generator to populate these fields, and their content will vary dependent on the underlying implementation. For instance, the `transformers` generator doesn't provide any usage information and the `vllm` generator will add metrics information to the `extra` field. -We intentionally keep these fields as generic as possible to allow for future expansion. You'll -often find deep information about the generation process in the [`Chat.extra`][rigging.chat.Chat.extra] field. +We intentionally keep these fields as generic as possible to allow for future expansion. You'll often find deep information about the generation process in the `Chat.extra` field. -```py +```python import rigging as rg pipeline = ( diff --git a/docs/topics/completions.md b/docs/topics/completions.md deleted file mode 100644 index 1dc141a..0000000 --- a/docs/topics/completions.md +++ /dev/null @@ -1,80 +0,0 @@ -# Completions - -The majority of Rigging was built around "instruct" or "chat" LLM interfaces where -a base model has been tuned to work with a structured layer on top of raw text completion. We typically -find that base models are more unpredictable with their outputs, tend to be more sensitive to small -changes in their context windows, and require frequent use of [stop tokens][rigging.generator.GenerateParams.stop] -to prevent unneccesary generation. - -However, there are some places where completing raw text and working with base models might be desirable: - -- Fewer restrictions on the types of content they will generate -- Speeding up generation and lowering token usage by discouraging verbose responses -- Leveraging prompts from popular libraries like [LangChain](https://python.langchain.com/) which assume - a completions-style interface - -## Interface Parity - -While we try to maintain parity between the "Chat" and "Completions" interfaces in Rigging, you'll -find some deviations here and there. Completions should be a simple transition if you are familiar -with the other code in rigging. Here are the highlights: - -- [`chat`][rigging.generator.Generator.chat] -> [`complete`][rigging.generator.Generator.complete] -- [`Chat`][rigging.chat.Chat] -> [`Completion`][rigging.completion.Completion] -- [`ChatPipeline`][rigging.chat.ChatPipeline] -> [`CompletionPipeline`][rigging.completion.CompletionPipeline] -- [`generate_messages`][rigging.generator.Generator.generate_messages] -> [`generate_texts`][rigging.generator.Generator.generate_texts] - -On all of these interfaces, you'll note that sequences of [`Message`][rigging.message.Message] objects have been -replaced with basic `str` objects for both inputs and ouputs. - -## Translator Example - -Let's build a simply translator object that we can store as a [`CompletionPipeline`][rigging.completion.CompletionPipeline] -and use it quickly translate a phrase to 3 different languages. - -```py -PROMPT = """\ -As an expert translator, you accept english text and translate it to $language. - -# Format - -Input: [english text] -Output: [translated text] ---- - -Input: $input -Output: """ - -translator = ( - rg.get_generator('gpt-3.5-turbo') # (1)! - .complete(PROMPT) - .with_(stop=["---", "Input:", "\n\n"]) # (2)! -) - -text = "Could you please tell me where the nearest train station is?" - -for language in ["spanish", "french", "german"]: - completion = await translator.apply( - language=language, - input=text - ).run() - print(f"[{language}]: {completion.generated}") - -# [spanish]: ¿Podría decirme por favor dónde está la estación de tren más cercana? -# [french]: Pouvez-vous me dire où se trouve la gare la plus proche, s'il vous plaît ? -# [german]: Könnten Sie mir bitte sagen, wo sich der nächste Bahnhof befindet? -``` - -1. OpenAPI supports the same model IDs for both completions and chats, but other - providers might require you to specify a specific model ID used for text completions. -2. We use [`.with_()`][rigging.completion.CompletionPipeline.with_] to set stop tokens - and prevent the generation from simply continuing until our max tokens are reached. This - is a very common and often required pattern when doing completions over chats. Here, we - aren't totally sure what the model might generate after our translation, so - we use a few different token sequences to be safe. - -!!! tip "Using .apply()" - - Text completion is a great place to use the [`.apply`][rigging.completion.CompletionPipeline.apply] - method as we can easily slot in our inputs without using [`.add`][rigging.completion.CompletionPipeline.add] - and following it with our output section of the prompt. \ No newline at end of file diff --git a/docs/topics/completions.mdx b/docs/topics/completions.mdx new file mode 100644 index 0000000..2e5ae76 --- /dev/null +++ b/docs/topics/completions.mdx @@ -0,0 +1,75 @@ +--- +title: "Completions" +description: "Using Rigging for text completions" +public: true +--- + +The majority of Rigging was built around "instruct" or "chat" LLM interfaces where a base model has been tuned to work with a structured layer on top of raw text completion. We typically find that base models are more unpredictable with their outputs, tend to be more sensitive to small changes in their context windows, and require frequent use of stop tokens to prevent unneccesary generation. + +However, there are some places where completing raw text and working with base models might be desirable: + +- Fewer restrictions on the types of content they will generate +- Speeding up generation and lowering token usage by discouraging verbose responses +- Leveraging prompts from popular libraries like [LangChain](https://python.langchain.com/) which assume + a completions-style interface + +## Interface Parity + +While we try to maintain parity between the "Chat" and "Completions" interfaces in Rigging, you'll +find some deviations here and there. Completions should be a simple transition if you are familiar +with the other code in rigging. Here are the highlights: + +- `chat` -> `complete` +- `Chat` -> `Completion` +- `ChatPipeline` -> `CompletionPipeline` +- `generate_messages` -> `generate_texts` + +On all of these interfaces, you'll note that sequences of `Message` objects have been +replaced with basic `str` objects for both inputs and ouputs. + +## Translator Example + +Let's build a simply translator object that we can store as a `CompletionPipeline` +and use it quickly translate a phrase to 3 different languages. + +```python {13, 15} +PROMPT = """\ +As an expert translator, you accept english text and translate it to $language. + +# Format + +Input: [english text] +Output: [translated text] +--- + +Input: $input +Output: """ + +translator = ( + rg.get_generator('gpt-3.5-turbo') + .complete(PROMPT) + .with_(stop=["---", "Input:", "\n\n"]) +) + +text = "Could you please tell me where the nearest train station is?" + +for language in ["spanish", "french", "german"]: + completion = await translator.apply( + language=language, + input=text + ).run() + print(f"[{language}]: {completion.generated}") + +# [spanish]: ¿Podría decirme por favor dónde está la estación de tren más cercana? +# [french]: Pouvez-vous me dire où se trouve la gare la plus proche, s'il vous plaît ? +# [german]: Könnten Sie mir bitte sagen, wo sich der nächste Bahnhof befindet? +``` + +1. OpenAPI supports the same model IDs for both completions and chats, but other providers might require you to specify a specific model ID used for text completions. +2. We use `.with_()` to set stop tokens and prevent the generation from simply continuing until our max tokens are reached. This is a very common and often required pattern when doing completions over chats. Here, we aren't totally sure what the model might generate after our translation, so we use a few different token sequences to be safe. + + +**Using .apply()** + +Text completion is a great place to use the `.apply` method as we can easily slot in our inputs without using `.add` and following it with our output section of the prompt. + \ No newline at end of file diff --git a/docs/topics/models.md b/docs/topics/data-models.mdx similarity index 50% rename from docs/topics/models.md rename to docs/topics/data-models.mdx index 5dc78af..3d13f5c 100644 --- a/docs/topics/models.md +++ b/docs/topics/data-models.mdx @@ -1,59 +1,60 @@ -# Writing Models +--- +title: "Data Models" +description: "How to define data models for parsing and validating text." +public: true +--- -Model definitions are at the core of Rigging, and provide an extremely powerful interface of defining exactly -what kinds of input data you support and how it should be validated. Unlike other LLM libraries, the definition -of strict types in code is how you navigate the complexity of working with stochastic text in your code. +Data model definitions are at the core of Rigging, and provide an extremely powerful interface of defining exactly what kinds of input data you support and how it should be validated. The definition of strict types in code is how you navigate the complexity of working with stochastic text in your code. +Data model definitions are at the core of Rigging, and provide an extremely powerful interface of defining exactly what kinds of input data you support and how it should be validated. The definition of strict types in code is how you navigate the complexity of working with stochastic text in your code. "If the parsing succeeds, I'm now safe to use this data inside my code." ## Fundamentals -Every model in rigging should extend the [`Model`][rigging.model.Model] base class. This is a lightweight wrapper around pydantic-xml's [`BaseXMLModel`](`https://pydantic-xml.readthedocs.io/en/latest/pages/api.html#pydantic_xml.BaseXmlModel`) +Every model in rigging should extend the `Model` base class. This is a lightweight wrapper around +pydantic-xml's [`BaseXMLModel`](`https://pydantic-xml.readthedocs.io/en/latest/pages/api.html#pydantic_xml.BaseXmlModel`) with some added features and functionality to make it easy for Rigging to manage. In general this includes: 1. More intelligent parsing for the imperfect text which LLMs frequently provide. Nested tags, unclear sub-structures, - multiple models scattered in the text, etc. See the [`.from_text()`][rigging.model.Model.from_text] method for the details. -2. A nicer [`.to_pretty_xml()`][rigging.model.Model.to_pretty_xml] to get new-line formatted outputs. + multiple models scattered in the text, etc. See the `.from_text()` method for the details. +2. A nicer `.to_pretty_xml()` to get new-line formatted outputs. 3. Some basic handling for escaping interior XML tags which normally wouldn't parse correctly. 4. Helpers to ensure the tag names from models are consistent and automatic. However, everything pydantic-xml (and by extention normal pydantic) models support is also supported in Rigging. -!!! tip "Background Knowledge" + +**Background Knowledge** - If you happen to be new to modern python like type hinting and pydantic models, we would highly - recommend you spend some time in their documentation to get more familiar. Without this background, - many of the Rigging features will seem confusing. +If you happen to be new to modern python like type hinting and pydantic models, we would highly +recommend you spend some time in their documentation to get more familiar. Without this background, +many of the Rigging features will seem confusing. - - [Python Typing](https://docs.python.org/3/library/typing.html) - - [Pydantic](https://docs.pydantic.dev/) - - [Pydantic XML](https://pydantic-xml.readthedocs.io/) +- [Python Typing](https://docs.python.org/3/library/typing.html) +- [Pydantic](https://docs.pydantic.dev/) +- [Pydantic XML](https://pydantic-xml.readthedocs.io/) + ## Primitive Models -Often, you just want to indicate to the LLM that it should place a block of text between -tags so you can extract just that portion of the message content. +Often, you just want to indicate to the LLM that it should place a block of text between tags so you can extract just that portion of the message content. -```py +```python import rigging as rg class FunFact(rg.Model): fact: str ``` -Pydantic XML refers to these as "primitive" because they have a single field. These models -leverage the minimal functionality of XML parsing and just take the contents between the start -and end tags. +Pydantic XML refers to these as "primitive" because they have a single field. These models leverage the minimal functionality of XML parsing and just take the contents between the start and end tags. -??? note "Parsing for Primitive Models" + +**Parsing for Primitive Models** - We have a pending TODO to replace the internals of Pydantic XML parsing to make it - more flexible for the kinds of "broken" XML that LLMs frequently produce. - Primitive models have special handling in Rigging to make them more flexible - for parsing this "broken" XML. If you are having issues with complex parsing, - using primitive models is a good escape hatch for now. +We have a pending TODO to replace the internals of Pydantic XML parsing to make it more flexible for the kinds of "broken" XML that LLMs frequently produce. Primitive models have special handling in Rigging to make them more flexible for parsing this "broken" XML. If you are having issues with complex parsing, using primitive models is a good escape hatch for now. + -```py +```python import rigging as rg class FunFact(rg.Model): @@ -63,27 +64,20 @@ FunFact(fact="Rigging is pretty easy to use").to_pretty_xml() # 'Rigging is pretty easy to use' ``` -Notice that the name of our interior field (`.fact`) isn't relevant to the XML structure. We only -use this to access the contents of that model field in code. However the name of our model (`FunFact`) -**is relevant** to the XML representation. What you name your models is important to how the underlying -LLM interprets it's meaning. You should be thoughtful and descriptive about your model names as they -will have effects on how well the LLM understands the intention, and how likely it is to output one -model over another (based on it's token probability distribution). +Notice that the name of our interior field (`.fact`) isn't relevant to the XML structure. We only use this to access the contents of that model field in code. However the name of our model (`FunFact`) **is relevant** to the XML representation. What you name your models is important to how the underlying LLM interprets it's meaning. You should be thoughtful and descriptive about your model names as they will have effects on how well the LLM understands the intention, and how likely it is to output one model over another (based on it's token probability distribution). If you want to seperate the model tag from it's class name, you specify it in the class construction: -```py +```python class FunFact(rg.Model, tag="a-super-fun-fact") ... ``` ### Dynamic Primitive Models -If you'd like to define a primitive model functionally, we support an experimental -[make_primitive()][rigging.model.make_primitive] utility. The interior field will always -be named `.content` and typed depending on the argument passed. +If you'd like to define a primitive model functionally, we support an experimental `make_primitive()` utility. The interior field will always be named `.content` and typed depending on the argument passed. -```py +```python import rigging as rg FunFact = rg.model.make_primitive("FunFact", str) # (1)! @@ -93,15 +87,13 @@ FunFact(content="Rigging is pretty easy to use").to_pretty_xml() # Rigging is pretty easy to use ``` -1. You can apply most basic types here like `bool`, `int`, and `float`. +*1. You can apply most basic types here like `bool`, `int`, and `float`.* ## Typing and Validation -Even if models are primitive, we can still use type hinting and pydantic validation to ensure -that the content between tags conforms to any constraints we need. Take this example from a default -Rigging model for instance: +Even if models are primitive, we can still use type hinting and pydantic validation to ensure that the content between tags conforms to any constraints we need. Take this example from a default Rigging model for instance: -```py +```python from pydantic import field_validator import typing as t @@ -121,10 +113,7 @@ class YesNoAnswer(Model): return v ``` -You can see the interior field of the model is now a `bool` type, which means pydantic will accept standard -values which could be reasonably interpreted as a boolean. We also add a custom field validator to -check for instances of `yes/no` as text strings. All of these XML values will parse correctly -into the `YesNoAnswer` model, so you can handle cases where the LLM outputs a variety of different +You can see the interior field of the model is now a `bool` type, which means pydantic will accept standard values which could be reasonably interpreted as a boolean. We also add a custom field validator to check for instances of `yes/no` as text strings. All of these XML values will parse correctly into the `YesNoAnswer` model, so you can handle cases where the LLM outputs a variety of different ```xml true @@ -134,24 +123,20 @@ into the `YesNoAnswer` model, so you can handle cases where the LLM outputs a va 1 ``` -```py +```python YesNoAnswer(boolean="true") YesNoAnswer(boolean=" NO ") YesNoAnswer(boolean="1") # ... ``` -The choice to build on Pydantic offers an incredible amount of flexibility for controlling exactly -how data is validated in your models. This kind of parsing work is exactly what these libraries were designed -to do. The sky is the limit, and **everything you find in Pydantic and Pydantic XML are compatible -with Rigging.** +The choice to build on Pydantic offers an incredible amount of flexibility for controlling exactly how data is validated in your models. This kind of parsing work is exactly what these libraries were designed to do. The sky is the limit, and **everything you find in Pydantic and Pydantic XML are compatible with Rigging.** ## Handling Multiple Fields -Unlike vanilla Pydantic, our use of Pydantic XML forces us to think about exactly how models with multiple fields -will be represented in XML syntax. Take this as an example: +Unlike vanilla Pydantic, our use of Pydantic XML forces us to think about exactly how models with multiple fields will be represented in XML syntax. Take this as an example: -```py +```python class Person(rg.Model): name: str age: int @@ -174,45 +159,34 @@ In XML this could be any of the following: ... ``` -*You get the idea.* Pydantic XML handles this all very well and offers different ways of defining -your fields to specific whether they should be **attributes** or child **elements**. You can read -more about this in [their documentation.](https://pydantic-xml.readthedocs.io/en/latest/pages/quickstart.html#primitives) +*You get the idea.* Pydantic XML handles this all very well and offers different ways of defining your fields to specific whether they should be **attributes** or child **elements**. You can read more about this in [their documentation.](https://pydantic-xml.readthedocs.io/en/latest/pages/quickstart.html#primitives) -The basic rule is this: **If your model has more than one field, you need to define every field as -either an attribute or an element** +The basic rule is this: **If your model has more than one field, you need to define every field as either an attribute or an element** -How exactly you structure your models and their associated representations is completely up to you. -Our general guide is that LLMs tend to work better with elements over attributes. +How exactly you structure your models and their associated representations is completely up to you. Our general guide is that LLMs tend to work better with elements over attributes. + +```python Model Definition +class Person(rg.Model): + name: str = rg.element() + age: int = rg.element() +``` -=== "Model Definition" - - ```py - class Person(rg.Model): - name: str = rg.element() - age: int = rg.element() - ``` - -=== "XML Format" - - ```xml - - - - - ``` +```xml XML format + + + + +``` + ## XML Examples -For primitive models, using the default [`.xml_tags()`][rigging.model.Model.xml_tags] or [`.xml_example()`][rigging.model.Model.xml_example] -works well for communicating to the model how it should respond, however for more complex models it's **highly recommended** to overload -the [`.xml_example()`][rigging.model.Model.xml_example] method to provide a more detailed example of the XML structure you expect. - -The easiest way to approach this overload is to instantiate your model class with some standard values -and use [`.to_prety_xml()`][rigging.model.Model.to_pretty_xml] +For primitive models, using the default `.xml_tags()` or `.xml_example()` works well for communicating to the model how it should respond, however for more complex models it's **highly recommended** to overload the `.xml_example()` method to provide a more detailed example of the XML structure you expect. +The easiest way to approach this overload is to instantiate your model class with some standard values and use `.to_prety_xml()` -```py +```python import rigging as rg from typing import Annotated from pydantic import PlainSerializer @@ -239,15 +213,13 @@ print(f"Use the following format:\n{SaveMemory.xml_example()}") # ``` -1. Using pydantic serializer annotations is an easy way to introduce subtle content changes - before they are formed into their XML form. Here we're injecting newlines to make the - XML more readable. +*1. Using pydantic serializer annotations is an easy way to introduce subtle content changes before they are formed into their XML form. Here we're injecting newlines to make the XML more readable.* ## Complex Models Let's design a model which will hold required information for making a web request. We'll begin with an outline of our model: -```py +```python class Header(rg.Model): name: str value: str @@ -264,10 +236,9 @@ class Request(rg.Model): body: str ``` -We'll start with a few standard string constraints to strip extra white-space (which LLMs tend to include) -and automatically convert our method to upper case. +We'll start with a few standard string constraints to strip extra white-space (which LLMs tend to include) and automatically convert our method to upper case. -```py +```python from pydantic import StringConstraints str_strip = t.Annotated[str, StringConstraints(strip_whitespace=True)] @@ -291,7 +262,7 @@ class Request(rg.Model): Next we'll assign our fields to attributes and elements. -```py +```python from pydantic import StringConstraints str_strip = t.Annotated[str, StringConstraints(strip_whitespace=True)] @@ -313,10 +284,9 @@ class Request(rg.Model): body: str_strip = rg.element() ``` -In terms of handling our headers and URL parameters, we want these to be a list of child -elements which are wrapped in a parent tag. We also want these and our body to be optional. +In terms of handling our headers and URL parameters, we want these to be a list of child elements which are wrapped in a parent tag. We also want these and our body to be optional. -```py +```python from pydantic import StringConstraints str_strip = t.Annotated[str, StringConstraints(strip_whitespace=True)] @@ -340,32 +310,30 @@ class Request(rg.Model): Let's check our final work: -=== "Model in Code" - - ```py - Request( - method="POST", - path="/api/v1/search", - headers=[ - Header(name="Authorization", value="Bearer sk-1234") - ], - url_params=[ - Parameter(name="max", value="100") - ], - body="search=rigging" - ) - ``` - -=== "Model as XML" - - ```xml - - -
Bearer sk-1234
-
- - 100 - - search=rigging -
- ``` \ No newline at end of file + +```python Model in Code +Request( + method="POST", + path="/api/v1/search", + headers=[ + Header(name="Authorization", value="Bearer sk-1234") + ], + url_params=[ + Parameter(name="max", value="100") + ], + body="search=rigging" +) +``` + +```xml Model as XML + + +
Bearer sk-1234
+
+ + 100 + + search=rigging +
+``` +
\ No newline at end of file diff --git a/docs/topics/generators.md b/docs/topics/generators.md deleted file mode 100644 index bbcc25c..0000000 --- a/docs/topics/generators.md +++ /dev/null @@ -1,419 +0,0 @@ -# Generators - -Underlying LLMs (or any function which completes text) is represented as a generator in Rigging. -They are typically instantiated using identifier strings and the -[`get_generator`][rigging.generator.get_generator] function. - -The base interface is flexible, and designed to support optimizations should the -underlying mechanisms support it (batching async, K/V cache, etc.) - -## Identifiers - -Much like database connection strings, Rigging generators can be represented as -strings which define what provider, model, API key, generation params, etc. -should be used. They are formatted as follows: - -``` -!,<**kwargs> -``` - -- `provider` maps to a particular subclass of [`Generator`][rigging.generator.Generator]. -- `model` is a any `str` value, typically used by the provider to indicate a specific LLM to target. -- `kwargs` are used to carry: - 1. Serialized [`GenerateParams`][rigging.generator.GenerateParams] fields like like temp, stop tokens, etc. - 2. Additional provider-specific attributes to set on the constructed generator class. For instance, you - can set the [`LiteLLMGenerator.max_connections`][rigging.generator.litellm_.LiteLLMGenerator] property - by passing `,max_connections=` in the identifier string. - -The provider is optional and Rigging will fallback to -[`litellm`](https://github.com/BerriAI/litellm)/[`LiteLLMGenerator`][rigging.generator.LiteLLMGenerator] -by default. You can view the [LiteLLM docs](https://docs.litellm.ai/docs/) for more -information about supported model providers and parameters. - -Here are some examples of valid identifiers: - -``` -gpt-3.5-turbo,temperature=0.5 -openai/gpt-4,api_key=sk-1234 -litellm!claude-3-sonnet-2024022 -anthropic/claude-2.1,stop=output:;---,seed=1337 -together_ai/meta-llama/Llama-3-70b-chat-hf -openai/google/gemma-7b,api_base=https://integrate.api.nvidia.com/v1 -``` - -Building generators from string identifiers is optional, but a convenient way to represent complex LLM configurations. - -!!! tip "Back to Strings" - - Any generator can be converted back into an identifier using either [`to_identifier`][rigging.generator.Generator.to_identifier] - or [`get_identifier`][rigging.generator.get_identifier]. - - ```py - generator = rg.get_generator("gpt-3.5-turbo,temperature=0.5") - print(generator.to_identifier()) - # litellm!gpt-3.5-turbo,temperature=0.5 - ``` - -## API Keys - -All generators carry a [`.api_key`][rigging.generator.Generator.api_key] attribute which can be set directly, or by -passing `,api_key=` as part of an identifier string. Not all generators will require one, but they are common enough -that we include the attribute as part of the base class. - -Typically you will be using a library like LiteLLM underneath, and can simply use environment variables: - -```bash -export OPENAI_API_KEY=... -export TOGETHER_API_KEY=... -export TOGETHERAI_API_KEY=... -export MISTRAL_API_KEY=... -export ANTHROPIC_API_KEY=... -``` - -## Rate Limits - -Generators that leverage remote services (LiteLLM) expose properties for managing connection/request limits: - -- [`LiteLLMGenerator.max_connections`][rigging.generator.litellm_.LiteLLMGenerator] -- [`LiteLLMGenerator.min_delay_between_requests`][rigging.generator.litellm_.LiteLLMGenerator] - -However, a more flexible solution is [`ChatPipeline.wrap()`][rigging.chat.ChatPipeline.wrap] -with a library like [**backoff**](https://github.com/litl/backoff) to catch -many, or specific errors, like rate limits or general connection issues. - -```py -import rigging as rg - -import backoff -import backoff.types - - -def on_backoff(details: backoff.types.Details) -> None: - print(f"Backing off {details['wait']:.2f}s") - -pipeline = ( - rg.get_generator("claude-3-haiku-20240307") - .chat("Give me a 4 word phrase about machines.") - .wrap( - backoff.on_exception( - backoff.expo, - Exception, # This should be scoped down - on_backoff=on_backoff, - ) - ) -) - -chats = await pipeline.run_many(50) -``` - -!!! note "Exception mess" - - You'll find that the exception consistency inside LiteLLM is quite poor. - Different providers throw different types of exceptions for all kinds of - status codes, response data, etc. With that said, you can typically find - a target list that works well for your use-case. - -## Local Models - -We have experimental support for both [`vLLM`](https://docs.vllm.ai/en/latest/) -and [`transformers`](https://huggingface.co/docs/transformers/index) generators for -loading and running local models. In general vLLM is more consistent with Rigging's -preferred API, but the dependency requirements are heavier. - -Where needed, you can wrap an existing model into a rigging generator by using the -[`VLLMGenerator.from_obj()`][rigging.generator.vllm_.VLLMGenerator.from_obj] or -[`TransformersGenerator.from_obj()`][rigging.generator.transformers_.TransformersGenerator.from_obj] methods. -These are helpful for any picky model construction that might not play well with our rigging constructors. - -!!! note "Required Packages" - - The use of these generators requires the `vllm` and `transformers` packages to be installed. - You can use `rigging[all]` to install them all at once, or pick your preferred package individually. - -```py -import rigging as rg - -tiny_llama = rg.get_generator( - "vllm!TinyLlama/TinyLlama-1.1B-Chat-v1.0," \ - "gpu_memory_utilization=0.3," \ - "trust_remote_code=True" -) - -llama_3 = rg.get_generator( - "transformers!meta-llama/Meta-Llama-3-8B-Instruct" -) -``` - -See more about them below: - -- [`vLLMGenerator`][rigging.generator.vllm_.VLLMGenerator] -- [`TransformersGenerator`][rigging.generator.transformers_.TransformersGenerator] - -!!! tip "Loading and Unloading" - - You can use the [`Generator.load`][rigging.generator.Generator.load] and - [`Generator.unload`][rigging.generator.Generator.unload] methods to better - control memory usage. Local providers typically are lazy and load the model - into memory only when first needed. - -## Overload Generation Params - -When working with both [`CompletionPipeline`][rigging.completion.CompletionPipeline] and -[`ChatPipeline`][rigging.chat.ChatPipeline], you can overload and update any generation -params by using the associated [`.with_()`][rigging.chat.ChatPipeline.with_] function. - -=== "with_() as keyword arguments" - - ```py - import rigging as rg - - pipeline = rg.get_generator("gpt-3.5-turbo,max_tokens=50").chat([ - {"role": "user", "content": "Say a haiku about boats"}, - ]) - - for temp in [0.1, 0.5, 1.0]: - chat = await pipeline.with_(temperature=temp).run() - print(chat.last.content) - ``` - -=== "with_() as `GenerateParams`" - - ```py - import rigging as rg - - pipeline = rg.get_generator("gpt-3.5-turbo,max_tokens=50").chat([ - {"role": "user", "content": "Say a haiku about boats"}, - ]) - - for temp in [0.1, 0.5, 1.0]: - chat = await pipeline.with_(rg.GenerateParams(temperature=temp)).run() - print(chat.last.content) - ``` - -## HTTP Generator - -The [`HTTPGenerator`][rigging.generator.http.HTTPGenerator] allows you to wrap any HTTP endpoint as a generator, -making it easy to integrate external LLMs or AI services into your Rigging pipelines. It works by -defining a specification that maps message content into HTTP requests and parses responses back into -messages. - -The specification is assigned to the [`.spec`][rigging.generator.http.HTTPGenerator.spec] field on the generator, -and can be applied as a Python dictionary, JSON string, YAML string, or base64 encoded JSON/YAML string. - -This flexibility allows you to easily share and reuse specifications across different parts of your application. - -```python -import rigging as rg - -spec = r""" -request: - url: "https://{{ model }}.crucible.dreadnode.io/submit" - headers: - "X-Api-Key": "{{ api_key }}" - "Content-Type": "application/json" - transforms: - - type: "json" - pattern: { - "data": "$content" - } -response: - transforms: - - type: "jsonpath" - pattern: $.flag,output,message -""" - -crucible = rg.get_generator("http!spanglish,api_key=") # (1) -crucible.spec = spec - -chat = await crucible.chat("A flag please").run() - -print(chat.conversation) -# [user]: A flag please -# -# [assistant]: Una bandera, por favor. -``` - -1. Were are using the `.model` field on the generator to carry our crucible challenge - -!!! tip "Saving schemas" - - Encoded YAML is the default storage when an HTTP generator is serialized to an indentifier using - [`to_identifier`][rigging.generator.Generator.to_identifier]. This also means that when we save - our chats to storage, they maintain their http specification. - - ```py - print(crucible.to_identifier()) - # http!spanglish,spec=eyJyZXF1ZXN0Ijp7InVyb... - ``` - -### Specification - -The [specification (`HTTPSpec`)][rigging.generator.http.HTTPSpec] controls how messages are transformed around HTTP interactions. It supports: - -- Template-based URLs -- Template-based header generation -- Configurable timeouts and HTTP methods -- Status code validation -- Flexible body transformations for both the request and response - -When building requests, the following [context variables (`RequestTransformContext`)][rigging.generator.http.RequestTransformContext] -are available in your transform patterns: - -- `role` - Role of the last message (user/assistant/system) -- `content` - Content of the last message -- `all_content` - Concatenated content of all messages -- `messages` - List of all message objects -- `params` - Generation parameters (temperature, max_tokens, etc.) -- `api_key` - API key from the generator -- `model` - Model identifier from the generator - -For both request and response transform chains, the previous result of each transform is -provided to the next transform via any of `data`, `output`, `result`, or `body`. - -### Transforms - -The HTTP generator supports different types of transforms for both request building and response parsing. -Each serves a specific purpose and has its own pattern syntax. - -!!! tip "Transform Chaining" - - Transforms are applied in sequence, with each transform's output becoming the input for the next. - This allows you to build complex processing pipelines: - - ```yaml - transforms: - - type: "jsonpath" - pattern: "$.data" # Extract data object - - type: "jinja" - pattern: "{{ result | tojson }}" # Convert to string - - type: "regex" - pattern: "message: (.*)" # Extract specific field - ``` - -**Jinja (request + response)** - -The `jinja` transform type provides full Jinja2 template syntax. Access context variables directly -and use Jinja2 filters and control structures. - -```yaml -transforms: - - type: "jinja" - pattern: > - { - "content": "{{ all_content }}", - "timestamp": "{{ now() }}", - {% if params.temperature > 0.5 %} - "mode": "creative" - {% endif %} - } -``` - -**JSON (request only)** - -The `json` transform type lets you build JSON request bodies using a template object. Use `$` prefix -to reference context variables, with dot notation for nested access: - -```yaml -transforms: - - type: "json" - pattern: { - "messages": "$messages", - "temperature": "$params.temperature", - "content": "$content", - "static_field": "hello" - } -``` - -**JSONPath (response only)** - -The `jsonpath` transform type uses [JSONPath](https://github.com/h2non/jsonpath-ng) expressions to extract data from JSON responses: - -```yaml -transforms: - - type: "jsonpath" - pattern: "$.choices[0].message.content" -``` - -**Regex (response only)** - -The `regex` transform type uses regular expressions to extract content from text responses: - -```yaml -transforms: - - type: "regex" - pattern: "(.*?)" -``` - -## Writing a Generator - -All generators should inherit from the [`Generator`][rigging.generator.Generator] base class, and -can elect to implement handlers for messages and/or texts: - -- [`async def generate_messages(...)`][rigging.generator.Generator.generate_messages] - Used for [`ChatPipeline.run`][rigging.chat.ChatPipeline.run] variants. -- [`async def generate_texts(...)`][rigging.generator.Generator.generate_texts] - Used for [`CompletionPipeline.run`][rigging.completion.CompletionPipeline.run] variants. - -!!! note "Optional Implementation" - - If your generator doesn't implement a particular method like text completions, Rigging - will simply raise a `NotImplementedError` for you. It's currently undecided whether generators - should prefer to provide weak overloads for compatibility, or whether they should ignore methods - which can't be used optimally to help provide clarity to the user about capability. You'll find - we've opted for the former strategy in our generators. - -Generators operate in a batch context by default, taking in groups of message lists or texts. Whether -your implementation takes advantage of this batching is up to you, but where possible you -should be optimizing as much as possible. - -!!! tip "Generators are Flexible" - - Generators don't make any assumptions about the underlying mechanism that completes text. - You might use a local model, API endpoint, or static code, etc. The base class is designed - to be flexible and support a wide variety of use cases. You'll obviously find that the inclusion - of `api_key`, `model`, and generation params are common enough that they are included in the base class. - -```py -from rigging import Generator, GenerateParams, Message, GeneratedMessage - -class Custom(Generator): - # model: str - # api_key: str - # params: GeneratorParams - - custom_field: bool - - async def generate_messages( - self, - messages: t.Sequence[t.Sequence[Message]], - params: t.Sequence[GenerateParams], - ) -> t.Sequence[GeneratedMessage]: - # merge_with is an easy way to combine overloads - params = [ - self.params.merge_with(p).to_dict() for p in params - ] - - # Access self vars where needed - api_key = self.api_key - model_id = self.model - custom = self.custom_field - - # Build output messages with stop reason, usage, etc. - # output_messages = ... - - return output_messages - - -generator = Custom(model='foo', custom_field=True) -generator.chat(...) -``` - -!!! tip "Registering Generators" - - Use the [`register_generator`][rigging.generator.register_generator] method to add your generator - class under a custom provider id so it can be used with [`get_generator`][rigging.generator.get_generator]. - - ```py - import rigging as rg - - rg.register_generator('custom', Custom) - custom = rg.get_generator('custom!foo') - ``` \ No newline at end of file diff --git a/docs/topics/generators.mdx b/docs/topics/generators.mdx new file mode 100644 index 0000000..adb4fee --- /dev/null +++ b/docs/topics/generators.mdx @@ -0,0 +1,387 @@ +--- +title: "Generators" +description: "The core of generating messages and text." +public: true +--- + +Underlying LLMs (or any function which completes text) is represented as a generator in Rigging. They are typically instantiated using identifier strings and the `get_generator` function. The base interface is flexible, and designed to support optimizations should the underlying mechanisms support it (batching async, K/V cache, etc.) + +## Identifiers + +Much like database connection strings, Rigging generators can be represented as strings which define what provider, model, API key, generation params, etc should be used. They are formatted as follows: + +``` +!,<**kwargs> +``` + +- `provider` maps to a particular subclass of `Generator`. +- `model` is a any `str` value, typically used by the provider to indicate a specific LLM to target. +- `kwargs` are used to carry: + 1. API key (`,api_key=...`) or the base URL (`,api_base=...`) for the model provider. + 1. Serialized `GenerateParams`fields like like temp, stop tokens, etc. + 1. Additional provider-specific attributes to set on the constructed generator class. For instance, you + can set the `LiteLLMGenerator.max_connections`property by passing `,max_connections=` in the identifier string. + +The provider is optional and Rigging will fallback to [`litellm`](https://github.com/BerriAI/litellm)/`LiteLLMGenerator` by default. +You can view the [LiteLLM docs](https://docs.litellm.ai/docs/) for more information about supported model providers and parameters. + +Here are some examples of valid identifiers: + +```text +gpt-3.5-turbo,temperature=0.5 +openai/gpt-4,api_key=sk-1234 +litellm!claude-3-sonnet-2024022 +anthropic/claude-2.1,stop=output:;---,seed=1337 +together_ai/meta-llama/Llama-3-70b-chat-hf +openai/google/gemma-7b,api_base=https://integrate.api.nvidia.com/v1 +``` + +Building generators from string identifiers is optional, but a convenient way to represent complex LLM configurations. + + +**Back to Strings** + +Any generator can be converted back into an identifier using either `to_identifier` or `get_identifier`. + +```python +generator = rg.get_generator("gpt-3.5-turbo,temperature=0.5") +print(generator.to_identifier()) +# litellm!gpt-3.5-turbo,temperature=0.5 +``` + + +## API Keys + +All generators carry a `.api_key` attribute which can be set directly, or by passing `,api_key=` as part of an identifier string. Not all generators will require one, but they are common enough that we include the attribute as part of the base class. + +Typically you will be using a library like LiteLLM underneath, and can simply use environment variables: + +```bash +export OPENAI_API_KEY=... +export TOGETHER_API_KEY=... +export TOGETHERAI_API_KEY=... +export MISTRAL_API_KEY=... +export ANTHROPIC_API_KEY=... +``` + +## Rate Limits + +Generators that leverage remote services (LiteLLM) expose properties for managing connection/request limits: + +- `LiteLLMGenerator.max_connections` +- `LiteLLMGenerator.min_delay_between_requests` + +However, a more flexible solution is `ChatPipeline.wrap()` with a library like [**backoff**](https://github.com/litl/backoff) to catch many, or specific errors, like rate limits or general connection issues. + +```python +import rigging as rg + +import backoff +import backoff.types + +def on_backoff(details: backoff.types.Details) -> None: + print(f"Backing off {details['wait']:.2f}s") + +pipeline = ( + rg.get_generator("claude-3-haiku-20240307") + .chat("Give me a 4 word phrase about machines.") + .wrap( + backoff.on_exception( + backoff.expo, + Exception, # This should be scoped down + on_backoff=on_backoff, + ) + ) +) + +chats = await pipeline.run_many(50) +``` + + +You'll find that the exception consistency inside LiteLLM can be quite poor. Different providers throw different types of exceptions for all kinds of status codes, response data, etc. With that said, you can typically find a target list that works well for your use-case. + + +## Local Models + +We have experimental support for both [`vLLM`](https://docs.vllm.ai/en/latest/) and [`transformers`](https://huggingface.co/docs/transformers/index) generators for loading and running local models. In general vLLM is more consistent with Rigging's preferred API, but the dependency requirements are heavier. + +Where needed, you can wrap an existing model into a rigging generator by using the `VLLMGenerator.from_obj()` or `TransformersGenerator.from_obj()` methods. These are helpful for any picky model construction that might not play well with our rigging constructors. + + +The use of these local generators requires the `vllm` and `transformers` packages to be installed. You can use `rigging[all]` to install them all at once, or pick your preferred package individually. + + +```python +import rigging as rg + +tiny_llama = rg.get_generator( + "vllm!TinyLlama/TinyLlama-1.1B-Chat-v1.0," \ + "gpu_memory_utilization=0.3," \ + "trust_remote_code=True" +) + +llama_3 = rg.get_generator( + "transformers!meta-llama/Meta-Llama-3-8B-Instruct" +) +``` + +See more about them below: + +- `vLLMGenerator` +- `TransformersGenerator` + + +**Loading and Unloading** + +You can use the `Generator.load` and `Generator.unload` methods to better control memory usage. Local providers typically are lazy and load the model into memory only when first needed. + + +## Overload Generation Params + +When working with both `CompletionPipeline` and `ChatPipeline`, you can overload and update any generation params by using the associated `.with_()` function. + + +```python with_() as keyword arguments +import rigging as rg + +pipeline = rg.get_generator("gpt-3.5-turbo,max_tokens=50").chat([ + {"role": "user", "content": "Say a haiku about boats"}, +]) + +for temp in [0.1, 0.5, 1.0]: + chat = await pipeline.with_(temperature=temp).run() + print(chat.last.content) +``` + +```python with_() as GenerateParams +import rigging as rg + +pipeline = rg.get_generator("gpt-3.5-turbo,max_tokens=50").chat([ + {"role": "user", "content": "Say a haiku about boats"}, +]) + +for temp in [0.1, 0.5, 1.0]: + chat = await pipeline.with_(rg.GenerateParams(temperature=temp)).run() + print(chat.last.content) +``` + + +## HTTP Generator + +The `HTTPGenerator` allows you to wrap any HTTP endpoint as a generator, making it easy to integrate external LLMs or AI services into your Rigging pipelines. It works by defining a specification that maps message content into HTTP requests and parses responses back into messages. + +The specification is assigned to the `.spec` field on the generator, and can be applied as a Python dictionary, JSON string, YAML string, or base64 encoded JSON/YAML string. + +This flexibility allows you to easily share and reuse specifications across different parts of your application. + +```python +import rigging as rg + +spec = r""" +request: + url: "https://{{ model }}.platform.dreadnode.io/submit" + headers: + "X-Api-Key": "{{ api_key }}" + "Content-Type": "application/json" + transforms: + - type: "json" + pattern: { + "data": "$content" + } +response: + transforms: + - type: "jsonpath" + pattern: $.flag,output,message +""" + +crucible = rg.get_generator("http!spanglish,api_key=") # (1) +crucible.spec = spec + +chat = await crucible.chat("A flag please").run() + +print(chat.conversation) +# [user]: A flag please +# +# [assistant]: Una bandera, por favor. +``` + +*1. Were are using the `.model` field on the generator to carry our crucible challenge* + + +**Saving schemas** + +Encoded YAML is the default storage when an HTTP generator is serialized to an indentifier using +`to_identifier`. This also means that when we save +our chats to storage, they maintain their http specification. + +```py +print(crucible.to_identifier()) +# http!spanglish,spec=eyJyZXF1ZXN0Ijp7InVyb... +``` + + +### Specification + +The specification (`HTTPSpec`) controls how messages are transformed around HTTP interactions. It supports: + +- Template-based URLs +- Template-based header generation +- Configurable timeouts and HTTP methods +- Status code validation +- Flexible body transformations for both the request and response + +When building requests, the following context variables (`RequestTransformContext`) +are available in your transform patterns: + +- `role` - Role of the last message (user/assistant/system) +- `content` - Content of the last message +- `all_content` - Concatenated content of all messages +- `messages` - List of all message objects +- `params` - Generation parameters (temperature, max_tokens, etc.) +- `api_key` - API key from the generator +- `model` - Model identifier from the generator + +For both request and response transform chains, the previous result of each transform is +provided to the next transform via any of `data`, `output`, `result`, or `body`. + +### Transforms + +The HTTP generator supports different types of transforms for both request building and response parsing. +Each serves a specific purpose and has its own pattern syntax. + + +**Transform Chaining** + +Transforms are applied in sequence, with each transform's output becoming the input for the next. +This allows you to build complex processing pipelines: + +```yaml +transforms: + - type: "jsonpath" + pattern: "$.data" # Extract data object + - type: "jinja" + pattern: "{{ result | tojson }}" # Convert to string + - type: "regex" + pattern: "message: (.*)" # Extract specific field +``` + + +**Jinja (request + response)** + +The `jinja` transform type provides full Jinja2 template syntax. Access context variables directly +and use Jinja2 filters and control structures. + +```yaml +transforms: + - type: "jinja" + pattern: > + { + "content": "{{ all_content }}", + "timestamp": "{{ now() }}", + {% if params.temperature > 0.5 %} + "mode": "creative" + {% endif %} + } +``` + +**JSON (request only)** + +The `json` transform type lets you build JSON request bodies using a template object. Use `$` prefix +to reference context variables, with dot notation for nested access: + +```yaml +transforms: + - type: "json" + pattern: { + "messages": "$messages", + "temperature": "$params.temperature", + "content": "$content", + "static_field": "hello" + } +``` + +**JSONPath (response only)** + +The `jsonpath` transform type uses [JSONPath](https://github.com/h2non/jsonpath-ng) expressions to extract data from JSON responses: + +```yaml +transforms: + - type: "jsonpath" + pattern: "$.choices[0].message.content" +``` + +**Regex (response only)** + +The `regex` transform type uses regular expressions to extract content from text responses: + +```yaml +transforms: + - type: "regex" + pattern: "(.*?)" +``` + +## Writing a Generator + +All generators should inherit from the `Generator` base class, and can elect to implement handlers for messages and/or texts: + +- `async def generate_messages(...)` - Used for `ChatPipeline.run` variants. +- `async def generate_texts(...)` - Used for `CompletionPipeline.run` variants. + + +If your generator doesn't implement a particular method like text completions, Rigging will simply raise a `NotImplementedError` for you. It's currently undecided whether generators should prefer to provide weak overloads for compatibility, or whether they should ignore methods which can't be used optimally to help provide clarity to the user about capability. You'll find we've opted for the former strategy in our generators. + + +Generators operate in a batch context by default, taking in groups of message lists or texts. Whether +your implementation takes advantage of this batching is up to you, but where possible you +should be optimizing as much as possible. + + +Generators don't make any assumptions about the underlying mechanism that completes text. You might use a local model, API endpoint, or static code, etc. The base class is designed to be flexible and support a wide variety of use cases. You'll find the inclusion of `api_key`, `model`, and generation params are common enough that they are included in the base class. + + +```python +from rigging import Generator, GenerateParams, Message, GeneratedMessage + +class Custom(Generator): + # model: str + # api_key: str + # params: GeneratorParams + + custom_field: bool + + async def generate_messages( + self, + messages: t.Sequence[t.Sequence[Message]], + params: t.Sequence[GenerateParams], + ) -> t.Sequence[GeneratedMessage]: + # merge_with is an easy way to combine overloads + params = [ + self.params.merge_with(p).to_dict() for p in params + ] + + # Access self vars where needed + api_key = self.api_key + model_id = self.model + custom = self.custom_field + + # Build output messages with stop reason, usage, etc. + # output_messages = ... + + return output_messages + + +generator = Custom(model='foo', custom_field=True) +generator.chat(...) +``` + + +Use the `register_generator` method to add your generator class under a custom providerid so it can be used with `get_generator`. + +```python +import rigging as rg + +rg.register_generator('custom', Custom) + +custom = rg.get_generator('custom!foo,custom_field=True') +``` + \ No newline at end of file diff --git a/docs/topics/iterating-and-batching.md b/docs/topics/iterating-and-batching.md deleted file mode 100644 index 983e525..0000000 --- a/docs/topics/iterating-and-batching.md +++ /dev/null @@ -1,258 +0,0 @@ -# Iterating and Batching - -Rigging has good support for iterating over messages, params, and generators, as well as -large batching of requests. How efficiently these mechanisms operates is dependent on the -underlying generator that's being used, but Rigging has been developed with scale in mind. - -## Multiple Generations - -The `run_many` functions let you scale out generation N times with the same inputs: - -- [`ChatPipeline.run_many()`][rigging.chat.ChatPipeline.run_many] -- [`CompletionPipeline.run_many()`][rigging.completion.CompletionPipeline.run_many] -- [`Prompt.run_many()`][rigging.prompt.Prompt.run_many] - -=== "Run Many Code" - - ```py - import rigging as rg - - async def check_animal(chats: list[rg.Chat]) -> list[rg.Chat]: - return [ - await chat.continue_(f"Why did you pick that animal?").meta(questioned=True).run() - if any(a in chat.last.content.lower() for a in ["cat", "dog", "cow", "mouse"]) - else chat - for chat in chats - ] - - chats = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat("Tell me a joke about an animal.") - .map(check_animal) - .run_many(3) - ) - - for i, chat in enumerate(chats): - questioned = chat.metadata.get("questioned", False) - print(f"--- Chat {i+1} (?: {questioned}) ---") - print(chat.conversation) - print() - ``` - -=== "Output" - - ``` - --- Chat 1 (?: False) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the spider go to the computer? - - To check his website! - - --- Chat 2 (?: False) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the chicken join a band? Because it had the drumsticks! - - --- Chat 3 (?: True) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why don't elephants use computers? - - Because they're afraid of the mouse! - - [user]: Why did you pick that animal? - - [assistant]: I chose an elephant because they are known for their intelligence and gentle nature, making them a popular subject for jokes and humorous anecdotes. Plus, imagining an elephant trying to use a computer and being scared of a tiny mouse is a funny visual image! - ``` - -## Batching Inputs - -The `run_batch` functions let you batch accross a set of inputs: - -- [`ChatPipeline.run_batch()`][rigging.chat.ChatPipeline.run_batch] -- [`CompletionPipeline.run_batch()`][rigging.completion.CompletionPipeline.run_batch] - -As processing proceeds with things like [`.then`][rigging.chat.ChatPipeline.then] or -[`.map`][rigging.chat.ChatPipeline.map], that chats will resolve individually and -collapse into the final results. - -=== "Batching Inputs Code" - - ```py - import rigging as rg - from rigging.model import CommaDelimitedAnswer - - pipeline = ( - rg.get_generator('gpt-4-turbo') - .chat({ - "role": "system", - "content": f"Always respond with {CommaDelimitedAnswer.xml_tags()} tags."} - ) - .until_parsed_as(CommaDelimitedAnswer, attempt_recovery=True) - ) - - many = [f"Give me 3 famous {thing}" for thing in ["authors", "painters", "musicians", "hackers"]] - - chats = await pipeline.run_batch(many, on_failed='skip') - - for i, chat in enumerate(chats): - print(f"--- Chat {i+1} ({len(chat)}) ---") - print(chat.last.parse(CommaDelimitedAnswer).items) - print() - ``` - -=== "Outputs" - - ``` - --- Chat 1 (2) --- - ['Leonardo da Vinci', 'Vincent van Gogh', 'Pablo Picasso'] - - --- Chat 2 (2) --- - ['Michael Jackson', 'Beyonce', 'The Beatles'] - ``` - -!!! tip "Skipping failed results" - - Passing `on_failed='skip'` to [`.run_batch`][rigging.chat.ChatPipeline.run_batch], or configuring - a pipeline with [`.catch(..., on_failed='skip')`][rigging.chat.ChatPipeline.catch] will cause the function to - ignore any parsing errors like [`ExhaustedMaxRoundsError`][rigging.error.ExhaustedMaxRoundsError] and only - return the chats that were successful. - - -## Batching Parameters - -In addition to batching against input messages or strings, you can fix a single input -and build a batch accross a set of generation parameters. The inputs to -[`.run_batch`][rigging.chat.ChatPipeline.run_batch] -will scale either the generate parameters or the input messages if either is a single item. - -=== "Batching Code" - - ```py - import rigging as rg - - pipeline = rg.get_generator("gpt-3.5-turbo").chat() - - chats = await pipeline.run_batch( - ["Tell me a short fact about an japanese city."], - [rg.GenerateParams(temperature=t) for t in [0.6, 0.9, 1.2, 1.5, 1.8]] - ) - - for i, chat in enumerate(chats): - print(f"--- Chat {i+1} ---") - print(chat.generator_id) - print() - print(chat.conversation) - print() - ``` - -=== "Outputs" - - ``` - --- Chat 1 --- - litellm!gpt-3.5-turbo,temperature=0.6 - - [assistant]: Tokyo, the capital city of Japan, is the most populous - metropolitan area in the world, with over 37 million residents. - - --- Chat 2 --- - litellm!gpt-3.5-turbo,temperature=0.9 - - [assistant]: Tokyo is the largest metropolitan area in the world, - with a population of over 37 million people. - - --- Chat 3 --- - litellm!gpt-3.5-turbo,temperature=1.2 - - [assistant]: Kyoto, a city in Japan known for its historic temples - and gardens, was once the capital of Japan for over 1,000 years from - 794 until the capital was moved to Tokyo in 1869. - - --- Chat 4 --- - litellm!gpt-3.5-turbo,temperature=1.5 - - [assistant]: Nagoya, Japan is known for being one of the leading - manufacturing and industrial regions in the country, with a strong - automotive presence including major factories for Toyota, Honda, and Mitsubishi. - - --- Chat 5 --- - litellm!gpt-3.5-turbo,temperature=1.8 - - [assistant]: Sendai is the largest city in the Tohoku region of - Japan and is known for its incredible natural scenery, such as the - nearby Sendai Bay and Zuihoden mausoleum. - ``` - -## Iterating over Models - -The `run_over` functions let you execute generation over a set of generators: - -- [`ChatPipeline.run_over()`][rigging.chat.ChatPipeline.run_over] -- [`CompletionPipeline.run_over()`][rigging.completion.CompletionPipeline.run_over] -- [`Prompt.run_over()`][rigging.prompt.Prompt.run_over] - -Generators can be passed as string identifiers or full instances of -[Generator][rigging.generator.Generator]. - -By default the original generator associated with the [`ChatPipeline`][rigging.chat.ChatPipeline] -is included in the iteration, configurable with the `include_original` parameter. - -Much like the [`run_many`][rigging.chat.ChatPipeline.run_many] -and [`run_batch`][rigging.chat.ChatPipeline.run_batch] functions, you can control the -handling of failures with the `on_failed` parameter. - -=== "Run Over Code" - - ```py - import rigging as rg - from rigging.model import Answer - - QUESTION = "What is the capital of France?" - ANSWER = "paris" - - async def score_output(chats: list[rg.Chat]) -> list[rg.Chat]: - return [ - chat.meta(correct=chat.last.parse(Answer).content.lower() == ANSWER) - for chat in chats - ] - - chats = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat([ - {"role": "system", "content": f"Always respond in one word between {Answer.xml_tags()} tags."}, - {"role": "user", "content": QUESTION} - ]) - .until_parsed_as(Answer, max_rounds=3) - .map(score_output) - .run_over("gpt-4-turbo", "claude-3-haiku-20240307,temperature=0.5", "claude-3-sonnet-20240229") - ) - - for chat in chats: - print("Model: ", chat.generator.model) - print("Msg: ", chat.last.content) - print("Meta: ", chat.metadata) - print() - ``` - -=== "Outputs" - - ``` - Model: gpt-4-turbo - Msg: Paris - Meta: {'correct': True} - - Model: claude-3-haiku-20240307 - Msg: Paris - Meta: {'correct': True} - - Model: claude-3-sonnet-20240229 - Msg: Paris - Meta: {'correct': True} - - Model: openai/gpt-3.5-turbo - Msg: Paris - Meta: {'correct': True} - ``` \ No newline at end of file diff --git a/docs/topics/iterating-and-batching.mdx b/docs/topics/iterating-and-batching.mdx new file mode 100644 index 0000000..10a73cf --- /dev/null +++ b/docs/topics/iterating-and-batching.mdx @@ -0,0 +1,239 @@ +--- +title: "Iterating and Batching" +description: "Iterating over messages, params, and generators, as well as batching of requests." +public: true +--- + +Rigging has good support for iterating over messages, params, and generators, as well as large batching of requests. How efficiently these mechanisms operates is dependent on the underlying generator that's being used, but Rigging has been developed with scale in mind. + +## Multiple Generations + +The `run_many` functions let you scale out generation N times with the same inputs: + +- `ChatPipeline.run_many()` +- `CompletionPipeline.run_many()` +- `Prompt.run_many()` + + +```python Run Many Code +import rigging as rg + +async def check_animal(chats: list[rg.Chat]) -> list[rg.Chat]: + return [ + await chat.continue_(f"Why did you pick that animal?").meta(questioned=True).run() + if any(a in chat.last.content.lower() for a in ["cat", "dog", "cow", "mouse"]) + else chat + for chat in chats + ] + +chats = ( + await + rg.get_generator("gpt-3.5-turbo") + .chat("Tell me a joke about an animal.") + .map(check_animal) + .run_many(3) +) + +for i, chat in enumerate(chats): + questioned = chat.metadata.get("questioned", False) + print(f"--- Chat {i+1} (?: {questioned}) ---") + print(chat.conversation) + print() +``` + +```text Outputs +--- Chat 1 (?: False) --- +[user]: Tell me a joke about an animal. + +[assistant]: Why did the spider go to the computer? + +To check his website! + +--- Chat 2 (?: False) --- +[user]: Tell me a joke about an animal. + +[assistant]: Why did the chicken join a band? Because it had the drumsticks! + +--- Chat 3 (?: True) --- +[user]: Tell me a joke about an animal. + +[assistant]: Why don't elephants use computers? + +Because they're afraid of the mouse! + +[user]: Why did you pick that animal? + +[assistant]: I chose an elephant because they are known for their intelligence and gentle nature, making them a popular subject for jokes and humorous anecdotes. Plus, imagining an elephant trying to use a computer and being scared of a tiny mouse is a funny visual image! +``` + + +## Batching Inputs + +The `run_batch` functions let you batch accross a set of inputs: + +- `ChatPipeline.run_batch()` +- `CompletionPipeline.run_batch()` + +As processing proceeds with things like `.then()` or `.map()`, that chats will resolve individually and collapse into the final results. + + +```python Batching Inputs +import rigging as rg +from rigging.model import CommaDelimitedAnswer + +pipeline = ( + rg.get_generator('gpt-4-turbo') + .chat({ + "role": "system", + "content": f"Always respond with {CommaDelimitedAnswer.xml_tags()} tags."} + ) + .until_parsed_as(CommaDelimitedAnswer, attempt_recovery=True) +) + +many = [f"Give me 3 famous {thing}" for thing in ["authors", "painters", "musicians", "hackers"]] + +chats = await pipeline.run_batch(many, on_failed='skip') + +for i, chat in enumerate(chats): + print(f"--- Chat {i+1} ({len(chat)}) ---") + print(chat.last.parse(CommaDelimitedAnswer).items) + print() +``` + +```text Output +--- Chat 1 (2) --- +['Leonardo da Vinci', 'Vincent van Gogh', 'Pablo Picasso'] + +--- Chat 2 (2) --- +['Michael Jackson', 'Beyonce', 'The Beatles'] +``` + + + +**Skipping failed results** + +Passing `on_failed='skip'` to `.run_batch`, or configuring a pipeline with `.catch(..., on_failed='skip')` will cause the function to ignore any parsing errors like `ExhaustedMaxRoundsError` and only return the chats that were successful. + + +## Batching Parameters + +In addition to batching against input messages or strings, you can fix a single input and build a batch accross a set of generation parameters. The inputs to `.run_batch` will scale either the generate parameters or the input messages if either is a single item. + + +```python Batching +import rigging as rg + +pipeline = rg.get_generator("gpt-3.5-turbo").chat() + +chats = await pipeline.run_batch( + ["Tell me a short fact about an japanese city."], + [rg.GenerateParams(temperature=t) for t in [0.6, 0.9, 1.2, 1.5, 1.8]] +) + +for i, chat in enumerate(chats): + print(f"--- Chat {i+1} ---") + print(chat.generator_id) + print() + print(chat.conversation) + print() +``` + +```text Output +--- Chat 1 --- +litellm!gpt-3.5-turbo,temperature=0.6 + +[assistant]: Tokyo, the capital city of Japan, is the most populous +metropolitan area in the world, with over 37 million residents. + +--- Chat 2 --- +litellm!gpt-3.5-turbo,temperature=0.9 + +[assistant]: Tokyo is the largest metropolitan area in the world, +with a population of over 37 million people. + +--- Chat 3 --- +litellm!gpt-3.5-turbo,temperature=1.2 + +[assistant]: Kyoto, a city in Japan known for its historic temples +and gardens, was once the capital of Japan for over 1,000 years from +794 until the capital was moved to Tokyo in 1869. + +--- Chat 4 --- +litellm!gpt-3.5-turbo,temperature=1.5 + +[assistant]: Nagoya, Japan is known for being one of the leading +manufacturing and industrial regions in the country, with a strong +automotive presence including major factories for Toyota, Honda, and Mitsubishi. + +--- Chat 5 --- +litellm!gpt-3.5-turbo,temperature=1.8 + +[assistant]: Sendai is the largest city in the Tohoku region of +Japan and is known for its incredible natural scenery, such as the +nearby Sendai Bay and Zuihoden mausoleum. +``` + + +## Iterating over Models + +The `run_over` functions let you execute generation over a set of generators: + +- `ChatPipeline.run_over()` +- `CompletionPipeline.run_over()` +- `Prompt.run_over()` + +Generators can be passed as string identifiers or full instances of `Generator`. By default the original generator associated with the `ChatPipeline` is included in the iteration, configurable with the `include_original` parameter. + +Much like the `run_many` and `run_batch` functions, you can control the handling of failures with the `on_failed` parameter. + + +```python Run Over +import rigging as rg +from rigging.model import Answer + +QUESTION = "What is the capital of France?" +ANSWER = "paris" + +async def score_output(chats: list[rg.Chat]) -> list[rg.Chat]: + return [ + chat.meta(correct=chat.last.parse(Answer).content.lower() == ANSWER) + for chat in chats + ] + +chats = ( + await + rg.get_generator("gpt-3.5-turbo") + .chat([ + {"role": "system", "content": f"Always respond in one word between {Answer.xml_tags()} tags."}, + {"role": "user", "content": QUESTION} + ]) + .until_parsed_as(Answer, max_rounds=3) + .map(score_output) + .run_over("gpt-4-turbo", "claude-3-haiku-20240307,temperature=0.5", "claude-3-sonnet-20240229") +) + +for chat in chats: + print("Model: ", chat.generator.model) + print("Msg: ", chat.last.content) + print("Meta: ", chat.metadata) + print() +``` + +```text Outputs +Model: gpt-4-turbo +Msg: Paris +Meta: {'correct': True} + +Model: claude-3-haiku-20240307 +Msg: Paris +Meta: {'correct': True} + +Model: claude-3-sonnet-20240229 +Msg: Paris +Meta: {'correct': True} + +Model: openai/gpt-3.5-turbo +Msg: Paris +Meta: {'correct': True} +``` + \ No newline at end of file diff --git a/docs/topics/logging.md b/docs/topics/logging.mdx similarity index 75% rename from docs/topics/logging.md rename to docs/topics/logging.mdx index 59d21dd..c1da5c0 100644 --- a/docs/topics/logging.md +++ b/docs/topics/logging.mdx @@ -1,19 +1,22 @@ -# Logging +--- +title: "Logging" +description: "How to view and configure logging." +public: true +--- Rigging uses [loguru](https://loguru.readthedocs.io/) for it's logging. By default it disables it's logger allowing users to choose when/how to gather messages. If you want to let rigging messages flow into loguru, you should enable it: -```py +```python from loguru import logger logger.enable('rigging') ``` -If you want to have some sane default handlers with dual console & file logging, -you can use the [rigging.logging.configure_logging][] function to configure loguru. +If you want to have some sane default handlers with dual console & file logging, you can use the `rigging.logging.configure_logging` function to configure loguru. -```py +```python from rigging.logging import configure_logging configure_logging( diff --git a/docs/topics/migrations.md b/docs/topics/migrations.md deleted file mode 100644 index 731ceff..0000000 --- a/docs/topics/migrations.md +++ /dev/null @@ -1,88 +0,0 @@ -# Migrations - -As we continue to develop and improve Rigging, we may introduce changes that -break backwards compatibility and/or signficantly change mechanics of the library. - -In general we try to follow best practices for semantic versioning: - -- **Major version**: Significant and breaking changes (e.g. v1.X to v2.X) -- **Minor version**: New features or improvements (e.g. v1.0 to v1.1) -- **Patch version**: Bug fixes or minor improvements (e.g. v1.0.0 to v1.0.1) - -## Migrating from v1.x to v2.x - -### Rigging is now exclusivley async - -Maintaining dual interface support was complex and error-prone, and we always -tried to implement the more performant code in the async interface. - -Ideally we could have maintained synchronous "gates" which managed asyncio loops for the user, but this -is has caveats in notebook/jupyter environments. Ultimately we've decided to migrate -exclusively to async to simplify the codebase and improve performance. - -- There are no longer any `a`-prefixed functions. Functions like [`run`][rigging.chat.ChatPipeline.run] and - [`generate_messages`][rigging.generator.Generator.generate_messages] are now coroutines that need to be awaited. -- [`map`][rigging.chat.ChatPipeline.map] and [`then`][rigging.chat.ChatPipeline.then] callbacks are now expected to be coroutines. - -Adapting these changes should be relatively straightforward. `await` can be used directly in Jupyter nodebooks -by default. Wrapping any entrypoint with `asyncio.run(...)` is a simple way to manage an event loop. If you're -in a more unique scenario, check out the [greenback](https://github.com/oremanj/greenback) to allow stepping in/out -of async code in a larger system. - -We also provide a helper [`await_`][rigging.util.await_] function which can be used in place -of standard `await` in synchronous code. Underneath rigging will manage an event loop for you -in a separate thread and pass coroutines into it for resolution. - - -=== "rg.await_()" - - ```py - import rigging as rg - - def main(): - generator = rg.get_generator(...) - pipeline = generator.chat(...) - - chat = rg.await_(pipeline.run()) # (1)! - - if __name__ == "__main__": - main() - ``` - - 1. You can pass a single coroutine or a positional list of coroutines to [`await_`][rigging.util.await_]. - - -=== "asyncio.run()" - - ```py - import asyncio - import rigging as rg - - async def main(): - generator = rg.get_generator(...) - pipeline = generatore.chat(...) - - chat = await pipeline.run() - - if __name__ == "__main__": - asyncio.run(main()) - ``` - - -### "Pending" -> "Pipeline" - -Language around chat pipelines and completions was confusing, and didn't accurately -communicate the power of the pipeline system. We've renamed `PendingChat` to `ChatPipeline` and -`PendingCompletion` to `CompletionPipeline`. - -This shouldn't affect most users unless you were manually accessing these classes. You'll see us -replace the frequently use of `pending` variables with `pipeline` in our code. - -### `on_failed` replaces `skip_failed`/`include_failed` - -Pipelines now provide better clarity for catching errors and translating -them into failed outputs. We've replaced the `skip_failed` and `include_failed` -arguments for a general string literal `on_failed` mapped to [`FailMode`][rigging.chat.FailMode]. - -This should help us clarity behaviors and expand them in the future without causing -argument bloat. \ No newline at end of file diff --git a/docs/topics/migrations.mdx b/docs/topics/migrations.mdx new file mode 100644 index 0000000..171ca0c --- /dev/null +++ b/docs/topics/migrations.mdx @@ -0,0 +1,316 @@ +--- +title: "Migrations" +description: "Migrating from one version of Rigging to another" +public: true +--- + +As we continue to develop and improve Rigging, we may introduce changes that break backwards compatibility and/or signficantly +change mechanics of the library. In general we try to follow best practices for semantic versioning: + +- **Major version**: Significant and breaking changes (e.g. v1.X to v2.X) +- **Minor version**: New features or improvements (e.g. v1.0 to v1.1) +- **Patch version**: Bug fixes or minor improvements (e.g. v1.0.0 to v1.0.1) + +## Migrating from v2.x to v3.x + +### Python 3.9 Support Dropped + +As we move forward with Rigging and other libraries, we've settled on Python 3.10 as the minimum supported version. This allows us to leverage new language features and optimizations that are otherwise difficult to support in older versions. + +```toml:pyproject.toml +[tool.poetry.dependencies] +python = "^3.10" +rigging = "^3.0.0" +``` + + +**Action Required** + +Ensure your environment uses Python 3.10 or later. Update your `pyproject.toml` or `requirements.txt` if necessary. + + +### Chat Pipeline Refactor (`ChatPipeline`) + +The internal mechanics of `ChatPipeline` have been substantially rewritten to allow for better control over iterative generation within `then()`, `map()`, and associated parsing and tool invocation. Previously it was difficult to control the process of multiple pipelines being executed within each other and callbacks. We've standardized the interface to use `PipelineStep` objects and a context manager to yield moments of control back to the user and wrapping libraries (`.step()`). For the most part, users don't need to worry about this interface directly, but it does have some downstream effects on the pipeline flow. + +#### Moving from `until()` to `then()` + +The `until()` mechanism inside pipelines and its parameters (`attempt_recovery`, `drop_dialog`, `max_rounds`) have been **removed**. In the early days of Rigging we used this interface to support iterative parsing, but it's been largely replaced by the new `then()` and `map()` callbacks since v2. We're commited to using this new structure and avoid internal complexities like calling a generator multiple times within a single pipeline step. + + +**Action Required** + +Logic previously implemented with `until()` needs to be refactored. + +- For simple validation/recovery based on parsing, the updated `until_parsed_as()` (see below) might suffice. +- For more complex iterative loops, use the `then()` or `map()` callbacks. These callbacks can now return or yield `PipelineStepContextManager` or `PipelineStepGenerator` objects, allowing you to recursively call the pipeline or trigger further generation steps. +- The new `step()` async context manager provides fine-grained control for advanced custom iteration patterns. + + +The `.until_parsed_as()` method still exists, but its internal implementation and parameters have changed as a function of migrating from `until()` to `then()`. +- The `max_rounds` parameter is deprecated. Use the new `max_depth` parameter, which controls the maximum *depth* of recursive parsing attempts (defaulting to `DEFAULT_MAX_DEPTH`). +- The `attempt_recovery` and `drop_dialog` parameters are deprecated and have no effect. Recovery is now implicit within the `max_depth` limit, and the full dialog history is + +We've also found much more success leveraging tool calling as the primary mechanism to parse structured data out of models and it presents a strong alternative to lots of parsing logic. We cover additional changes to the tool system below. + + +```python v2.x +from rigging.model import YesNoAnswer + +chat = ( + await pipeline + .until_parsed_as(YesNoAnswer, max_rounds=5, attempt_recovery=True, drop_dialog=False) + .run() +) +``` + +```python v3.x +from rigging.model import YesNoAnswer + +# max_depth controls parsing retry depth +chat = ( + await pipeline + .until_parsed_as(YesNoAnswer, max_depth=5) + .run() +) +``` + + + +**Action Required** + +Update calls to `.until_parsed_as()`: +- Replace `max_rounds=N` with `max_depth=N`. +- Remove `attempt_recovery` and `drop_dialog` arguments. + + + +#### Error Handling + +- `MessagesExhaustedMaxRoundsError` is replaced by `MaxDepthError`. This error is now raised when the recursive depth limit (set via `max_depth` in `then`, `map`, or `until_parsed_as`) is exceeded. +- The `errors_to_fail_on` parameter in the `.catch()` method is renamed to `errors_to_catch`. + + +```python v2.x +from rigging.error import MessagesExhaustedMaxRoundsError + +try: + chat = await pipeline.run() +except MessagesExhaustedMaxRoundsError: + # Handle max rounds error + ... +``` + +```python v3.x +from rigging.error import MaxDepthError + +try: + chat = await pipeline.run() +except MaxDepthError: + # Handle max depth error + ... +``` + + + +**Action Required** + +- Update any `try...except` blocks catching `MessagesExhaustedMaxRoundsError` to catch `MaxDepthError`. +- Rename `errors_to_fail_on` to `errors_to_catch` in calls to `.catch()`. Review the default caught errors if relying on implicit behavior. + + +### Unified Tool System (`rigging.tool`) + +We worked hard in v3 to bring together some of the early tool systems and unify them under a single interface with clean support for **Robopages** and **MCP**. The previous `ApiTool` and native `Tool` classes have been merged into a single, more flexible system. This change simplifies the way tools are defined, used, and integrated into pipelines. Beyond that, the new system allows for the same tools to be used by models using various calling conventions - regardless of whether they underlying provider supports JSON tool calling or not. + +- `ApiTool` and the previous native `Tool` class are **removed**. Just build functions or methods inside classes, decorate them, and use them as tools anywhere. +- The primary way to define tools is now via the **`@tool`** and **`@tool_method`** decorators applied to functions and class methods, respectively. +- The `ChatPipeline.using()` method signature has changed significantly: + - It now accepts `Tool` instances or callables directly: `using(*tools: Tool | Callable)` + - It uses a `mode: ToolMode` parameter (`"auto"`, `"api"`, `"xml"`, `"json-in-xml"`) to control calling convention. + - It uses `max_depth: int` to limit recursive tool calls. + - Parameters like `force`, `attempt_recovery`, `drop_dialog`, `max_rounds` are removed. +- The `rigging.integrations` module is removed. Use `rigging.tool.robopages` and the new `rigging.tool.mcp` to use those integrations as tools. + + +```python v2.x +from rigging.tool import ApiTool, Tool as NativeTool + +# API Tool definition +def get_weather_v2(city: str) -> str: + ... + +# Native Tool definition +class CalculatorV2(NativeTool): + name = "calculator" + description = "Performs calculations" + + def add( + self, + a: Annotated[int, "First number"], + b: Annotated[int, "Second number"] + ) -> str: + ... + +native_tool = CalculatorV2() + +# Pipeline usage +chat = ( + await pipeline + .using_api_tools(get_weather_v2) + .using_native_tools(native_tool, max_rounds=3) + .run() +) + +``` + +```python v3.x +import rigging as rg +from typing import Annotated + +# Tool definition using decorators +@rg.tool +def get_weather_v3(city: str) -> str: + ... + +class CalculatorV3: + @rg.tool_method # Use tool_method for class methods needing `self` + def add( + self, + a: Annotated[int, "First number"], + b: Annotated[int, "Second number"] + ) -> str: + ... + +calc = CalculatorV3() + +# Robopages Tools +robo_tools = rg.tool.robopages("http://localhost:8080") + +# MCP Tools +async with rg.mcp("sse", url="http://localhost:8787/sse") as mcp: + + # Pipeline usage + chat = ( + await pipeline + .using(mcp.tools, robo_tools, get_weather_v3, calc.add) + .run() + ) + +``` + + + +**Action Required** + +- **Redefine Tools**: Convert all tool definitions to use the `@tool` or `@tool_method` decorators instead of inheriting from `Tool` in your class. +- **Update `using()` Calls**: Modify calls to `.using()` to pass `Tool` instances/callables directly and use the new parameters (`mode`, `max_depth`, etc.). +- **Update Imports**: Change imports for integrations like Robopages and MCP. + + +### Message Content Model (`rigging.message`) + +Handling of message content, especially multi-modal content, has been standardized. + +- `Message.all_content` is **deprecated**. Use `Message.content_parts` (a `list[Content]`) to access the full list of text, image, and audio parts. +- `Message.content` property now *only* gets/sets the concatenated text from `ContentText` parts. Use it for simple text manipulation. +- New `ContentAudioInput` type for audio messages. +- Message serialization (e.g., `to_openai_spec()`) is updated for the `content_parts` structure. + + +```python v2.x +# Accessing content +text_content = message.content +mixed_content = message.all_content # Could be str or list[Content] + +# Modifying content +message.content = "New text" +# Handling mixed types required checking type of all_content +``` + +```python v3.x +# Accessing content +text_content = message.content # Gets only text parts joined by \n +all_parts = message.content_parts # Access list directly + +# Modifying text content +message.content = "New text" # Replaces only the ContentText parts + +# Adding different content types +from rigging.message import ContentImageUrl, ContentText +message.content_parts.append(ContentText(text="More text")) +message.content_parts.append(ContentImageUrl.from_file("image.png")) +``` + + + +**Action Required** + +- Replace `message.all_content` with `message.content_parts` when dealing with multi-modal data. +- Use `message.content` only when working solely with the text components. +- Verify any custom serialization or direct API interactions involving message content. + + +### Other Changes & Notes + +* **`ChatPipeline.add` Default**: The `merge_strategy` default changed to `"none"`. Messages of the same role are *not* merged automatically anymore. Use `merge_strategy="all"` or `"only-user-role"` explicitly if merging is desired. +* **Caching**: New `ChatPipeline.cache()` method provides basic control over prompt caching hints. +* **Dependencies**: Check for updated versions of core dependencies (`litellm`, `openai`, etc.) and new additions (`mcp`, `httpx-sse`). + +## Migrating from v1.x to v2.x + +### Rigging is now exclusivley async + +Maintaining dual interface support was complex and error-prone, and we always tried to implement the more performant code in the async interface. + +Ideally we could have maintained synchronous "gates" which managed asyncio loops for the user, but this is has caveats in notebook/jupyter environments. Ultimately we've decided to migrate exclusively to async to simplify the codebase and improve performance. + +- There are no longer any `a`-prefixed functions. Functions like `run() and `generate_messages()` are now coroutines that need to be awaited. +- `map()` and `then()`callbacks are now expected to be coroutines. + +Adapting these changes should be relatively straightforward. `await` can be used directly in Jupyter nodebooks by default. Wrapping any entrypoint with `asyncio.run(...)` is a simple way to manage an event loop. If you're in a more unique scenario, check out the [greenback](https://github.com/oremanj/greenback)to allow stepping in/out of async code in a larger system. + +We also provide a helper `rg.await_` function which can be used in place of standard `await` in synchronous code.Underneath rigging will manage an event loop for you in a separate thread and pass coroutines into it for resolution. + + +```python rg.await_() {7} +import rigging as rg + +def main(): + generator = rg.get_generator(...) + pipeline = generator.chat(...) + + chat = rg.await_(pipeline.run()) + +if __name__ == "__main__": + main() +``` + +```python asyncio.run() +import asyncio +import rigging as rg + +async def main(): + generator = rg.get_generator(...) + pipeline = generatore.chat(...) + + chat = await pipeline.run() + +if __name__ == "__main__": + asyncio.run(main()) +``` + + +1. You can pass a single coroutine or a positional list of coroutines to `await_`. This will manage an event loop for you in a separate thread and resolve the coroutines. + +### "Pending" -> "Pipeline" + +Language around chat pipelines and completions was confusing, and didn't accurately communicate the power of the pipeline system. We'verenamed `PendingChat` to `ChatPipeline` and `PendingCompletion` to `CompletionPipeline`. + +This shouldn't affect most users unless you were manually accessing these classes. You'll see us replace the frequently use of `pending` variables with `pipeline` in our code. + +### `on_failed` replaces `skip_failed`/`include_failed` + +Pipelines now provide better clarity for catching errors and translating them into failed outputs. We've replaced the `skip_failed` and `include_failed` arguments for a general string literal `on_failed` mapped to `FailMode`. + +This should help us clarity behaviors and expand them in the future without causing argument bloat. \ No newline at end of file diff --git a/docs/topics/pipelines.mdx b/docs/topics/pipelines.mdx new file mode 100644 index 0000000..2b1d9b2 --- /dev/null +++ b/docs/topics/pipelines.mdx @@ -0,0 +1,232 @@ +--- +title: "Pipelines" +description: "Control the generation process and react to outputs using callbacks and pipeline steps." +public: true +--- + +Rigging's pipelines (`ChatPipeline`, `CompletionPipeline`) offer powerful ways to control the generation flow and process results. This includes passive monitoring, adding post-processing steps, and even creating complex iterative generation loops. + +## Watch Callbacks (Passive Monitoring) + +The simplest way to observe the pipeline is using **watch callbacks**. These are passive listeners that receive `Chat` or `Completion` objects as they are finalized within a pipeline run, but they don't modify the pipeline's execution. They are ideal for logging, monitoring, or triggering external actions based on generated content. + +Register watch callbacks using the `.watch()` method on Generators, Pipelines, or Prompts. Rigging also provides pre-built watchers in the `rigging.watchers` module. + +- `Generator.watch()` +- `ChatPipeline.watch()` +- `CompletionPipeline.watch()` +- `Prompt.watch()` + +```python +import rigging as rg + +# Use a pre-built watcher to log chats to a file +log_to_file = rg.watchers.write_chats_to_jsonl("chats.jsonl") + +# Define a custom async watch callback +async def print_chat_ids(chats: list[rg.Chat]) -> None: + print(f"Watched {len(chats)} chats: {[chat.uuid for chat in chats]}") + +pipeline = ( + rg.get_generator("openai/gpt-4o-mini") + .chat("Explain why the sky is blue") + .watch(log_to_file, print_chat_ids) # Register multiple watchers +) + +# Watchers will be called during the run_many execution +chats = await pipeline.run_many(3) +``` + +## Controlling Flow with `then` and `map` Callbacks + +To actively modify chats or influence the generation process *after* a generation step completes, use the **`then()`** and **`map()`** callback methods. + +- `ChatPipeline.then()`: Processes each `Chat` object individually. +- `ChatPipeline.map()`: Processes a `list[Chat]` objects all at once (useful for batch operations). +- These methods can also be called directly on `Prompt` objects. + +### Basic Post-Processing + +The simplest use case is to modify a chat after generation. Your callback receives the `Chat` (for `then`) or `list[Chat]` (for `map`) and can return the modified chat(s) or `None` (for `then`) to keep the original. + +```python +import rigging as rg + +# Example: Add metadata based on content +async def add_sentiment_tag(chat: rg.Chat) -> rg.Chat | None: + content = chat.last.content.lower() + if "positive" in content: + chat.meta(sentiment="positive") + elif "negative" in content: + chat.meta(sentiment="negative") + # Return the modified chat (or None to keep original) + return chat + +pipeline = ( + rg.get_generator("openai/gpt-4o-mini") + .chat("Generate a positive sentence.") + .then(add_sentiment_tag) +) +chat = await pipeline.run() + +print(chat.metadata.get("sentiment")) +# > positive +``` + +### Iterative Generation and Validation + +Callbacks can also drive *further* generation steps, enabling complex validation loops, conditional branching, or agent-like behavior. + +To achieve this, a `then` or `map` callback can do either of the following: +1. Return or yield a `PipelineStepGenerator` or `PipelineStepContextManager`. This is typically done by calling `.step()` on a new or restarted pipeline. +2. Call `run()` or derivatives like `run_many()` or `run_batch()` directly inside the callback to execute new generation steps and return the final result. + +Option 1 is generally preferred as it allows for more control over iterative pipeline execution when generations are nested. Without this, the pipelines above won't be able to properly track the depth of the nested calls. + +```python +import rigging as rg +from rigging.chat import PipelineStepContextManager + +# Example: Ensure the model mentions a specific animal +TARGET_ANIMAL = "cat" + +async def ensure_animal_mentioned(chat: rg.Chat) -> PipelineStepContextManager | None: + if TARGET_ANIMAL in chat.last.content.lower(): + return None # Condition met, stop iterating + + # Condition not met, ask the model to try again + print(f"-> Assistant didn't mention '{TARGET_ANIMAL}', asking for revision.") + follow_up_pipeline = chat.continue_(f"Please revise your previous response to include the animal '{TARGET_ANIMAL}'.") + + # Return the context manager from .step() to trigger another generation round + return follow_up_pipeline.step() + +# Limit recursion depth to prevent infinite loops +MAX_RECURSION = 3 + +pipeline = ( + rg.get_generator("openai/gpt-4o-mini") + .chat(f"Tell me a short story about an animal.") + .then(ensure_animal_mentioned, max_depth=MAX_RECURSION) # Control recursion depth +) + +final_chat = await pipeline.run() + +print("\n--- Final Conversation ---") +print(final_chat.conversation) + +if TARGET_ANIMAL in final_chat.last.content.lower(): + print(f"\nSuccess: Final response mentions '{TARGET_ANIMAL}'.") +else: + print(f"\nFailed: Final response did not mention '{TARGET_ANIMAL}' after {MAX_RECURSION} attempts.") + +``` + +- **Recursion Control**: The `max_depth` parameter on `then()` and `map()` is crucial. It limits how many nested pipeline steps can be triggered from within a callback, preventing infinite loops. If this depth is exceeded, a `MaxDepthError` is raised (or handled based on `on_failed` mode). + +## Handling Parsing Requirements (`until_parsed_as`) + +A common use case for iterative generation is ensuring the model's output successfully parses into a specific `Model`. Rigging provides the convenient `ChatPipeline.until_parsed_as()` method for this. + +Internally, this method uses the `then` callback mechanism described above, attempting to parse the required model(s) and triggering regeneration with validation feedback if parsing fails. + +```python +import rigging as rg +from rigging.model import YesNoAnswer + +# Define the desired output model +pipeline = ( + rg.get_generator("openai/gpt-4o-mini") + .chat(f"Answer yes or no. Is the sky blue? Respond within {YesNoAnswer.xml_tags()} tags.") + # Ensure the output parses as YesNoAnswer, retrying up to 3 times if needed + .until_parsed_as(YesNoAnswer, max_depth=3) +) + +chat = await pipeline.run() + +if not chat.failed: + parsed_answer = chat.last.parse(YesNoAnswer) + print(f"Parsed answer: {parsed_answer.boolean}") +else: + print(f"Failed to get a valid YesNoAnswer after multiple attempts. Error: {chat.error}") + +``` + +- **Parameter Change**: Note that `max_rounds` from v2 is replaced by `max_depth`. The `attempt_recovery` and `drop_dialog` parameters are removed as recovery is implicit and dialog is preserved. + +## Fine-grained Control (`step()`) + +For maximum control and introspection, you can use the `ChatPipeline.step()` (or `step_many`, `step_batch`) async context manager. This yields `PipelineStep` objects representing each stage of the execution (`generated`, `callback`, `final`). + +This allows you to examine intermediate states, inject custom logic between steps, or build highly complex generation flows beyond the standard callback system. + +```python +import rigging as rg + +pipeline = rg.get_generator("openai/gpt-4o-mini").chat("Hello there!") + +print("Stepping through pipeline execution:") +async with pipeline.step() as steps: + async for step in steps: + print(f"- Step State: {step.state}") + print(f" Chats ({len(step.chats)}): {[chat.uuid for chat in step.chats]}") + if step.callback: + print(f" Callback: {rg.util.get_qualified_name(step.callback)}") + if step.parent: + print(f" Parent Pipeline ID: {id(step.parent.pipeline)}") + # Add custom logic here based on step state or content +print("Pipeline finished.") +``` + +## Handling Failures (`on_failed`, `catch`) + +Pipelines provide robust ways to handle errors during generation or callback execution. + +### `on_failed` Mode + +The `on_failed` parameter (set via `.catch()` or on `run_many`/`run_batch`/`run_over`) determines behavior when a catchable error occurs: + +- **`"raise"` (Default):** The exception is raised, halting execution. +- **`"skip"`:** The chat where the error occurred is discarded. `run_many`/`run_batch`/`run_over` will return only the successful chats. (Not valid for single `.run()`). +- **`"include"`:** The chat is marked with `.failed = True` and the exception stored in `.error`. The chat is included in the results. + +```python +import rigging as rg + +pipeline = ( + rg.get_generator("openai/gpt-4o-mini") + .chat("Potentially problematic prompt") + .catch(on_failed="include") +) + +# Example: run_many might succeed for some, fail for others +chats = await pipeline.run_many(5) + +successful_chats = [c for c in chats if not c.failed] +failed_chats = [c for c in chats if c.failed] + +print(f"Succeeded: {len(successful_chats)}, Failed: {len(failed_chats)}") +for failed_chat in failed_chats: + print(f" Error in chat {failed_chat.uuid}: {failed_chat.error}") + +``` + +### Defining Catchable Errors (`catch`) + +By default, pipelines catch critical internal errors like `ValidationError` and `MaxDepthError` when `on_failed` is `"skip"` or `"include"`. You can specify additional exception types to be treated as non-fatal errors using `ChatPipeline.catch()`. + +```python +import rigging as rg +# Assume SomeCustomAPIError exists +from some_api import SomeCustomAPIError + +pipeline = ( + rg.get_generator(...) + .chat(...) + # Treat SomeCustomAPIError as a non-fatal error + .catch(SomeCustomAPIError, on_failed="skip") +) + +# Now, if SomeCustomAPIError occurs, the chat will be skipped instead of raising +chats = await pipeline.run_many(10) +``` \ No newline at end of file diff --git a/docs/topics/principles.md b/docs/topics/principles.md deleted file mode 100644 index 46eafb3..0000000 --- a/docs/topics/principles.md +++ /dev/null @@ -1,23 +0,0 @@ -# Principles - -LLMs are extremely capable machine learning systems, but they operate purely in textual spaces as a byproduct of -their training data. We have access to the compression of a huge repository of human knowledge, but are limited to quering -that information via natural language. Our first inclination is to let these language interfaces drive -our design decisions. We build chat bots and text search, and when it comes time to align them with closely -with the rest of our fixed software stack, we quickly get frustrated by their inconsistencies and limited -control over their products. - -In software we operate on the principle of known interfaces as the basis for composability. In the functional paradigm, we want our -software functions to operate like mathmatical ones, where the same input always produces the same output with no side effects. -Funny enough LLMs (like all models) also operate in that way (minus things like floating point errors), but we intentionally -inject randomness to our sampling process to give them the freedom to explore and produce novel outputs. Therefore we shouldn't -aim for "purity" in the strict sense, but we should aim for consistency in their interface. - -Once you start to think of a "prompt", "completion", or "chat interaction" as being the temporary textual interface by which we pass in -structured inputs and produce structured outputs, we can begin to link them with traditional software. Many libraries get close to this -idea, but they rarely hold the opinion that programing types and structures, and not text, are the best way to make LLM-based -systems composible. - -Reframing these language models as tools which use tokens of text in context windows to navigate latest space and produce -probabilities of output tokens, but do not need to have the data they consume or produce be holistically constrained to -textual spaces in our use of them is a core opinion of Rigging. \ No newline at end of file diff --git a/docs/topics/prompt-functions.md b/docs/topics/prompt-functions.md deleted file mode 100644 index 59e55cb..0000000 --- a/docs/topics/prompt-functions.md +++ /dev/null @@ -1,507 +0,0 @@ -# Prompt Functions - -Defining prompts as python functions is a great abstraction on top of chat pipelines, allowing you -to leverage parsing logic and type hints to define a code-less function that will use a generator -underneath. - -## Defining Prompts - -A [`Prompt`][rigging.prompt.Prompt] is typically created using one of the following decorators: - -- [`@rg.prompt`][rigging.prompt.prompt] - Optionally takes a generator id, generator, or pipeline. -- [`@generator.prompt`][rigging.generator.Generator.prompt] - Use this generator when executing the prompt. -- [`@pipeline.prompt`][rigging.chat.ChatPipeline.prompt] - Use this pipeline when executing the prompt. - -!!! note "Async Conversion" - - Prompt functions can be defined with or without the `async` keyword, but they will always be - represented as async calls once wrapped based on their connection to chat pipelines. - - In other words, wrapping a synchronous function with `@rg.prompt` will result in an async - callable. - -=== "Generator ID" - - ```py - import rigging as rg - - @rg.prompt(generator_id="mistral/mistral-medium-latest") - def summarize(text: str) -> str: - """Summarize this text.""" - - ``` - -=== "Generator Instance" - - ```py - import rigging as rg - - generator = rg.get_generator("mistral/mistral-medium-latest") - - @generator.prompt - def summarize(text: str) -> str: - """Summarize this text.""" - - ``` - -=== "Chat Pipeline" - - ```py - import rigging as rg - - pipeline = ( - rg.get_pipeline("mistral/mistral-medium-latest") - .chat([ - {"role": "system", "content": "Respond like a pirate."} - ]) - ) - - @pipeline.prompt - def summarize(text: str) -> str: - """Summarize this text.""" - - ``` - -=== "Unbound" - - ```py - import rigging as rg - - @rg.prompt - def summarize(text: str) -> str: - """Summarize this text.""" - - ``` - -Prompts are optionally bound to a pipeline/generator underneath, hence the generator and pipeline -decorator variants, but they don't have to be. We refer to bound prompts as "standalone", because -they can be executed directly as functions. Otherwise, you are required first "bind" the prompt to -a specific generator id, generator, or pipeline to make it callable. Do this with -[`Prompt.bind`][rigging.prompt.Prompt.bind] or related methods. - -```py -import rigging as rg - -@rg.prompt -def summarize(text: str) -> str: - """Summarize this text.""" - -generator = rg.get_generator("gpt-4o-mini") - -await summarize.bind(generator)("...") -``` - -### Templates and Docstrings - -Underneath, the function signature will be analyzed for inputs and outputs, and the docstring -will be used to create a final jinja2 template which will be used as the prompt text. You can -always access this [`.template`][rigging.prompt.Prompt.template] attribute to inspect how the prompt -will be formatted. - -Here are the general processing docstring rules: - -- Any docstring content will always be at the top of the prompt. -- If no docstring is provided, a generic one will be substituted. -- Any inputs not explicitly defined in the docstring will be appended after the docstring. - -=== "No Docstring" - - ```py - import rigging as rg - - @rg.prompt - def summarize(text: str) -> str: - ... - - print(summarize.template) - - # Convert the following inputs to outputs (summarize). - # - # {{ text }} - # - # Produce the following output: - # - # - ``` - -=== "Basic Docstring" - - ```py - import rigging as rg - - @rg.prompt - def summarize(text: str) -> str: - """Summarize this text.""" - - print(summarize.template) - - # Summarize this text. - # - # {{ text }} - # - # Produce the following output: - # - # - ``` - -=== "Inputs in Docstring" - - ```py - import rigging as rg - - @rg.prompt - def summarize(text: str) -> str: - """ - Summarize the following: - - {{ text }} - - Be concise. - """ - - print(summarize.template) - - # Summarize the following: - # - # {{ text }} - # - # Be concise. - # - # Produce the following output: - # - # - ``` - -In other words, you can define how inputs will be included in the prompt by passing them inside -the docstring in jinja2 formats, or let Rigging handle this for you by ommiting them. - -### Outputs and Context - -In the example above, you'll notice that defining our function to output a `str` results -in the following text be appended to the prompt template: - -```py -# Produce the following output: -# -# -``` - -This is pretty light on context, and we can improve this by updating our signature -with a [`Ctx`][rigging.prompt.Ctx] annotation: - -```py -from typing import Annotated -import rigging as rg - -summary = Annotated[str, rg.Ctx(tag="summary", example="[2-3 sentences]")] - -@rg.prompt -def summarize(text: Annotated[str, rg.Ctx(tag="long-text")]) -> summary: - """Summarize this text.""" - -print(summarize.template) - -# Summarize this text. -# -# {{ long_text }} -# -# Produce the following output: -# -# [2-3 sentences] -``` - -We can apply [`Ctx`][rigging.prompt.Ctx] annotations to any of the inputs and outputs -of a prompt. We can override the xml tag, provide an example, and add prefix text. - -Output processing is optional, and can be omitted by returning a [`Chat`][rigging.chat.Chat] object -from the wrapped function. This allows you to do with the generated output as you please. - -```py -import rigging as rg - -@rg.prompt -def summarize(text: str) -> rg.Chat: - """Summarize this text.""" - -print(summarize.template) - -# Summarize this text. -# -# {{ text }} -``` - -### Complex Outputs - -You can also define more complex outputs by using a rigging model, list, tuple, or dataclass. Not every -construction will be supported, and we attempt to pre-validate the output structure to ensure it can be -processed correctly. - -=== "Model" - - ```py - import rigging as rg - - class User(rg.Model): - name: str - email: str - age: int - - @rg.prompt - def generate_user() -> User: - """Generate a fake test user.""" - - print(generate_user.template) - - # Generate a fake test user. - # - # Produce the following output: - # - # - # - # - # - # - ``` - -=== "List of Models" - - ```py - import rigging as rg - - class User(rg.Model): - name: str - email: str - age: int - - @rg.prompt - def generate_users(count: int = 3) -> list[User]: - """Generate fake test users.""" - - print(generate_users.template) - - # Generate fake test users. - # - # {{ count }} - # - # Produce the following output for each item: - # - # - # - # - # - # - ``` - -=== "Dataclass" - - ```py - from dataclasses import dataclass - import rigging as rg - - @dataclass - class User: - name: str - email: str - age: int - - @rg.prompt - def generate_user() -> User: - """Generate a fake test user.""" - - print(generate_user.template) - - # Generate a fake test user. - # - # Produce the following outputs: - # - # - # - # - # - # - ``` - -=== "Tuple" - - ```py - import rigging as rg - - name = Annotated[str, rg.Ctx(tag="name")] - email = Annotated[str, rg.Ctx(tag="email")] - age = Annotated[int, rg.Ctx(tag="age")] - - @rg.prompt - def generate_user() -> tuple[name, email, age]: - """Generate a fake test user.""" - - print(generate_user.template) - - # Generate a fake test user. - # - # Produce the following outputs: - # - # - # - # - # - # - ``` - -You can also embed a [`Chat`][rigging.chat.Chat] object inside other objects, which -will be excluded from any prompt guidance, but supplied the value when the prompt -is executed. This is great for gathering both structured data and the original chat. - -=== "Chat in Tuple" - - ```py - import rigging as rg - - joke = Annotated[str, rg.Ctx(tag="joke")] - - @rg.prompt - def tell_joke() -> tuple[joke, rg.Chat]: - """Tell a joke.""" - - print(tell_joke.template) - - # Tell a joke. - # - # Produce the following output: - # - # - ``` - -=== "Chat in Dataclass" - - ```py - from dataclasses import dataclass - import rigging as rg - - @dataclass - class Joke: - setup: str - punchline: str - chat: rg.Chat - - @rg.prompt - def tell_joke() -> Joke: - """Tell a joke.""" - - print(tell_joke.template) - - # Tell a joke. - # - # Produce the following outputs: - # - # - # - # - ``` - -## Rendering Prompts - -In addition to templates, you can use [`.render`][rigging.prompt.Prompt.render] with valid -inputs to view the exact prompt as it will be sent to a generator. You can also use this -to pass your prompt into pipelines at your discretion. - -```py -from typing import Annotated -import rigging as rg - -email = Annotated[str, rg.Ctx(tag="email")] - -@rg.prompt -def convert_to_email(name: str, top: int = 5) -> list[email]: - """Convert this name into the best {{ top }} email addresses.""" - -print(convert_to_email.render("John Doe")) - -# Convert this name into the best 5 email addresses. -# -# John Doe -# -# Produce the following output for each item: -# -# -``` - -## Running Prompts - -Prompt objects expose the following methods for execution: - -- [`Prompt.run()`][rigging.prompt.Prompt.run] (Aliased with `__call__`) -- [`Prompt.run_many()`][rigging.prompt.Prompt.run_many] -- [`Prompt.run_over()`][rigging.prompt.Prompt.run_over] - -*(Available if the prompt was supplied/bonded to a pipeline or generator)* - -You can also bind a prompt at runtime with any of the following: - -- [`Prompt.bind()`][rigging.prompt.Prompt.bind] -- [`Prompt.bind_many()`][rigging.prompt.Prompt.bind_many] -- [`Prompt.bind_over()`][rigging.prompt.Prompt.bind_over] - -!!! Note "Pipeline Context" - - Everything configured on a pipeline or generator will be used when running - the prompt. Watch/Then/Map callbacks, tools, and generate params can all - be used to alter the behavior of the prompt. - - In general, you should consider prompts as producers of user messages, which - will be passed to [`.fork()`][rigging.chat.ChatPipeline.fork], then handle - the parsing of outputs. - -=== "Run Standalone" - - ```py - import rigging as rg - - @rg.prompt(generator_id="claude-3-sonnet-20240229") - def write_code(description: str, language: str = "python") -> str: - """Write a single function.""" - - code = await write_code("Calculate the factorial of a number.") - ``` - -=== "Run with Pipeline" - - ```py - from typing import Annotated - import rigging as rg - - pipeline = ( - rg.get_generator("claude-3-sonnet-20240229") - .chat([ - {"role": "system", "content": "You are a senior software developer"} - ]) - ) - - code_str = Annotated[str, rg.Ctx(tag="code")] - - @rg.prompt - def write_code(description: str, language: str = "python") -> code_str: - """Write a single function.""" - - code = await write_code.bind(pipeline)("Calculate the factorial of a number.") - ``` - -=== "Run Manually" - - ```py - import rigging as rg - - pipeline = ( - rg.get_generator("claude-3-sonnet-20240229") - .chat([ - {"role": "system", "content": "You are a senior software developer"} - ]) - ) - - @rg.prompt - def write_code(description: str, language: str = "python") -> rg.Chat: - """Write a single function.""" - - prompt = write_code.render("Calculate the factorial of a number.") - - chat = await pipeline.fork(prompt).run() - ``` \ No newline at end of file diff --git a/docs/topics/prompt-functions.mdx b/docs/topics/prompt-functions.mdx new file mode 100644 index 0000000..0a18e17 --- /dev/null +++ b/docs/topics/prompt-functions.mdx @@ -0,0 +1,465 @@ +--- +title: "Prompt Functions" +description: "Create dynamic functions out of thin air." +public: true +--- + +Defining chat interactions as python functions is a great abstraction on top of chat pipelines, allowing you to leverage parsing logic and type hints to define a code-less function that will use a generator/pipline underneath to produce an output. + +A `Prompt` is typically created using one of the following decorators: + +- `@rg.prompt` - Optionally takes a generator id, generator, or pipeline. +- `@generator.prompt` - Use this generator when executing the prompt. +- `@pipeline.prompt` - Use this pipeline when executing the prompt. + + +Prompt functions can be defined with or without the `async` keyword, but they will always be represented as async calls once wrapped based on their connection to chat pipelines. + +In other words, wrapping a synchronous function with `@rg.prompt` will result in an async callable. + + + +```python Generator id +import rigging as rg + +@rg.prompt(generator_id="mistral/mistral-medium-latest") +def summarize(text: str) -> str: + """Summarize this text.""" + +``` + +```python Generator instance +import rigging as rg + +generator = rg.get_generator("mistral/mistral-medium-latest") + +@generator.prompt +def summarize(text: str) -> str: + """Summarize this text.""" + +``` + +```python Chat Pipeline +import rigging as rg + +pipeline = ( + rg.get_pipeline("mistral/mistral-medium-latest") + .chat([ + {"role": "system", "content": "Respond like a pirate."} + ]) +) + +@pipeline.prompt +def summarize(text: str) -> str: + """Summarize this text.""" + +``` + +```python Unbound +import rigging as rg + +@rg.prompt +def summarize(text: str) -> str: + """Summarize this text.""" + +``` + + +Prompts are optionally bound to a pipeline/generator underneath, hence the generator and pipeline decorator variants, but they don't have to be. We refer to bound prompts as "standalone", because they can be executed directly as functions. Otherwise, you are required first "bind" the prompt to a specific generator id, generator, or pipeline to make it callable. Do this with `Prompt.bind` or related methods. + +```python +import rigging as rg + +@rg.prompt +def summarize(text: str) -> str: + """Summarize this text.""" + +generator = rg.get_generator("gpt-4o-mini") + +await summarize.bind(generator)("...") +``` + +### Templates and Docstrings + +Underneath, the function signature will be analyzed for inputs and outputs, and the docstring +will be used to create a final jinja2 template which will be used as the prompt text. You can +always access this `.template` attribute to inspect how the prompt +will be formatted. + +Here are the general processing docstring rules: + +- Any docstring content will always be at the top of the prompt. +- If no docstring is provided, a generic one will be substituted. +- Any inputs not explicitly defined in the docstring will be appended after the docstring. + + + +```python No docstring +import rigging as rg + +@rg.prompt +def summarize(text: str) -> str: + ... + +print(summarize.template) + +# Convert the following inputs to outputs (summarize). +# +# {{ text }} +# +# Produce the following output: +# +# +``` + +```python Basic docstring +import rigging as rg + +@rg.prompt +def summarize(text: str) -> str: + """Summarize this text.""" + +print(summarize.template) + +# Summarize this text. +# +# {{ text }} +# +# Produce the following output: +# +# +``` + +```python Inputs in docstring +import rigging as rg + +@rg.prompt +def summarize(text: str) -> str: + """ + Summarize the following: + + {{ text }} + + Be concise. + """ + +print(summarize.template) + +# Summarize the following: +# +# {{ text }} +# +# Be concise. +# +# Produce the following output: +# +# +``` + + +In other words, you can define how inputs will be included in the prompt by passing them inside the docstring in jinja2 formats, or let Rigging handle this for you by ommiting them. + +### Outputs and Context + +In the example above, you'll notice that defining our function to output a `str` results in the following text be appended to the prompt template: + +```python No docstring +# Produce the following output: +# +# +``` + +This is pretty light on context, and we can improve this by updating our signature with a `Ctx` annotation: + +```python +from typing import Annotated +import rigging as rg + +Summary = Annotated[str, rg.Ctx(tag="summary", example="[2-3 sentences]")] + +@rg.prompt +def summarize(text: Annotated[str, rg.Ctx(tag="long-text")]) -> Summary: + """Summarize this text.""" + +print(summarize.template) + +# Summarize this text. +# +# {{ long_text }} +# +# Produce the following output: +# +# [2-3 sentences] +``` + +We can apply `Ctx` annotations to any of the inputs and outputs of a prompt. We can override the xml tag, provide an example, and add prefix text. + +Output processing is optional, and can be omitted by returning a `Chat` object from the wrapped function. This allows you to do with the generated output as you please. + +```python +import rigging as rg + +@rg.prompt +def summarize(text: str) -> rg.Chat: + """Summarize this text.""" + +print(summarize.template) + +# Summarize this text. +# +# {{ text }} +``` + +### Complex Outputs + +You can also define more complex outputs by using a rigging model, list, tuple, or dataclass. Not every construction will be supported, and we attempt to pre-validate the output structure to ensure it can be processed correctly. + + +```python Model +import rigging as rg + +class User(rg.Model): + name: str + email: str + age: int + +@rg.prompt +def generate_user() -> User: + """Generate a fake test user.""" + +print(generate_user.template) + +# Generate a fake test user. +# +# Produce the following output: +# +# +# +# +# +# +``` + +```python List of models +import rigging as rg + +class User(rg.Model): + name: str + email: str + age: int + +@rg.prompt +def generate_users(count: int = 3) -> list[User]: + """Generate fake test users.""" + +print(generate_users.template) + +# Generate fake test users. +# +# {{ count }} +# +# Produce the following output for each item: +# +# +# +# +# +# +``` + +```python Dataclass +from dataclasses import dataclass +import rigging as rg + +@dataclass +class User: + name: str + email: str + age: int + +@rg.prompt +def generate_user() -> User: + """Generate a fake test user.""" + +print(generate_user.template) + +# Generate a fake test user. +# +# Produce the following outputs: +# +# +# +# +# +# +``` + +```python Tuple +import rigging as rg + +name = Annotated[str, rg.Ctx(tag="name")] +email = Annotated[str, rg.Ctx(tag="email")] +age = Annotated[int, rg.Ctx(tag="age")] + +@rg.prompt +def generate_user() -> tuple[name, email, age]: + """Generate a fake test user.""" + +print(generate_user.template) + +# Generate a fake test user. +# +# Produce the following outputs: +# +# +# +# +# +# +``` + + +You can also embed a `Chat` object inside other objects, which will be excluded from any prompt guidance, but supplied the value when the prompt is executed. This is great for gathering both structured data and the original chat. + + +```python Chat in Tuple +import rigging as rg + +joke = Annotated[str, rg.Ctx(tag="joke")] + +@rg.prompt +def tell_joke() -> tuple[joke, rg.Chat]: + """Tell a joke.""" + +print(tell_joke.template) + +# Tell a joke. +# +# Produce the following output: +# +# +``` + +```python Chat in Dataclass +from dataclasses import dataclass +import rigging as rg + +@dataclass +class Joke: + setup: str + punchline: str + chat: rg.Chat + +@rg.prompt +def tell_joke() -> Joke: + """Tell a joke.""" + +print(tell_joke.template) + +# Tell a joke. +# +# Produce the following outputs: +# +# +# +# +``` + + +## Rendering Prompts + +In addition to templates, you can use `.render` with valid inputs to view the exact prompt as it will be sent to a generator. You can also use this to pass your prompt into pipelines at your discretion. + +```python +from typing import Annotated +import rigging as rg + +email = Annotated[str, rg.Ctx(tag="email")] + +@rg.prompt +def convert_to_email(name: str, top: int = 5) -> list[email]: + """Convert this name into the best {{ top }} email addresses.""" + +print(convert_to_email.render("John Doe")) + +# Convert this name into the best 5 email addresses. +# +# John Doe +# +# Produce the following output for each item: +# +# +``` + +## Running Prompts + +Prompt objects expose the following methods for execution: + +- `Prompt.run()` (Aliased with `__call__`) +- `Prompt.run_many()` +- `Prompt.run_over()` + +*(Available if the prompt was supplied/bonded to a pipeline or generator)* + +You can also bind a prompt at runtime with any of the following: + +- `Prompt.bind()` +- `Prompt.bind_many()` +- `Prompt.bind_over()` + + +Everything configured on a pipeline or generator will be used when running the prompt. Watch/Then/Map callbacks, tools, and generate params can all be used to alter the behavior of the prompt. + +In general, you should consider prompts as producers of user messages, which will be passed to `.fork()`, then handle the parsing of outputs. + + + +```python Run Standalone +import rigging as rg + +@rg.prompt(generator_id="claude-3-sonnet-20240229") +def write_code(description: str, language: str = "python") -> str: + """Write a single function.""" + +code = await write_code("Calculate the factorial of a number.") +``` + +```python Run with Pipeline +from typing import Annotated +import rigging as rg + +pipeline = ( + rg.get_generator("claude-3-sonnet-20240229") + .chat([ + {"role": "system", "content": "You are a senior software developer"} + ]) +) + +code_str = Annotated[str, rg.Ctx(tag="code")] + +@rg.prompt +def write_code(description: str, language: str = "python") -> code_str: + """Write a single function.""" + +code = await write_code.bind(pipeline)("Calculate the factorial of a number.") +``` + +```python Run Manually +import rigging as rg + +pipeline = ( + rg.get_generator("claude-3-sonnet-20240229") + .chat([ + {"role": "system", "content": "You are a senior software developer"} + ]) +) + +@rg.prompt +def write_code(description: str, language: str = "python") -> rg.Chat: + """Write a single function.""" + +prompt = write_code.render("Calculate the factorial of a number.") + +chat = await pipeline.fork(prompt).run() +``` + \ No newline at end of file diff --git a/docs/topics/serialization.md b/docs/topics/serialization.md deleted file mode 100644 index 9d05e98..0000000 --- a/docs/topics/serialization.md +++ /dev/null @@ -1,178 +0,0 @@ -# Serialization - -The following objects in Rigging have great serialization support for storage and retrieval: - -- [`Chat`][rigging.chat.Chat] -- [`Completion`][rigging.completion.Completion] -- [`Generator`][rigging.generator.Generator] -- [`Model`][rigging.model.Model] - -Most of this stems from our use of Pydantic for core models, and we've included some helpful -fields for reconstructing Chats and Completions. - -## JSON Serialization - -Let's build a joke pipeline and serialize the final chat into JSON. - -=== "Serialization Code" - - ```py - import rigging as rg - - class Joke(rg.Model): - content: str - - chat = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat(f"Provide 3 jokes each between {Joke.xml_tags()} tags.") - .meta(tags=['joke']) - .with_(temperature=1.25) - .run() - ) - - chat.last.parse_set(Joke) - - serialized = chat.model_dump_json(indent=2) - print(serialized) - ``` - -=== "Serialized JSON" - - ```json - { - "uuid": "891c3834-2588-4652-8371-e9746086fd46", - "timestamp": "2024-05-10T11:44:15.501326", - "messages": [ - { - "role": "user", - "parts": [], - "content": "Provide 3 jokes each between tags." - } - ], - "generated": [ - { - "role": "assistant", - "parts": [ - { - "model": { - "content": " Why was the math book sad? Because it had too many problems. " - }, - "slice_": [ - 0, - 75 - ] - }, - { - "model": { - "content": " I told my wife she should embrace her mistakes. She gave me a hug. " - }, - "slice_": [ - 76, - 157 - ] - }, - { - "model": { - "content": " Why did the scarecrow win an award? Because he was outstanding in his field. " - }, - "slice_": [ - 158, - 249 - ] - } - ], - "content": " Why was the math book sad? Because it had too many problems. \n I told my wife she should embrace her mistakes. She gave me a hug. \n Why did the scarecrow win an award? Because he was outstanding in his field. " - } - ], - "metadata": { - "tags": [ - "joke" - ] - }, - "generator_id": "litellm!gpt-3.5-turbo,temperature=1.25" - } - ``` - -You'll notice that every Chat gets a unique `id` field to help track them in a datastore like Elastic or Pandas. We also -assign a `timestamp` to understand when the generation took place. We are also taking advantage of the -[`.meta()`][rigging.chat.ChatPipeline.meta] to add a tracking tag for filtering later. - -## JSON Deserialization - -The JSON has everything required to reconstruct a Chat including a `generator_id` dynamically -constructed to perserve the parameters used to create the generated message(s). We can now -deserialize a chat from a datastore, and immediately step back into a -[`ChatPipeline`][rigging.chat.ChatPipeline] for exploration. - -```py -chat = rg.Chat.model_validate_json(serialized) -print(chat.conversation) -# [user]: Provide 3 jokes each between tags. - -# [assistant]: -# Why was the math book sad? Because it had too many problems. -# I told my wife she should embrace her mistakes. She gave me a hug. -# Why did the scarecrow win an award? Because he was outstanding in his field. - -continued = chat.continue_("Please explain the first joke to me.").run() -print(continued.last) -# [assistant]: In the first joke, the pun is based on the double meaning of the word "problems." -# The math book is described as being sad because it has "too many problems," which could be -# interpreted as having both mathematical problems (equations to solve) and emotional difficulties. -# This play on words adds humor to the joke. -``` - -## Pandas DataFrames - -Rigging also has helpers in the [`rigging.data`][] module for performing conversions -between Chat objects and other storage formats like Pandas. In [`chats_to_df`][rigging.data.chats_to_df] -the messages are flattened and stored with a `chat_id` column for grouping. -[`df_to_chats`][rigging.data.df_to_chats] allows you to reconstruct a list of Chat objects back from a DataFrame. - -```py -import rigging as rg - -chats = ( - await - rg.get_generator("claude-3-haiku-20240307") - .chat("Write me a haiku.") - .run_many(3) -) - -df = rg.data.chats_to_df(chats) -# or -df = chats.to_df() - -print(df.info()) - -# RangeIndex: 6 entries, 0 to 5 -# Data columns (total 9 columns): -# # Column Non-Null Count Dtype -# --- ------ -------------- ----- -# 0 chat_id 6 non-null string -# 1 chat_metadata 6 non-null string -# 2 chat_generator_id 6 non-null string -# 3 chat_timestamp 6 non-null datetime64[ms] -# 4 generated 6 non-null bool -# 5 role 6 non-null category -# 6 parts 6 non-null string -# 7 content 6 non-null string -# 8 message_id 6 non-null string -# dtypes: bool(1), category(1), datetime64[ms](1), string(6) - -df.content.apply(lambda x: len(x)).mean() - -# 60.166666666666664 - -back = rg.data.df_to_chats(df) -print(back[0].conversation) - -# [user]: Write me a haiku. -# -# [assistant]: Here's a haiku for you: -# -# Gentle breeze whispers, -# Flowers bloom in vibrant hues, -# Nature's simple bliss. -``` \ No newline at end of file diff --git a/docs/topics/serialization.mdx b/docs/topics/serialization.mdx new file mode 100644 index 0000000..6cd14ad --- /dev/null +++ b/docs/topics/serialization.mdx @@ -0,0 +1,171 @@ +--- +title: "Serialization" +description: "Serialization and Deserialization of Rigging objects" +public: true +--- + +The following objects in Rigging have great serialization support for storage and retrieval: + +- `Chat` +- `Completion` +- `Generator` +- `Model` + +Most of this stems from our use of Pydantic for core models, and we've included some helpful fields for reconstructing Chats and Completions. + +## JSON Serialization + +Let's build a joke pipeline and serialize the final chat into JSON. + + +```python Serialization +import rigging as rg + +class Joke(rg.Model): + content: str + +chat = ( + await + rg.get_generator("gpt-3.5-turbo") + .chat(f"Provide 3 jokes each between {Joke.xml_tags()} tags.") + .meta(tags=['joke']) + .with_(temperature=1.25) + .run() +) + +chat.last.parse_set(Joke) + +serialized = chat.model_dump_json(indent=2) +print(serialized) +``` + +```json Serialized JSON +{ +"uuid": "891c3834-2588-4652-8371-e9746086fd46", +"timestamp": "2024-05-10T11:44:15.501326", +"messages": [ + { + "role": "user", + "parts": [], + "content": "Provide 3 jokes each between tags." + } +], +"generated": [ + { + "role": "assistant", + "parts": [ + { + "model": { + "content": " Why was the math book sad? Because it had too many problems. " + }, + "slice_": [ + 0, + 75 + ] + }, + { + "model": { + "content": " I told my wife she should embrace her mistakes. She gave me a hug. " + }, + "slice_": [ + 76, + 157 + ] + }, + { + "model": { + "content": " Why did the scarecrow win an award? Because he was outstanding in his field. " + }, + "slice_": [ + 158, + 249 + ] + } + ], + "content": " Why was the math book sad? Because it had too many problems. \n I told my wife she should embrace her mistakes. She gave me a hug. \n Why did the scarecrow win an award? Because he was outstanding in his field. " + } +], +"metadata": { + "tags": [ + "joke" + ] +}, +"generator_id": "litellm!gpt-3.5-turbo,temperature=1.25" +} +``` + + +You'll notice that every Chat gets a unique `id` field to help track them in a datastore like Elastic or Pandas. We also assign a `timestamp` to understand when the generation took place. We are also taking advantage of the `.meta()` rigging.chat.ChatPipeline.meta to add a tracking tag for filtering later. + +## JSON Deserialization + +The JSON has everything required to reconstruct a Chat including a `generator_id` dynamically constructed to perserve the parameters used to create the generated message(s). We can now deserialize a chat from a datastore, and immediately step back into a `ChatPipeline` for exploration. + +```python +chat = rg.Chat.model_validate_json(serialized) +print(chat.conversation) +# [user]: Provide 3 jokes each between tags. + +# [assistant]: +# Why was the math book sad? Because it had too many problems. +# I told my wife she should embrace her mistakes. She gave me a hug. +# Why did the scarecrow win an award? Because he was outstanding in his field. + +continued = chat.continue_("Please explain the first joke to me.").run() +print(continued.last) +# [assistant]: In the first joke, the pun is based on the double meaning of the word "problems." +# The math book is described as being sad because it has "too many problems," which could be +# interpreted as having both mathematical problems (equations to solve) and emotional difficulties. +# This play on words adds humor to the joke. +``` + +## Pandas DataFrames + +Rigging also has helpers in the `rigging.data` module for performing conversions between Chat objects and other storage formats like Pandas. In `chats_to_df` the messages are flattened and stored with a `chat_id` column for grouping. `df_to_chats` allows you to reconstruct a list of Chat objects back from a DataFrame. + +```python +import rigging as rg + +chats = ( + await + rg.get_generator("claude-3-haiku-20240307") + .chat("Write me a haiku.") + .run_many(3) +) + +df = rg.data.chats_to_df(chats) +# or +df = chats.to_df() + +print(df.info()) + +# RangeIndex: 6 entries, 0 to 5 +# Data columns (total 9 columns): +# # Column Non-Null Count Dtype +# --- ------ -------------- ----- +# 0 chat_id 6 non-null string +# 1 chat_metadata 6 non-null string +# 2 chat_generator_id 6 non-null string +# 3 chat_timestamp 6 non-null datetime64[ms] +# 4 generated 6 non-null bool +# 5 role 6 non-null category +# 6 parts 6 non-null string +# 7 content 6 non-null string +# 8 message_id 6 non-null string +# dtypes: bool(1), category(1), datetime64[ms](1), string(6) + +df.content.apply(lambda x: len(x)).mean() + +# 60.166666666666664 + +back = rg.data.df_to_chats(df) +print(back[0].conversation) + +# [user]: Write me a haiku. +# +# [assistant]: Here's a haiku for you: +# +# Gentle breeze whispers, +# Flowers bloom in vibrant hues, +# Nature's simple bliss. +``` \ No newline at end of file diff --git a/docs/topics/tools.md b/docs/topics/tools.md deleted file mode 100644 index 54ad302..0000000 --- a/docs/topics/tools.md +++ /dev/null @@ -1,242 +0,0 @@ -# Tools - -Rigging supports the concept of tools through 2 implementations: - -- **'API' Tools**: These are API-level tool definitions which require a support from a model provder. -- **'Native' Tools**: These are internally defined, parsed, and handled by Rigging (the original implementation). - -In most cases, users should opt for API tools with better provider integrations and performance. - -Regardless of tool type, the [`ChatPipeline.using()`][rigging.chat.ChatPipeline.using] method should be -used to register tools for use during generation. - -=== "API Tools" - - ```py - from typing import Annotated - import requests - import rigging as rg - - def get_weather(city: Annotated[str, "The city name to get weather for"]) -> str: - "A tool to get the weather for a location" - try: - city = city.replace(" ", "+") - return requests.get(f"http://wttr.in/{city}?format=2").text - except: - return "Failed to call the API" - - chat = ( - await - rg.get_generator("gpt-4o-mini") - .chat("How is the weather in london?") - .using(get_weather) - .run() - ) - - # [user]: How is the weather in london? - - # [assistant]: - # |- get_weather({"city":"London"}) - - # [tool]: 🌦 🌡️+6°C 🌬️↘35km/h - - # [assistant]: The weather in London is currently overcast with light rain ... - ``` - -=== "Native Tools" - - ```py - from typing import Annotated - import requests - import rigging as rg - - class WeatherTool(rg.Tool): - name = "weather" - description = "A tool to get the weather for a location" - - def get_for_city(self, city: Annotated[str, "The city name to get weather for"]) -> str: - try: - city = city.replace(" ", "+") - return requests.get(f"http://wttr.in/{city}?format=2").text - except: - return "Failed to call the API" - - chat = ( - await - rg.get_generator("gpt-4o-mini") - .chat("How is the weather in london?") - .using(WeatherTool()) - .run() - ) - ``` - - -## API Tools - -API tools are defined as standard callables (async supported) and get wrapped in the -[`rg.ApiTool`][rigging.tool.ApiTool] class before being used during generation. - -We use Pydantic to introspect the callable and extract schema information from the signature with some great benefits: - -1. API-compatible schema information from any function -2. Robust argument validation for incoming inference data -3. Flexible type handling for BaseModels, Fields, TypedDicts, and Dataclasses - -Just after the tool is converted, we take the function schema and add it to the -[GenerateParams.tools][rigging.generator.GenerateParams] inside the `ChatPipeline`. - -```py -from typing_extensions import TypedDict -from typing import Annotated -from pydantic import Field -import rigging as rg - -class Filters(TypedDict): - city: Annotated[str | None, Field(description="The city to filter by")] - age: int | None - -def lookup_person(name: Annotated[str, "Full name"], filters: Filters) -> str: - "Search the database for a person" - ... - - -tool = rg.ApiTool(lookup_person) - -print(tool.name) -# lookup_person - -print(tool.description) -# Search the database for a person - -print(tool.schema) -# {'$defs': {'Filters': {'properties': ...} -``` - -Internally, we leverage [`ChatPipeline.then()`][rigging.chat.ChatPipeline.then] to handle responses from the model and -attempt to resolve tool calls before starting another generation loop. This means that when you pass the tool function -into your chat pipeline will define it's order amongst other callbacks like [`.then()`][rigging.chat.ChatPipeline.then] -and [`.map()`][rigging.chat.ChatPipeline.map] - -## Native Tools - -Much like models, native tools inherit from a base [`rg.Tool`][rigging.tool.Tool] class. These subclasses are required -to provide at least 1 function along with a name and description property to present to the LLM during generation. - -Every function you define and the parameters within are required to carry both type hints and annotations that -describe their function. - -```py -from typing import Annotated -import requests -import rigging as rg - -class WeatherTool(rg.Tool): - name = "weather" - description = "A tool to get the weather for a location" - - def get_for_city(self, city: Annotated[str, "The city name to get weather for"]) -> str: - try: - city = city.replace(" ", "+") - return requests.get(f"http://wttr.in/{city}?format=2").text - except: - return "Failed to call the API" -``` - -Integrating native tools into the generation process is as easy as passing an instantiation -of your tool class to the [`ChatPipeline.using()`][rigging.chat.ChatPipeline.using] method. - -```py -chat = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat("What is the weather in London?") - .using(WeatherTool(), force=True) # (1)! - .run() -) - -print(chat.last.content) -# The current weather in London is 57°F with a light breeze of 2mph. -``` - -1. The use of `force=True` here is optional, but results in the internal generation - ensuring at least one tool is called before the generation completes. - -If/when the LLM elects to emit a valid tool call in Riggings format, it will -side-step, process the arguments, ensure they conform to your function spec, -and execute the desired function. Results will be injected back into the chat -and the final message which does not include any tool calls will trigger the end -of the generation process. - -??? tip "Tool State" - - It's worth noting that tools are passed as instantiated classes into Rigging, - which means your tool is free to carry state about it's operations as time - progresses. Whether this is a good software design decision is up to you. - - -### Under the Hood - -If you are curious what is occuring "under the hood" (as you should), you can -print the entire conversation text and see our injected system prompt of -instructions for using a tool, along with the auto-generated XML description -of the `WeatherTool` we supplied to the model - -```xml -[system]: # Tool Use -In this environment you have access to a set of tools you can use to improve your responses. - -## Tool Call Format - - - - - - - - - -## Available Tools - - - - - - - - - - - - -You can use any of the available tools by responding in the call format above. The XML will be -parsed and the tool(s) will be executed with the parameters you provided. The results of each -tool call will be provided back to you before you continue the conversation. You can execute -multiple tool calls by continuing to respond in the format above until you are finished. -Function calls take explicit values and are independent of each other. Tool calls cannot share, -re-use, and transfer values between eachother. The use of placeholders is forbidden. - -The user will not see the results of your tool calls, only the final message of your conversation. -Wait to perform your full response until after you have used any required tools. If you intend to -use a tool, please do so before you continue the conversation. - - -[user]: What is the weather in London? - -[assistant]: - - London - - - -[user]: - - ☀️ 🌡️+57°F 🌬️→2mph - - - - -[assistant]: The current weather in London is 57°F with a light breeze of 2mph. -``` - -Every tool assigned to the `ChatPipeline` will be processed by calling [`.get_description()`][rigging.tool.Tool.get_description] -and a minimal tool-use prompt will be injected as, or appended to, the system message. \ No newline at end of file diff --git a/docs/topics/tools.mdx b/docs/topics/tools.mdx new file mode 100644 index 0000000..b33b3f8 --- /dev/null +++ b/docs/topics/tools.mdx @@ -0,0 +1,322 @@ +--- +title: "Tools" +description: "Define and use tools within Rigging pipelines." +public: true +--- + +Tools in Rigging allow language models to interact with external systems, execute code, or perform well-defined tasks during generation. Rigging v3 introduces a **unified tool system**, making it easier to define tools and control how they interact with different language models. + +For most uses, you can just build any function with type hints, and pass that into a pipeline: + +```python +import rigging as rg + +def add_numbers(a: int, b: int) -> int: + """Adds two numbers together.""" + return a + b + +chat = ( + await rg.get_generator("openai/gpt-4o-mini") + .chat("What is 2 + 3?") + .using(add_numbers) # Pass the function directly + .run() +) +``` + +## Defining Tools + +Rigging uses function signatures (type hints and docstrings) to automatically generate the necessary schema and description for the language model. If you'd like to make any modifications to your tool's name, description, or schema, you can use the `@tool` decorator and pass that into pipelines. + +### Using `@tool` for Functions + +Decorate any regular Python function (including static methods) with `@rigging.tool.tool` to make it usable by the Rigging framework. + +```python +import typing as t +from typing import Annotated +import rigging as rg +import requests + +@rg.tool +def get_weather(city: Annotated[str, "The city name to get weather for"]) -> str: + """Gets the current weather for a specified city.""" + try: + city = city.replace(" ", "+") + # Use a real weather API in practice + return requests.get(f"http://wttr.in/{city}?format=3").text.strip() + except Exception as e: + return f"Failed to get weather: {e}" + +# The 'get_weather' object is now a rigging.tool.Tool instance +print(get_weather.name) +# > get_weather +print(get_weather.description) +# > Gets the current weather for a specified city. +print(get_weather.parameters_schema) +# > {'type': 'object', 'properties': {'city': {'title': 'City', 'description': 'The city name to get weather for', 'type': 'string'}}, 'required': ['city']} +``` + +- Type hints are crucial for defining the parameters the model needs to provide. +- Use `typing.Annotated` to provide descriptions for parameters where needed. +- The function's docstring is used as the tool's description for the model. + +### Using `@tool_method` for Class Methods + +If your tool logic resides within a class and needs access to instance state (`self`), use the `@rigging.tool.tool_method` decorator instead. + +```python +import typing as t +from typing import Annotated +import rigging as rg + +class UserProfileManager: + def __init__(self, api_key: str): + self.api_key = api_key + self._users = {"123": {"name": "Alice", "email": "alice@example.com"}} # Dummy data + + @rg.tool_method(name="fetch_user_profile") # Optional: override name + def get_profile(self, user_id: Annotated[str, "The ID of the user to fetch"]) -> dict[str, t.Any] | str: + """Retrieves a user's profile information using their ID.""" + # Use self.api_key etc. here + profile = self._users.get(user_id) + if profile: + return profile + return f"User with ID {user_id} not found." + +# Instantiate the class +profile_manager = UserProfileManager(api_key="dummy_key") + +# Access the tool method *through the instance* +user_profile_tool = profile_manager.get_profile + +print(user_profile_tool.name) +# > fetch_user_profile +print(user_profile_tool.description) +# > Retrieves a user's profile information using their ID. +``` + + +`@tool_method` correctly handles the `self` argument, ensuring it's not included in the schema presented to the language model. Use `@tool` for static methods if they don't require `self`. + + +### Underlying Mechanism + +Both decorators use `Tool.from_callable()` internally to wrap your function/method into a `Tool` object. This object holds the function, its generated schema, name, and description. + +## Using Tools in Pipelines + +To make tools available during generation, pass them to the `ChatPipeline.using()` method. + +```python +import rigging as rg + +# Assume get_weather tool is defined as above +# Assume profile_manager.get_profile tool is defined and instantiated + +chat = ( + await rg.get_generator("openai/gpt-4o-mini") + .chat("What's the weather like in Paris, and what's Alice's email (user ID 123)?") + .using( + get_weather, # Pass the decorated function directly + profile_manager.get_profile, # Pass the bound method from the instance + mode="auto", # Let Rigging choose the best invocation method (API or native) + max_depth=5 # Limit recursive tool calls (e.g., if a tool calls another prompt) + ) + .run() +) + +print(chat.conversation) +``` + +### Tool Invocation Modes (`mode`) + +The `mode` parameter in `.using()` controls how Rigging interacts with the language model for tool calls: + +- **`"auto"` (Default):** Rigging checks if the model provider supports API-level function calling (like OpenAI, Anthropic, Google). If yes, it uses that native, efficient mechanism. If not, it falls back to `"xml"`. +- **`"api"`:** Forces the use of the provider's function calling API. Will fail if the provider doesn't support it. +- **`"xml"`:** Rigging injects instructions and an XML schema into the prompt, telling the model how to format its output to request a tool call using specific XML tags. Rigging parses this XML. +- **`"json-in-xml"`:** Similar to `"xml"`, but the model is instructed to place a JSON object containing the arguments within the XML tags. + +Generally, `"auto"` is recommended as it leverages the most efficient method available. + +### Controlling Recursion (`max_depth`) + +The `max_depth` parameter limits how many levels deep tool calls can go. If a tool itself triggers another prompt that uses tools, this prevents infinite loops. + +## MCP Integration (Model Context Protocol) + +The [Model Context Protocol (MCP)](https://github.com/model-context-protocol/specification) is an open standard for language models to interact with external tools and services. Rigging provides a client to connect to MCP-compliant servers. + +Use the `rigging.tool.mcp` function, specifying the transport method (`"stdio"` or `"sse"`) and the connection parameters. It returns an *async context manager*. + +### Using `stdio` (Standard Input/Output) + +Connect to an MCP server launched as a local process that communicates over standard input/output. + +```python +import rigging as rg + +# Example: Assuming 'my-mcp-server' is an executable that starts the MCP server +command = "my-mcp-server" +args = ["--port", "stdio"] # Example arguments + +async with rg.tool.mcp("stdio", command=command, args=args) as mcp_client: + # mcp_client.tools contains the list of tools discovered from the server + print(f"Discovered {len(mcp_client.tools)} MCP tools via stdio.") + + if mcp_client.tools: + chat = ( + await rg.get_generator("openai/gpt-4o-mini") + .chat("Use the MCP tool Y.") # Adjust prompt based on available tools + .using(*mcp_client.tools) + .run() + ) + print(chat.conversation) + else: + print("No MCP tools found.") + +``` + +### Using `sse` (Server-Sent Events) + +Connect to an MCP server exposed via an HTTP endpoint using Server-Sent Events. + +```python +import rigging as rg + +# URL of the MCP SSE endpoint +MCP_SSE_URL = "http://localhost:8001/mcp" # Example URL + +async with rg.tool.mcp("sse", url=MCP_SSE_URL) as mcp_client: + # mcp_client.tools contains the list of tools discovered from the server + print(f"Discovered {len(mcp_client.tools)} MCP tools via SSE.") + + if mcp_client.tools: + chat = ( + await rg.get_generator("openai/gpt-4o-mini") + .chat("Use the MCP tool Z.") # Adjust prompt based on available tools + .using(*mcp_client.tools) + .run() + ) + print(chat.conversation) + else: + print("No MCP tools found.") +``` + +The `mcp` context manager handles the connection, tool discovery, and communication with the MCP server. Inside the `async with` block, `mcp_client.tools` provides the list of discovered `Tool` objects ready to be used with `.using()`. + +## Robopages + +[Robopages](https://github.com/context-labs/robopages) is a framework for building and hosting tool-enabled "pages" or APIs. Rigging can dynamically fetch the available tools from a running Robopages server and make them available to your language model. + +Use the `rigging.tool.robopages` function to connect to a Robopages endpoint and retrieve its tools. + +```python +import rigging as rg + +# URL of your running Robopages server +ROBOPAGES_URL = "http://localhost:8080" # Example URL + +try: + # Fetch tools from the server + robopages_tools = rg.tool.robopages(ROBOPAGES_URL) + + # Use the fetched tools in a pipeline + chat = ( + await rg.get_generator("openai/gpt-4o-mini") + .chat("Use the available Robopages tool to do X.") # Adjust prompt based on available tools + .using(*robopages_tools) # Unpack the list of tools + .run() + ) + print(chat.conversation) + +except Exception as e: + print(f"Failed to connect to Robopages or use tools: {e}") + # Handle connection errors or cases where no tools are found +``` + +This fetches the tool definitions (name, description, parameters) from the Robopages server. When the language model requests one of these tools, Rigging sends the request back to the Robopages server for execution. + +## Tool Execution Flow + +When you call `.using()`: + +1. Rigging prepares the tool definitions based on the selected `mode`. +2. For `"api"` mode, the definitions (`ApiToolDefinition`) are passed to the model provider's API. +3. For `"xml"` or `"json-in-xml"` modes, Rigging injects the tool schemas and usage instructions into the system prompt. +4. During generation, if the model decides to call a tool: + * In `"api"` mode, the provider returns structured tool call information (`ApiToolCall`). + * In native modes, Rigging parses the model's output for the expected XML format (`XmlToolCall` or `JsonInXmlToolCall`). +5. Rigging validates the arguments provided by the model against the tool's signature using Pydantic. +6. Your original Python function/method (or the relevant MCP/Robopages call) is executed with the validated arguments. +7. The return value is formatted into a message (`Message` with `role="tool"` for API calls, or a user message containing `NativeToolResult` for native calls) and sent back to the model. +8. The generation process continues until the model produces a final response without tool calls or the `max_depth` is reached. + +### Error Handling + +You can control how errors raised within your tool function are handled using the `catch` parameter in `@tool` / `@tool_method`: + +- `catch=False` (Default): Errors propagate, potentially failing the pipeline run (unless caught by `pipeline.catch()`). +- `catch=True`: Catches *any* exception, converts it to an error message, and sends that back to the model as the tool result. +- `catch={ValueError, TypeError}`: Catches only specified exception types. + +```python +@rg.tool(catch=True) # Catch any exception +def potentially_failing_tool(input: str) -> str: + if input == "fail": + raise ValueError("Intentional failure") + return "Success" +``` + +## Tool State + +Since tools defined with `@tool_method` operate on class instances, they can maintain and modify state across multiple calls within a single pipeline execution. + +```python +class CounterTool: + def __init__(self): + self.count = 0 + + @rg.tool_method + def increment(self, amount: int = 1) -> str: + """Increments the counter and returns the new value.""" + self.count += amount + return f"Counter is now {self.count}" + +# Usage +counter = CounterTool() + +chat = ( + await + rg.get_generator(...) + .chat("Increment twice") + .using(counter.increment) + .run() +) +``` + +You could also do the same thing by returning a stateful tool function defined in a closure: + +```python +def counter_tool(): + count = 0 + + @rg.tool + def increment(amount: int = 1) -> str: + nonlocal count + count += amount + return f"Counter is now {count}" + + return increment + +# Usage +counter = counter_tool() + +chat = ( + await + rg.get_generator(...) + .chat("Increment twice") + .using(counter.increment) + .run() +) +``` \ No newline at end of file diff --git a/docs/topics/tracing.md b/docs/topics/tracing.mdx similarity index 53% rename from docs/topics/tracing.md rename to docs/topics/tracing.mdx index 38b1aae..7e9cd1d 100644 --- a/docs/topics/tracing.md +++ b/docs/topics/tracing.mdx @@ -1,17 +1,16 @@ -# Tracing +--- +title: "Tracing" +description: "Trace all the internal behaviors of rigging with OpenTelemetry." +public: true +--- -Rigging integrates with the [Logfire](https://logfire.pydantic.dev/docs/) library for exposing tracing information -about execution. Specifically we use the logfire-api no-op package, making it optional for users with no overhead -if you don't need it. +Rigging integrates with the [Logfire](https://logfire.pydantic.dev/docs/) library for exposing tracing information about execution. Specifically we use the logfire-api no-op package, making it optional for users with no overhead if you don't need it. -Logfire is capable of reporting trace information to any Open Telemetry compatible system, and provides some -convient abstractions on top of the standard open-telemetry-sdk which we like. If the `logfire` package is installed and -configured, details about pipelines, prompts, and tools will be traced during rigging use. +Logfire is capable of reporting trace information to any Open Telemetry compatible system, and provides some convient abstractions on top of the standard open-telemetry-sdk which we like. If the `logfire` package is installed and configured, details about pipelines, prompts, and tools will be traced during rigging use. -You can configure Logfire to use [alternative backends](https://logfire.pydantic.dev/docs/how-to-guides/alternative-backends/) -as needed to integrate with your preferred tracing stack. +You can configure Logfire to use [alternative backends](https://logfire.pydantic.dev/docs/how-to-guides/alternative-backends/) as needed to integrate with your preferred tracing stack. -```py +```python import rigging as rg import logfire @@ -39,24 +38,17 @@ await summarize.run_many(3, text) # 23:46:32.874 Watch with rigging.watchers.write_chats_to_jsonl() ``` -???+ "What's Stored?" - - Rigging will attach call parameters and results for both tools and prompt functions, as well - as finalized chat objects at the end of a pipeline. Logfire will serialize these items as - JSON values inside attributes, and include a dynamic json schema for reference. When using - their platform, these items deserialize into the web view directly. + +Rigging will attach call parameters and results for both tools and prompt functions, as well as finalized chat objects at the end of a pipeline. Logfire will serialize these items as JSON values inside attributes, and include a dynamic json schema for reference. When using their platform, these items deserialize into the web view directly. + ![Logfire trace](../assets/tracing_logfire.png) ## Inference Tracing -We've opted to exclude tracing at the generator level in Rigging (for now) and focus on -instrumenting higher-order abstractions like pipelines, tools, etc. which are specific to the framework. +We've opted to exclude tracing at the generator level in Rigging (for now) and focus on instrumenting higher-order abstractions like pipelines, tools, etc. which are specific to the framework. -There are a suite of powerful instrumentation libraries (including Logfire!) which will add tracing -to underlying libraries like LiteLLM (recommended), OpenAI, Anthropic, VertexAI, etc. These snap -right into the tracing spans from rigging, and provide insight into the raw inference -traffic before it's sent to API endpoints and inference libraries. +There are a suite of powerful instrumentation libraries (including Logfire!) which will add tracing to underlying libraries like LiteLLM (recommended), OpenAI, Anthropic, VertexAI, etc. These snap right into the tracing spans from rigging, and provide insight into the raw inference traffic before it's sent to API endpoints and inference libraries. - [LiteLLM - Logfire](https://docs.litellm.ai/docs/observability/logfire_integration) - [LiteLLM - OpenTelemetry](https://docs.litellm.ai/docs/observability/opentelemetry_integration) @@ -65,7 +57,7 @@ traffic before it's sent to API endpoints and inference libraries. Here is an example of adding LiteLLM tracing on top of rigging: -```py +```python import rigging as rg import logfire import litellm @@ -78,5 +70,4 @@ litellm.callbacks = ["logfire"] # ... ``` -1. As of writing, LiteLLM requires this environment var, even if empty -and logfire is managing tokens for you +*1. As of writing, LiteLLM requires this environment var, even if empty and logfire is managing tokens for you* diff --git a/docs/topics/workflow.md b/docs/topics/workflow.md deleted file mode 100644 index e16d662..0000000 --- a/docs/topics/workflow.md +++ /dev/null @@ -1,38 +0,0 @@ -## Workflow - -1. Get a [`Generator`][rigging.generator.Generator] object - usually with [`get_generator()`][rigging.generator.get_generator]. -2. Call [`generator.chat()`][rigging.generator.Generator.chat] to produce a [`ChatPipeline`][rigging.chat.ChatPipeline] and ready it for generation. -3. Call [`pipeline.run()`][rigging.chat.ChatPipeline.run] to kick off generation and get your final [`Chat`][rigging.chat.Chat] object. - -[`ChatPipeline`][rigging.chat.ChatPipeline] objects hold any messages waiting to be delivered to an LLM in exchange -for a new response message. These objects are also where most of the power in rigging comes from. You'll build a -generation pipeline with options, parsing, callbacks, etc. After prep this pipeline is used to make a -final [`Chat`][rigging.chat.Chat] which holding all messages prior to generation ([`.prev`][rigging.chat.Chat.prev]) -and after generation ([`.next`][rigging.chat.Chat.next]). - -You should think of [`ChatPipeline`][rigging.chat.ChatPipeline] objects like the configurable pre-generation step -with calls like [`.with_()`][rigging.chat.ChatPipeline.with_], [`.apply()`][rigging.chat.ChatPipeline.apply], -[`.until()`][rigging.chat.ChatPipeline.until], [`.using()`][rigging.chat.ChatPipeline.using], etc. Once you call one -of the many [`.run()`][rigging.chat.ChatPipeline.run] functions, the generator is used to produce the next -message (or many messages) based on the prior context and any constraints you have in place. Once you have a -[`Chat`][rigging.chat.Chat] object, the interation is "done" and you can inspect and operate on the messages. - -??? tip "Chats vs Completions" - - Rigging supports both Chat objects (messages with roles in a "conversation" format), as well - as raw text completions. While we use Chat objects in most of our examples, you can check - out the [Completions](completions.md) section to learn more about their feature parity. - -You'll often see us use functional styling chaining as most of our -utility functions return the object back to you. - -```go -chat = ( - await - generator.chat(...) - .using(...) - .until(...) - .with_(...) - .run() -) -``` \ No newline at end of file diff --git a/docs/topics/workflow.mdx b/docs/topics/workflow.mdx new file mode 100644 index 0000000..aa338b6 --- /dev/null +++ b/docs/topics/workflow.mdx @@ -0,0 +1,77 @@ +--- +title: "Workflow" +description: "How to use Rigging to generate messages." +public: true +--- + +There are two main ways to use Rigging: "prompts" and "pipelines". + +Prompts are the most friendly way to get started, and you can get a lot done with them before you dive into pipelines. However, pipelines are where the real power of Rigging lies, and prompts are just a convenient way to leverage pipelines. + +## Using Prompts + +1. Establish a function decorated with `@prompt` with the inference model you want to use. +2. Call the function just like normal and recieve structured data back. + +```python +import rigging as rg + +@rg.prompt(generator_id="claude-3-5-sonnet-latest") +async def get_authors(count: int = 3) -> list[str]: + """Provide famous authors.""" + +print(await get_authors()) + +# ['William Shakespeare', 'J.K. Rowling', 'Jane Austen'] +``` + +Underneath, Rigging will produce a `Generator` with `get_generator("claude-3-5-sonnet-latest")`, prepare a small template that will establish the required context and output structure, pass it into a new `ChatPipeline`, run the generation process, and parse the output into our structured list with `ChatPipeline.then()`. + +If you want to see the resulting `Chat` object, you can set that as your return value and no output parsing + +```python +@rg.prompt(generator_id="claude-3-5-sonnet-latest") +async def get_authors(count: int = 3) -> rg.Chat: + ... +``` + +Now the prompt is only responsible for abstracting the generator, pipeline, and content for you. You can also use a nested object like a `tuple` and include both your structured data and the `Chat` object. + +```python +@rg.prompt(generator_id="claude-3-5-sonnet-latest") +async def get_authors(count: int = 3) -> tuple[list[str], rg.Chat]: + """Provide famous authors.""" +``` + +This will return a tuple with the first element being the parsed output and the second element being raw `Chat` object. + +You can learn more about the `@prompt` decorator in the [Prompt Functions](/topics/) section. + +## Using Pipelines + +1. Get a `Generator` object - usually with `get_generator()`. +2. Call `generator.chat()`to produce a `ChatPipeline` and ready it for generation. +3. Call `pipeline.run()` to kick off generation and get your final `Chat` object. + +`ChatPipeline` objects hold any messages waiting to be delivered to an LLM in exchange for a new response message. These objects are also where most of the power in rigging comes from. You'll build a generation pipeline with options, parsing, callbacks, etc. After prep this pipeline is used to make a final `Chat` which holding all messages prior to generation (`.prev`) and after generation (`.next`). + +You should think of `ChatPipeline` objects like the configurable pre-generation step with calls like `.with_()`, `.apply()`, `.until()`, `.using()`, etc. Once you call one of the many `.run()`functions, the generator is used to produce the next message (or many messages) based on the prior context and any constraints you have in place. Once you have a `Chat` object, the interation is "done" and you can inspect and operate on the messages. + + +Rigging supports both Chat objects (messages with roles in a "conversation" format), as well as raw text completions. While we use Chat objects in most of our examples, you can check out the [Completions](/topics/completions) section to learn more about their feature parity. + + +You'll often see us use functional styling chaining as most of our utility functions return the object back to you. + +```python +chat = ( + await + generator.chat(...) + .using(...) # tools + .then(...) # follow up functions + .with_(...) # generation params + .run() +) +``` + +Learn more about the `ChatPipeline` object in the [Pipelines](/topics/pipelines) section. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index a9dc722..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,110 +0,0 @@ -site_name: Rigging -site_description: A lightweight LLM interaction framework -site_author: Dreadnode -site_url: https://rigging.dreadnode.io -repo_url: https://github.com/dreadnode/rigging - -nav: - - index.md - - Topics: - - Workflow: topics/workflow.md - - Models: topics/models.md - - Generators: topics/generators.md - - Chats and Messages: topics/chats-and-messages.md - - Prompt Functions: topics/prompt-functions.md - - Completions: topics/completions.md - - Callbacks and Mapping: topics/callbacks-and-mapping.md - - Iterating and Batching: topics/iterating-and-batching.md - - Tools: topics/tools.md - - Tracing: topics/tracing.md - - Serialization: topics/serialization.md - - Logging: topics/logging.md - - Migrations: topics/migrations.md - - Principles: topics/principles.md - - API: - - rigging.chat: api/chat.md - - rigging.completion: api/completion.md - - rigging.generator: api/generator.md - - rigging.model: api/model.md - - rigging.message: api/message.md - - rigging.prompt: api/prompt.md - - rigging.tool: api/tool.md - - rigging.data: api/data.md - - rigging.watchers: api/watchers.md - - rigging.parsing: api/parsing.md - - rigging.interact: api/interact.md - - rigging.logging: api/logging.md - - rigging.error: api/error.md - - rigging.util: api/util.md - - -theme: - logo: assets/logo_black.png - favicon: assets/logo_white.png - name: material - icon: - repo: fontawesome/brands/github - palette: - scheme: slate - primary: custom - features: - - content.code.copy - - content.code.annotate - - navigation.footer - - navigation.indexes - - navigation.sections - - navigation.expand - - navigation.path - - navigation.top - -plugins: - - search - - section-index - - social - - mkdocstrings: - handlers: - python: - paths: [rigging] - options: - docstring_options: - ignore_init_summary: true - docstring_section_style: list - heading_level: 3 - merge_init_into_class: true - show_signature_annotations: true - show_symbol_type_heading: true - show_symbol_type_toc: true - signature_crossrefs: true - -watch: - - rigging/ - -markdown_extensions: - - admonition - - pymdownx.highlight: - anchor_linenums: true - pygments_lang_class: true - - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.superfences - - pymdownx.details - - pymdownx.tabbed: - alternate_style: true - - toc: - permalink: "#" - -extra_css: - - stylesheets/extra.css - -extra_javascript: - - https://polyfill.io/v3/polyfill.min.js?features=es6 - -extra: - homepage: https://dreadnode.io - social: - - icon: fontawesome/brands/github - link: https://github.com/dreadnode - - icon: fontawesome/brands/twitter - link: https://twitter.com/dreadnode - - icon: fontawesome/brands/python - link: https://pypi.org/project/rigging/ \ No newline at end of file From 42c2b127dad8be31d9e5edea08bf53e5db281f44 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Wed, 7 May 2025 23:57:55 -0700 Subject: [PATCH 19/25] Add truncation for messages and tools. Version to 3.0.0. --- pyproject.toml | 2 +- rigging/message.py | 22 +++++++++++++++++++--- rigging/tool/base.py | 21 ++++++++++++++++++++- rigging/util.py | 19 +++++++++++++++++-- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 68b6802..6674f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rigging" -version = "3.0.0-rc.4" +version = "3.0.0" description = "LLM Interaction Framework" authors = ["Nick Landers "] license = "MIT" diff --git a/rigging/message.py b/rigging/message.py index 7a28a8e..edc55bb 100644 --- a/rigging/message.py +++ b/rigging/message.py @@ -30,7 +30,7 @@ from rigging.model import Model, ModelT from rigging.parsing import try_parse_many from rigging.tool.api import ApiToolCall -from rigging.util import AudioFormat, identify_audio_format, truncate_string +from rigging.util import AudioFormat, identify_audio_format, shorten_string, truncate_string Role = t.Literal["system", "user", "assistant", "tool"] """The role of a message. Can be 'system', 'user', 'assistant', or 'tool'.""" @@ -111,7 +111,7 @@ class ImageUrl(BaseModel): """Cache control entry for prompt caching.""" def __str__(self) -> str: - return f"" + return f"" @classmethod def from_file( @@ -241,7 +241,7 @@ def __str__(self) -> str: return ( f"" + f"data='{shorten_string(self.input_audio.data, 50)}'>" ) @classmethod @@ -655,6 +655,8 @@ def clone(self) -> "Message": self.role, copy.deepcopy(self.content_parts), parts=copy.deepcopy(self.parts), + tool_calls=copy.deepcopy(self.tool_calls), + tool_call_id=self.tool_call_id, ) def cache(self, cache_control: dict[str, str] | bool = True) -> "Message": # noqa: FBT002 @@ -705,6 +707,20 @@ def apply(self, **kwargs: str) -> "Message": new.content = template.safe_substitute(**kwargs) return new + def truncate(self, max_length: int, suffix: str = "\n[truncated]") -> "Message": + """ + Truncates the message content to a maximum length. + + Args: + max_length: The maximum length of the message content. + + Returns: + The truncated message. + """ + new = self.clone() + new.content = truncate_string(new.content, max_length, suf=suffix) + return new + def strip( self, model_type: type[Model], diff --git a/rigging/tool/base.py b/rigging/tool/base.py index 5ac781c..79a962b 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -73,6 +73,8 @@ class Tool(t.Generic[P, R]): - `True`: Catch all exceptions. - `list[type[Exception]]`: Catch only the specified exceptions. """ + truncate: int | None = None + """If set, the maximum number of characters to truncate any tool output to.""" _signature: inspect.Signature | None = field(default=None, init=False, repr=False) _type_adapter: TypeAdapter[t.Any] | None = field( @@ -100,6 +102,7 @@ def from_callable( name: str | None = None, description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, + truncate: int | None = None, ) -> te.Self: from rigging.prompt import Prompt @@ -194,6 +197,7 @@ def empty_func(*args, **kwargs): # type: ignore [no-untyped-def] # noqa: ARG001 parameters_schema=schema, fn=fn, catch=catch if isinstance(catch, bool) else set(catch), + truncate=truncate, ) self._signature = signature @@ -367,6 +371,9 @@ async def handle_tool_call( else: message.content_parts = [ContentText(text=str(result))] + if self.truncate: + message = message.truncate(self.truncate) + # If this is a native tool call, we should wrap up our # result in a NativeToolResult object to provide clarity to the # generator. Otherwise we can rely on the `tool` role and associated @@ -402,6 +409,7 @@ def tool( name: str | None = None, description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, + truncate: int | None = None, ) -> t.Callable[[t.Callable[P, R]], Tool[P, R]]: ... @@ -421,6 +429,7 @@ def tool( name: str | None = None, description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, + truncate: int | None = None, ) -> t.Callable[[t.Callable[P, R]], Tool[P, R]] | Tool[P, R]: """ Decorator for creating a Tool, useful for overriding a name or description. @@ -433,6 +442,7 @@ def tool( - `False`: Do not catch exceptions. - `True`: Catch all exceptions. - `list[type[Exception]]`: Catch only the specified exceptions. + truncate: If set, the maximum number of characters to truncate any tool output to. Returns: The decorated Tool object. @@ -453,7 +463,13 @@ def make_tool(func: t.Callable[..., t.Any]) -> Tool[P, R]: stacklevel=3, ) - return Tool.from_callable(func, name=name, description=description, catch=catch) + return Tool.from_callable( + func, + name=name, + description=description, + catch=catch, + truncate=truncate, + ) if func is not None: return make_tool(func) @@ -496,6 +512,7 @@ def tool_method( name: str | None = None, description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, + truncate: int | None = None, ) -> t.Callable[[t.Callable[t.Concatenate[t.Any, P], R]], ToolMethod[P, R]]: ... @@ -515,6 +532,7 @@ def tool_method( name: str | None = None, description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, + truncate: int | None = None, ) -> t.Callable[[t.Callable[t.Concatenate[t.Any, P], R]], ToolMethod[P, R]] | ToolMethod[P, R]: """ Decorator for creating a Tool from a class method. @@ -570,6 +588,7 @@ def wrapper(self: t.Any, *args: P.args, **kwargs: P.kwargs) -> R: name=name, description=description, catch=catch, + truncate=truncate, ) if func is not None: diff --git a/rigging/util.py b/rigging/util.py index d938db4..2b56e2f 100644 --- a/rigging/util.py +++ b/rigging/util.py @@ -150,16 +150,31 @@ def get_qualified_name(obj: t.Callable[..., t.Any]) -> str: # Formatting -def truncate_string(content: str, max_length: int, *, sep: str = "...") -> str: - """Return a string at most max_length characters long.""" +def shorten_string(content: str, max_length: int, *, sep: str = "...") -> str: + """Return a string at most max_length characters long by removing the middle of the string.""" if len(content) <= max_length: return content remaining = max_length - len(sep) + if remaining <= 0: + return sep + middle = remaining // 2 return content[:middle] + sep + content[-middle:] +def truncate_string(content: str, max_length: int, *, suf: str = "...") -> str: + """Return a string at most max_length characters long by removing the end of the string.""" + if len(content) <= max_length: + return content + + remaining = max_length - len(suf) + if remaining <= 0: + return suf + + return content[:remaining] + suf + + # List utilities From 613417a4931e203d5d91cacc62684f00345292ad Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 8 May 2025 00:04:31 -0700 Subject: [PATCH 20/25] Fix vllm generator type annotations --- rigging/generator/vllm_.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rigging/generator/vllm_.py b/rigging/generator/vllm_.py index bac71b8..944b2f5 100644 --- a/rigging/generator/vllm_.py +++ b/rigging/generator/vllm_.py @@ -81,7 +81,7 @@ def from_obj( llm: vllm.LLM, *, params: GenerateParams | None = None, - ) -> VLLMGenerator: + ) -> "VLLMGenerator": """Create a generator from an existing vLLM instance. Args: @@ -94,11 +94,11 @@ def from_obj( generator._llm = llm # noqa: SLF001 return generator - def load(self) -> VLLMGenerator: + def load(self) -> "VLLMGenerator": _ = self.llm return self - def unload(self) -> VLLMGenerator: + def unload(self) -> "VLLMGenerator": del self._llm gc.collect() torch.cuda.empty_cache() From a8ac4424b2b85e856752e19704127f829fff5d52 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 8 May 2025 00:13:31 -0700 Subject: [PATCH 21/25] More type fixes --- rigging/generator/transformers_.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rigging/generator/transformers_.py b/rigging/generator/transformers_.py index 9c93612..0ae9616 100644 --- a/rigging/generator/transformers_.py +++ b/rigging/generator/transformers_.py @@ -2,8 +2,8 @@ import typing as t import torch -import transformers -from transformers import ( +import transformers # type: ignore [import-untyped] +from transformers import ( # type: ignore [import-untyped] AutoModelForCausalLM, AutoTokenizer, PreTrainedTokenizer, From ff55151a915108d7e419edacdf6a288be441657a Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 8 May 2025 00:25:43 -0700 Subject: [PATCH 22/25] More type fixes --- rigging/generator/transformers_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rigging/generator/transformers_.py b/rigging/generator/transformers_.py index 0ae9616..03ad0ce 100644 --- a/rigging/generator/transformers_.py +++ b/rigging/generator/transformers_.py @@ -3,7 +3,7 @@ import torch import transformers # type: ignore [import-untyped] -from transformers import ( # type: ignore [import-untyped] +from transformers import ( AutoModelForCausalLM, AutoTokenizer, PreTrainedTokenizer, From 30ddd55e4fbecd1ad7d184f33214e62b60f77da9 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 8 May 2025 00:30:09 -0700 Subject: [PATCH 23/25] relocking dependencies --- poetry.lock | 4917 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 2901 insertions(+), 2016 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4992a69..b424654 100644 --- a/poetry.lock +++ b/poetry.lock @@ -68,93 +68,93 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.14" +version = "3.11.18" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e2bc827c01f75803de77b134afdbf74fa74b62970eafdf190f3244931d7a5c0d"}, - {file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e365034c5cf6cf74f57420b57682ea79e19eb29033399dd3f40de4d0171998fa"}, - {file = "aiohttp-3.11.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c32593ead1a8c6aabd58f9d7ee706e48beac796bb0cb71d6b60f2c1056f0a65f"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4e7c7ec4146a94a307ca4f112802a8e26d969018fabed526efc340d21d3e7d0"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8b2df9feac55043759aa89f722a967d977d80f8b5865a4153fc41c93b957efc"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7571f99525c76a6280f5fe8e194eeb8cb4da55586c3c61c59c33a33f10cfce7"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b59d096b5537ec7c85954cb97d821aae35cfccce3357a2cafe85660cc6295628"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b42dbd097abb44b3f1156b4bf978ec5853840802d6eee2784857be11ee82c6a0"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b05774864c87210c531b48dfeb2f7659407c2dda8643104fb4ae5e2c311d12d9"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4e2e8ef37d4bc110917d038807ee3af82700a93ab2ba5687afae5271b8bc50ff"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e9faafa74dbb906b2b6f3eb9942352e9e9db8d583ffed4be618a89bd71a4e914"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e7abe865504f41b10777ac162c727af14e9f4db9262e3ed8254179053f63e6d"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4848ae31ad44330b30f16c71e4f586cd5402a846b11264c412de99fa768f00f3"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d0b46abee5b5737cb479cc9139b29f010a37b1875ee56d142aefc10686a390b"}, - {file = "aiohttp-3.11.14-cp310-cp310-win32.whl", hash = "sha256:a0d2c04a623ab83963576548ce098baf711a18e2c32c542b62322a0b4584b990"}, - {file = "aiohttp-3.11.14-cp310-cp310-win_amd64.whl", hash = "sha256:5409a59d5057f2386bb8b8f8bbcfb6e15505cedd8b2445db510563b5d7ea1186"}, - {file = "aiohttp-3.11.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f296d637a50bb15fb6a229fbb0eb053080e703b53dbfe55b1e4bb1c5ed25d325"}, - {file = "aiohttp-3.11.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec6cd1954ca2bbf0970f531a628da1b1338f594bf5da7e361e19ba163ecc4f3b"}, - {file = "aiohttp-3.11.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:572def4aad0a4775af66d5a2b5923c7de0820ecaeeb7987dcbccda2a735a993f"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c68e41c4d576cd6aa6c6d2eddfb32b2acfb07ebfbb4f9da991da26633a3db1a"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b8bbfc8111826aa8363442c0fc1f5751456b008737ff053570f06a151650b3"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b0a200e85da5c966277a402736a96457b882360aa15416bf104ca81e6f5807b"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d173c0ac508a2175f7c9a115a50db5fd3e35190d96fdd1a17f9cb10a6ab09aa1"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:413fe39fd929329f697f41ad67936f379cba06fcd4c462b62e5b0f8061ee4a77"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65c75b14ee74e8eeff2886321e76188cbe938d18c85cff349d948430179ad02c"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:321238a42ed463848f06e291c4bbfb3d15ba5a79221a82c502da3e23d7525d06"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59a05cdc636431f7ce843c7c2f04772437dd816a5289f16440b19441be6511f1"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:daf20d9c3b12ae0fdf15ed92235e190f8284945563c4b8ad95b2d7a31f331cd3"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:05582cb2d156ac7506e68b5eac83179faedad74522ed88f88e5861b78740dc0e"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:12c5869e7ddf6b4b1f2109702b3cd7515667b437da90a5a4a50ba1354fe41881"}, - {file = "aiohttp-3.11.14-cp311-cp311-win32.whl", hash = "sha256:92868f6512714efd4a6d6cb2bfc4903b997b36b97baea85f744229f18d12755e"}, - {file = "aiohttp-3.11.14-cp311-cp311-win_amd64.whl", hash = "sha256:bccd2cb7aa5a3bfada72681bdb91637094d81639e116eac368f8b3874620a654"}, - {file = "aiohttp-3.11.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:70ab0f61c1a73d3e0342cedd9a7321425c27a7067bebeeacd509f96695b875fc"}, - {file = "aiohttp-3.11.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:602d4db80daf4497de93cb1ce00b8fc79969c0a7cf5b67bec96fa939268d806a"}, - {file = "aiohttp-3.11.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a8a0d127c10b8d89e69bbd3430da0f73946d839e65fec00ae48ca7916a31948"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9f835cdfedcb3f5947304e85b8ca3ace31eef6346d8027a97f4de5fb687534"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aa5c68e1e68fff7cd3142288101deb4316b51f03d50c92de6ea5ce646e6c71f"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b512f1de1c688f88dbe1b8bb1283f7fbeb7a2b2b26e743bb2193cbadfa6f307"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc9253069158d57e27d47a8453d8a2c5a370dc461374111b5184cf2f147a3cc3"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b2501f1b981e70932b4a552fc9b3c942991c7ae429ea117e8fba57718cdeed0"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:28a3d083819741592685762d51d789e6155411277050d08066537c5edc4066e6"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0df3788187559c262922846087e36228b75987f3ae31dd0a1e5ee1034090d42f"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e73fa341d8b308bb799cf0ab6f55fc0461d27a9fa3e4582755a3d81a6af8c09"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51ba80d473eb780a329d73ac8afa44aa71dfb521693ccea1dea8b9b5c4df45ce"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8d1dd75aa4d855c7debaf1ef830ff2dfcc33f893c7db0af2423ee761ebffd22b"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41cf0cefd9e7b5c646c2ef529c8335e7eafd326f444cc1cdb0c47b6bc836f9be"}, - {file = "aiohttp-3.11.14-cp312-cp312-win32.whl", hash = "sha256:948abc8952aff63de7b2c83bfe3f211c727da3a33c3a5866a0e2cf1ee1aa950f"}, - {file = "aiohttp-3.11.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b420d076a46f41ea48e5fcccb996f517af0d406267e31e6716f480a3d50d65c"}, - {file = "aiohttp-3.11.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d14e274828561db91e4178f0057a915f3af1757b94c2ca283cb34cbb6e00b50"}, - {file = "aiohttp-3.11.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f30fc72daf85486cdcdfc3f5e0aea9255493ef499e31582b34abadbfaafb0965"}, - {file = "aiohttp-3.11.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4edcbe34e6dba0136e4cabf7568f5a434d89cc9de5d5155371acda275353d228"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7169ded15505f55a87f8f0812c94c9412623c744227b9e51083a72a48b68a5"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad1f2fb9fe9b585ea4b436d6e998e71b50d2b087b694ab277b30e060c434e5db"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20412c7cc3720e47a47e63c0005f78c0c2370020f9f4770d7fc0075f397a9fb0"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd9766da617855f7e85f27d2bf9a565ace04ba7c387323cd3e651ac4329db91"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:599b66582f7276ebefbaa38adf37585e636b6a7a73382eb412f7bc0fc55fb73d"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b41693b7388324b80f9acfabd479bd1c84f0bc7e8f17bab4ecd9675e9ff9c734"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:86135c32d06927339c8c5e64f96e4eee8825d928374b9b71a3c42379d7437058"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04eb541ce1e03edc1e3be1917a0f45ac703e913c21a940111df73a2c2db11d73"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dc311634f6f28661a76cbc1c28ecf3b3a70a8edd67b69288ab7ca91058eb5a33"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69bb252bfdca385ccabfd55f4cd740d421dd8c8ad438ded9637d81c228d0da49"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b86efe23684b58a88e530c4ab5b20145f102916bbb2d82942cafec7bd36a647"}, - {file = "aiohttp-3.11.14-cp313-cp313-win32.whl", hash = "sha256:b9c60d1de973ca94af02053d9b5111c4fbf97158e139b14f1be68337be267be6"}, - {file = "aiohttp-3.11.14-cp313-cp313-win_amd64.whl", hash = "sha256:0a29be28e60e5610d2437b5b2fed61d6f3dcde898b57fb048aa5079271e7f6f3"}, - {file = "aiohttp-3.11.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:14fc03508359334edc76d35b2821832f092c8f092e4b356e74e38419dfe7b6de"}, - {file = "aiohttp-3.11.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92007c89a8cb7be35befa2732b0b32bf3a394c1b22ef2dff0ef12537d98a7bda"}, - {file = "aiohttp-3.11.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6d3986112e34eaa36e280dc8286b9dd4cc1a5bcf328a7f147453e188f6fe148f"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:749f1eb10e51dbbcdba9df2ef457ec060554842eea4d23874a3e26495f9e87b1"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:781c8bd423dcc4641298c8c5a2a125c8b1c31e11f828e8d35c1d3a722af4c15a"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:997b57e38aa7dc6caab843c5e042ab557bc83a2f91b7bd302e3c3aebbb9042a1"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a8b0321e40a833e381d127be993b7349d1564b756910b28b5f6588a159afef3"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8778620396e554b758b59773ab29c03b55047841d8894c5e335f12bfc45ebd28"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e906da0f2bcbf9b26cc2b144929e88cb3bf943dd1942b4e5af066056875c7618"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:87f0e003fb4dd5810c7fbf47a1239eaa34cd929ef160e0a54c570883125c4831"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7f2dadece8b85596ac3ab1ec04b00694bdd62abc31e5618f524648d18d9dd7fa"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:fe846f0a98aa9913c2852b630cd39b4098f296e0907dd05f6c7b30d911afa4c3"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ced66c5c6ad5bcaf9be54560398654779ec1c3695f1a9cf0ae5e3606694a000a"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a40087b82f83bd671cbeb5f582c233d196e9653220404a798798bfc0ee189fff"}, - {file = "aiohttp-3.11.14-cp39-cp39-win32.whl", hash = "sha256:95d7787f2bcbf7cb46823036a8d64ccfbc2ffc7d52016b4044d901abceeba3db"}, - {file = "aiohttp-3.11.14-cp39-cp39-win_amd64.whl", hash = "sha256:22a8107896877212130c58f74e64b77f7007cb03cea8698be317272643602d45"}, - {file = "aiohttp-3.11.14.tar.gz", hash = "sha256:d6edc538c7480fa0a3b2bdd705f8010062d74700198da55d16498e1b49549b9c"}, + {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4"}, + {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6"}, + {file = "aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1"}, + {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643"}, + {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868"}, + {file = "aiohttp-3.11.18-cp310-cp310-win32.whl", hash = "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f"}, + {file = "aiohttp-3.11.18-cp310-cp310-win_amd64.whl", hash = "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9"}, + {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9"}, + {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b"}, + {file = "aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f"}, + {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f"}, + {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd"}, + {file = "aiohttp-3.11.18-cp311-cp311-win32.whl", hash = "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d"}, + {file = "aiohttp-3.11.18-cp311-cp311-win_amd64.whl", hash = "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6"}, + {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2"}, + {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508"}, + {file = "aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6"}, + {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1"}, + {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea"}, + {file = "aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8"}, + {file = "aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8"}, + {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811"}, + {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804"}, + {file = "aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000"}, + {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261"}, + {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7"}, + {file = "aiohttp-3.11.18-cp313-cp313-win32.whl", hash = "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78"}, + {file = "aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01"}, + {file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:469ac32375d9a716da49817cd26f1916ec787fc82b151c1c832f58420e6d3533"}, + {file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cec21dd68924179258ae14af9f5418c1ebdbba60b98c667815891293902e5e0"}, + {file = "aiohttp-3.11.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b426495fb9140e75719b3ae70a5e8dd3a79def0ae3c6c27e012fc59f16544a4a"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2f41203e2808616292db5d7170cccf0c9f9c982d02544443c7eb0296e8b0c7"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc0ae0a5e9939e423e065a3e5b00b24b8379f1db46046d7ab71753dfc7dd0e1"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7cdd3f7d1df43200e1c80f1aed86bb36033bf65e3c7cf46a2b97a253ef8798"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5199be2a2f01ffdfa8c3a6f5981205242986b9e63eb8ae03fd18f736e4840721"}, + {file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ccec9e72660b10f8e283e91aa0295975c7bd85c204011d9f5eb69310555cf30"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1596ebf17e42e293cbacc7a24c3e0dc0f8f755b40aff0402cb74c1ff6baec1d3"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:eab7b040a8a873020113ba814b7db7fa935235e4cbaf8f3da17671baa1024863"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5d61df4a05476ff891cff0030329fee4088d40e4dc9b013fac01bc3c745542c2"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:46533e6792e1410f9801d09fd40cbbff3f3518d1b501d6c3c5b218f427f6ff08"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c1b90407ced992331dd6d4f1355819ea1c274cc1ee4d5b7046c6761f9ec11829"}, + {file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a2fd04ae4971b914e54fe459dd7edbbd3f2ba875d69e057d5e3c8e8cac094935"}, + {file = "aiohttp-3.11.18-cp39-cp39-win32.whl", hash = "sha256:b2f317d1678002eee6fe85670039fb34a757972284614638f82b903a03feacdc"}, + {file = "aiohttp-3.11.18-cp39-cp39-win_amd64.whl", hash = "sha256:5e7007b8d1d09bce37b54111f593d173691c530b80f27c6493b928dabed9e6ef"}, + {file = "aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a"}, ] [package.dependencies] @@ -185,19 +185,6 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" -[[package]] -name = "airportsdata" -version = "20250224" -description = "Extensive database of location and timezone data for nearly every airport and landing strip in the world." -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"all\"" -files = [ - {file = "airportsdata-20250224-py3-none-any.whl", hash = "sha256:006128bca2cc1983dc5ed4fb1227e8df2289b5e95b8ab30d9bdd4eb7c6d2160d"}, - {file = "airportsdata-20250224.tar.gz", hash = "sha256:7f4538a613504444a13149be701aac5f9599b86da476d26b46aa24fd54714eda"}, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -277,15 +264,15 @@ files = [ [[package]] name = "asyncssh" -version = "2.20.0" +version = "2.21.0" description = "AsyncSSH: Asynchronous SSHv2 client and server library" optional = true python-versions = ">=3.6" groups = ["main"] markers = "extra == \"examples\" or extra == \"all\"" files = [ - {file = "asyncssh-2.20.0-py3-none-any.whl", hash = "sha256:af6888d937c07a4bf31293335a6166b4d87608cdb5957b49547da6ad87ecf174"}, - {file = "asyncssh-2.20.0.tar.gz", hash = "sha256:020b6e384b2328ef8683908ad8e73de9ec2b9b62fd964571ea957bba98412983"}, + {file = "asyncssh-2.21.0-py3-none-any.whl", hash = "sha256:cf7f3dfa52b2cb4ad31f0d77ff0d0a8fdd850203da84a0e72e62c36fdd4daf4b"}, + {file = "asyncssh-2.21.0.tar.gz", hash = "sha256:450fe13bb8d86a8f4e7d7b5fafce7791181ca3e7c92e15bbc45dfb25866e48b3"}, ] [package.dependencies] @@ -321,6 +308,66 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +[[package]] +name = "audioop-lts" +version = "0.2.1" +description = "LTS Port of Python audioop" +optional = true +python-versions = ">=3.13" +groups = ["main"] +markers = "extra == \"all\" and python_version >= \"3.13\"" +files = [ + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"}, + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"}, + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0"}, + {file = "audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387"}, +] + +[[package]] +name = "audioread" +version = "3.0.1" +description = "Multi-library, cross-platform audio decoding." +optional = true +python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "audioread-3.0.1-py3-none-any.whl", hash = "sha256:4cdce70b8adc0da0a3c9e0d85fb10b3ace30fbdf8d1670fd443929b61d117c33"}, + {file = "audioread-3.0.1.tar.gz", hash = "sha256:ac5460a5498c48bdf2e8e767402583a4dcd13f4414d286f42ce4379e8b35066d"}, +] + +[package.extras] +test = ["tox"] + [[package]] name = "babel" version = "2.17.0" @@ -357,462 +404,462 @@ extras = ["regex"] [[package]] name = "boto3" -version = "1.37.14" +version = "1.38.11" description = "The AWS SDK for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "boto3-1.37.14-py3-none-any.whl", hash = "sha256:56b4d1e084dbca43d5fdd070f633a84de61a6ce592655b4d239d263d1a0097fc"}, - {file = "boto3-1.37.14.tar.gz", hash = "sha256:cf2e5e6d56efd5850db8ce3d9094132e4759cf2d4b5fd8200d69456bf61a20f3"}, + {file = "boto3-1.38.11-py3-none-any.whl", hash = "sha256:dc3ff3a382d33d1e7e4141a5c63d084faf90657d6008ef917a3e64cfcbbb47e0"}, + {file = "boto3-1.38.11.tar.gz", hash = "sha256:ecf141395b8ede64dbc975c62a55fbb2231fc3e50904d65c088bc83ae98595cf"}, ] [package.dependencies] -botocore = ">=1.37.14,<1.38.0" +botocore = ">=1.38.11,<1.39.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.11.0,<0.12.0" +s3transfer = ">=0.12.0,<0.13.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.37.14" -description = "Type annotations for boto3 1.37.14 generated with mypy-boto3-builder 8.10.1" +version = "1.38.11" +description = "Type annotations for boto3 1.38.11 generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "boto3_stubs-1.37.14-py3-none-any.whl", hash = "sha256:e9570809f034af9a758b11aed7a8f3bc781053863c4f5fd445bccda613aaaf17"}, - {file = "boto3_stubs-1.37.14.tar.gz", hash = "sha256:7425e4ea9efc8df31ffc60fa108caf542048b6431f3514f3b1231839a033dbae"}, + {file = "boto3_stubs-1.38.11-py3-none-any.whl", hash = "sha256:cec7bc6d47be6ae2d77e7716c1521f4248cb484d83af0c4b0a50b8dd45ee9254"}, + {file = "boto3_stubs-1.38.11.tar.gz", hash = "sha256:427aea4639fb629b42d418e13972dcd156a113da042909207872eb801a8f09c3"}, ] [package.dependencies] botocore-stubs = "*" -mypy-boto3-s3 = {version = ">=1.37.0,<1.38.0", optional = true, markers = "extra == \"s3\""} +mypy-boto3-s3 = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"s3\""} types-s3transfer = "*" typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} [package.extras] -accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.37.0,<1.38.0)"] -account = ["mypy-boto3-account (>=1.37.0,<1.38.0)"] -acm = ["mypy-boto3-acm (>=1.37.0,<1.38.0)"] -acm-pca = ["mypy-boto3-acm-pca (>=1.37.0,<1.38.0)"] -all = ["mypy-boto3-accessanalyzer (>=1.37.0,<1.38.0)", "mypy-boto3-account (>=1.37.0,<1.38.0)", "mypy-boto3-acm (>=1.37.0,<1.38.0)", "mypy-boto3-acm-pca (>=1.37.0,<1.38.0)", "mypy-boto3-amp (>=1.37.0,<1.38.0)", "mypy-boto3-amplify (>=1.37.0,<1.38.0)", "mypy-boto3-amplifybackend (>=1.37.0,<1.38.0)", "mypy-boto3-amplifyuibuilder (>=1.37.0,<1.38.0)", "mypy-boto3-apigateway (>=1.37.0,<1.38.0)", "mypy-boto3-apigatewaymanagementapi (>=1.37.0,<1.38.0)", "mypy-boto3-apigatewayv2 (>=1.37.0,<1.38.0)", "mypy-boto3-appconfig (>=1.37.0,<1.38.0)", "mypy-boto3-appconfigdata (>=1.37.0,<1.38.0)", "mypy-boto3-appfabric (>=1.37.0,<1.38.0)", "mypy-boto3-appflow (>=1.37.0,<1.38.0)", "mypy-boto3-appintegrations (>=1.37.0,<1.38.0)", "mypy-boto3-application-autoscaling (>=1.37.0,<1.38.0)", "mypy-boto3-application-insights (>=1.37.0,<1.38.0)", "mypy-boto3-application-signals (>=1.37.0,<1.38.0)", "mypy-boto3-applicationcostprofiler (>=1.37.0,<1.38.0)", "mypy-boto3-appmesh (>=1.37.0,<1.38.0)", "mypy-boto3-apprunner (>=1.37.0,<1.38.0)", "mypy-boto3-appstream (>=1.37.0,<1.38.0)", "mypy-boto3-appsync (>=1.37.0,<1.38.0)", "mypy-boto3-apptest (>=1.37.0,<1.38.0)", "mypy-boto3-arc-zonal-shift (>=1.37.0,<1.38.0)", "mypy-boto3-artifact (>=1.37.0,<1.38.0)", "mypy-boto3-athena (>=1.37.0,<1.38.0)", "mypy-boto3-auditmanager (>=1.37.0,<1.38.0)", "mypy-boto3-autoscaling (>=1.37.0,<1.38.0)", "mypy-boto3-autoscaling-plans (>=1.37.0,<1.38.0)", "mypy-boto3-b2bi (>=1.37.0,<1.38.0)", "mypy-boto3-backup (>=1.37.0,<1.38.0)", "mypy-boto3-backup-gateway (>=1.37.0,<1.38.0)", "mypy-boto3-backupsearch (>=1.37.0,<1.38.0)", "mypy-boto3-batch (>=1.37.0,<1.38.0)", "mypy-boto3-bcm-data-exports (>=1.37.0,<1.38.0)", "mypy-boto3-bcm-pricing-calculator (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-agent (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-agent-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-data-automation (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-billing (>=1.37.0,<1.38.0)", "mypy-boto3-billingconductor (>=1.37.0,<1.38.0)", "mypy-boto3-braket (>=1.37.0,<1.38.0)", "mypy-boto3-budgets (>=1.37.0,<1.38.0)", "mypy-boto3-ce (>=1.37.0,<1.38.0)", "mypy-boto3-chatbot (>=1.37.0,<1.38.0)", "mypy-boto3-chime (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-identity (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-meetings (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-messaging (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-voice (>=1.37.0,<1.38.0)", "mypy-boto3-cleanrooms (>=1.37.0,<1.38.0)", "mypy-boto3-cleanroomsml (>=1.37.0,<1.38.0)", "mypy-boto3-cloud9 (>=1.37.0,<1.38.0)", "mypy-boto3-cloudcontrol (>=1.37.0,<1.38.0)", "mypy-boto3-clouddirectory (>=1.37.0,<1.38.0)", "mypy-boto3-cloudformation (>=1.37.0,<1.38.0)", "mypy-boto3-cloudfront (>=1.37.0,<1.38.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.37.0,<1.38.0)", "mypy-boto3-cloudhsm (>=1.37.0,<1.38.0)", "mypy-boto3-cloudhsmv2 (>=1.37.0,<1.38.0)", "mypy-boto3-cloudsearch (>=1.37.0,<1.38.0)", "mypy-boto3-cloudsearchdomain (>=1.37.0,<1.38.0)", "mypy-boto3-cloudtrail (>=1.37.0,<1.38.0)", "mypy-boto3-cloudtrail-data (>=1.37.0,<1.38.0)", "mypy-boto3-cloudwatch (>=1.37.0,<1.38.0)", "mypy-boto3-codeartifact (>=1.37.0,<1.38.0)", "mypy-boto3-codebuild (>=1.37.0,<1.38.0)", "mypy-boto3-codecatalyst (>=1.37.0,<1.38.0)", "mypy-boto3-codecommit (>=1.37.0,<1.38.0)", "mypy-boto3-codeconnections (>=1.37.0,<1.38.0)", "mypy-boto3-codedeploy (>=1.37.0,<1.38.0)", "mypy-boto3-codeguru-reviewer (>=1.37.0,<1.38.0)", "mypy-boto3-codeguru-security (>=1.37.0,<1.38.0)", "mypy-boto3-codeguruprofiler (>=1.37.0,<1.38.0)", "mypy-boto3-codepipeline (>=1.37.0,<1.38.0)", "mypy-boto3-codestar-connections (>=1.37.0,<1.38.0)", "mypy-boto3-codestar-notifications (>=1.37.0,<1.38.0)", "mypy-boto3-cognito-identity (>=1.37.0,<1.38.0)", "mypy-boto3-cognito-idp (>=1.37.0,<1.38.0)", "mypy-boto3-cognito-sync (>=1.37.0,<1.38.0)", "mypy-boto3-comprehend (>=1.37.0,<1.38.0)", "mypy-boto3-comprehendmedical (>=1.37.0,<1.38.0)", "mypy-boto3-compute-optimizer (>=1.37.0,<1.38.0)", "mypy-boto3-config (>=1.37.0,<1.38.0)", "mypy-boto3-connect (>=1.37.0,<1.38.0)", "mypy-boto3-connect-contact-lens (>=1.37.0,<1.38.0)", "mypy-boto3-connectcampaigns (>=1.37.0,<1.38.0)", "mypy-boto3-connectcampaignsv2 (>=1.37.0,<1.38.0)", "mypy-boto3-connectcases (>=1.37.0,<1.38.0)", "mypy-boto3-connectparticipant (>=1.37.0,<1.38.0)", "mypy-boto3-controlcatalog (>=1.37.0,<1.38.0)", "mypy-boto3-controltower (>=1.37.0,<1.38.0)", "mypy-boto3-cost-optimization-hub (>=1.37.0,<1.38.0)", "mypy-boto3-cur (>=1.37.0,<1.38.0)", "mypy-boto3-customer-profiles (>=1.37.0,<1.38.0)", "mypy-boto3-databrew (>=1.37.0,<1.38.0)", "mypy-boto3-dataexchange (>=1.37.0,<1.38.0)", "mypy-boto3-datapipeline (>=1.37.0,<1.38.0)", "mypy-boto3-datasync (>=1.37.0,<1.38.0)", "mypy-boto3-datazone (>=1.37.0,<1.38.0)", "mypy-boto3-dax (>=1.37.0,<1.38.0)", "mypy-boto3-deadline (>=1.37.0,<1.38.0)", "mypy-boto3-detective (>=1.37.0,<1.38.0)", "mypy-boto3-devicefarm (>=1.37.0,<1.38.0)", "mypy-boto3-devops-guru (>=1.37.0,<1.38.0)", "mypy-boto3-directconnect (>=1.37.0,<1.38.0)", "mypy-boto3-discovery (>=1.37.0,<1.38.0)", "mypy-boto3-dlm (>=1.37.0,<1.38.0)", "mypy-boto3-dms (>=1.37.0,<1.38.0)", "mypy-boto3-docdb (>=1.37.0,<1.38.0)", "mypy-boto3-docdb-elastic (>=1.37.0,<1.38.0)", "mypy-boto3-drs (>=1.37.0,<1.38.0)", "mypy-boto3-ds (>=1.37.0,<1.38.0)", "mypy-boto3-ds-data (>=1.37.0,<1.38.0)", "mypy-boto3-dsql (>=1.37.0,<1.38.0)", "mypy-boto3-dynamodb (>=1.37.0,<1.38.0)", "mypy-boto3-dynamodbstreams (>=1.37.0,<1.38.0)", "mypy-boto3-ebs (>=1.37.0,<1.38.0)", "mypy-boto3-ec2 (>=1.37.0,<1.38.0)", "mypy-boto3-ec2-instance-connect (>=1.37.0,<1.38.0)", "mypy-boto3-ecr (>=1.37.0,<1.38.0)", "mypy-boto3-ecr-public (>=1.37.0,<1.38.0)", "mypy-boto3-ecs (>=1.37.0,<1.38.0)", "mypy-boto3-efs (>=1.37.0,<1.38.0)", "mypy-boto3-eks (>=1.37.0,<1.38.0)", "mypy-boto3-eks-auth (>=1.37.0,<1.38.0)", "mypy-boto3-elasticache (>=1.37.0,<1.38.0)", "mypy-boto3-elasticbeanstalk (>=1.37.0,<1.38.0)", "mypy-boto3-elastictranscoder (>=1.37.0,<1.38.0)", "mypy-boto3-elb (>=1.37.0,<1.38.0)", "mypy-boto3-elbv2 (>=1.37.0,<1.38.0)", "mypy-boto3-emr (>=1.37.0,<1.38.0)", "mypy-boto3-emr-containers (>=1.37.0,<1.38.0)", "mypy-boto3-emr-serverless (>=1.37.0,<1.38.0)", "mypy-boto3-entityresolution (>=1.37.0,<1.38.0)", "mypy-boto3-es (>=1.37.0,<1.38.0)", "mypy-boto3-events (>=1.37.0,<1.38.0)", "mypy-boto3-evidently (>=1.37.0,<1.38.0)", "mypy-boto3-finspace (>=1.37.0,<1.38.0)", "mypy-boto3-finspace-data (>=1.37.0,<1.38.0)", "mypy-boto3-firehose (>=1.37.0,<1.38.0)", "mypy-boto3-fis (>=1.37.0,<1.38.0)", "mypy-boto3-fms (>=1.37.0,<1.38.0)", "mypy-boto3-forecast (>=1.37.0,<1.38.0)", "mypy-boto3-forecastquery (>=1.37.0,<1.38.0)", "mypy-boto3-frauddetector (>=1.37.0,<1.38.0)", "mypy-boto3-freetier (>=1.37.0,<1.38.0)", "mypy-boto3-fsx (>=1.37.0,<1.38.0)", "mypy-boto3-gamelift (>=1.37.0,<1.38.0)", "mypy-boto3-gameliftstreams (>=1.37.0,<1.38.0)", "mypy-boto3-geo-maps (>=1.37.0,<1.38.0)", "mypy-boto3-geo-places (>=1.37.0,<1.38.0)", "mypy-boto3-geo-routes (>=1.37.0,<1.38.0)", "mypy-boto3-glacier (>=1.37.0,<1.38.0)", "mypy-boto3-globalaccelerator (>=1.37.0,<1.38.0)", "mypy-boto3-glue (>=1.37.0,<1.38.0)", "mypy-boto3-grafana (>=1.37.0,<1.38.0)", "mypy-boto3-greengrass (>=1.37.0,<1.38.0)", "mypy-boto3-greengrassv2 (>=1.37.0,<1.38.0)", "mypy-boto3-groundstation (>=1.37.0,<1.38.0)", "mypy-boto3-guardduty (>=1.37.0,<1.38.0)", "mypy-boto3-health (>=1.37.0,<1.38.0)", "mypy-boto3-healthlake (>=1.37.0,<1.38.0)", "mypy-boto3-iam (>=1.37.0,<1.38.0)", "mypy-boto3-identitystore (>=1.37.0,<1.38.0)", "mypy-boto3-imagebuilder (>=1.37.0,<1.38.0)", "mypy-boto3-importexport (>=1.37.0,<1.38.0)", "mypy-boto3-inspector (>=1.37.0,<1.38.0)", "mypy-boto3-inspector-scan (>=1.37.0,<1.38.0)", "mypy-boto3-inspector2 (>=1.37.0,<1.38.0)", "mypy-boto3-internetmonitor (>=1.37.0,<1.38.0)", "mypy-boto3-invoicing (>=1.37.0,<1.38.0)", "mypy-boto3-iot (>=1.37.0,<1.38.0)", "mypy-boto3-iot-data (>=1.37.0,<1.38.0)", "mypy-boto3-iot-jobs-data (>=1.37.0,<1.38.0)", "mypy-boto3-iot-managed-integrations (>=1.37.0,<1.38.0)", "mypy-boto3-iotanalytics (>=1.37.0,<1.38.0)", "mypy-boto3-iotdeviceadvisor (>=1.37.0,<1.38.0)", "mypy-boto3-iotevents (>=1.37.0,<1.38.0)", "mypy-boto3-iotevents-data (>=1.37.0,<1.38.0)", "mypy-boto3-iotfleethub (>=1.37.0,<1.38.0)", "mypy-boto3-iotfleetwise (>=1.37.0,<1.38.0)", "mypy-boto3-iotsecuretunneling (>=1.37.0,<1.38.0)", "mypy-boto3-iotsitewise (>=1.37.0,<1.38.0)", "mypy-boto3-iotthingsgraph (>=1.37.0,<1.38.0)", "mypy-boto3-iottwinmaker (>=1.37.0,<1.38.0)", "mypy-boto3-iotwireless (>=1.37.0,<1.38.0)", "mypy-boto3-ivs (>=1.37.0,<1.38.0)", "mypy-boto3-ivs-realtime (>=1.37.0,<1.38.0)", "mypy-boto3-ivschat (>=1.37.0,<1.38.0)", "mypy-boto3-kafka (>=1.37.0,<1.38.0)", "mypy-boto3-kafkaconnect (>=1.37.0,<1.38.0)", "mypy-boto3-kendra (>=1.37.0,<1.38.0)", "mypy-boto3-kendra-ranking (>=1.37.0,<1.38.0)", "mypy-boto3-keyspaces (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-archived-media (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-media (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-signaling (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.37.0,<1.38.0)", "mypy-boto3-kinesisanalytics (>=1.37.0,<1.38.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.37.0,<1.38.0)", "mypy-boto3-kinesisvideo (>=1.37.0,<1.38.0)", "mypy-boto3-kms (>=1.37.0,<1.38.0)", "mypy-boto3-lakeformation (>=1.37.0,<1.38.0)", "mypy-boto3-lambda (>=1.37.0,<1.38.0)", "mypy-boto3-launch-wizard (>=1.37.0,<1.38.0)", "mypy-boto3-lex-models (>=1.37.0,<1.38.0)", "mypy-boto3-lex-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-lexv2-models (>=1.37.0,<1.38.0)", "mypy-boto3-lexv2-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-license-manager (>=1.37.0,<1.38.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.37.0,<1.38.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.37.0,<1.38.0)", "mypy-boto3-lightsail (>=1.37.0,<1.38.0)", "mypy-boto3-location (>=1.37.0,<1.38.0)", "mypy-boto3-logs (>=1.37.0,<1.38.0)", "mypy-boto3-lookoutequipment (>=1.37.0,<1.38.0)", "mypy-boto3-lookoutmetrics (>=1.37.0,<1.38.0)", "mypy-boto3-lookoutvision (>=1.37.0,<1.38.0)", "mypy-boto3-m2 (>=1.37.0,<1.38.0)", "mypy-boto3-machinelearning (>=1.37.0,<1.38.0)", "mypy-boto3-macie2 (>=1.37.0,<1.38.0)", "mypy-boto3-mailmanager (>=1.37.0,<1.38.0)", "mypy-boto3-managedblockchain (>=1.37.0,<1.38.0)", "mypy-boto3-managedblockchain-query (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-agreement (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-catalog (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-deployment (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-entitlement (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-reporting (>=1.37.0,<1.38.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.37.0,<1.38.0)", "mypy-boto3-mediaconnect (>=1.37.0,<1.38.0)", "mypy-boto3-mediaconvert (>=1.37.0,<1.38.0)", "mypy-boto3-medialive (>=1.37.0,<1.38.0)", "mypy-boto3-mediapackage (>=1.37.0,<1.38.0)", "mypy-boto3-mediapackage-vod (>=1.37.0,<1.38.0)", "mypy-boto3-mediapackagev2 (>=1.37.0,<1.38.0)", "mypy-boto3-mediastore (>=1.37.0,<1.38.0)", "mypy-boto3-mediastore-data (>=1.37.0,<1.38.0)", "mypy-boto3-mediatailor (>=1.37.0,<1.38.0)", "mypy-boto3-medical-imaging (>=1.37.0,<1.38.0)", "mypy-boto3-memorydb (>=1.37.0,<1.38.0)", "mypy-boto3-meteringmarketplace (>=1.37.0,<1.38.0)", "mypy-boto3-mgh (>=1.37.0,<1.38.0)", "mypy-boto3-mgn (>=1.37.0,<1.38.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.37.0,<1.38.0)", "mypy-boto3-migrationhub-config (>=1.37.0,<1.38.0)", "mypy-boto3-migrationhuborchestrator (>=1.37.0,<1.38.0)", "mypy-boto3-migrationhubstrategy (>=1.37.0,<1.38.0)", "mypy-boto3-mq (>=1.37.0,<1.38.0)", "mypy-boto3-mturk (>=1.37.0,<1.38.0)", "mypy-boto3-mwaa (>=1.37.0,<1.38.0)", "mypy-boto3-neptune (>=1.37.0,<1.38.0)", "mypy-boto3-neptune-graph (>=1.37.0,<1.38.0)", "mypy-boto3-neptunedata (>=1.37.0,<1.38.0)", "mypy-boto3-network-firewall (>=1.37.0,<1.38.0)", "mypy-boto3-networkflowmonitor (>=1.37.0,<1.38.0)", "mypy-boto3-networkmanager (>=1.37.0,<1.38.0)", "mypy-boto3-networkmonitor (>=1.37.0,<1.38.0)", "mypy-boto3-notifications (>=1.37.0,<1.38.0)", "mypy-boto3-notificationscontacts (>=1.37.0,<1.38.0)", "mypy-boto3-oam (>=1.37.0,<1.38.0)", "mypy-boto3-observabilityadmin (>=1.37.0,<1.38.0)", "mypy-boto3-omics (>=1.37.0,<1.38.0)", "mypy-boto3-opensearch (>=1.37.0,<1.38.0)", "mypy-boto3-opensearchserverless (>=1.37.0,<1.38.0)", "mypy-boto3-opsworks (>=1.37.0,<1.38.0)", "mypy-boto3-opsworkscm (>=1.37.0,<1.38.0)", "mypy-boto3-organizations (>=1.37.0,<1.38.0)", "mypy-boto3-osis (>=1.37.0,<1.38.0)", "mypy-boto3-outposts (>=1.37.0,<1.38.0)", "mypy-boto3-panorama (>=1.37.0,<1.38.0)", "mypy-boto3-partnercentral-selling (>=1.37.0,<1.38.0)", "mypy-boto3-payment-cryptography (>=1.37.0,<1.38.0)", "mypy-boto3-payment-cryptography-data (>=1.37.0,<1.38.0)", "mypy-boto3-pca-connector-ad (>=1.37.0,<1.38.0)", "mypy-boto3-pca-connector-scep (>=1.37.0,<1.38.0)", "mypy-boto3-pcs (>=1.37.0,<1.38.0)", "mypy-boto3-personalize (>=1.37.0,<1.38.0)", "mypy-boto3-personalize-events (>=1.37.0,<1.38.0)", "mypy-boto3-personalize-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-pi (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint-email (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint-sms-voice (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.37.0,<1.38.0)", "mypy-boto3-pipes (>=1.37.0,<1.38.0)", "mypy-boto3-polly (>=1.37.0,<1.38.0)", "mypy-boto3-pricing (>=1.37.0,<1.38.0)", "mypy-boto3-privatenetworks (>=1.37.0,<1.38.0)", "mypy-boto3-proton (>=1.37.0,<1.38.0)", "mypy-boto3-qapps (>=1.37.0,<1.38.0)", "mypy-boto3-qbusiness (>=1.37.0,<1.38.0)", "mypy-boto3-qconnect (>=1.37.0,<1.38.0)", "mypy-boto3-qldb (>=1.37.0,<1.38.0)", "mypy-boto3-qldb-session (>=1.37.0,<1.38.0)", "mypy-boto3-quicksight (>=1.37.0,<1.38.0)", "mypy-boto3-ram (>=1.37.0,<1.38.0)", "mypy-boto3-rbin (>=1.37.0,<1.38.0)", "mypy-boto3-rds (>=1.37.0,<1.38.0)", "mypy-boto3-rds-data (>=1.37.0,<1.38.0)", "mypy-boto3-redshift (>=1.37.0,<1.38.0)", "mypy-boto3-redshift-data (>=1.37.0,<1.38.0)", "mypy-boto3-redshift-serverless (>=1.37.0,<1.38.0)", "mypy-boto3-rekognition (>=1.37.0,<1.38.0)", "mypy-boto3-repostspace (>=1.37.0,<1.38.0)", "mypy-boto3-resiliencehub (>=1.37.0,<1.38.0)", "mypy-boto3-resource-explorer-2 (>=1.37.0,<1.38.0)", "mypy-boto3-resource-groups (>=1.37.0,<1.38.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.37.0,<1.38.0)", "mypy-boto3-robomaker (>=1.37.0,<1.38.0)", "mypy-boto3-rolesanywhere (>=1.37.0,<1.38.0)", "mypy-boto3-route53 (>=1.37.0,<1.38.0)", "mypy-boto3-route53-recovery-cluster (>=1.37.0,<1.38.0)", "mypy-boto3-route53-recovery-control-config (>=1.37.0,<1.38.0)", "mypy-boto3-route53-recovery-readiness (>=1.37.0,<1.38.0)", "mypy-boto3-route53domains (>=1.37.0,<1.38.0)", "mypy-boto3-route53profiles (>=1.37.0,<1.38.0)", "mypy-boto3-route53resolver (>=1.37.0,<1.38.0)", "mypy-boto3-rum (>=1.37.0,<1.38.0)", "mypy-boto3-s3 (>=1.37.0,<1.38.0)", "mypy-boto3-s3control (>=1.37.0,<1.38.0)", "mypy-boto3-s3outposts (>=1.37.0,<1.38.0)", "mypy-boto3-s3tables (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-edge (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-geospatial (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-metrics (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-savingsplans (>=1.37.0,<1.38.0)", "mypy-boto3-scheduler (>=1.37.0,<1.38.0)", "mypy-boto3-schemas (>=1.37.0,<1.38.0)", "mypy-boto3-sdb (>=1.37.0,<1.38.0)", "mypy-boto3-secretsmanager (>=1.37.0,<1.38.0)", "mypy-boto3-security-ir (>=1.37.0,<1.38.0)", "mypy-boto3-securityhub (>=1.37.0,<1.38.0)", "mypy-boto3-securitylake (>=1.37.0,<1.38.0)", "mypy-boto3-serverlessrepo (>=1.37.0,<1.38.0)", "mypy-boto3-service-quotas (>=1.37.0,<1.38.0)", "mypy-boto3-servicecatalog (>=1.37.0,<1.38.0)", "mypy-boto3-servicecatalog-appregistry (>=1.37.0,<1.38.0)", "mypy-boto3-servicediscovery (>=1.37.0,<1.38.0)", "mypy-boto3-ses (>=1.37.0,<1.38.0)", "mypy-boto3-sesv2 (>=1.37.0,<1.38.0)", "mypy-boto3-shield (>=1.37.0,<1.38.0)", "mypy-boto3-signer (>=1.37.0,<1.38.0)", "mypy-boto3-simspaceweaver (>=1.37.0,<1.38.0)", "mypy-boto3-sms (>=1.37.0,<1.38.0)", "mypy-boto3-sms-voice (>=1.37.0,<1.38.0)", "mypy-boto3-snow-device-management (>=1.37.0,<1.38.0)", "mypy-boto3-snowball (>=1.37.0,<1.38.0)", "mypy-boto3-sns (>=1.37.0,<1.38.0)", "mypy-boto3-socialmessaging (>=1.37.0,<1.38.0)", "mypy-boto3-sqs (>=1.37.0,<1.38.0)", "mypy-boto3-ssm (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-contacts (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-incidents (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-quicksetup (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-sap (>=1.37.0,<1.38.0)", "mypy-boto3-sso (>=1.37.0,<1.38.0)", "mypy-boto3-sso-admin (>=1.37.0,<1.38.0)", "mypy-boto3-sso-oidc (>=1.37.0,<1.38.0)", "mypy-boto3-stepfunctions (>=1.37.0,<1.38.0)", "mypy-boto3-storagegateway (>=1.37.0,<1.38.0)", "mypy-boto3-sts (>=1.37.0,<1.38.0)", "mypy-boto3-supplychain (>=1.37.0,<1.38.0)", "mypy-boto3-support (>=1.37.0,<1.38.0)", "mypy-boto3-support-app (>=1.37.0,<1.38.0)", "mypy-boto3-swf (>=1.37.0,<1.38.0)", "mypy-boto3-synthetics (>=1.37.0,<1.38.0)", "mypy-boto3-taxsettings (>=1.37.0,<1.38.0)", "mypy-boto3-textract (>=1.37.0,<1.38.0)", "mypy-boto3-timestream-influxdb (>=1.37.0,<1.38.0)", "mypy-boto3-timestream-query (>=1.37.0,<1.38.0)", "mypy-boto3-timestream-write (>=1.37.0,<1.38.0)", "mypy-boto3-tnb (>=1.37.0,<1.38.0)", "mypy-boto3-transcribe (>=1.37.0,<1.38.0)", "mypy-boto3-transfer (>=1.37.0,<1.38.0)", "mypy-boto3-translate (>=1.37.0,<1.38.0)", "mypy-boto3-trustedadvisor (>=1.37.0,<1.38.0)", "mypy-boto3-verifiedpermissions (>=1.37.0,<1.38.0)", "mypy-boto3-voice-id (>=1.37.0,<1.38.0)", "mypy-boto3-vpc-lattice (>=1.37.0,<1.38.0)", "mypy-boto3-waf (>=1.37.0,<1.38.0)", "mypy-boto3-waf-regional (>=1.37.0,<1.38.0)", "mypy-boto3-wafv2 (>=1.37.0,<1.38.0)", "mypy-boto3-wellarchitected (>=1.37.0,<1.38.0)", "mypy-boto3-wisdom (>=1.37.0,<1.38.0)", "mypy-boto3-workdocs (>=1.37.0,<1.38.0)", "mypy-boto3-workmail (>=1.37.0,<1.38.0)", "mypy-boto3-workmailmessageflow (>=1.37.0,<1.38.0)", "mypy-boto3-workspaces (>=1.37.0,<1.38.0)", "mypy-boto3-workspaces-thin-client (>=1.37.0,<1.38.0)", "mypy-boto3-workspaces-web (>=1.37.0,<1.38.0)", "mypy-boto3-xray (>=1.37.0,<1.38.0)"] -amp = ["mypy-boto3-amp (>=1.37.0,<1.38.0)"] -amplify = ["mypy-boto3-amplify (>=1.37.0,<1.38.0)"] -amplifybackend = ["mypy-boto3-amplifybackend (>=1.37.0,<1.38.0)"] -amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.37.0,<1.38.0)"] -apigateway = ["mypy-boto3-apigateway (>=1.37.0,<1.38.0)"] -apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.37.0,<1.38.0)"] -apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.37.0,<1.38.0)"] -appconfig = ["mypy-boto3-appconfig (>=1.37.0,<1.38.0)"] -appconfigdata = ["mypy-boto3-appconfigdata (>=1.37.0,<1.38.0)"] -appfabric = ["mypy-boto3-appfabric (>=1.37.0,<1.38.0)"] -appflow = ["mypy-boto3-appflow (>=1.37.0,<1.38.0)"] -appintegrations = ["mypy-boto3-appintegrations (>=1.37.0,<1.38.0)"] -application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.37.0,<1.38.0)"] -application-insights = ["mypy-boto3-application-insights (>=1.37.0,<1.38.0)"] -application-signals = ["mypy-boto3-application-signals (>=1.37.0,<1.38.0)"] -applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.37.0,<1.38.0)"] -appmesh = ["mypy-boto3-appmesh (>=1.37.0,<1.38.0)"] -apprunner = ["mypy-boto3-apprunner (>=1.37.0,<1.38.0)"] -appstream = ["mypy-boto3-appstream (>=1.37.0,<1.38.0)"] -appsync = ["mypy-boto3-appsync (>=1.37.0,<1.38.0)"] -apptest = ["mypy-boto3-apptest (>=1.37.0,<1.38.0)"] -arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.37.0,<1.38.0)"] -artifact = ["mypy-boto3-artifact (>=1.37.0,<1.38.0)"] -athena = ["mypy-boto3-athena (>=1.37.0,<1.38.0)"] -auditmanager = ["mypy-boto3-auditmanager (>=1.37.0,<1.38.0)"] -autoscaling = ["mypy-boto3-autoscaling (>=1.37.0,<1.38.0)"] -autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.37.0,<1.38.0)"] -b2bi = ["mypy-boto3-b2bi (>=1.37.0,<1.38.0)"] -backup = ["mypy-boto3-backup (>=1.37.0,<1.38.0)"] -backup-gateway = ["mypy-boto3-backup-gateway (>=1.37.0,<1.38.0)"] -backupsearch = ["mypy-boto3-backupsearch (>=1.37.0,<1.38.0)"] -batch = ["mypy-boto3-batch (>=1.37.0,<1.38.0)"] -bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.37.0,<1.38.0)"] -bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.37.0,<1.38.0)"] -bedrock = ["mypy-boto3-bedrock (>=1.37.0,<1.38.0)"] -bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.37.0,<1.38.0)"] -bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.37.0,<1.38.0)"] -bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.37.0,<1.38.0)"] -bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.37.0,<1.38.0)"] -bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.37.0,<1.38.0)"] -billing = ["mypy-boto3-billing (>=1.37.0,<1.38.0)"] -billingconductor = ["mypy-boto3-billingconductor (>=1.37.0,<1.38.0)"] -boto3 = ["boto3 (==1.37.14)"] -braket = ["mypy-boto3-braket (>=1.37.0,<1.38.0)"] -budgets = ["mypy-boto3-budgets (>=1.37.0,<1.38.0)"] -ce = ["mypy-boto3-ce (>=1.37.0,<1.38.0)"] -chatbot = ["mypy-boto3-chatbot (>=1.37.0,<1.38.0)"] -chime = ["mypy-boto3-chime (>=1.37.0,<1.38.0)"] -chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.37.0,<1.38.0)"] -chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.37.0,<1.38.0)"] -chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.37.0,<1.38.0)"] -chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.37.0,<1.38.0)"] -chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.37.0,<1.38.0)"] -cleanrooms = ["mypy-boto3-cleanrooms (>=1.37.0,<1.38.0)"] -cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.37.0,<1.38.0)"] -cloud9 = ["mypy-boto3-cloud9 (>=1.37.0,<1.38.0)"] -cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.37.0,<1.38.0)"] -clouddirectory = ["mypy-boto3-clouddirectory (>=1.37.0,<1.38.0)"] -cloudformation = ["mypy-boto3-cloudformation (>=1.37.0,<1.38.0)"] -cloudfront = ["mypy-boto3-cloudfront (>=1.37.0,<1.38.0)"] -cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.37.0,<1.38.0)"] -cloudhsm = ["mypy-boto3-cloudhsm (>=1.37.0,<1.38.0)"] -cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.37.0,<1.38.0)"] -cloudsearch = ["mypy-boto3-cloudsearch (>=1.37.0,<1.38.0)"] -cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.37.0,<1.38.0)"] -cloudtrail = ["mypy-boto3-cloudtrail (>=1.37.0,<1.38.0)"] -cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.37.0,<1.38.0)"] -cloudwatch = ["mypy-boto3-cloudwatch (>=1.37.0,<1.38.0)"] -codeartifact = ["mypy-boto3-codeartifact (>=1.37.0,<1.38.0)"] -codebuild = ["mypy-boto3-codebuild (>=1.37.0,<1.38.0)"] -codecatalyst = ["mypy-boto3-codecatalyst (>=1.37.0,<1.38.0)"] -codecommit = ["mypy-boto3-codecommit (>=1.37.0,<1.38.0)"] -codeconnections = ["mypy-boto3-codeconnections (>=1.37.0,<1.38.0)"] -codedeploy = ["mypy-boto3-codedeploy (>=1.37.0,<1.38.0)"] -codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.37.0,<1.38.0)"] -codeguru-security = ["mypy-boto3-codeguru-security (>=1.37.0,<1.38.0)"] -codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.37.0,<1.38.0)"] -codepipeline = ["mypy-boto3-codepipeline (>=1.37.0,<1.38.0)"] -codestar-connections = ["mypy-boto3-codestar-connections (>=1.37.0,<1.38.0)"] -codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.37.0,<1.38.0)"] -cognito-identity = ["mypy-boto3-cognito-identity (>=1.37.0,<1.38.0)"] -cognito-idp = ["mypy-boto3-cognito-idp (>=1.37.0,<1.38.0)"] -cognito-sync = ["mypy-boto3-cognito-sync (>=1.37.0,<1.38.0)"] -comprehend = ["mypy-boto3-comprehend (>=1.37.0,<1.38.0)"] -comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.37.0,<1.38.0)"] -compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.37.0,<1.38.0)"] -config = ["mypy-boto3-config (>=1.37.0,<1.38.0)"] -connect = ["mypy-boto3-connect (>=1.37.0,<1.38.0)"] -connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.37.0,<1.38.0)"] -connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.37.0,<1.38.0)"] -connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.37.0,<1.38.0)"] -connectcases = ["mypy-boto3-connectcases (>=1.37.0,<1.38.0)"] -connectparticipant = ["mypy-boto3-connectparticipant (>=1.37.0,<1.38.0)"] -controlcatalog = ["mypy-boto3-controlcatalog (>=1.37.0,<1.38.0)"] -controltower = ["mypy-boto3-controltower (>=1.37.0,<1.38.0)"] -cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.37.0,<1.38.0)"] -cur = ["mypy-boto3-cur (>=1.37.0,<1.38.0)"] -customer-profiles = ["mypy-boto3-customer-profiles (>=1.37.0,<1.38.0)"] -databrew = ["mypy-boto3-databrew (>=1.37.0,<1.38.0)"] -dataexchange = ["mypy-boto3-dataexchange (>=1.37.0,<1.38.0)"] -datapipeline = ["mypy-boto3-datapipeline (>=1.37.0,<1.38.0)"] -datasync = ["mypy-boto3-datasync (>=1.37.0,<1.38.0)"] -datazone = ["mypy-boto3-datazone (>=1.37.0,<1.38.0)"] -dax = ["mypy-boto3-dax (>=1.37.0,<1.38.0)"] -deadline = ["mypy-boto3-deadline (>=1.37.0,<1.38.0)"] -detective = ["mypy-boto3-detective (>=1.37.0,<1.38.0)"] -devicefarm = ["mypy-boto3-devicefarm (>=1.37.0,<1.38.0)"] -devops-guru = ["mypy-boto3-devops-guru (>=1.37.0,<1.38.0)"] -directconnect = ["mypy-boto3-directconnect (>=1.37.0,<1.38.0)"] -discovery = ["mypy-boto3-discovery (>=1.37.0,<1.38.0)"] -dlm = ["mypy-boto3-dlm (>=1.37.0,<1.38.0)"] -dms = ["mypy-boto3-dms (>=1.37.0,<1.38.0)"] -docdb = ["mypy-boto3-docdb (>=1.37.0,<1.38.0)"] -docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.37.0,<1.38.0)"] -drs = ["mypy-boto3-drs (>=1.37.0,<1.38.0)"] -ds = ["mypy-boto3-ds (>=1.37.0,<1.38.0)"] -ds-data = ["mypy-boto3-ds-data (>=1.37.0,<1.38.0)"] -dsql = ["mypy-boto3-dsql (>=1.37.0,<1.38.0)"] -dynamodb = ["mypy-boto3-dynamodb (>=1.37.0,<1.38.0)"] -dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.37.0,<1.38.0)"] -ebs = ["mypy-boto3-ebs (>=1.37.0,<1.38.0)"] -ec2 = ["mypy-boto3-ec2 (>=1.37.0,<1.38.0)"] -ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.37.0,<1.38.0)"] -ecr = ["mypy-boto3-ecr (>=1.37.0,<1.38.0)"] -ecr-public = ["mypy-boto3-ecr-public (>=1.37.0,<1.38.0)"] -ecs = ["mypy-boto3-ecs (>=1.37.0,<1.38.0)"] -efs = ["mypy-boto3-efs (>=1.37.0,<1.38.0)"] -eks = ["mypy-boto3-eks (>=1.37.0,<1.38.0)"] -eks-auth = ["mypy-boto3-eks-auth (>=1.37.0,<1.38.0)"] -elasticache = ["mypy-boto3-elasticache (>=1.37.0,<1.38.0)"] -elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.37.0,<1.38.0)"] -elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.37.0,<1.38.0)"] -elb = ["mypy-boto3-elb (>=1.37.0,<1.38.0)"] -elbv2 = ["mypy-boto3-elbv2 (>=1.37.0,<1.38.0)"] -emr = ["mypy-boto3-emr (>=1.37.0,<1.38.0)"] -emr-containers = ["mypy-boto3-emr-containers (>=1.37.0,<1.38.0)"] -emr-serverless = ["mypy-boto3-emr-serverless (>=1.37.0,<1.38.0)"] -entityresolution = ["mypy-boto3-entityresolution (>=1.37.0,<1.38.0)"] -es = ["mypy-boto3-es (>=1.37.0,<1.38.0)"] -essential = ["mypy-boto3-cloudformation (>=1.37.0,<1.38.0)", "mypy-boto3-dynamodb (>=1.37.0,<1.38.0)", "mypy-boto3-ec2 (>=1.37.0,<1.38.0)", "mypy-boto3-lambda (>=1.37.0,<1.38.0)", "mypy-boto3-rds (>=1.37.0,<1.38.0)", "mypy-boto3-s3 (>=1.37.0,<1.38.0)", "mypy-boto3-sqs (>=1.37.0,<1.38.0)"] -events = ["mypy-boto3-events (>=1.37.0,<1.38.0)"] -evidently = ["mypy-boto3-evidently (>=1.37.0,<1.38.0)"] -finspace = ["mypy-boto3-finspace (>=1.37.0,<1.38.0)"] -finspace-data = ["mypy-boto3-finspace-data (>=1.37.0,<1.38.0)"] -firehose = ["mypy-boto3-firehose (>=1.37.0,<1.38.0)"] -fis = ["mypy-boto3-fis (>=1.37.0,<1.38.0)"] -fms = ["mypy-boto3-fms (>=1.37.0,<1.38.0)"] -forecast = ["mypy-boto3-forecast (>=1.37.0,<1.38.0)"] -forecastquery = ["mypy-boto3-forecastquery (>=1.37.0,<1.38.0)"] -frauddetector = ["mypy-boto3-frauddetector (>=1.37.0,<1.38.0)"] -freetier = ["mypy-boto3-freetier (>=1.37.0,<1.38.0)"] -fsx = ["mypy-boto3-fsx (>=1.37.0,<1.38.0)"] -full = ["boto3-stubs-full (>=1.37.0,<1.38.0)"] -gamelift = ["mypy-boto3-gamelift (>=1.37.0,<1.38.0)"] -gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.37.0,<1.38.0)"] -geo-maps = ["mypy-boto3-geo-maps (>=1.37.0,<1.38.0)"] -geo-places = ["mypy-boto3-geo-places (>=1.37.0,<1.38.0)"] -geo-routes = ["mypy-boto3-geo-routes (>=1.37.0,<1.38.0)"] -glacier = ["mypy-boto3-glacier (>=1.37.0,<1.38.0)"] -globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.37.0,<1.38.0)"] -glue = ["mypy-boto3-glue (>=1.37.0,<1.38.0)"] -grafana = ["mypy-boto3-grafana (>=1.37.0,<1.38.0)"] -greengrass = ["mypy-boto3-greengrass (>=1.37.0,<1.38.0)"] -greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.37.0,<1.38.0)"] -groundstation = ["mypy-boto3-groundstation (>=1.37.0,<1.38.0)"] -guardduty = ["mypy-boto3-guardduty (>=1.37.0,<1.38.0)"] -health = ["mypy-boto3-health (>=1.37.0,<1.38.0)"] -healthlake = ["mypy-boto3-healthlake (>=1.37.0,<1.38.0)"] -iam = ["mypy-boto3-iam (>=1.37.0,<1.38.0)"] -identitystore = ["mypy-boto3-identitystore (>=1.37.0,<1.38.0)"] -imagebuilder = ["mypy-boto3-imagebuilder (>=1.37.0,<1.38.0)"] -importexport = ["mypy-boto3-importexport (>=1.37.0,<1.38.0)"] -inspector = ["mypy-boto3-inspector (>=1.37.0,<1.38.0)"] -inspector-scan = ["mypy-boto3-inspector-scan (>=1.37.0,<1.38.0)"] -inspector2 = ["mypy-boto3-inspector2 (>=1.37.0,<1.38.0)"] -internetmonitor = ["mypy-boto3-internetmonitor (>=1.37.0,<1.38.0)"] -invoicing = ["mypy-boto3-invoicing (>=1.37.0,<1.38.0)"] -iot = ["mypy-boto3-iot (>=1.37.0,<1.38.0)"] -iot-data = ["mypy-boto3-iot-data (>=1.37.0,<1.38.0)"] -iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.37.0,<1.38.0)"] -iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.37.0,<1.38.0)"] -iotanalytics = ["mypy-boto3-iotanalytics (>=1.37.0,<1.38.0)"] -iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.37.0,<1.38.0)"] -iotevents = ["mypy-boto3-iotevents (>=1.37.0,<1.38.0)"] -iotevents-data = ["mypy-boto3-iotevents-data (>=1.37.0,<1.38.0)"] -iotfleethub = ["mypy-boto3-iotfleethub (>=1.37.0,<1.38.0)"] -iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.37.0,<1.38.0)"] -iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.37.0,<1.38.0)"] -iotsitewise = ["mypy-boto3-iotsitewise (>=1.37.0,<1.38.0)"] -iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.37.0,<1.38.0)"] -iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.37.0,<1.38.0)"] -iotwireless = ["mypy-boto3-iotwireless (>=1.37.0,<1.38.0)"] -ivs = ["mypy-boto3-ivs (>=1.37.0,<1.38.0)"] -ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.37.0,<1.38.0)"] -ivschat = ["mypy-boto3-ivschat (>=1.37.0,<1.38.0)"] -kafka = ["mypy-boto3-kafka (>=1.37.0,<1.38.0)"] -kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.37.0,<1.38.0)"] -kendra = ["mypy-boto3-kendra (>=1.37.0,<1.38.0)"] -kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.37.0,<1.38.0)"] -keyspaces = ["mypy-boto3-keyspaces (>=1.37.0,<1.38.0)"] -kinesis = ["mypy-boto3-kinesis (>=1.37.0,<1.38.0)"] -kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.37.0,<1.38.0)"] -kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.37.0,<1.38.0)"] -kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.37.0,<1.38.0)"] -kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.37.0,<1.38.0)"] -kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.37.0,<1.38.0)"] -kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.37.0,<1.38.0)"] -kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.37.0,<1.38.0)"] -kms = ["mypy-boto3-kms (>=1.37.0,<1.38.0)"] -lakeformation = ["mypy-boto3-lakeformation (>=1.37.0,<1.38.0)"] -lambda = ["mypy-boto3-lambda (>=1.37.0,<1.38.0)"] -launch-wizard = ["mypy-boto3-launch-wizard (>=1.37.0,<1.38.0)"] -lex-models = ["mypy-boto3-lex-models (>=1.37.0,<1.38.0)"] -lex-runtime = ["mypy-boto3-lex-runtime (>=1.37.0,<1.38.0)"] -lexv2-models = ["mypy-boto3-lexv2-models (>=1.37.0,<1.38.0)"] -lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.37.0,<1.38.0)"] -license-manager = ["mypy-boto3-license-manager (>=1.37.0,<1.38.0)"] -license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.37.0,<1.38.0)"] -license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.37.0,<1.38.0)"] -lightsail = ["mypy-boto3-lightsail (>=1.37.0,<1.38.0)"] -location = ["mypy-boto3-location (>=1.37.0,<1.38.0)"] -logs = ["mypy-boto3-logs (>=1.37.0,<1.38.0)"] -lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.37.0,<1.38.0)"] -lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.37.0,<1.38.0)"] -lookoutvision = ["mypy-boto3-lookoutvision (>=1.37.0,<1.38.0)"] -m2 = ["mypy-boto3-m2 (>=1.37.0,<1.38.0)"] -machinelearning = ["mypy-boto3-machinelearning (>=1.37.0,<1.38.0)"] -macie2 = ["mypy-boto3-macie2 (>=1.37.0,<1.38.0)"] -mailmanager = ["mypy-boto3-mailmanager (>=1.37.0,<1.38.0)"] -managedblockchain = ["mypy-boto3-managedblockchain (>=1.37.0,<1.38.0)"] -managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.37.0,<1.38.0)"] -marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.37.0,<1.38.0)"] -marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.37.0,<1.38.0)"] -marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.37.0,<1.38.0)"] -marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.37.0,<1.38.0)"] -marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.37.0,<1.38.0)"] -marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.37.0,<1.38.0)"] -mediaconnect = ["mypy-boto3-mediaconnect (>=1.37.0,<1.38.0)"] -mediaconvert = ["mypy-boto3-mediaconvert (>=1.37.0,<1.38.0)"] -medialive = ["mypy-boto3-medialive (>=1.37.0,<1.38.0)"] -mediapackage = ["mypy-boto3-mediapackage (>=1.37.0,<1.38.0)"] -mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.37.0,<1.38.0)"] -mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.37.0,<1.38.0)"] -mediastore = ["mypy-boto3-mediastore (>=1.37.0,<1.38.0)"] -mediastore-data = ["mypy-boto3-mediastore-data (>=1.37.0,<1.38.0)"] -mediatailor = ["mypy-boto3-mediatailor (>=1.37.0,<1.38.0)"] -medical-imaging = ["mypy-boto3-medical-imaging (>=1.37.0,<1.38.0)"] -memorydb = ["mypy-boto3-memorydb (>=1.37.0,<1.38.0)"] -meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.37.0,<1.38.0)"] -mgh = ["mypy-boto3-mgh (>=1.37.0,<1.38.0)"] -mgn = ["mypy-boto3-mgn (>=1.37.0,<1.38.0)"] -migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.37.0,<1.38.0)"] -migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.37.0,<1.38.0)"] -migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.37.0,<1.38.0)"] -migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.37.0,<1.38.0)"] -mq = ["mypy-boto3-mq (>=1.37.0,<1.38.0)"] -mturk = ["mypy-boto3-mturk (>=1.37.0,<1.38.0)"] -mwaa = ["mypy-boto3-mwaa (>=1.37.0,<1.38.0)"] -neptune = ["mypy-boto3-neptune (>=1.37.0,<1.38.0)"] -neptune-graph = ["mypy-boto3-neptune-graph (>=1.37.0,<1.38.0)"] -neptunedata = ["mypy-boto3-neptunedata (>=1.37.0,<1.38.0)"] -network-firewall = ["mypy-boto3-network-firewall (>=1.37.0,<1.38.0)"] -networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.37.0,<1.38.0)"] -networkmanager = ["mypy-boto3-networkmanager (>=1.37.0,<1.38.0)"] -networkmonitor = ["mypy-boto3-networkmonitor (>=1.37.0,<1.38.0)"] -notifications = ["mypy-boto3-notifications (>=1.37.0,<1.38.0)"] -notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.37.0,<1.38.0)"] -oam = ["mypy-boto3-oam (>=1.37.0,<1.38.0)"] -observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.37.0,<1.38.0)"] -omics = ["mypy-boto3-omics (>=1.37.0,<1.38.0)"] -opensearch = ["mypy-boto3-opensearch (>=1.37.0,<1.38.0)"] -opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.37.0,<1.38.0)"] -opsworks = ["mypy-boto3-opsworks (>=1.37.0,<1.38.0)"] -opsworkscm = ["mypy-boto3-opsworkscm (>=1.37.0,<1.38.0)"] -organizations = ["mypy-boto3-organizations (>=1.37.0,<1.38.0)"] -osis = ["mypy-boto3-osis (>=1.37.0,<1.38.0)"] -outposts = ["mypy-boto3-outposts (>=1.37.0,<1.38.0)"] -panorama = ["mypy-boto3-panorama (>=1.37.0,<1.38.0)"] -partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.37.0,<1.38.0)"] -payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.37.0,<1.38.0)"] -payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.37.0,<1.38.0)"] -pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.37.0,<1.38.0)"] -pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.37.0,<1.38.0)"] -pcs = ["mypy-boto3-pcs (>=1.37.0,<1.38.0)"] -personalize = ["mypy-boto3-personalize (>=1.37.0,<1.38.0)"] -personalize-events = ["mypy-boto3-personalize-events (>=1.37.0,<1.38.0)"] -personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.37.0,<1.38.0)"] -pi = ["mypy-boto3-pi (>=1.37.0,<1.38.0)"] -pinpoint = ["mypy-boto3-pinpoint (>=1.37.0,<1.38.0)"] -pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.37.0,<1.38.0)"] -pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.37.0,<1.38.0)"] -pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.37.0,<1.38.0)"] -pipes = ["mypy-boto3-pipes (>=1.37.0,<1.38.0)"] -polly = ["mypy-boto3-polly (>=1.37.0,<1.38.0)"] -pricing = ["mypy-boto3-pricing (>=1.37.0,<1.38.0)"] -privatenetworks = ["mypy-boto3-privatenetworks (>=1.37.0,<1.38.0)"] -proton = ["mypy-boto3-proton (>=1.37.0,<1.38.0)"] -qapps = ["mypy-boto3-qapps (>=1.37.0,<1.38.0)"] -qbusiness = ["mypy-boto3-qbusiness (>=1.37.0,<1.38.0)"] -qconnect = ["mypy-boto3-qconnect (>=1.37.0,<1.38.0)"] -qldb = ["mypy-boto3-qldb (>=1.37.0,<1.38.0)"] -qldb-session = ["mypy-boto3-qldb-session (>=1.37.0,<1.38.0)"] -quicksight = ["mypy-boto3-quicksight (>=1.37.0,<1.38.0)"] -ram = ["mypy-boto3-ram (>=1.37.0,<1.38.0)"] -rbin = ["mypy-boto3-rbin (>=1.37.0,<1.38.0)"] -rds = ["mypy-boto3-rds (>=1.37.0,<1.38.0)"] -rds-data = ["mypy-boto3-rds-data (>=1.37.0,<1.38.0)"] -redshift = ["mypy-boto3-redshift (>=1.37.0,<1.38.0)"] -redshift-data = ["mypy-boto3-redshift-data (>=1.37.0,<1.38.0)"] -redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.37.0,<1.38.0)"] -rekognition = ["mypy-boto3-rekognition (>=1.37.0,<1.38.0)"] -repostspace = ["mypy-boto3-repostspace (>=1.37.0,<1.38.0)"] -resiliencehub = ["mypy-boto3-resiliencehub (>=1.37.0,<1.38.0)"] -resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.37.0,<1.38.0)"] -resource-groups = ["mypy-boto3-resource-groups (>=1.37.0,<1.38.0)"] -resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.37.0,<1.38.0)"] -robomaker = ["mypy-boto3-robomaker (>=1.37.0,<1.38.0)"] -rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.37.0,<1.38.0)"] -route53 = ["mypy-boto3-route53 (>=1.37.0,<1.38.0)"] -route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.37.0,<1.38.0)"] -route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.37.0,<1.38.0)"] -route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.37.0,<1.38.0)"] -route53domains = ["mypy-boto3-route53domains (>=1.37.0,<1.38.0)"] -route53profiles = ["mypy-boto3-route53profiles (>=1.37.0,<1.38.0)"] -route53resolver = ["mypy-boto3-route53resolver (>=1.37.0,<1.38.0)"] -rum = ["mypy-boto3-rum (>=1.37.0,<1.38.0)"] -s3 = ["mypy-boto3-s3 (>=1.37.0,<1.38.0)"] -s3control = ["mypy-boto3-s3control (>=1.37.0,<1.38.0)"] -s3outposts = ["mypy-boto3-s3outposts (>=1.37.0,<1.38.0)"] -s3tables = ["mypy-boto3-s3tables (>=1.37.0,<1.38.0)"] -sagemaker = ["mypy-boto3-sagemaker (>=1.37.0,<1.38.0)"] -sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.37.0,<1.38.0)"] -sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.37.0,<1.38.0)"] -sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.37.0,<1.38.0)"] -sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.37.0,<1.38.0)"] -sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.37.0,<1.38.0)"] -sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.37.0,<1.38.0)"] -savingsplans = ["mypy-boto3-savingsplans (>=1.37.0,<1.38.0)"] -scheduler = ["mypy-boto3-scheduler (>=1.37.0,<1.38.0)"] -schemas = ["mypy-boto3-schemas (>=1.37.0,<1.38.0)"] -sdb = ["mypy-boto3-sdb (>=1.37.0,<1.38.0)"] -secretsmanager = ["mypy-boto3-secretsmanager (>=1.37.0,<1.38.0)"] -security-ir = ["mypy-boto3-security-ir (>=1.37.0,<1.38.0)"] -securityhub = ["mypy-boto3-securityhub (>=1.37.0,<1.38.0)"] -securitylake = ["mypy-boto3-securitylake (>=1.37.0,<1.38.0)"] -serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.37.0,<1.38.0)"] -service-quotas = ["mypy-boto3-service-quotas (>=1.37.0,<1.38.0)"] -servicecatalog = ["mypy-boto3-servicecatalog (>=1.37.0,<1.38.0)"] -servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.37.0,<1.38.0)"] -servicediscovery = ["mypy-boto3-servicediscovery (>=1.37.0,<1.38.0)"] -ses = ["mypy-boto3-ses (>=1.37.0,<1.38.0)"] -sesv2 = ["mypy-boto3-sesv2 (>=1.37.0,<1.38.0)"] -shield = ["mypy-boto3-shield (>=1.37.0,<1.38.0)"] -signer = ["mypy-boto3-signer (>=1.37.0,<1.38.0)"] -simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.37.0,<1.38.0)"] -sms = ["mypy-boto3-sms (>=1.37.0,<1.38.0)"] -sms-voice = ["mypy-boto3-sms-voice (>=1.37.0,<1.38.0)"] -snow-device-management = ["mypy-boto3-snow-device-management (>=1.37.0,<1.38.0)"] -snowball = ["mypy-boto3-snowball (>=1.37.0,<1.38.0)"] -sns = ["mypy-boto3-sns (>=1.37.0,<1.38.0)"] -socialmessaging = ["mypy-boto3-socialmessaging (>=1.37.0,<1.38.0)"] -sqs = ["mypy-boto3-sqs (>=1.37.0,<1.38.0)"] -ssm = ["mypy-boto3-ssm (>=1.37.0,<1.38.0)"] -ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.37.0,<1.38.0)"] -ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.37.0,<1.38.0)"] -ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.37.0,<1.38.0)"] -ssm-sap = ["mypy-boto3-ssm-sap (>=1.37.0,<1.38.0)"] -sso = ["mypy-boto3-sso (>=1.37.0,<1.38.0)"] -sso-admin = ["mypy-boto3-sso-admin (>=1.37.0,<1.38.0)"] -sso-oidc = ["mypy-boto3-sso-oidc (>=1.37.0,<1.38.0)"] -stepfunctions = ["mypy-boto3-stepfunctions (>=1.37.0,<1.38.0)"] -storagegateway = ["mypy-boto3-storagegateway (>=1.37.0,<1.38.0)"] -sts = ["mypy-boto3-sts (>=1.37.0,<1.38.0)"] -supplychain = ["mypy-boto3-supplychain (>=1.37.0,<1.38.0)"] -support = ["mypy-boto3-support (>=1.37.0,<1.38.0)"] -support-app = ["mypy-boto3-support-app (>=1.37.0,<1.38.0)"] -swf = ["mypy-boto3-swf (>=1.37.0,<1.38.0)"] -synthetics = ["mypy-boto3-synthetics (>=1.37.0,<1.38.0)"] -taxsettings = ["mypy-boto3-taxsettings (>=1.37.0,<1.38.0)"] -textract = ["mypy-boto3-textract (>=1.37.0,<1.38.0)"] -timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.37.0,<1.38.0)"] -timestream-query = ["mypy-boto3-timestream-query (>=1.37.0,<1.38.0)"] -timestream-write = ["mypy-boto3-timestream-write (>=1.37.0,<1.38.0)"] -tnb = ["mypy-boto3-tnb (>=1.37.0,<1.38.0)"] -transcribe = ["mypy-boto3-transcribe (>=1.37.0,<1.38.0)"] -transfer = ["mypy-boto3-transfer (>=1.37.0,<1.38.0)"] -translate = ["mypy-boto3-translate (>=1.37.0,<1.38.0)"] -trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.37.0,<1.38.0)"] -verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.37.0,<1.38.0)"] -voice-id = ["mypy-boto3-voice-id (>=1.37.0,<1.38.0)"] -vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.37.0,<1.38.0)"] -waf = ["mypy-boto3-waf (>=1.37.0,<1.38.0)"] -waf-regional = ["mypy-boto3-waf-regional (>=1.37.0,<1.38.0)"] -wafv2 = ["mypy-boto3-wafv2 (>=1.37.0,<1.38.0)"] -wellarchitected = ["mypy-boto3-wellarchitected (>=1.37.0,<1.38.0)"] -wisdom = ["mypy-boto3-wisdom (>=1.37.0,<1.38.0)"] -workdocs = ["mypy-boto3-workdocs (>=1.37.0,<1.38.0)"] -workmail = ["mypy-boto3-workmail (>=1.37.0,<1.38.0)"] -workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.37.0,<1.38.0)"] -workspaces = ["mypy-boto3-workspaces (>=1.37.0,<1.38.0)"] -workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.37.0,<1.38.0)"] -workspaces-web = ["mypy-boto3-workspaces-web (>=1.37.0,<1.38.0)"] -xray = ["mypy-boto3-xray (>=1.37.0,<1.38.0)"] +accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.38.0,<1.39.0)"] +account = ["mypy-boto3-account (>=1.38.0,<1.39.0)"] +acm = ["mypy-boto3-acm (>=1.38.0,<1.39.0)"] +acm-pca = ["mypy-boto3-acm-pca (>=1.38.0,<1.39.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.38.0,<1.39.0)", "mypy-boto3-account (>=1.38.0,<1.39.0)", "mypy-boto3-acm (>=1.38.0,<1.39.0)", "mypy-boto3-acm-pca (>=1.38.0,<1.39.0)", "mypy-boto3-amp (>=1.38.0,<1.39.0)", "mypy-boto3-amplify (>=1.38.0,<1.39.0)", "mypy-boto3-amplifybackend (>=1.38.0,<1.39.0)", "mypy-boto3-amplifyuibuilder (>=1.38.0,<1.39.0)", "mypy-boto3-apigateway (>=1.38.0,<1.39.0)", "mypy-boto3-apigatewaymanagementapi (>=1.38.0,<1.39.0)", "mypy-boto3-apigatewayv2 (>=1.38.0,<1.39.0)", "mypy-boto3-appconfig (>=1.38.0,<1.39.0)", "mypy-boto3-appconfigdata (>=1.38.0,<1.39.0)", "mypy-boto3-appfabric (>=1.38.0,<1.39.0)", "mypy-boto3-appflow (>=1.38.0,<1.39.0)", "mypy-boto3-appintegrations (>=1.38.0,<1.39.0)", "mypy-boto3-application-autoscaling (>=1.38.0,<1.39.0)", "mypy-boto3-application-insights (>=1.38.0,<1.39.0)", "mypy-boto3-application-signals (>=1.38.0,<1.39.0)", "mypy-boto3-applicationcostprofiler (>=1.38.0,<1.39.0)", "mypy-boto3-appmesh (>=1.38.0,<1.39.0)", "mypy-boto3-apprunner (>=1.38.0,<1.39.0)", "mypy-boto3-appstream (>=1.38.0,<1.39.0)", "mypy-boto3-appsync (>=1.38.0,<1.39.0)", "mypy-boto3-apptest (>=1.38.0,<1.39.0)", "mypy-boto3-arc-zonal-shift (>=1.38.0,<1.39.0)", "mypy-boto3-artifact (>=1.38.0,<1.39.0)", "mypy-boto3-athena (>=1.38.0,<1.39.0)", "mypy-boto3-auditmanager (>=1.38.0,<1.39.0)", "mypy-boto3-autoscaling (>=1.38.0,<1.39.0)", "mypy-boto3-autoscaling-plans (>=1.38.0,<1.39.0)", "mypy-boto3-b2bi (>=1.38.0,<1.39.0)", "mypy-boto3-backup (>=1.38.0,<1.39.0)", "mypy-boto3-backup-gateway (>=1.38.0,<1.39.0)", "mypy-boto3-backupsearch (>=1.38.0,<1.39.0)", "mypy-boto3-batch (>=1.38.0,<1.39.0)", "mypy-boto3-bcm-data-exports (>=1.38.0,<1.39.0)", "mypy-boto3-bcm-pricing-calculator (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-agent (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-agent-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-data-automation (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-billing (>=1.38.0,<1.39.0)", "mypy-boto3-billingconductor (>=1.38.0,<1.39.0)", "mypy-boto3-braket (>=1.38.0,<1.39.0)", "mypy-boto3-budgets (>=1.38.0,<1.39.0)", "mypy-boto3-ce (>=1.38.0,<1.39.0)", "mypy-boto3-chatbot (>=1.38.0,<1.39.0)", "mypy-boto3-chime (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-identity (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-meetings (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-messaging (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-voice (>=1.38.0,<1.39.0)", "mypy-boto3-cleanrooms (>=1.38.0,<1.39.0)", "mypy-boto3-cleanroomsml (>=1.38.0,<1.39.0)", "mypy-boto3-cloud9 (>=1.38.0,<1.39.0)", "mypy-boto3-cloudcontrol (>=1.38.0,<1.39.0)", "mypy-boto3-clouddirectory (>=1.38.0,<1.39.0)", "mypy-boto3-cloudformation (>=1.38.0,<1.39.0)", "mypy-boto3-cloudfront (>=1.38.0,<1.39.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.38.0,<1.39.0)", "mypy-boto3-cloudhsm (>=1.38.0,<1.39.0)", "mypy-boto3-cloudhsmv2 (>=1.38.0,<1.39.0)", "mypy-boto3-cloudsearch (>=1.38.0,<1.39.0)", "mypy-boto3-cloudsearchdomain (>=1.38.0,<1.39.0)", "mypy-boto3-cloudtrail (>=1.38.0,<1.39.0)", "mypy-boto3-cloudtrail-data (>=1.38.0,<1.39.0)", "mypy-boto3-cloudwatch (>=1.38.0,<1.39.0)", "mypy-boto3-codeartifact (>=1.38.0,<1.39.0)", "mypy-boto3-codebuild (>=1.38.0,<1.39.0)", "mypy-boto3-codecatalyst (>=1.38.0,<1.39.0)", "mypy-boto3-codecommit (>=1.38.0,<1.39.0)", "mypy-boto3-codeconnections (>=1.38.0,<1.39.0)", "mypy-boto3-codedeploy (>=1.38.0,<1.39.0)", "mypy-boto3-codeguru-reviewer (>=1.38.0,<1.39.0)", "mypy-boto3-codeguru-security (>=1.38.0,<1.39.0)", "mypy-boto3-codeguruprofiler (>=1.38.0,<1.39.0)", "mypy-boto3-codepipeline (>=1.38.0,<1.39.0)", "mypy-boto3-codestar-connections (>=1.38.0,<1.39.0)", "mypy-boto3-codestar-notifications (>=1.38.0,<1.39.0)", "mypy-boto3-cognito-identity (>=1.38.0,<1.39.0)", "mypy-boto3-cognito-idp (>=1.38.0,<1.39.0)", "mypy-boto3-cognito-sync (>=1.38.0,<1.39.0)", "mypy-boto3-comprehend (>=1.38.0,<1.39.0)", "mypy-boto3-comprehendmedical (>=1.38.0,<1.39.0)", "mypy-boto3-compute-optimizer (>=1.38.0,<1.39.0)", "mypy-boto3-config (>=1.38.0,<1.39.0)", "mypy-boto3-connect (>=1.38.0,<1.39.0)", "mypy-boto3-connect-contact-lens (>=1.38.0,<1.39.0)", "mypy-boto3-connectcampaigns (>=1.38.0,<1.39.0)", "mypy-boto3-connectcampaignsv2 (>=1.38.0,<1.39.0)", "mypy-boto3-connectcases (>=1.38.0,<1.39.0)", "mypy-boto3-connectparticipant (>=1.38.0,<1.39.0)", "mypy-boto3-controlcatalog (>=1.38.0,<1.39.0)", "mypy-boto3-controltower (>=1.38.0,<1.39.0)", "mypy-boto3-cost-optimization-hub (>=1.38.0,<1.39.0)", "mypy-boto3-cur (>=1.38.0,<1.39.0)", "mypy-boto3-customer-profiles (>=1.38.0,<1.39.0)", "mypy-boto3-databrew (>=1.38.0,<1.39.0)", "mypy-boto3-dataexchange (>=1.38.0,<1.39.0)", "mypy-boto3-datapipeline (>=1.38.0,<1.39.0)", "mypy-boto3-datasync (>=1.38.0,<1.39.0)", "mypy-boto3-datazone (>=1.38.0,<1.39.0)", "mypy-boto3-dax (>=1.38.0,<1.39.0)", "mypy-boto3-deadline (>=1.38.0,<1.39.0)", "mypy-boto3-detective (>=1.38.0,<1.39.0)", "mypy-boto3-devicefarm (>=1.38.0,<1.39.0)", "mypy-boto3-devops-guru (>=1.38.0,<1.39.0)", "mypy-boto3-directconnect (>=1.38.0,<1.39.0)", "mypy-boto3-discovery (>=1.38.0,<1.39.0)", "mypy-boto3-dlm (>=1.38.0,<1.39.0)", "mypy-boto3-dms (>=1.38.0,<1.39.0)", "mypy-boto3-docdb (>=1.38.0,<1.39.0)", "mypy-boto3-docdb-elastic (>=1.38.0,<1.39.0)", "mypy-boto3-drs (>=1.38.0,<1.39.0)", "mypy-boto3-ds (>=1.38.0,<1.39.0)", "mypy-boto3-ds-data (>=1.38.0,<1.39.0)", "mypy-boto3-dsql (>=1.38.0,<1.39.0)", "mypy-boto3-dynamodb (>=1.38.0,<1.39.0)", "mypy-boto3-dynamodbstreams (>=1.38.0,<1.39.0)", "mypy-boto3-ebs (>=1.38.0,<1.39.0)", "mypy-boto3-ec2 (>=1.38.0,<1.39.0)", "mypy-boto3-ec2-instance-connect (>=1.38.0,<1.39.0)", "mypy-boto3-ecr (>=1.38.0,<1.39.0)", "mypy-boto3-ecr-public (>=1.38.0,<1.39.0)", "mypy-boto3-ecs (>=1.38.0,<1.39.0)", "mypy-boto3-efs (>=1.38.0,<1.39.0)", "mypy-boto3-eks (>=1.38.0,<1.39.0)", "mypy-boto3-eks-auth (>=1.38.0,<1.39.0)", "mypy-boto3-elasticache (>=1.38.0,<1.39.0)", "mypy-boto3-elasticbeanstalk (>=1.38.0,<1.39.0)", "mypy-boto3-elastictranscoder (>=1.38.0,<1.39.0)", "mypy-boto3-elb (>=1.38.0,<1.39.0)", "mypy-boto3-elbv2 (>=1.38.0,<1.39.0)", "mypy-boto3-emr (>=1.38.0,<1.39.0)", "mypy-boto3-emr-containers (>=1.38.0,<1.39.0)", "mypy-boto3-emr-serverless (>=1.38.0,<1.39.0)", "mypy-boto3-entityresolution (>=1.38.0,<1.39.0)", "mypy-boto3-es (>=1.38.0,<1.39.0)", "mypy-boto3-events (>=1.38.0,<1.39.0)", "mypy-boto3-evidently (>=1.38.0,<1.39.0)", "mypy-boto3-finspace (>=1.38.0,<1.39.0)", "mypy-boto3-finspace-data (>=1.38.0,<1.39.0)", "mypy-boto3-firehose (>=1.38.0,<1.39.0)", "mypy-boto3-fis (>=1.38.0,<1.39.0)", "mypy-boto3-fms (>=1.38.0,<1.39.0)", "mypy-boto3-forecast (>=1.38.0,<1.39.0)", "mypy-boto3-forecastquery (>=1.38.0,<1.39.0)", "mypy-boto3-frauddetector (>=1.38.0,<1.39.0)", "mypy-boto3-freetier (>=1.38.0,<1.39.0)", "mypy-boto3-fsx (>=1.38.0,<1.39.0)", "mypy-boto3-gamelift (>=1.38.0,<1.39.0)", "mypy-boto3-gameliftstreams (>=1.38.0,<1.39.0)", "mypy-boto3-geo-maps (>=1.38.0,<1.39.0)", "mypy-boto3-geo-places (>=1.38.0,<1.39.0)", "mypy-boto3-geo-routes (>=1.38.0,<1.39.0)", "mypy-boto3-glacier (>=1.38.0,<1.39.0)", "mypy-boto3-globalaccelerator (>=1.38.0,<1.39.0)", "mypy-boto3-glue (>=1.38.0,<1.39.0)", "mypy-boto3-grafana (>=1.38.0,<1.39.0)", "mypy-boto3-greengrass (>=1.38.0,<1.39.0)", "mypy-boto3-greengrassv2 (>=1.38.0,<1.39.0)", "mypy-boto3-groundstation (>=1.38.0,<1.39.0)", "mypy-boto3-guardduty (>=1.38.0,<1.39.0)", "mypy-boto3-health (>=1.38.0,<1.39.0)", "mypy-boto3-healthlake (>=1.38.0,<1.39.0)", "mypy-boto3-iam (>=1.38.0,<1.39.0)", "mypy-boto3-identitystore (>=1.38.0,<1.39.0)", "mypy-boto3-imagebuilder (>=1.38.0,<1.39.0)", "mypy-boto3-importexport (>=1.38.0,<1.39.0)", "mypy-boto3-inspector (>=1.38.0,<1.39.0)", "mypy-boto3-inspector-scan (>=1.38.0,<1.39.0)", "mypy-boto3-inspector2 (>=1.38.0,<1.39.0)", "mypy-boto3-internetmonitor (>=1.38.0,<1.39.0)", "mypy-boto3-invoicing (>=1.38.0,<1.39.0)", "mypy-boto3-iot (>=1.38.0,<1.39.0)", "mypy-boto3-iot-data (>=1.38.0,<1.39.0)", "mypy-boto3-iot-jobs-data (>=1.38.0,<1.39.0)", "mypy-boto3-iot-managed-integrations (>=1.38.0,<1.39.0)", "mypy-boto3-iotanalytics (>=1.38.0,<1.39.0)", "mypy-boto3-iotdeviceadvisor (>=1.38.0,<1.39.0)", "mypy-boto3-iotevents (>=1.38.0,<1.39.0)", "mypy-boto3-iotevents-data (>=1.38.0,<1.39.0)", "mypy-boto3-iotfleethub (>=1.38.0,<1.39.0)", "mypy-boto3-iotfleetwise (>=1.38.0,<1.39.0)", "mypy-boto3-iotsecuretunneling (>=1.38.0,<1.39.0)", "mypy-boto3-iotsitewise (>=1.38.0,<1.39.0)", "mypy-boto3-iotthingsgraph (>=1.38.0,<1.39.0)", "mypy-boto3-iottwinmaker (>=1.38.0,<1.39.0)", "mypy-boto3-iotwireless (>=1.38.0,<1.39.0)", "mypy-boto3-ivs (>=1.38.0,<1.39.0)", "mypy-boto3-ivs-realtime (>=1.38.0,<1.39.0)", "mypy-boto3-ivschat (>=1.38.0,<1.39.0)", "mypy-boto3-kafka (>=1.38.0,<1.39.0)", "mypy-boto3-kafkaconnect (>=1.38.0,<1.39.0)", "mypy-boto3-kendra (>=1.38.0,<1.39.0)", "mypy-boto3-kendra-ranking (>=1.38.0,<1.39.0)", "mypy-boto3-keyspaces (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-archived-media (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-media (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-signaling (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.38.0,<1.39.0)", "mypy-boto3-kinesisanalytics (>=1.38.0,<1.39.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.38.0,<1.39.0)", "mypy-boto3-kinesisvideo (>=1.38.0,<1.39.0)", "mypy-boto3-kms (>=1.38.0,<1.39.0)", "mypy-boto3-lakeformation (>=1.38.0,<1.39.0)", "mypy-boto3-lambda (>=1.38.0,<1.39.0)", "mypy-boto3-launch-wizard (>=1.38.0,<1.39.0)", "mypy-boto3-lex-models (>=1.38.0,<1.39.0)", "mypy-boto3-lex-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-lexv2-models (>=1.38.0,<1.39.0)", "mypy-boto3-lexv2-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-license-manager (>=1.38.0,<1.39.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.38.0,<1.39.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.38.0,<1.39.0)", "mypy-boto3-lightsail (>=1.38.0,<1.39.0)", "mypy-boto3-location (>=1.38.0,<1.39.0)", "mypy-boto3-logs (>=1.38.0,<1.39.0)", "mypy-boto3-lookoutequipment (>=1.38.0,<1.39.0)", "mypy-boto3-lookoutmetrics (>=1.38.0,<1.39.0)", "mypy-boto3-lookoutvision (>=1.38.0,<1.39.0)", "mypy-boto3-m2 (>=1.38.0,<1.39.0)", "mypy-boto3-machinelearning (>=1.38.0,<1.39.0)", "mypy-boto3-macie2 (>=1.38.0,<1.39.0)", "mypy-boto3-mailmanager (>=1.38.0,<1.39.0)", "mypy-boto3-managedblockchain (>=1.38.0,<1.39.0)", "mypy-boto3-managedblockchain-query (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-agreement (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-catalog (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-deployment (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-entitlement (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-reporting (>=1.38.0,<1.39.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.38.0,<1.39.0)", "mypy-boto3-mediaconnect (>=1.38.0,<1.39.0)", "mypy-boto3-mediaconvert (>=1.38.0,<1.39.0)", "mypy-boto3-medialive (>=1.38.0,<1.39.0)", "mypy-boto3-mediapackage (>=1.38.0,<1.39.0)", "mypy-boto3-mediapackage-vod (>=1.38.0,<1.39.0)", "mypy-boto3-mediapackagev2 (>=1.38.0,<1.39.0)", "mypy-boto3-mediastore (>=1.38.0,<1.39.0)", "mypy-boto3-mediastore-data (>=1.38.0,<1.39.0)", "mypy-boto3-mediatailor (>=1.38.0,<1.39.0)", "mypy-boto3-medical-imaging (>=1.38.0,<1.39.0)", "mypy-boto3-memorydb (>=1.38.0,<1.39.0)", "mypy-boto3-meteringmarketplace (>=1.38.0,<1.39.0)", "mypy-boto3-mgh (>=1.38.0,<1.39.0)", "mypy-boto3-mgn (>=1.38.0,<1.39.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.38.0,<1.39.0)", "mypy-boto3-migrationhub-config (>=1.38.0,<1.39.0)", "mypy-boto3-migrationhuborchestrator (>=1.38.0,<1.39.0)", "mypy-boto3-migrationhubstrategy (>=1.38.0,<1.39.0)", "mypy-boto3-mq (>=1.38.0,<1.39.0)", "mypy-boto3-mturk (>=1.38.0,<1.39.0)", "mypy-boto3-mwaa (>=1.38.0,<1.39.0)", "mypy-boto3-neptune (>=1.38.0,<1.39.0)", "mypy-boto3-neptune-graph (>=1.38.0,<1.39.0)", "mypy-boto3-neptunedata (>=1.38.0,<1.39.0)", "mypy-boto3-network-firewall (>=1.38.0,<1.39.0)", "mypy-boto3-networkflowmonitor (>=1.38.0,<1.39.0)", "mypy-boto3-networkmanager (>=1.38.0,<1.39.0)", "mypy-boto3-networkmonitor (>=1.38.0,<1.39.0)", "mypy-boto3-notifications (>=1.38.0,<1.39.0)", "mypy-boto3-notificationscontacts (>=1.38.0,<1.39.0)", "mypy-boto3-oam (>=1.38.0,<1.39.0)", "mypy-boto3-observabilityadmin (>=1.38.0,<1.39.0)", "mypy-boto3-omics (>=1.38.0,<1.39.0)", "mypy-boto3-opensearch (>=1.38.0,<1.39.0)", "mypy-boto3-opensearchserverless (>=1.38.0,<1.39.0)", "mypy-boto3-opsworks (>=1.38.0,<1.39.0)", "mypy-boto3-opsworkscm (>=1.38.0,<1.39.0)", "mypy-boto3-organizations (>=1.38.0,<1.39.0)", "mypy-boto3-osis (>=1.38.0,<1.39.0)", "mypy-boto3-outposts (>=1.38.0,<1.39.0)", "mypy-boto3-panorama (>=1.38.0,<1.39.0)", "mypy-boto3-partnercentral-selling (>=1.38.0,<1.39.0)", "mypy-boto3-payment-cryptography (>=1.38.0,<1.39.0)", "mypy-boto3-payment-cryptography-data (>=1.38.0,<1.39.0)", "mypy-boto3-pca-connector-ad (>=1.38.0,<1.39.0)", "mypy-boto3-pca-connector-scep (>=1.38.0,<1.39.0)", "mypy-boto3-pcs (>=1.38.0,<1.39.0)", "mypy-boto3-personalize (>=1.38.0,<1.39.0)", "mypy-boto3-personalize-events (>=1.38.0,<1.39.0)", "mypy-boto3-personalize-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-pi (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint-email (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint-sms-voice (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.38.0,<1.39.0)", "mypy-boto3-pipes (>=1.38.0,<1.39.0)", "mypy-boto3-polly (>=1.38.0,<1.39.0)", "mypy-boto3-pricing (>=1.38.0,<1.39.0)", "mypy-boto3-privatenetworks (>=1.38.0,<1.39.0)", "mypy-boto3-proton (>=1.38.0,<1.39.0)", "mypy-boto3-qapps (>=1.38.0,<1.39.0)", "mypy-boto3-qbusiness (>=1.38.0,<1.39.0)", "mypy-boto3-qconnect (>=1.38.0,<1.39.0)", "mypy-boto3-qldb (>=1.38.0,<1.39.0)", "mypy-boto3-qldb-session (>=1.38.0,<1.39.0)", "mypy-boto3-quicksight (>=1.38.0,<1.39.0)", "mypy-boto3-ram (>=1.38.0,<1.39.0)", "mypy-boto3-rbin (>=1.38.0,<1.39.0)", "mypy-boto3-rds (>=1.38.0,<1.39.0)", "mypy-boto3-rds-data (>=1.38.0,<1.39.0)", "mypy-boto3-redshift (>=1.38.0,<1.39.0)", "mypy-boto3-redshift-data (>=1.38.0,<1.39.0)", "mypy-boto3-redshift-serverless (>=1.38.0,<1.39.0)", "mypy-boto3-rekognition (>=1.38.0,<1.39.0)", "mypy-boto3-repostspace (>=1.38.0,<1.39.0)", "mypy-boto3-resiliencehub (>=1.38.0,<1.39.0)", "mypy-boto3-resource-explorer-2 (>=1.38.0,<1.39.0)", "mypy-boto3-resource-groups (>=1.38.0,<1.39.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.38.0,<1.39.0)", "mypy-boto3-robomaker (>=1.38.0,<1.39.0)", "mypy-boto3-rolesanywhere (>=1.38.0,<1.39.0)", "mypy-boto3-route53 (>=1.38.0,<1.39.0)", "mypy-boto3-route53-recovery-cluster (>=1.38.0,<1.39.0)", "mypy-boto3-route53-recovery-control-config (>=1.38.0,<1.39.0)", "mypy-boto3-route53-recovery-readiness (>=1.38.0,<1.39.0)", "mypy-boto3-route53domains (>=1.38.0,<1.39.0)", "mypy-boto3-route53profiles (>=1.38.0,<1.39.0)", "mypy-boto3-route53resolver (>=1.38.0,<1.39.0)", "mypy-boto3-rum (>=1.38.0,<1.39.0)", "mypy-boto3-s3 (>=1.38.0,<1.39.0)", "mypy-boto3-s3control (>=1.38.0,<1.39.0)", "mypy-boto3-s3outposts (>=1.38.0,<1.39.0)", "mypy-boto3-s3tables (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-edge (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-geospatial (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-metrics (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-savingsplans (>=1.38.0,<1.39.0)", "mypy-boto3-scheduler (>=1.38.0,<1.39.0)", "mypy-boto3-schemas (>=1.38.0,<1.39.0)", "mypy-boto3-sdb (>=1.38.0,<1.39.0)", "mypy-boto3-secretsmanager (>=1.38.0,<1.39.0)", "mypy-boto3-security-ir (>=1.38.0,<1.39.0)", "mypy-boto3-securityhub (>=1.38.0,<1.39.0)", "mypy-boto3-securitylake (>=1.38.0,<1.39.0)", "mypy-boto3-serverlessrepo (>=1.38.0,<1.39.0)", "mypy-boto3-service-quotas (>=1.38.0,<1.39.0)", "mypy-boto3-servicecatalog (>=1.38.0,<1.39.0)", "mypy-boto3-servicecatalog-appregistry (>=1.38.0,<1.39.0)", "mypy-boto3-servicediscovery (>=1.38.0,<1.39.0)", "mypy-boto3-ses (>=1.38.0,<1.39.0)", "mypy-boto3-sesv2 (>=1.38.0,<1.39.0)", "mypy-boto3-shield (>=1.38.0,<1.39.0)", "mypy-boto3-signer (>=1.38.0,<1.39.0)", "mypy-boto3-simspaceweaver (>=1.38.0,<1.39.0)", "mypy-boto3-sms (>=1.38.0,<1.39.0)", "mypy-boto3-snow-device-management (>=1.38.0,<1.39.0)", "mypy-boto3-snowball (>=1.38.0,<1.39.0)", "mypy-boto3-sns (>=1.38.0,<1.39.0)", "mypy-boto3-socialmessaging (>=1.38.0,<1.39.0)", "mypy-boto3-sqs (>=1.38.0,<1.39.0)", "mypy-boto3-ssm (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-contacts (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-guiconnect (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-incidents (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-quicksetup (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-sap (>=1.38.0,<1.39.0)", "mypy-boto3-sso (>=1.38.0,<1.39.0)", "mypy-boto3-sso-admin (>=1.38.0,<1.39.0)", "mypy-boto3-sso-oidc (>=1.38.0,<1.39.0)", "mypy-boto3-stepfunctions (>=1.38.0,<1.39.0)", "mypy-boto3-storagegateway (>=1.38.0,<1.39.0)", "mypy-boto3-sts (>=1.38.0,<1.39.0)", "mypy-boto3-supplychain (>=1.38.0,<1.39.0)", "mypy-boto3-support (>=1.38.0,<1.39.0)", "mypy-boto3-support-app (>=1.38.0,<1.39.0)", "mypy-boto3-swf (>=1.38.0,<1.39.0)", "mypy-boto3-synthetics (>=1.38.0,<1.39.0)", "mypy-boto3-taxsettings (>=1.38.0,<1.39.0)", "mypy-boto3-textract (>=1.38.0,<1.39.0)", "mypy-boto3-timestream-influxdb (>=1.38.0,<1.39.0)", "mypy-boto3-timestream-query (>=1.38.0,<1.39.0)", "mypy-boto3-timestream-write (>=1.38.0,<1.39.0)", "mypy-boto3-tnb (>=1.38.0,<1.39.0)", "mypy-boto3-transcribe (>=1.38.0,<1.39.0)", "mypy-boto3-transfer (>=1.38.0,<1.39.0)", "mypy-boto3-translate (>=1.38.0,<1.39.0)", "mypy-boto3-trustedadvisor (>=1.38.0,<1.39.0)", "mypy-boto3-verifiedpermissions (>=1.38.0,<1.39.0)", "mypy-boto3-voice-id (>=1.38.0,<1.39.0)", "mypy-boto3-vpc-lattice (>=1.38.0,<1.39.0)", "mypy-boto3-waf (>=1.38.0,<1.39.0)", "mypy-boto3-waf-regional (>=1.38.0,<1.39.0)", "mypy-boto3-wafv2 (>=1.38.0,<1.39.0)", "mypy-boto3-wellarchitected (>=1.38.0,<1.39.0)", "mypy-boto3-wisdom (>=1.38.0,<1.39.0)", "mypy-boto3-workdocs (>=1.38.0,<1.39.0)", "mypy-boto3-workmail (>=1.38.0,<1.39.0)", "mypy-boto3-workmailmessageflow (>=1.38.0,<1.39.0)", "mypy-boto3-workspaces (>=1.38.0,<1.39.0)", "mypy-boto3-workspaces-thin-client (>=1.38.0,<1.39.0)", "mypy-boto3-workspaces-web (>=1.38.0,<1.39.0)", "mypy-boto3-xray (>=1.38.0,<1.39.0)"] +amp = ["mypy-boto3-amp (>=1.38.0,<1.39.0)"] +amplify = ["mypy-boto3-amplify (>=1.38.0,<1.39.0)"] +amplifybackend = ["mypy-boto3-amplifybackend (>=1.38.0,<1.39.0)"] +amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.38.0,<1.39.0)"] +apigateway = ["mypy-boto3-apigateway (>=1.38.0,<1.39.0)"] +apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.38.0,<1.39.0)"] +apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.38.0,<1.39.0)"] +appconfig = ["mypy-boto3-appconfig (>=1.38.0,<1.39.0)"] +appconfigdata = ["mypy-boto3-appconfigdata (>=1.38.0,<1.39.0)"] +appfabric = ["mypy-boto3-appfabric (>=1.38.0,<1.39.0)"] +appflow = ["mypy-boto3-appflow (>=1.38.0,<1.39.0)"] +appintegrations = ["mypy-boto3-appintegrations (>=1.38.0,<1.39.0)"] +application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.38.0,<1.39.0)"] +application-insights = ["mypy-boto3-application-insights (>=1.38.0,<1.39.0)"] +application-signals = ["mypy-boto3-application-signals (>=1.38.0,<1.39.0)"] +applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.38.0,<1.39.0)"] +appmesh = ["mypy-boto3-appmesh (>=1.38.0,<1.39.0)"] +apprunner = ["mypy-boto3-apprunner (>=1.38.0,<1.39.0)"] +appstream = ["mypy-boto3-appstream (>=1.38.0,<1.39.0)"] +appsync = ["mypy-boto3-appsync (>=1.38.0,<1.39.0)"] +apptest = ["mypy-boto3-apptest (>=1.38.0,<1.39.0)"] +arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.38.0,<1.39.0)"] +artifact = ["mypy-boto3-artifact (>=1.38.0,<1.39.0)"] +athena = ["mypy-boto3-athena (>=1.38.0,<1.39.0)"] +auditmanager = ["mypy-boto3-auditmanager (>=1.38.0,<1.39.0)"] +autoscaling = ["mypy-boto3-autoscaling (>=1.38.0,<1.39.0)"] +autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.38.0,<1.39.0)"] +b2bi = ["mypy-boto3-b2bi (>=1.38.0,<1.39.0)"] +backup = ["mypy-boto3-backup (>=1.38.0,<1.39.0)"] +backup-gateway = ["mypy-boto3-backup-gateway (>=1.38.0,<1.39.0)"] +backupsearch = ["mypy-boto3-backupsearch (>=1.38.0,<1.39.0)"] +batch = ["mypy-boto3-batch (>=1.38.0,<1.39.0)"] +bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.38.0,<1.39.0)"] +bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.38.0,<1.39.0)"] +bedrock = ["mypy-boto3-bedrock (>=1.38.0,<1.39.0)"] +bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.38.0,<1.39.0)"] +bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.38.0,<1.39.0)"] +bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.38.0,<1.39.0)"] +bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.38.0,<1.39.0)"] +bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"] +billing = ["mypy-boto3-billing (>=1.38.0,<1.39.0)"] +billingconductor = ["mypy-boto3-billingconductor (>=1.38.0,<1.39.0)"] +boto3 = ["boto3 (==1.38.11)"] +braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"] +budgets = ["mypy-boto3-budgets (>=1.38.0,<1.39.0)"] +ce = ["mypy-boto3-ce (>=1.38.0,<1.39.0)"] +chatbot = ["mypy-boto3-chatbot (>=1.38.0,<1.39.0)"] +chime = ["mypy-boto3-chime (>=1.38.0,<1.39.0)"] +chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.38.0,<1.39.0)"] +chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.38.0,<1.39.0)"] +chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.38.0,<1.39.0)"] +chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.38.0,<1.39.0)"] +chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.38.0,<1.39.0)"] +cleanrooms = ["mypy-boto3-cleanrooms (>=1.38.0,<1.39.0)"] +cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.38.0,<1.39.0)"] +cloud9 = ["mypy-boto3-cloud9 (>=1.38.0,<1.39.0)"] +cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.38.0,<1.39.0)"] +clouddirectory = ["mypy-boto3-clouddirectory (>=1.38.0,<1.39.0)"] +cloudformation = ["mypy-boto3-cloudformation (>=1.38.0,<1.39.0)"] +cloudfront = ["mypy-boto3-cloudfront (>=1.38.0,<1.39.0)"] +cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.38.0,<1.39.0)"] +cloudhsm = ["mypy-boto3-cloudhsm (>=1.38.0,<1.39.0)"] +cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.38.0,<1.39.0)"] +cloudsearch = ["mypy-boto3-cloudsearch (>=1.38.0,<1.39.0)"] +cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.38.0,<1.39.0)"] +cloudtrail = ["mypy-boto3-cloudtrail (>=1.38.0,<1.39.0)"] +cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.38.0,<1.39.0)"] +cloudwatch = ["mypy-boto3-cloudwatch (>=1.38.0,<1.39.0)"] +codeartifact = ["mypy-boto3-codeartifact (>=1.38.0,<1.39.0)"] +codebuild = ["mypy-boto3-codebuild (>=1.38.0,<1.39.0)"] +codecatalyst = ["mypy-boto3-codecatalyst (>=1.38.0,<1.39.0)"] +codecommit = ["mypy-boto3-codecommit (>=1.38.0,<1.39.0)"] +codeconnections = ["mypy-boto3-codeconnections (>=1.38.0,<1.39.0)"] +codedeploy = ["mypy-boto3-codedeploy (>=1.38.0,<1.39.0)"] +codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.38.0,<1.39.0)"] +codeguru-security = ["mypy-boto3-codeguru-security (>=1.38.0,<1.39.0)"] +codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.38.0,<1.39.0)"] +codepipeline = ["mypy-boto3-codepipeline (>=1.38.0,<1.39.0)"] +codestar-connections = ["mypy-boto3-codestar-connections (>=1.38.0,<1.39.0)"] +codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.38.0,<1.39.0)"] +cognito-identity = ["mypy-boto3-cognito-identity (>=1.38.0,<1.39.0)"] +cognito-idp = ["mypy-boto3-cognito-idp (>=1.38.0,<1.39.0)"] +cognito-sync = ["mypy-boto3-cognito-sync (>=1.38.0,<1.39.0)"] +comprehend = ["mypy-boto3-comprehend (>=1.38.0,<1.39.0)"] +comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.38.0,<1.39.0)"] +compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.38.0,<1.39.0)"] +config = ["mypy-boto3-config (>=1.38.0,<1.39.0)"] +connect = ["mypy-boto3-connect (>=1.38.0,<1.39.0)"] +connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.38.0,<1.39.0)"] +connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.38.0,<1.39.0)"] +connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.38.0,<1.39.0)"] +connectcases = ["mypy-boto3-connectcases (>=1.38.0,<1.39.0)"] +connectparticipant = ["mypy-boto3-connectparticipant (>=1.38.0,<1.39.0)"] +controlcatalog = ["mypy-boto3-controlcatalog (>=1.38.0,<1.39.0)"] +controltower = ["mypy-boto3-controltower (>=1.38.0,<1.39.0)"] +cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.38.0,<1.39.0)"] +cur = ["mypy-boto3-cur (>=1.38.0,<1.39.0)"] +customer-profiles = ["mypy-boto3-customer-profiles (>=1.38.0,<1.39.0)"] +databrew = ["mypy-boto3-databrew (>=1.38.0,<1.39.0)"] +dataexchange = ["mypy-boto3-dataexchange (>=1.38.0,<1.39.0)"] +datapipeline = ["mypy-boto3-datapipeline (>=1.38.0,<1.39.0)"] +datasync = ["mypy-boto3-datasync (>=1.38.0,<1.39.0)"] +datazone = ["mypy-boto3-datazone (>=1.38.0,<1.39.0)"] +dax = ["mypy-boto3-dax (>=1.38.0,<1.39.0)"] +deadline = ["mypy-boto3-deadline (>=1.38.0,<1.39.0)"] +detective = ["mypy-boto3-detective (>=1.38.0,<1.39.0)"] +devicefarm = ["mypy-boto3-devicefarm (>=1.38.0,<1.39.0)"] +devops-guru = ["mypy-boto3-devops-guru (>=1.38.0,<1.39.0)"] +directconnect = ["mypy-boto3-directconnect (>=1.38.0,<1.39.0)"] +discovery = ["mypy-boto3-discovery (>=1.38.0,<1.39.0)"] +dlm = ["mypy-boto3-dlm (>=1.38.0,<1.39.0)"] +dms = ["mypy-boto3-dms (>=1.38.0,<1.39.0)"] +docdb = ["mypy-boto3-docdb (>=1.38.0,<1.39.0)"] +docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.38.0,<1.39.0)"] +drs = ["mypy-boto3-drs (>=1.38.0,<1.39.0)"] +ds = ["mypy-boto3-ds (>=1.38.0,<1.39.0)"] +ds-data = ["mypy-boto3-ds-data (>=1.38.0,<1.39.0)"] +dsql = ["mypy-boto3-dsql (>=1.38.0,<1.39.0)"] +dynamodb = ["mypy-boto3-dynamodb (>=1.38.0,<1.39.0)"] +dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.38.0,<1.39.0)"] +ebs = ["mypy-boto3-ebs (>=1.38.0,<1.39.0)"] +ec2 = ["mypy-boto3-ec2 (>=1.38.0,<1.39.0)"] +ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.38.0,<1.39.0)"] +ecr = ["mypy-boto3-ecr (>=1.38.0,<1.39.0)"] +ecr-public = ["mypy-boto3-ecr-public (>=1.38.0,<1.39.0)"] +ecs = ["mypy-boto3-ecs (>=1.38.0,<1.39.0)"] +efs = ["mypy-boto3-efs (>=1.38.0,<1.39.0)"] +eks = ["mypy-boto3-eks (>=1.38.0,<1.39.0)"] +eks-auth = ["mypy-boto3-eks-auth (>=1.38.0,<1.39.0)"] +elasticache = ["mypy-boto3-elasticache (>=1.38.0,<1.39.0)"] +elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.38.0,<1.39.0)"] +elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.38.0,<1.39.0)"] +elb = ["mypy-boto3-elb (>=1.38.0,<1.39.0)"] +elbv2 = ["mypy-boto3-elbv2 (>=1.38.0,<1.39.0)"] +emr = ["mypy-boto3-emr (>=1.38.0,<1.39.0)"] +emr-containers = ["mypy-boto3-emr-containers (>=1.38.0,<1.39.0)"] +emr-serverless = ["mypy-boto3-emr-serverless (>=1.38.0,<1.39.0)"] +entityresolution = ["mypy-boto3-entityresolution (>=1.38.0,<1.39.0)"] +es = ["mypy-boto3-es (>=1.38.0,<1.39.0)"] +essential = ["mypy-boto3-cloudformation (>=1.38.0,<1.39.0)", "mypy-boto3-dynamodb (>=1.38.0,<1.39.0)", "mypy-boto3-ec2 (>=1.38.0,<1.39.0)", "mypy-boto3-lambda (>=1.38.0,<1.39.0)", "mypy-boto3-rds (>=1.38.0,<1.39.0)", "mypy-boto3-s3 (>=1.38.0,<1.39.0)", "mypy-boto3-sqs (>=1.38.0,<1.39.0)"] +events = ["mypy-boto3-events (>=1.38.0,<1.39.0)"] +evidently = ["mypy-boto3-evidently (>=1.38.0,<1.39.0)"] +finspace = ["mypy-boto3-finspace (>=1.38.0,<1.39.0)"] +finspace-data = ["mypy-boto3-finspace-data (>=1.38.0,<1.39.0)"] +firehose = ["mypy-boto3-firehose (>=1.38.0,<1.39.0)"] +fis = ["mypy-boto3-fis (>=1.38.0,<1.39.0)"] +fms = ["mypy-boto3-fms (>=1.38.0,<1.39.0)"] +forecast = ["mypy-boto3-forecast (>=1.38.0,<1.39.0)"] +forecastquery = ["mypy-boto3-forecastquery (>=1.38.0,<1.39.0)"] +frauddetector = ["mypy-boto3-frauddetector (>=1.38.0,<1.39.0)"] +freetier = ["mypy-boto3-freetier (>=1.38.0,<1.39.0)"] +fsx = ["mypy-boto3-fsx (>=1.38.0,<1.39.0)"] +full = ["boto3-stubs-full (>=1.38.0,<1.39.0)"] +gamelift = ["mypy-boto3-gamelift (>=1.38.0,<1.39.0)"] +gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.38.0,<1.39.0)"] +geo-maps = ["mypy-boto3-geo-maps (>=1.38.0,<1.39.0)"] +geo-places = ["mypy-boto3-geo-places (>=1.38.0,<1.39.0)"] +geo-routes = ["mypy-boto3-geo-routes (>=1.38.0,<1.39.0)"] +glacier = ["mypy-boto3-glacier (>=1.38.0,<1.39.0)"] +globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.38.0,<1.39.0)"] +glue = ["mypy-boto3-glue (>=1.38.0,<1.39.0)"] +grafana = ["mypy-boto3-grafana (>=1.38.0,<1.39.0)"] +greengrass = ["mypy-boto3-greengrass (>=1.38.0,<1.39.0)"] +greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.38.0,<1.39.0)"] +groundstation = ["mypy-boto3-groundstation (>=1.38.0,<1.39.0)"] +guardduty = ["mypy-boto3-guardduty (>=1.38.0,<1.39.0)"] +health = ["mypy-boto3-health (>=1.38.0,<1.39.0)"] +healthlake = ["mypy-boto3-healthlake (>=1.38.0,<1.39.0)"] +iam = ["mypy-boto3-iam (>=1.38.0,<1.39.0)"] +identitystore = ["mypy-boto3-identitystore (>=1.38.0,<1.39.0)"] +imagebuilder = ["mypy-boto3-imagebuilder (>=1.38.0,<1.39.0)"] +importexport = ["mypy-boto3-importexport (>=1.38.0,<1.39.0)"] +inspector = ["mypy-boto3-inspector (>=1.38.0,<1.39.0)"] +inspector-scan = ["mypy-boto3-inspector-scan (>=1.38.0,<1.39.0)"] +inspector2 = ["mypy-boto3-inspector2 (>=1.38.0,<1.39.0)"] +internetmonitor = ["mypy-boto3-internetmonitor (>=1.38.0,<1.39.0)"] +invoicing = ["mypy-boto3-invoicing (>=1.38.0,<1.39.0)"] +iot = ["mypy-boto3-iot (>=1.38.0,<1.39.0)"] +iot-data = ["mypy-boto3-iot-data (>=1.38.0,<1.39.0)"] +iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.38.0,<1.39.0)"] +iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.38.0,<1.39.0)"] +iotanalytics = ["mypy-boto3-iotanalytics (>=1.38.0,<1.39.0)"] +iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.38.0,<1.39.0)"] +iotevents = ["mypy-boto3-iotevents (>=1.38.0,<1.39.0)"] +iotevents-data = ["mypy-boto3-iotevents-data (>=1.38.0,<1.39.0)"] +iotfleethub = ["mypy-boto3-iotfleethub (>=1.38.0,<1.39.0)"] +iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.38.0,<1.39.0)"] +iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.38.0,<1.39.0)"] +iotsitewise = ["mypy-boto3-iotsitewise (>=1.38.0,<1.39.0)"] +iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.38.0,<1.39.0)"] +iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.38.0,<1.39.0)"] +iotwireless = ["mypy-boto3-iotwireless (>=1.38.0,<1.39.0)"] +ivs = ["mypy-boto3-ivs (>=1.38.0,<1.39.0)"] +ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.38.0,<1.39.0)"] +ivschat = ["mypy-boto3-ivschat (>=1.38.0,<1.39.0)"] +kafka = ["mypy-boto3-kafka (>=1.38.0,<1.39.0)"] +kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.38.0,<1.39.0)"] +kendra = ["mypy-boto3-kendra (>=1.38.0,<1.39.0)"] +kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.38.0,<1.39.0)"] +keyspaces = ["mypy-boto3-keyspaces (>=1.38.0,<1.39.0)"] +kinesis = ["mypy-boto3-kinesis (>=1.38.0,<1.39.0)"] +kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.38.0,<1.39.0)"] +kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.38.0,<1.39.0)"] +kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.38.0,<1.39.0)"] +kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.38.0,<1.39.0)"] +kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.38.0,<1.39.0)"] +kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.38.0,<1.39.0)"] +kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.38.0,<1.39.0)"] +kms = ["mypy-boto3-kms (>=1.38.0,<1.39.0)"] +lakeformation = ["mypy-boto3-lakeformation (>=1.38.0,<1.39.0)"] +lambda = ["mypy-boto3-lambda (>=1.38.0,<1.39.0)"] +launch-wizard = ["mypy-boto3-launch-wizard (>=1.38.0,<1.39.0)"] +lex-models = ["mypy-boto3-lex-models (>=1.38.0,<1.39.0)"] +lex-runtime = ["mypy-boto3-lex-runtime (>=1.38.0,<1.39.0)"] +lexv2-models = ["mypy-boto3-lexv2-models (>=1.38.0,<1.39.0)"] +lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.38.0,<1.39.0)"] +license-manager = ["mypy-boto3-license-manager (>=1.38.0,<1.39.0)"] +license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.38.0,<1.39.0)"] +license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.38.0,<1.39.0)"] +lightsail = ["mypy-boto3-lightsail (>=1.38.0,<1.39.0)"] +location = ["mypy-boto3-location (>=1.38.0,<1.39.0)"] +logs = ["mypy-boto3-logs (>=1.38.0,<1.39.0)"] +lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.38.0,<1.39.0)"] +lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.38.0,<1.39.0)"] +lookoutvision = ["mypy-boto3-lookoutvision (>=1.38.0,<1.39.0)"] +m2 = ["mypy-boto3-m2 (>=1.38.0,<1.39.0)"] +machinelearning = ["mypy-boto3-machinelearning (>=1.38.0,<1.39.0)"] +macie2 = ["mypy-boto3-macie2 (>=1.38.0,<1.39.0)"] +mailmanager = ["mypy-boto3-mailmanager (>=1.38.0,<1.39.0)"] +managedblockchain = ["mypy-boto3-managedblockchain (>=1.38.0,<1.39.0)"] +managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.38.0,<1.39.0)"] +marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.38.0,<1.39.0)"] +marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.38.0,<1.39.0)"] +marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.38.0,<1.39.0)"] +marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.38.0,<1.39.0)"] +marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.38.0,<1.39.0)"] +marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.38.0,<1.39.0)"] +mediaconnect = ["mypy-boto3-mediaconnect (>=1.38.0,<1.39.0)"] +mediaconvert = ["mypy-boto3-mediaconvert (>=1.38.0,<1.39.0)"] +medialive = ["mypy-boto3-medialive (>=1.38.0,<1.39.0)"] +mediapackage = ["mypy-boto3-mediapackage (>=1.38.0,<1.39.0)"] +mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.38.0,<1.39.0)"] +mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.38.0,<1.39.0)"] +mediastore = ["mypy-boto3-mediastore (>=1.38.0,<1.39.0)"] +mediastore-data = ["mypy-boto3-mediastore-data (>=1.38.0,<1.39.0)"] +mediatailor = ["mypy-boto3-mediatailor (>=1.38.0,<1.39.0)"] +medical-imaging = ["mypy-boto3-medical-imaging (>=1.38.0,<1.39.0)"] +memorydb = ["mypy-boto3-memorydb (>=1.38.0,<1.39.0)"] +meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.38.0,<1.39.0)"] +mgh = ["mypy-boto3-mgh (>=1.38.0,<1.39.0)"] +mgn = ["mypy-boto3-mgn (>=1.38.0,<1.39.0)"] +migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.38.0,<1.39.0)"] +migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.38.0,<1.39.0)"] +migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.38.0,<1.39.0)"] +migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.38.0,<1.39.0)"] +mq = ["mypy-boto3-mq (>=1.38.0,<1.39.0)"] +mturk = ["mypy-boto3-mturk (>=1.38.0,<1.39.0)"] +mwaa = ["mypy-boto3-mwaa (>=1.38.0,<1.39.0)"] +neptune = ["mypy-boto3-neptune (>=1.38.0,<1.39.0)"] +neptune-graph = ["mypy-boto3-neptune-graph (>=1.38.0,<1.39.0)"] +neptunedata = ["mypy-boto3-neptunedata (>=1.38.0,<1.39.0)"] +network-firewall = ["mypy-boto3-network-firewall (>=1.38.0,<1.39.0)"] +networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.38.0,<1.39.0)"] +networkmanager = ["mypy-boto3-networkmanager (>=1.38.0,<1.39.0)"] +networkmonitor = ["mypy-boto3-networkmonitor (>=1.38.0,<1.39.0)"] +notifications = ["mypy-boto3-notifications (>=1.38.0,<1.39.0)"] +notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.38.0,<1.39.0)"] +oam = ["mypy-boto3-oam (>=1.38.0,<1.39.0)"] +observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.38.0,<1.39.0)"] +omics = ["mypy-boto3-omics (>=1.38.0,<1.39.0)"] +opensearch = ["mypy-boto3-opensearch (>=1.38.0,<1.39.0)"] +opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.38.0,<1.39.0)"] +opsworks = ["mypy-boto3-opsworks (>=1.38.0,<1.39.0)"] +opsworkscm = ["mypy-boto3-opsworkscm (>=1.38.0,<1.39.0)"] +organizations = ["mypy-boto3-organizations (>=1.38.0,<1.39.0)"] +osis = ["mypy-boto3-osis (>=1.38.0,<1.39.0)"] +outposts = ["mypy-boto3-outposts (>=1.38.0,<1.39.0)"] +panorama = ["mypy-boto3-panorama (>=1.38.0,<1.39.0)"] +partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.38.0,<1.39.0)"] +payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.38.0,<1.39.0)"] +payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.38.0,<1.39.0)"] +pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.38.0,<1.39.0)"] +pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.38.0,<1.39.0)"] +pcs = ["mypy-boto3-pcs (>=1.38.0,<1.39.0)"] +personalize = ["mypy-boto3-personalize (>=1.38.0,<1.39.0)"] +personalize-events = ["mypy-boto3-personalize-events (>=1.38.0,<1.39.0)"] +personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.38.0,<1.39.0)"] +pi = ["mypy-boto3-pi (>=1.38.0,<1.39.0)"] +pinpoint = ["mypy-boto3-pinpoint (>=1.38.0,<1.39.0)"] +pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.38.0,<1.39.0)"] +pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.38.0,<1.39.0)"] +pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.38.0,<1.39.0)"] +pipes = ["mypy-boto3-pipes (>=1.38.0,<1.39.0)"] +polly = ["mypy-boto3-polly (>=1.38.0,<1.39.0)"] +pricing = ["mypy-boto3-pricing (>=1.38.0,<1.39.0)"] +privatenetworks = ["mypy-boto3-privatenetworks (>=1.38.0,<1.39.0)"] +proton = ["mypy-boto3-proton (>=1.38.0,<1.39.0)"] +qapps = ["mypy-boto3-qapps (>=1.38.0,<1.39.0)"] +qbusiness = ["mypy-boto3-qbusiness (>=1.38.0,<1.39.0)"] +qconnect = ["mypy-boto3-qconnect (>=1.38.0,<1.39.0)"] +qldb = ["mypy-boto3-qldb (>=1.38.0,<1.39.0)"] +qldb-session = ["mypy-boto3-qldb-session (>=1.38.0,<1.39.0)"] +quicksight = ["mypy-boto3-quicksight (>=1.38.0,<1.39.0)"] +ram = ["mypy-boto3-ram (>=1.38.0,<1.39.0)"] +rbin = ["mypy-boto3-rbin (>=1.38.0,<1.39.0)"] +rds = ["mypy-boto3-rds (>=1.38.0,<1.39.0)"] +rds-data = ["mypy-boto3-rds-data (>=1.38.0,<1.39.0)"] +redshift = ["mypy-boto3-redshift (>=1.38.0,<1.39.0)"] +redshift-data = ["mypy-boto3-redshift-data (>=1.38.0,<1.39.0)"] +redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.38.0,<1.39.0)"] +rekognition = ["mypy-boto3-rekognition (>=1.38.0,<1.39.0)"] +repostspace = ["mypy-boto3-repostspace (>=1.38.0,<1.39.0)"] +resiliencehub = ["mypy-boto3-resiliencehub (>=1.38.0,<1.39.0)"] +resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.38.0,<1.39.0)"] +resource-groups = ["mypy-boto3-resource-groups (>=1.38.0,<1.39.0)"] +resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.38.0,<1.39.0)"] +robomaker = ["mypy-boto3-robomaker (>=1.38.0,<1.39.0)"] +rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.38.0,<1.39.0)"] +route53 = ["mypy-boto3-route53 (>=1.38.0,<1.39.0)"] +route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.38.0,<1.39.0)"] +route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.38.0,<1.39.0)"] +route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.38.0,<1.39.0)"] +route53domains = ["mypy-boto3-route53domains (>=1.38.0,<1.39.0)"] +route53profiles = ["mypy-boto3-route53profiles (>=1.38.0,<1.39.0)"] +route53resolver = ["mypy-boto3-route53resolver (>=1.38.0,<1.39.0)"] +rum = ["mypy-boto3-rum (>=1.38.0,<1.39.0)"] +s3 = ["mypy-boto3-s3 (>=1.38.0,<1.39.0)"] +s3control = ["mypy-boto3-s3control (>=1.38.0,<1.39.0)"] +s3outposts = ["mypy-boto3-s3outposts (>=1.38.0,<1.39.0)"] +s3tables = ["mypy-boto3-s3tables (>=1.38.0,<1.39.0)"] +sagemaker = ["mypy-boto3-sagemaker (>=1.38.0,<1.39.0)"] +sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.38.0,<1.39.0)"] +sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.38.0,<1.39.0)"] +sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.38.0,<1.39.0)"] +sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.38.0,<1.39.0)"] +sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.38.0,<1.39.0)"] +sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.38.0,<1.39.0)"] +savingsplans = ["mypy-boto3-savingsplans (>=1.38.0,<1.39.0)"] +scheduler = ["mypy-boto3-scheduler (>=1.38.0,<1.39.0)"] +schemas = ["mypy-boto3-schemas (>=1.38.0,<1.39.0)"] +sdb = ["mypy-boto3-sdb (>=1.38.0,<1.39.0)"] +secretsmanager = ["mypy-boto3-secretsmanager (>=1.38.0,<1.39.0)"] +security-ir = ["mypy-boto3-security-ir (>=1.38.0,<1.39.0)"] +securityhub = ["mypy-boto3-securityhub (>=1.38.0,<1.39.0)"] +securitylake = ["mypy-boto3-securitylake (>=1.38.0,<1.39.0)"] +serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.38.0,<1.39.0)"] +service-quotas = ["mypy-boto3-service-quotas (>=1.38.0,<1.39.0)"] +servicecatalog = ["mypy-boto3-servicecatalog (>=1.38.0,<1.39.0)"] +servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.38.0,<1.39.0)"] +servicediscovery = ["mypy-boto3-servicediscovery (>=1.38.0,<1.39.0)"] +ses = ["mypy-boto3-ses (>=1.38.0,<1.39.0)"] +sesv2 = ["mypy-boto3-sesv2 (>=1.38.0,<1.39.0)"] +shield = ["mypy-boto3-shield (>=1.38.0,<1.39.0)"] +signer = ["mypy-boto3-signer (>=1.38.0,<1.39.0)"] +simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.38.0,<1.39.0)"] +sms = ["mypy-boto3-sms (>=1.38.0,<1.39.0)"] +snow-device-management = ["mypy-boto3-snow-device-management (>=1.38.0,<1.39.0)"] +snowball = ["mypy-boto3-snowball (>=1.38.0,<1.39.0)"] +sns = ["mypy-boto3-sns (>=1.38.0,<1.39.0)"] +socialmessaging = ["mypy-boto3-socialmessaging (>=1.38.0,<1.39.0)"] +sqs = ["mypy-boto3-sqs (>=1.38.0,<1.39.0)"] +ssm = ["mypy-boto3-ssm (>=1.38.0,<1.39.0)"] +ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.38.0,<1.39.0)"] +ssm-guiconnect = ["mypy-boto3-ssm-guiconnect (>=1.38.0,<1.39.0)"] +ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.38.0,<1.39.0)"] +ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.38.0,<1.39.0)"] +ssm-sap = ["mypy-boto3-ssm-sap (>=1.38.0,<1.39.0)"] +sso = ["mypy-boto3-sso (>=1.38.0,<1.39.0)"] +sso-admin = ["mypy-boto3-sso-admin (>=1.38.0,<1.39.0)"] +sso-oidc = ["mypy-boto3-sso-oidc (>=1.38.0,<1.39.0)"] +stepfunctions = ["mypy-boto3-stepfunctions (>=1.38.0,<1.39.0)"] +storagegateway = ["mypy-boto3-storagegateway (>=1.38.0,<1.39.0)"] +sts = ["mypy-boto3-sts (>=1.38.0,<1.39.0)"] +supplychain = ["mypy-boto3-supplychain (>=1.38.0,<1.39.0)"] +support = ["mypy-boto3-support (>=1.38.0,<1.39.0)"] +support-app = ["mypy-boto3-support-app (>=1.38.0,<1.39.0)"] +swf = ["mypy-boto3-swf (>=1.38.0,<1.39.0)"] +synthetics = ["mypy-boto3-synthetics (>=1.38.0,<1.39.0)"] +taxsettings = ["mypy-boto3-taxsettings (>=1.38.0,<1.39.0)"] +textract = ["mypy-boto3-textract (>=1.38.0,<1.39.0)"] +timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.38.0,<1.39.0)"] +timestream-query = ["mypy-boto3-timestream-query (>=1.38.0,<1.39.0)"] +timestream-write = ["mypy-boto3-timestream-write (>=1.38.0,<1.39.0)"] +tnb = ["mypy-boto3-tnb (>=1.38.0,<1.39.0)"] +transcribe = ["mypy-boto3-transcribe (>=1.38.0,<1.39.0)"] +transfer = ["mypy-boto3-transfer (>=1.38.0,<1.39.0)"] +translate = ["mypy-boto3-translate (>=1.38.0,<1.39.0)"] +trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.38.0,<1.39.0)"] +verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.38.0,<1.39.0)"] +voice-id = ["mypy-boto3-voice-id (>=1.38.0,<1.39.0)"] +vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.38.0,<1.39.0)"] +waf = ["mypy-boto3-waf (>=1.38.0,<1.39.0)"] +waf-regional = ["mypy-boto3-waf-regional (>=1.38.0,<1.39.0)"] +wafv2 = ["mypy-boto3-wafv2 (>=1.38.0,<1.39.0)"] +wellarchitected = ["mypy-boto3-wellarchitected (>=1.38.0,<1.39.0)"] +wisdom = ["mypy-boto3-wisdom (>=1.38.0,<1.39.0)"] +workdocs = ["mypy-boto3-workdocs (>=1.38.0,<1.39.0)"] +workmail = ["mypy-boto3-workmail (>=1.38.0,<1.39.0)"] +workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.38.0,<1.39.0)"] +workspaces = ["mypy-boto3-workspaces (>=1.38.0,<1.39.0)"] +workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.38.0,<1.39.0)"] +workspaces-web = ["mypy-boto3-workspaces-web (>=1.38.0,<1.39.0)"] +xray = ["mypy-boto3-xray (>=1.38.0,<1.39.0)"] [[package]] name = "botocore" -version = "1.37.14" +version = "1.38.11" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "botocore-1.37.14-py3-none-any.whl", hash = "sha256:709a1796f436f8e378e52170e58501c1f3b5f2d1308238cf1d6a3bdba2e32851"}, - {file = "botocore-1.37.14.tar.gz", hash = "sha256:b0adce3f0fb42b914eb05079f50cf368cb9cf9745fdd206bd91fe6ac67b29aca"}, + {file = "botocore-1.38.11-py3-none-any.whl", hash = "sha256:f5cb7a292774d4a212347475d95668bb96c1fe322e71c19bf871fb06503597a5"}, + {file = "botocore-1.38.11.tar.gz", hash = "sha256:44c5cb042fefedbe0b0ae4c3f96aed03f4cab6973d4317410c8d7c6d32807aef"}, ] [package.dependencies] @@ -825,14 +872,14 @@ crt = ["awscrt (==0.23.8)"] [[package]] name = "botocore-stubs" -version = "1.37.14" +version = "1.38.11" description = "Type annotations and code completion for botocore" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "botocore_stubs-1.37.14-py3-none-any.whl", hash = "sha256:2af28b15e379318a55f2e31cd43f4ccec87ec28ac6d19f3c692ee606bc9e82a3"}, - {file = "botocore_stubs-1.37.14.tar.gz", hash = "sha256:02c64f36f5be8828cf0e9c7e954088e4e1c0beda2d0f5e0c5d3d5f09ab974a3c"}, + {file = "botocore_stubs-1.38.11-py3-none-any.whl", hash = "sha256:962c9eaeb7d1ae2287007d72eb2785cc92e58b10f53dbd749361556c7fa8e96c"}, + {file = "botocore_stubs-1.38.11.tar.gz", hash = "sha256:5898dd855ae152725d60ff48e1f400fc23d18fb0ff0b9bd4a45458af2c446ebe"}, ] [package.dependencies] @@ -886,14 +933,14 @@ test = ["flake8", "isort", "pytest"] [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main", "docs"] files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, ] [[package]] @@ -972,111 +1019,111 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] -markers = {main = "(extra == \"examples\" or extra == \"all\") and platform_python_implementation != \"PyPy\"", dev = "implementation_name == \"pypy\""} +markers = {main = "(extra == \"examples\" or extra == \"all\") and platform_python_implementation != \"PyPy\" or extra == \"all\"", dev = "implementation_name == \"pypy\""} [package.dependencies] pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] @@ -1107,36 +1154,6 @@ files = [ {file = "cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64"}, ] -[[package]] -name = "cmake" -version = "3.31.6" -description = "CMake is an open-source, cross-platform family of tools designed to build, test and package software" -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"all\"" -files = [ - {file = "cmake-3.31.6-py3-none-macosx_10_10_universal2.whl", hash = "sha256:da9d4fd9abd571fd016ddb27da0428b10277010b23bb21e3678f8b9e96e1686e"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:689441fc74fbb03673c67e20d4636614a231634d5e803387cd213d2cdf9675fc"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2297e9591307d9c61e557efe737bcf4d7c13a30f1f860732f684a204fee24dca"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42d9883b8958da285d53d5f69d40d9650c2d1bcf922d82b3ebdceb2b3a7d4521"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cefb910be81e1b4fdc3b89ef61819c3e848b3906ed56ac36d090f37cfa05666b"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4326f6c6f39867a60e2822fea8e6aedbcac09c9f59ad3f0f3386a890a2c8d89d"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f77db820af725bb92fab60c4c9d67f64442ac0ea9b933aca4cd4586219cbd1f"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c8b05df0602365da91ee6a3336fe57525b137706c4ab5675498f662ae1dbcec"}, - {file = "cmake-3.31.6-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:9eed74a1f2a29a7cd92a9f071a35d64645b19802beb393ec250d6e7c09441314"}, - {file = "cmake-3.31.6-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:112b36427e59bd26145b705a49d5f70b16433a655ce807cb8fdd81dd4d0e60c2"}, - {file = "cmake-3.31.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:13f2e636dc27834fe096f53301d6efb913b4b501fdc0ed03f386c0a7e7ec1a21"}, - {file = "cmake-3.31.6-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:8b67bf9613dfb59c12ce643c6be582c49c981e6eee28c4c244aeb3248b33f05e"}, - {file = "cmake-3.31.6-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:024a79ca3d2c355f75875b6cc92d907afd710d1a4ffde2f20a7da712a2f4b1c3"}, - {file = "cmake-3.31.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ce5fc0299ecafe489b2614daa6176c3c2baacea6bc3b359bac9aa25b46ed43e9"}, - {file = "cmake-3.31.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:547efc1d0e27a194da819a0392fe645a9b8f1485bc2c3f34ae4f1e682cfd3153"}, - {file = "cmake-3.31.6-py3-none-win32.whl", hash = "sha256:9f170e3c6933dba64f333cb456823bbb1d0ac126f94aa4a577e40855d2b1ca49"}, - {file = "cmake-3.31.6-py3-none-win_amd64.whl", hash = "sha256:bbaed969cef3c427f4f17591feb28db4ae595e3a4bbd45cb35522cee14df6a32"}, - {file = "cmake-3.31.6-py3-none-win_arm64.whl", hash = "sha256:6cb97adae7e5390ce68f8b7f38e1be1c72bf19e9f6727f31f8fa1c095b39be88"}, - {file = "cmake-3.31.6.tar.gz", hash = "sha256:8edddfbf367fa1bcf4b9f3064470bc0e1022f70609c0cf69c863961897826205"}, -] - [[package]] name = "colorama" version = "0.4.6" @@ -1169,75 +1186,75 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.7.0" +version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a538a23119d1e2e2ce077e902d02ea3d8e0641786ef6e0faf11ce82324743944"}, - {file = "coverage-7.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1586ad158523f4133499a4f322b230e2cfef9cc724820dbd58595a5a236186f4"}, - {file = "coverage-7.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b6c96d69928a3a6767fab8dc1ce8a02cf0156836ccb1e820c7f45a423570d98"}, - {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f18d47641282664276977c604b5a261e51fefc2980f5271d547d706b06a837f"}, - {file = "coverage-7.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a1e18a85bd066c7c556d85277a7adf4651f259b2579113844835ba1a74aafd"}, - {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:70f0925c4e2bfc965369f417e7cc72538fd1ba91639cf1e4ef4b1a6b50439b3b"}, - {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b0fac2088ec4aaeb5468b814bd3ff5e5978364bfbce5e567c44c9e2854469f6c"}, - {file = "coverage-7.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3e212a894d8ae07fde2ca8b43d666a6d49bbbddb10da0f6a74ca7bd31f20054"}, - {file = "coverage-7.7.0-cp310-cp310-win32.whl", hash = "sha256:f32b165bf6dfea0846a9c9c38b7e1d68f313956d60a15cde5d1709fddcaf3bee"}, - {file = "coverage-7.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2454b12a3f12cc4698f3508912e6225ec63682e2ca5a96f80a2b93cef9e63f3"}, - {file = "coverage-7.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0a207c87a9f743c8072d059b4711f8d13c456eb42dac778a7d2e5d4f3c253a7"}, - {file = "coverage-7.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d673e3add00048215c2cc507f1228a7523fd8bf34f279ac98334c9b07bd2656"}, - {file = "coverage-7.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f81fe93dc1b8e5673f33443c0786c14b77e36f1025973b85e07c70353e46882b"}, - {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8c7524779003d59948c51b4fcbf1ca4e27c26a7d75984f63488f3625c328b9b"}, - {file = "coverage-7.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c124025430249118d018dcedc8b7426f39373527c845093132196f2a483b6dd"}, - {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f559c36d5cdc448ee13e7e56ed7b6b5d44a40a511d584d388a0f5d940977ba"}, - {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:37cbc7b0d93dfd133e33c7ec01123fbb90401dce174c3b6661d8d36fb1e30608"}, - {file = "coverage-7.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7d2a65876274acf544703e943c010b60bd79404e3623a1e5d52b64a6e2728de5"}, - {file = "coverage-7.7.0-cp311-cp311-win32.whl", hash = "sha256:f5a2f71d6a91238e7628f23538c26aa464d390cbdedf12ee2a7a0fb92a24482a"}, - {file = "coverage-7.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae8006772c6b0fa53c33747913473e064985dac4d65f77fd2fdc6474e7cd54e4"}, - {file = "coverage-7.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:056d3017ed67e7ddf266e6f57378ece543755a4c9231e997789ab3bd11392c94"}, - {file = "coverage-7.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33c1394d8407e2771547583b66a85d07ed441ff8fae5a4adb4237ad39ece60db"}, - {file = "coverage-7.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fbb7a0c3c21908520149d7751cf5b74eb9b38b54d62997b1e9b3ac19a8ee2fe"}, - {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb356e7ae7c2da13f404bf8f75be90f743c6df8d4607022e759f5d7d89fe83f8"}, - {file = "coverage-7.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce730d484038e97f27ea2dbe5d392ec5c2261f28c319a3bb266f6b213650135"}, - {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa4dff57fc21a575672176d5ab0ef15a927199e775c5e8a3d75162ab2b0c7705"}, - {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b667b91f4f714b17af2a18e220015c941d1cf8b07c17f2160033dbe1e64149f0"}, - {file = "coverage-7.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:693d921621a0c8043bfdc61f7d4df5ea6d22165fe8b807cac21eb80dd94e4bbd"}, - {file = "coverage-7.7.0-cp312-cp312-win32.whl", hash = "sha256:52fc89602cde411a4196c8c6894afb384f2125f34c031774f82a4f2608c59d7d"}, - {file = "coverage-7.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ce8cf59e09d31a4915ff4c3b94c6514af4c84b22c4cc8ad7c3c546a86150a92"}, - {file = "coverage-7.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4545485fef7a8a2d8f30e6f79ce719eb154aab7e44217eb444c1d38239af2072"}, - {file = "coverage-7.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1393e5aa9441dafb0162c36c8506c648b89aea9565b31f6bfa351e66c11bcd82"}, - {file = "coverage-7.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316f29cc3392fa3912493ee4c83afa4a0e2db04ff69600711f8c03997c39baaa"}, - {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ffde1d6bc2a92f9c9207d1ad808550873748ac2d4d923c815b866baa343b3f"}, - {file = "coverage-7.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:416e2a8845eaff288f97eaf76ab40367deafb9073ffc47bf2a583f26b05e5265"}, - {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5efdeff5f353ed3352c04e6b318ab05c6ce9249c25ed3c2090c6e9cadda1e3b2"}, - {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:57f3bd0d29bf2bd9325c0ff9cc532a175110c4bf8f412c05b2405fd35745266d"}, - {file = "coverage-7.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ab7090f04b12dc6469882ce81244572779d3a4b67eea1c96fb9ecc8c607ef39"}, - {file = "coverage-7.7.0-cp313-cp313-win32.whl", hash = "sha256:180e3fc68ee4dc5af8b33b6ca4e3bb8aa1abe25eedcb958ba5cff7123071af68"}, - {file = "coverage-7.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:55143aa13c49491f5606f05b49ed88663446dce3a4d3c5d77baa4e36a16d3573"}, - {file = "coverage-7.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc41374d2f27d81d6558f8a24e5c114580ffefc197fd43eabd7058182f743322"}, - {file = "coverage-7.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:89078312f06237417adda7c021c33f80f7a6d2db8572a5f6c330d89b080061ce"}, - {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b2f144444879363ea8834cd7b6869d79ac796cb8f864b0cfdde50296cd95816"}, - {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60e6347d1ed882b1159ffea172cb8466ee46c665af4ca397edbf10ff53e9ffaf"}, - {file = "coverage-7.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb203c0afffaf1a8f5b9659a013f8f16a1b2cad3a80a8733ceedc968c0cf4c57"}, - {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ad0edaa97cb983d9f2ff48cadddc3e1fb09f24aa558abeb4dc9a0dbacd12cbb4"}, - {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c5f8a5364fc37b2f172c26a038bc7ec4885f429de4a05fc10fdcb53fb5834c5c"}, - {file = "coverage-7.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4e09534037933bf6eb31d804e72c52ec23219b32c1730f9152feabbd7499463"}, - {file = "coverage-7.7.0-cp313-cp313t-win32.whl", hash = "sha256:1b336d06af14f8da5b1f391e8dec03634daf54dfcb4d1c4fb6d04c09d83cef90"}, - {file = "coverage-7.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b54a1ee4c6f1905a436cbaa04b26626d27925a41cbc3a337e2d3ff7038187f07"}, - {file = "coverage-7.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c8fbce80b2b8bf135d105aa8f5b36eae0c57d702a1cc3ebdea2a6f03f6cdde5"}, - {file = "coverage-7.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9710521f07f526de30ccdead67e6b236fe996d214e1a7fba8b36e2ba2cd8261"}, - {file = "coverage-7.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7789e700f33f2b133adae582c9f437523cd5db8de845774988a58c360fc88253"}, - {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c36093aca722db73633cf2359026ed7782a239eb1c6db2abcff876012dc4cf"}, - {file = "coverage-7.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c075d167a6ec99b798c1fdf6e391a1d5a2d054caffe9593ba0f97e3df2c04f0e"}, - {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d013c07061751ae81861cae6ec3a4fe04e84781b11fd4b6b4201590234b25c7b"}, - {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:104bf640f408f4e115b85110047c7f27377e1a8b7ba86f7db4fa47aa49dc9a8e"}, - {file = "coverage-7.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:39abcacd1ed54e2c33c54bdc488b310e8ef6705833f7148b6eb9a547199d375d"}, - {file = "coverage-7.7.0-cp39-cp39-win32.whl", hash = "sha256:8e336b56301774ace6be0017ff85c3566c556d938359b61b840796a0202f805c"}, - {file = "coverage-7.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c938c6ae59be67ac19a7204e079efc94b38222cd7d0269f96e45e18cddeaa59"}, - {file = "coverage-7.7.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:3b0e6e54591ae0d7427def8a4d40fca99df6b899d10354bab73cd5609807261c"}, - {file = "coverage-7.7.0-py3-none-any.whl", hash = "sha256:708f0a1105ef2b11c79ed54ed31f17e6325ac936501fc373f24be3e6a578146a"}, - {file = "coverage-7.7.0.tar.gz", hash = "sha256:cd879d4646055a573775a1cec863d00c9ff8c55860f8b17f6d8eee9140c06166"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, ] [package.extras] @@ -1245,53 +1262,63 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "43.0.3" +version = "44.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = true -python-versions = ">=3.7" +python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] markers = "extra == \"examples\" or extra == \"all\"" files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, + {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"}, + {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"}, + {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"}, + {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"}, + {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"}, + {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"}, + {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] +pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -1314,40 +1341,84 @@ webencodings = "*" doc = ["furo", "sphinx"] test = ["pytest", "ruff"] +[[package]] +name = "datasets" +version = "3.6.0" +description = "HuggingFace community-driven open-source library of datasets" +optional = true +python-versions = ">=3.9.0" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "datasets-3.6.0-py3-none-any.whl", hash = "sha256:25000c4a2c0873a710df127d08a202a06eab7bf42441a6bc278b499c2f72cd1b"}, + {file = "datasets-3.6.0.tar.gz", hash = "sha256:1b2bf43b19776e2787e181cfd329cb0ca1a358ea014780c3581e0f276375e041"}, +] + +[package.dependencies] +dill = ">=0.3.0,<0.3.9" +filelock = "*" +fsspec = {version = ">=2023.1.0,<=2025.3.0", extras = ["http"]} +huggingface-hub = ">=0.24.0" +multiprocess = "<0.70.17" +numpy = ">=1.17" +packaging = "*" +pandas = "*" +pyarrow = ">=15.0.0" +pyyaml = ">=5.1" +requests = ">=2.32.2" +tqdm = ">=4.66.3" +xxhash = "*" + +[package.extras] +audio = ["librosa", "soundfile (>=0.12.1)", "soxr (>=0.4.0)"] +benchmarks = ["tensorflow (==2.12.0)", "torch (==2.0.1)", "transformers (==4.30.1)"] +dev = ["Pillow (>=9.4.0)", "absl-py", "aiohttp", "decorator", "elasticsearch (>=7.17.12,<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14) ; sys_platform != \"win32\"", "jaxlib (>=0.3.14) ; sys_platform != \"win32\"", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyav", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0) ; python_version >= \"3.10\"", "tensorflow (>=2.6.0)", "tensorflow (>=2.6.0) ; python_version < \"3.10\"", "tiktoken", "torch", "torch (>=2.0.0)", "torchdata", "torchvision", "transformers", "transformers (>=4.42.0)", "zstandard"] +docs = ["s3fs", "tensorflow (>=2.6.0)", "torch", "transformers"] +jax = ["jax (>=0.3.14)", "jaxlib (>=0.3.14)"] +pdfs = ["pdfplumber (>=0.11.4)"] +quality = ["ruff (>=0.3.0)"] +s3 = ["s3fs"] +tensorflow = ["tensorflow (>=2.6.0)"] +tensorflow-gpu = ["tensorflow (>=2.6.0)"] +tests = ["Pillow (>=9.4.0)", "absl-py", "aiohttp", "decorator", "elasticsearch (>=7.17.12,<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14) ; sys_platform != \"win32\"", "jaxlib (>=0.3.14) ; sys_platform != \"win32\"", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyav", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0) ; python_version >= \"3.10\"", "tensorflow (>=2.6.0) ; python_version < \"3.10\"", "tiktoken", "torch (>=2.0.0)", "torchdata", "torchvision", "transformers (>=4.42.0)", "zstandard"] +tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "aiohttp", "decorator", "elasticsearch (>=7.17.12,<8.0.0)", "jax (>=0.3.14) ; sys_platform != \"win32\"", "jaxlib (>=0.3.14) ; sys_platform != \"win32\"", "joblib (<1.3.0)", "joblibspark", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyav", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tiktoken", "torch (>=2.0.0)", "torchdata", "torchvision", "transformers (>=4.42.0)", "zstandard"] +torch = ["torch"] +vision = ["Pillow (>=9.4.0)"] + [[package]] name = "debugpy" -version = "1.8.13" +version = "1.8.14" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "debugpy-1.8.13-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:06859f68e817966723ffe046b896b1bd75c665996a77313370336ee9e1de3e90"}, - {file = "debugpy-1.8.13-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c2db69fb8df3168bc857d7b7d2494fed295dfdbde9a45f27b4b152f37520"}, - {file = "debugpy-1.8.13-cp310-cp310-win32.whl", hash = "sha256:46abe0b821cad751fc1fb9f860fb2e68d75e2c5d360986d0136cd1db8cad4428"}, - {file = "debugpy-1.8.13-cp310-cp310-win_amd64.whl", hash = "sha256:dc7b77f5d32674686a5f06955e4b18c0e41fb5a605f5b33cf225790f114cfeec"}, - {file = "debugpy-1.8.13-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:eee02b2ed52a563126c97bf04194af48f2fe1f68bb522a312b05935798e922ff"}, - {file = "debugpy-1.8.13-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4caca674206e97c85c034c1efab4483f33971d4e02e73081265ecb612af65377"}, - {file = "debugpy-1.8.13-cp311-cp311-win32.whl", hash = "sha256:7d9a05efc6973b5aaf076d779cf3a6bbb1199e059a17738a2aa9d27a53bcc888"}, - {file = "debugpy-1.8.13-cp311-cp311-win_amd64.whl", hash = "sha256:62f9b4a861c256f37e163ada8cf5a81f4c8d5148fc17ee31fb46813bd658cdcc"}, - {file = "debugpy-1.8.13-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:2b8de94c5c78aa0d0ed79023eb27c7c56a64c68217d881bee2ffbcb13951d0c1"}, - {file = "debugpy-1.8.13-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887d54276cefbe7290a754424b077e41efa405a3e07122d8897de54709dbe522"}, - {file = "debugpy-1.8.13-cp312-cp312-win32.whl", hash = "sha256:3872ce5453b17837ef47fb9f3edc25085ff998ce63543f45ba7af41e7f7d370f"}, - {file = "debugpy-1.8.13-cp312-cp312-win_amd64.whl", hash = "sha256:63ca7670563c320503fea26ac688988d9d6b9c6a12abc8a8cf2e7dd8e5f6b6ea"}, - {file = "debugpy-1.8.13-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:31abc9618be4edad0b3e3a85277bc9ab51a2d9f708ead0d99ffb5bb750e18503"}, - {file = "debugpy-1.8.13-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0bd87557f97bced5513a74088af0b84982b6ccb2e254b9312e29e8a5c4270eb"}, - {file = "debugpy-1.8.13-cp313-cp313-win32.whl", hash = "sha256:5268ae7fdca75f526d04465931cb0bd24577477ff50e8bb03dab90983f4ebd02"}, - {file = "debugpy-1.8.13-cp313-cp313-win_amd64.whl", hash = "sha256:79ce4ed40966c4c1631d0131606b055a5a2f8e430e3f7bf8fd3744b09943e8e8"}, - {file = "debugpy-1.8.13-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:acf39a6e98630959763f9669feddee540745dfc45ad28dbc9bd1f9cd60639391"}, - {file = "debugpy-1.8.13-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:924464d87e7d905eb0d79fb70846558910e906d9ee309b60c4fe597a2e802590"}, - {file = "debugpy-1.8.13-cp38-cp38-win32.whl", hash = "sha256:3dae443739c6b604802da9f3e09b0f45ddf1cf23c99161f3a1a8039f61a8bb89"}, - {file = "debugpy-1.8.13-cp38-cp38-win_amd64.whl", hash = "sha256:ed93c3155fc1f888ab2b43626182174e457fc31b7781cd1845629303790b8ad1"}, - {file = "debugpy-1.8.13-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:6fab771639332bd8ceb769aacf454a30d14d7a964f2012bf9c4e04c60f16e85b"}, - {file = "debugpy-1.8.13-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32b6857f8263a969ce2ca098f228e5cc0604d277447ec05911a8c46cf3e7e307"}, - {file = "debugpy-1.8.13-cp39-cp39-win32.whl", hash = "sha256:f14d2c4efa1809da125ca62df41050d9c7cd9cb9e380a2685d1e453c4d450ccb"}, - {file = "debugpy-1.8.13-cp39-cp39-win_amd64.whl", hash = "sha256:ea869fe405880327497e6945c09365922c79d2a1eed4c3ae04d77ac7ae34b2b5"}, - {file = "debugpy-1.8.13-py2.py3-none-any.whl", hash = "sha256:d4ba115cdd0e3a70942bd562adba9ec8c651fe69ddde2298a1be296fc331906f"}, - {file = "debugpy-1.8.13.tar.gz", hash = "sha256:837e7bef95bdefba426ae38b9a94821ebdc5bea55627879cd48165c90b9e50ce"}, + {file = "debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339"}, + {file = "debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79"}, + {file = "debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987"}, + {file = "debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84"}, + {file = "debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9"}, + {file = "debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2"}, + {file = "debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2"}, + {file = "debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01"}, + {file = "debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84"}, + {file = "debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826"}, + {file = "debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f"}, + {file = "debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f"}, + {file = "debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f"}, + {file = "debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15"}, + {file = "debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e"}, + {file = "debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e"}, + {file = "debugpy-1.8.14-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:d5582bcbe42917bc6bbe5c12db1bffdf21f6bfc28d4554b738bf08d50dc0c8c3"}, + {file = "debugpy-1.8.14-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5349b7c3735b766a281873fbe32ca9cca343d4cc11ba4a743f84cb854339ff35"}, + {file = "debugpy-1.8.14-cp38-cp38-win32.whl", hash = "sha256:7118d462fe9724c887d355eef395fae68bc764fd862cdca94e70dcb9ade8a23d"}, + {file = "debugpy-1.8.14-cp38-cp38-win_amd64.whl", hash = "sha256:d235e4fa78af2de4e5609073972700523e372cf5601742449970110d565ca28c"}, + {file = "debugpy-1.8.14-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:413512d35ff52c2fb0fd2d65e69f373ffd24f0ecb1fac514c04a668599c5ce7f"}, + {file = "debugpy-1.8.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c9156f7524a0d70b7a7e22b2e311d8ba76a15496fb00730e46dcdeedb9e1eea"}, + {file = "debugpy-1.8.14-cp39-cp39-win32.whl", hash = "sha256:b44985f97cc3dd9d52c42eb59ee9d7ee0c4e7ecd62bca704891f997de4cef23d"}, + {file = "debugpy-1.8.14-cp39-cp39-win_amd64.whl", hash = "sha256:b1528cfee6c1b1c698eb10b6b096c598738a8238822d218173d21c3086de8123"}, + {file = "debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20"}, + {file = "debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322"}, ] [[package]] @@ -1356,11 +1427,12 @@ version = "5.2.1" description = "Decorators for Humans" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] +markers = {main = "extra == \"all\""} [[package]] name = "defusedxml" @@ -1374,6 +1446,23 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "dill" +version = "0.3.8" +description = "serialize all of Python" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + [[package]] name = "diskcache" version = "5.6.3" @@ -1420,22 +1509,24 @@ develop = ["aiohttp", "furo", "httpx", "opentelemetry-api", "opentelemetry-sdk", [[package]] name = "elasticsearch" -version = "8.17.2" +version = "8.18.1" description = "Python client for Elasticsearch" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "elasticsearch-8.17.2-py3-none-any.whl", hash = "sha256:2d058dcddd8f2686cd431a916cdf983f9fb7d211d902834f564ab7df05ba6478"}, - {file = "elasticsearch-8.17.2.tar.gz", hash = "sha256:ff7f1db8aeefd87ceba4edce3aa4070994582e6cf029d2e67b74e66d634509db"}, + {file = "elasticsearch-8.18.1-py3-none-any.whl", hash = "sha256:1a8c8b5ec3ce5be88f96d2f898375671648e96272978bce0dee3137d9326aabb"}, + {file = "elasticsearch-8.18.1.tar.gz", hash = "sha256:998035f17a8c1fba7ae26b183dca797dcf95db86da6a7ecba56d31afc40f07c7"}, ] [package.dependencies] elastic-transport = ">=8.15.1,<9" +python-dateutil = "*" +typing-extensions = "*" [package.extras] async = ["aiohttp (>=3,<4)"] -dev = ["aiohttp", "black", "build", "coverage", "isort", "jinja2", "mapbox-vector-tile", "nox", "numpy", "orjson", "pandas", "pyarrow", "pytest", "pytest-asyncio", "pytest-cov", "python-dateutil", "pyyaml (>=5.4)", "requests (>=2,<3)", "simsimd", "twine", "unasync"] +dev = ["aiohttp", "black", "build", "coverage", "isort", "jinja2", "mapbox-vector-tile", "mypy", "nltk", "nox", "numpy", "orjson", "pandas", "pyarrow", "pyright", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "python-dateutil", "pyyaml (>=5.4)", "requests (>=2,<3)", "sentence-transformers", "simsimd", "tqdm", "twine", "types-python-dateutil", "types-tqdm", "unasync"] docs = ["sphinx", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=2.0)"] orjson = ["orjson (>=3)"] pyarrow = ["pyarrow (>=1)"] @@ -1490,15 +1581,15 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fastapi" -version = "0.115.11" +version = "0.115.12" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"}, - {file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"}, + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, ] [package.dependencies] @@ -1529,104 +1620,116 @@ typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "frozenlist" -version = "1.5.0" +version = "1.6.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, - {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, - {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, - {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, - {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, - {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, - {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, - {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, - {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, - {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, - {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, - {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, - {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, - {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, - {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, - {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, - {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, + {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"}, + {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"}, + {file = "frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd"}, + {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02"}, + {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3"}, + {file = "frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812"}, + {file = "frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"}, + {file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"}, + {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"}, + {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"}, + {file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"}, + {file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"}, + {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1"}, + {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29"}, + {file = "frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590"}, + {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046"}, + {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770"}, + {file = "frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc"}, + {file = "frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878"}, + {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e"}, + {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117"}, + {file = "frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f"}, + {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188"}, + {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e"}, + {file = "frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4"}, + {file = "frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd"}, + {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64"}, + {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91"}, + {file = "frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e"}, + {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911"}, + {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497"}, + {file = "frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f"}, + {file = "frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348"}, + {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0"}, + {file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70"}, + {file = "frozenlist-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c"}, + {file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa"}, + {file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c"}, + {file = "frozenlist-1.6.0-cp39-cp39-win32.whl", hash = "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530"}, + {file = "frozenlist-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572"}, + {file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"}, + {file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"}, ] [[package]] @@ -1641,6 +1744,9 @@ files = [ {file = "fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972"}, ] +[package.dependencies] +aiohttp = {version = "<4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1", optional = true, markers = "extra == \"http\""} + [package.extras] abfs = ["adlfs"] adl = ["adlfs"] @@ -1670,18 +1776,22 @@ test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "d tqdm = ["tqdm"] [[package]] -name = "genson" -version = "1.3.0" -description = "GenSON is a powerful, user-friendly JSON Schema generator." +name = "gguf" +version = "0.9.1" +description = "Read and write ML models in GGUF for GGML" optional = true -python-versions = "*" +python-versions = ">=3.8" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7"}, - {file = "genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37"}, + {file = "gguf-0.9.1-py3-none-any.whl", hash = "sha256:3c2e921e59c022584de201726f8bf3cabe8801fe0e53d931774cd88d16702bbc"}, + {file = "gguf-0.9.1.tar.gz", hash = "sha256:f5e70987e15b19c545f6a9f7533b4033fb306330eb1f3c5c95fd28b14c3fd33b"}, ] +[package.dependencies] +numpy = ">=1.17" +tqdm = ">=4.27" + [[package]] name = "ghp-import" version = "2.1.0" @@ -1702,14 +1812,14 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "1.6.1" +version = "1.7.3" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "griffe-1.6.1-py3-none-any.whl", hash = "sha256:b0131670db16834f82383bcf4f788778853c9bf4dc7a1a2b708bb0808ca56a98"}, - {file = "griffe-1.6.1.tar.gz", hash = "sha256:ff0acf706b2680f8c721412623091c891e752b2c61b7037618f7b77d06732cf5"}, + {file = "griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75"}, + {file = "griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b"}, ] [package.dependencies] @@ -1717,31 +1827,53 @@ colorama = ">=0.4" [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "hf-xet" +version = "1.1.0" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" +files = [ + {file = "hf_xet-1.1.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0322c42551e275fcb7949c083a54a81b2898e50787c9aa74284fcb8d2c58c12c"}, + {file = "hf_xet-1.1.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:667153a0304ac2debf2af95a8ff7687186f885b493f4cd16344869af270cd110"}, + {file = "hf_xet-1.1.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:995eeffb119636ea617b96c7d7bf3c3f5ea8727fa57974574e25d700b8532d48"}, + {file = "hf_xet-1.1.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3aee847da362393331f515c4010d0aaa1c2669acfcca1f4b28946d6949cc0086"}, + {file = "hf_xet-1.1.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68c5813a6074aa36e12ef5983230e3b03148cce61e0fcdd294096493795565b4"}, + {file = "hf_xet-1.1.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ee9222bf9274b1c198b88a929de0b5a49349c4962d89c5b3b2f0f7f47d9761c"}, + {file = "hf_xet-1.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:73153eab9abf3d6973b21e94a67ccba5d595c3e12feb8c0bf50be02964e7f126"}, + {file = "hf_xet-1.1.0.tar.gz", hash = "sha256:a7c2a4c2b6eee9ce0a1a367a82b60d95ba634420ef1c250addad7aa4af419cf4"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] @@ -1846,19 +1978,20 @@ files = [ [[package]] name = "huggingface-hub" -version = "0.29.3" +version = "0.31.1" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "huggingface_hub-0.29.3-py3-none-any.whl", hash = "sha256:0b25710932ac649c08cdbefa6c6ccb8e88eef82927cacdb048efb726429453aa"}, - {file = "huggingface_hub-0.29.3.tar.gz", hash = "sha256:64519a25716e0ba382ba2d3fb3ca082e7c7eb4a2fc634d200e8380006e0760e5"}, + {file = "huggingface_hub-0.31.1-py3-none-any.whl", hash = "sha256:43f73124819b48b42d140cbc0d7a2e6bd15b2853b1b9d728d4d55ad1750cac5b"}, + {file = "huggingface_hub-0.31.1.tar.gz", hash = "sha256:492bb5f545337aa9e2f59b75ef4c5f535a371e8958a6ce90af056387e67f1180"}, ] [package.dependencies] filelock = "*" fsspec = ">=2023.5.0" +hf-xet = {version = ">=1.1.0,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} packaging = ">=20.9" pyyaml = ">=5.1" requests = "*" @@ -1871,6 +2004,7 @@ cli = ["InquirerPy (==0.3.4)"] dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] hf-transfer = ["hf-transfer (>=0.1.4)"] +hf-xet = ["hf-xet (>=1.1.0,<2.0.0)"] inference = ["aiohttp"] quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.9.0)"] tensorflow = ["graphviz", "pydot", "tensorflow"] @@ -1896,14 +2030,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.6.1" +version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, - {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] @@ -1920,30 +2054,14 @@ type = ["pytest-mypy"] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] 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 = "intel-openmp" -version = "2021.4.0" -description = "Intel OpenMP* Runtime Library" -optional = true -python-versions = "*" -groups = ["main"] -markers = "extra == \"all\" and platform_system == \"Windows\"" -files = [ - {file = "intel_openmp-2021.4.0-py2.py3-none-macosx_10_15_x86_64.macosx_11_0_x86_64.whl", hash = "sha256:41c01e266a7fdb631a7609191709322da2bbf24b252ba763f125dd651bcc7675"}, - {file = "intel_openmp-2021.4.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:3b921236a38384e2016f0f3d65af6732cf2c12918087128a9163225451e776f2"}, - {file = "intel_openmp-2021.4.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:e2240ab8d01472fed04f3544a878cda5da16c26232b7ea1b59132dbfb48b186e"}, - {file = "intel_openmp-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:6e863d8fd3d7e8ef389d52cf97a50fe2afe1a19247e8c0d168ce021546f96fc9"}, - {file = "intel_openmp-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:eef4c8bcc8acefd7f5cd3b9384dbf73d59e2c99fc56545712ded913f43c4a94f"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1995,14 +2113,15 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.18.1" +version = "8.36.0" description = "IPython: Productive Interactive Computing" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ - {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, - {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, + {file = "ipython-8.36.0-py3-none-any.whl", hash = "sha256:12b913914d010dcffa2711505ec8be4bf0180742d97f1e5175e51f22086428c1"}, + {file = "ipython-8.36.0.tar.gz", hash = "sha256:24658e9fe5c5c819455043235ba59cfffded4a35936eefceceab6b192f7092ff"}, ] [package.dependencies] @@ -2011,43 +2130,95 @@ decorator = "*" exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.41,<3.1.0" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" +stack_data = "*" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli ; python_version < \"3.11\"", "typing_extensions"] kernel = ["ipykernel"] +matplotlib = ["matplotlib"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython" +version = "9.2.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.11" +groups = ["dev"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "ipython-9.2.0-py3-none-any.whl", hash = "sha256:fef5e33c4a1ae0759e0bba5917c9db4eb8c53fee917b6a526bd973e1ca5159f6"}, + {file = "ipython-9.2.0.tar.gz", hash = "sha256:62a9373dbc12f28f9feaf4700d052195bf89806279fc8ca11f3f54017d04751b"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +ipython-pygments-lexers = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack_data = "*" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[doc,matplotlib,test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"] +matplotlib = ["matplotlib"] +test = ["packaging", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +description = "Defines a variety of Pygments lexers for highlighting IPython code." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, + {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, +] + +[package.dependencies] +pygments = "*" [[package]] name = "ipywidgets" -version = "8.1.5" +version = "8.1.7" description = "Jupyter interactive widgets" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"}, - {file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"}, + {file = "ipywidgets-8.1.7-py3-none-any.whl", hash = "sha256:764f2602d25471c213919b8a1997df04bef869251db4ca8efba1b76b1bd9f7bb"}, + {file = "ipywidgets-8.1.7.tar.gz", hash = "sha256:15f1ac050b9ccbefd45dccfbb2ef6bed0029d8278682d569d71b8dd96bee0376"}, ] [package.dependencies] comm = ">=0.1.3" ipython = ">=6.1.0" -jupyterlab-widgets = ">=3.0.12,<3.1.0" +jupyterlab_widgets = ">=3.0.15,<3.1.0" traitlets = ">=4.3.1" -widgetsnbextension = ">=4.0.12,<4.1.0" +widgetsnbextension = ">=4.0.14,<4.1.0" [package.extras] test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] @@ -2188,6 +2359,19 @@ files = [ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] +[[package]] +name = "joblib" +version = "1.5.0" +description = "Lightweight pipelining with Python functions" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491"}, + {file = "joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5"}, +] + [[package]] name = "jsonpath-ng" version = "1.7.0" @@ -2240,14 +2424,14 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2024.10.1" +version = "2025.4.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, - {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, + {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, + {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, ] [package.dependencies] @@ -2299,14 +2483,14 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout" [[package]] name = "jupyterlab-widgets" -version = "3.0.13" +version = "3.0.15" description = "Jupyter interactive widgets for JupyterLab" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, - {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, + {file = "jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c"}, + {file = "jupyterlab_widgets-3.0.15.tar.gz", hash = "sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b"}, ] [[package]] @@ -2328,16 +2512,72 @@ interegular = ["interegular (>=0.3.1,<0.4.0)"] nearley = ["js2py"] regex = ["regex"] +[[package]] +name = "lazy-loader" +version = "0.4" +description = "Makes it easy to load subpackages and functions on demand." +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc"}, + {file = "lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +dev = ["changelist (==0.5)"] +lint = ["pre-commit (==3.7.0)"] +test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] + +[[package]] +name = "librosa" +version = "0.11.0" +description = "Python module for audio and music processing" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1"}, + {file = "librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908"}, +] + +[package.dependencies] +audioread = ">=2.1.9" +decorator = ">=4.3.0" +joblib = ">=1.0" +lazy_loader = ">=0.1" +msgpack = ">=1.0" +numba = ">=0.51.0" +numpy = ">=1.22.3" +pooch = ">=1.1" +scikit-learn = ">=1.1.0" +scipy = ">=1.6.0" +soundfile = ">=0.12.1" +soxr = ">=0.3.2" +standard-aifc = {version = "*", markers = "python_version >= \"3.13\""} +standard-sunau = {version = "*", markers = "python_version >= \"3.13\""} +typing_extensions = ">=4.1.1" + +[package.extras] +display = ["matplotlib (>=3.5.0)"] +docs = ["ipython (>=7.0)", "matplotlib (>=3.5.0)", "mir_eval (>=0.5)", "numba (>=0.51)", "numpydoc", "presets", "sphinx (!=1.3.1)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.7)", "sphinx-multiversion (>=0.2.3)", "sphinx_rtd_theme (>=1.2.0)", "sphinxcontrib-googleanalytics (>=0.4)", "sphinxcontrib-svg2pdfconverter"] +tests = ["matplotlib (>=3.5.0)", "packaging (>=20.0)", "pytest", "pytest-cov", "pytest-mpl", "resampy (>=0.2.2)", "samplerate", "types-decorator"] + [[package]] name = "litellm" -version = "1.67.2" +version = "1.67.5" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] files = [ - {file = "litellm-1.67.2-py3-none-any.whl", hash = "sha256:32df4d17b3ead17d04793311858965e41e83a7bdf9bd661895c0e6bc9c78dc8b"}, - {file = "litellm-1.67.2.tar.gz", hash = "sha256:9e108827bff16af04fd4c35b0c1a1d6c7746c96db3870189a60141d449797487"}, + {file = "litellm-1.67.5-py3-none-any.whl", hash = "sha256:bd3329731a36200539293521d312adf4f05fc4a6312a84baff2ce5a8b1507a43"}, + {file = "litellm-1.67.5.tar.gz", hash = "sha256:a9c73feed05aba33b3f2879658f57bb3480b43404ae693ebc827f1c157affde5"}, ] [package.dependencies] @@ -2355,19 +2595,51 @@ tokenizers = "*" [package.extras] extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.11)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.13)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"] + +[[package]] +name = "llvmlite" +version = "0.44.0" +description = "lightweight wrapper around basic LLVM functionality" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614"}, + {file = "llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791"}, + {file = "llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8"}, + {file = "llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408"}, + {file = "llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2"}, + {file = "llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3"}, + {file = "llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427"}, + {file = "llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1"}, + {file = "llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610"}, + {file = "llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955"}, + {file = "llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad"}, + {file = "llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db"}, + {file = "llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9"}, + {file = "llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d"}, + {file = "llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1"}, + {file = "llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516"}, + {file = "llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e"}, + {file = "llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf"}, + {file = "llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc"}, + {file = "llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930"}, + {file = "llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4"}, +] [[package]] name = "lm-format-enforcer" -version = "0.10.1" +version = "0.10.6" description = "Enforce the output format (JSON Schema, Regex etc) of a language model" optional = true python-versions = "<4.0,>=3.8" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "lm_format_enforcer-0.10.1-py3-none-any.whl", hash = "sha256:5520004af248d787930327ead052aeff75e21fad595f388e5eade9f062ffddda"}, - {file = "lm_format_enforcer-0.10.1.tar.gz", hash = "sha256:23e65a4199714fca348063e8c906838622619f905a673c4d6d428eee7e7d2095"}, + {file = "lm_format_enforcer-0.10.6-py3-none-any.whl", hash = "sha256:cad5a0cfbc9708332b57586a43d2945612fbcf40f9098f3ba95740d1a467a896"}, + {file = "lm_format_enforcer-0.10.6.tar.gz", hash = "sha256:ca2f061e9cf22fbe9f5ada2bc774c2bd369e8649b806fb5d9080eee37449f4a6"}, ] [package.dependencies] @@ -2378,14 +2650,14 @@ pyyaml = "*" [[package]] name = "logfire-api" -version = "3.9.0" +version = "3.14.1" description = "Shim for the Logfire SDK which does nothing unless Logfire is installed" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "logfire_api-3.9.0-py3-none-any.whl", hash = "sha256:a313eba49976ccca62ba6acb2f454d28941e53a114b73a29c50e8c09ea38767d"}, - {file = "logfire_api-3.9.0.tar.gz", hash = "sha256:b03bdcf368595510b4417270b5f02b268eb571a25692248ac1894b841a983a90"}, + {file = "logfire_api-3.14.1-py3-none-any.whl", hash = "sha256:61f786457d712b4f0bf99486b67ce33d7a6576e77056e1d91862df353cb5f4ed"}, + {file = "logfire_api-3.14.1.tar.gz", hash = "sha256:513708709d843c36bcd4a909f2da589d7ff23970a225b76a11499090c82101e8"}, ] [[package]] @@ -2409,18 +2681,18 @@ dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; pytho [[package]] name = "markdown" -version = "3.7" +version = "3.8" description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, - {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, + {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, + {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, ] [package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] [[package]] @@ -2511,14 +2783,14 @@ traitlets = "*" [[package]] name = "mcp" -version = "1.5.0" +version = "1.7.1" description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527"}, - {file = "mcp-1.5.0.tar.gz", hash = "sha256:5b2766c05e68e01a2034875e250139839498c61792163a7b221fc170c12f5aa9"}, + {file = "mcp-1.7.1-py3-none-any.whl", hash = "sha256:f7e6108977db6d03418495426c7ace085ba2341b75197f8727f96f9cfd30057a"}, + {file = "mcp-1.7.1.tar.gz", hash = "sha256:eb4f1f53bd717f75dda8a1416e00804b831a8f3c331e23447a03b78f04b43a6e"}, ] [package.dependencies] @@ -2527,9 +2799,10 @@ httpx = ">=0.27" httpx-sse = ">=0.4" pydantic = ">=2.7.2,<3.0.0" pydantic-settings = ">=2.5.2" +python-multipart = ">=0.0.9" sse-starlette = ">=1.6.1" starlette = ">=0.27" -uvicorn = ">=0.23.1" +uvicorn = {version = ">=0.23.1", markers = "sys_platform != \"emscripten\""} [package.extras] cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] @@ -2615,14 +2888,14 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.6.9" +version = "9.6.12" description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1"}, - {file = "mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c"}, + {file = "mkdocs_material-9.6.12-py3-none-any.whl", hash = "sha256:92b4fbdc329e4febc267ca6e2c51e8501fa97b2225c5f4deb4d4e43550f8e61e"}, + {file = "mkdocs_material-9.6.12.tar.gz", hash = "sha256:add6a6337b29f9ea7912cb1efc661de2c369060b040eb5119855d794ea85b473"}, ] [package.dependencies] @@ -2630,7 +2903,7 @@ babel = ">=2.10,<3.0" backrefs = ">=5.7.post1,<6.0" cairosvg = {version = ">=2.6,<3.0", optional = true, markers = "extra == \"imaging\""} colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" +jinja2 = ">=3.1,<4.0" markdown = ">=3.2,<4.0" mkdocs = ">=1.6,<2.0" mkdocs-material-extensions = ">=1.3,<2.0" @@ -2659,14 +2932,14 @@ files = [ [[package]] name = "mkdocs-section-index" -version = "0.3.9" +version = "0.3.10" description = "MkDocs plugin to allow clickable sections that lead to an index page" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "mkdocs_section_index-0.3.9-py3-none-any.whl", hash = "sha256:5e5eb288e8d7984d36c11ead5533f376fdf23498f44e903929d72845b24dfe34"}, - {file = "mkdocs_section_index-0.3.9.tar.gz", hash = "sha256:b66128d19108beceb08b226ee1ba0981840d14baf8a652b6c59e650f3f92e4f8"}, + {file = "mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776"}, + {file = "mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8"}, ] [package.dependencies] @@ -2716,26 +2989,6 @@ griffe = ">=0.49" mkdocs-autorefs = ">=1.0" mkdocstrings = ">=0.25" -[[package]] -name = "mkl" -version = "2021.4.0" -description = "Intel® oneAPI Math Kernel Library" -optional = true -python-versions = "*" -groups = ["main"] -markers = "extra == \"all\" and platform_system == \"Windows\"" -files = [ - {file = "mkl-2021.4.0-py2.py3-none-macosx_10_15_x86_64.macosx_11_0_x86_64.whl", hash = "sha256:67460f5cd7e30e405b54d70d1ed3ca78118370b65f7327d495e9c8847705e2fb"}, - {file = "mkl-2021.4.0-py2.py3-none-manylinux1_i686.whl", hash = "sha256:636d07d90e68ccc9630c654d47ce9fdeb036bb46e2b193b3a9ac8cfea683cce5"}, - {file = "mkl-2021.4.0-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:398dbf2b0d12acaf54117a5210e8f191827f373d362d796091d161f610c1ebfb"}, - {file = "mkl-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:439c640b269a5668134e3dcbcea4350459c4a8bc46469669b2d67e07e3d330e8"}, - {file = "mkl-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:ceef3cafce4c009dd25f65d7ad0d833a0fbadc3d8903991ec92351fe5de1e718"}, -] - -[package.dependencies] -intel-openmp = "==2021.*" -tbb = "==2021.*" - [[package]] name = "mpmath" version = "1.3.0" @@ -2830,111 +3083,203 @@ files = [ {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, ] +[[package]] +name = "msgspec" +version = "0.19.0" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259"}, + {file = "msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36"}, + {file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947"}, + {file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909"}, + {file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a"}, + {file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633"}, + {file = "msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90"}, + {file = "msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e"}, + {file = "msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551"}, + {file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7"}, + {file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011"}, + {file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063"}, + {file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716"}, + {file = "msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c"}, + {file = "msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f"}, + {file = "msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2"}, + {file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12"}, + {file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc"}, + {file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"}, + {file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"}, + {file = "msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"}, + {file = "msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86"}, + {file = "msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314"}, + {file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e"}, + {file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5"}, + {file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9"}, + {file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327"}, + {file = "msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f"}, + {file = "msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044"}, + {file = "msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229"}, + {file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12"}, + {file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446"}, + {file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19"}, + {file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db"}, + {file = "msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe"}, + {file = "msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e"}, +] + +[package.extras] +dev = ["attrs", "coverage", "eval-type-backport ; python_version < \"3.10\"", "furo", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli ; python_version < \"3.11\"", "tomli_w"] +doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] +test = ["attrs", "eval-type-backport ; python_version < \"3.10\"", "msgpack", "pytest", "pyyaml", "tomli ; python_version < \"3.11\"", "tomli_w"] +toml = ["tomli ; python_version < \"3.11\"", "tomli_w"] +yaml = ["pyyaml"] + [[package]] name = "multidict" -version = "6.2.0" +version = "6.4.3" description = "multidict implementation" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b9f6392d98c0bd70676ae41474e2eecf4c7150cb419237a41f8f96043fcb81d1"}, - {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3501621d5e86f1a88521ea65d5cad0a0834c77b26f193747615b7c911e5422d2"}, - {file = "multidict-6.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32ed748ff9ac682eae7859790d3044b50e3076c7d80e17a44239683769ff485e"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc826b9a8176e686b67aa60fd6c6a7047b0461cae5591ea1dc73d28f72332a8a"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:214207dcc7a6221d9942f23797fe89144128a71c03632bf713d918db99bd36de"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05fefbc3cddc4e36da209a5e49f1094bbece9a581faa7f3589201fd95df40e5d"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e851e6363d0dbe515d8de81fd544a2c956fdec6f8a049739562286727d4a00c3"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32c9b4878f48be3e75808ea7e499d6223b1eea6d54c487a66bc10a1871e3dc6a"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7243c5a6523c5cfeca76e063efa5f6a656d1d74c8b1fc64b2cd1e84e507f7e2a"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0e5a644e50ef9fb87878d4d57907f03a12410d2aa3b93b3acdf90a741df52c49"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0dc25a3293c50744796e87048de5e68996104d86d940bb24bc3ec31df281b191"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a49994481b99cd7dedde07f2e7e93b1d86c01c0fca1c32aded18f10695ae17eb"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641cf2e3447c9ecff2f7aa6e9eee9eaa286ea65d57b014543a4911ff2799d08a"}, - {file = "multidict-6.2.0-cp310-cp310-win32.whl", hash = "sha256:0c383d28857f66f5aebe3e91d6cf498da73af75fbd51cedbe1adfb85e90c0460"}, - {file = "multidict-6.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a33273a541f1e1a8219b2a4ed2de355848ecc0254264915b9290c8d2de1c74e1"}, - {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e87a7d75fa36839a3a432286d719975362d230c70ebfa0948549cc38bd5b46"}, - {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8de4d42dffd5ced9117af2ce66ba8722402541a3aa98ffdf78dde92badb68932"}, - {file = "multidict-6.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d91a230c7f8af86c904a5a992b8c064b66330544693fd6759c3d6162382ecf"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f6cad071960ba1914fa231677d21b1b4a3acdcce463cee41ea30bc82e6040cf"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f74f2fc51555f4b037ef278efc29a870d327053aba5cb7d86ae572426c7cccc"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14ed9ed1bfedd72a877807c71113deac292bf485159a29025dfdc524c326f3e1"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac3fcf9a2d369bd075b2c2965544036a27ccd277fc3c04f708338cc57533081"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fc6af8e39f7496047c7876314f4317736eac82bf85b54c7c76cf1a6f8e35d98"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8cb1329f42fadfb40d6211e5ff568d71ab49be36e759345f91c69d1033d633"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5389445f0173c197f4a3613713b5fb3f3879df1ded2a1a2e4bc4b5b9c5441b7e"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94a7bb972178a8bfc4055db80c51efd24baefaced5e51c59b0d598a004e8305d"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da51d8928ad8b4244926fe862ba1795f0b6e68ed8c42cd2f822d435db9c2a8f4"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:063be88bd684782a0715641de853e1e58a2f25b76388538bd62d974777ce9bc2"}, - {file = "multidict-6.2.0-cp311-cp311-win32.whl", hash = "sha256:52b05e21ff05729fbea9bc20b3a791c3c11da61649ff64cce8257c82a020466d"}, - {file = "multidict-6.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e2a2193d3aa5cbf5758f6d5680a52aa848e0cf611da324f71e5e48a9695cc86"}, - {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b"}, - {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4"}, - {file = "multidict-6.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:095a2eabe8c43041d3e6c2cb8287a257b5f1801c2d6ebd1dd877424f1e89cf29"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b"}, - {file = "multidict-6.2.0-cp312-cp312-win32.whl", hash = "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626"}, - {file = "multidict-6.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c"}, - {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80"}, - {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16"}, - {file = "multidict-6.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7d48207926edbf8b16b336f779c557dd8f5a33035a85db9c4b0febb0706817"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c099d3899b14e1ce52262eb82a5f5cb92157bb5106bf627b618c090a0eadc"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e16e7297f29a544f49340012d6fc08cf14de0ab361c9eb7529f6a57a30cbfda1"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042028348dc5a1f2be6c666437042a98a5d24cee50380f4c0902215e5ec41844"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08549895e6a799bd551cf276f6e59820aa084f0f90665c0f03dd3a50db5d3c48"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ccfd74957ef53fa7380aaa1c961f523d582cd5e85a620880ffabd407f8202c0"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83b78c680d4b15d33042d330c2fa31813ca3974197bddb3836a5c635a5fd013f"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b4c153863dd6569f6511845922c53e39c8d61f6e81f228ad5443e690fca403de"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98aa8325c7f47183b45588af9c434533196e241be0a4e4ae2190b06d17675c02"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e658d1373c424457ddf6d55ec1db93c280b8579276bebd1f72f113072df8a5d"}, - {file = "multidict-6.2.0-cp313-cp313-win32.whl", hash = "sha256:3157126b028c074951839233647bd0e30df77ef1fedd801b48bdcad242a60f4e"}, - {file = "multidict-6.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e87f1926e91855ae61769ba3e3f7315120788c099677e0842e697b0bfb659f2"}, - {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2529ddbdaa424b2c6c2eb668ea684dd6b75b839d0ad4b21aad60c168269478d7"}, - {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:13551d0e2d7201f0959725a6a769b6f7b9019a168ed96006479c9ac33fe4096b"}, - {file = "multidict-6.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1996ee1330e245cd3aeda0887b4409e3930524c27642b046e4fae88ffa66c5e"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c537da54ce4ff7c15e78ab1292e5799d0d43a2108e006578a57f531866f64025"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f249badb360b0b4d694307ad40f811f83df4da8cef7b68e429e4eea939e49dd"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48d39b1824b8d6ea7de878ef6226efbe0773f9c64333e1125e0efcfdd18a24c7"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99aac6bb2c37db336fa03a39b40ed4ef2818bf2dfb9441458165ebe88b793af"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bfa8bc649783e703263f783f73e27fef8cd37baaad4389816cf6a133141331"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c00ad31fbc2cbac85d7d0fcf90853b2ca2e69d825a2d3f3edb842ef1544a2c"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d57a01a2a9fa00234aace434d8c131f0ac6e0ac6ef131eda5962d7e79edfb5b"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:abf5b17bc0cf626a8a497d89ac691308dbd825d2ac372aa990b1ca114e470151"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f7716f7e7138252d88607228ce40be22660d6608d20fd365d596e7ca0738e019"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a36953389f35f0a4e88dc796048829a2f467c9197265504593f0e420571547"}, - {file = "multidict-6.2.0-cp313-cp313t-win32.whl", hash = "sha256:e653d36b1bf48fa78c7fcebb5fa679342e025121ace8c87ab05c1cefd33b34fc"}, - {file = "multidict-6.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ca23db5fb195b5ef4fd1f77ce26cadefdf13dba71dab14dadd29b34d457d7c44"}, - {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b4f3d66dd0354b79761481fc15bdafaba0b9d9076f1f42cc9ce10d7fcbda205a"}, - {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e2a2d6749e1ff2c9c76a72c6530d5baa601205b14e441e6d98011000f47a7ac"}, - {file = "multidict-6.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cca83a629f77402cfadd58352e394d79a61c8015f1694b83ab72237ec3941f88"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781b5dd1db18c9e9eacc419027b0acb5073bdec9de1675c0be25ceb10e2ad133"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf8d370b2fea27fb300825ec3984334f7dd54a581bde6456799ba3776915a656"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25bb96338512e2f46f615a2bb7c6012fe92a4a5ebd353e5020836a7e33120349"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e2819b0b468174de25c0ceed766606a07cedeab132383f1e83b9a4e96ccb4f"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aed763b6a1b28c46c055692836879328f0b334a6d61572ee4113a5d0c859872"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a1133414b771619aa3c3000701c11b2e4624a7f492f12f256aedde97c28331a2"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:639556758c36093b35e2e368ca485dada6afc2bd6a1b1207d85ea6dfc3deab27"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:163f4604e76639f728d127293d24c3e208b445b463168af3d031b92b0998bb90"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2325105e16d434749e1be8022f942876a936f9bece4ec41ae244e3d7fae42aaf"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e4371591e621579cb6da8401e4ea405b33ff25a755874a3567c4075ca63d56e2"}, - {file = "multidict-6.2.0-cp39-cp39-win32.whl", hash = "sha256:d1175b0e0d6037fab207f05774a176d71210ebd40b1c51f480a04b65ec5c786d"}, - {file = "multidict-6.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad81012b24b88aad4c70b2cbc2dad84018783221b7f923e926f4690ff8569da3"}, - {file = "multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530"}, - {file = "multidict-6.2.0.tar.gz", hash = "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8"}, + {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5"}, + {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188"}, + {file = "multidict-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef"}, + {file = "multidict-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c"}, + {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5"}, + {file = "multidict-6.4.3-cp310-cp310-win32.whl", hash = "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e"}, + {file = "multidict-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8"}, + {file = "multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7"}, + {file = "multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618"}, + {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7"}, + {file = "multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378"}, + {file = "multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589"}, + {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676"}, + {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1"}, + {file = "multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c"}, + {file = "multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713"}, + {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a"}, + {file = "multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124"}, + {file = "multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db"}, + {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474"}, + {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd"}, + {file = "multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0"}, + {file = "multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9"}, + {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8"}, + {file = "multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3"}, + {file = "multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5"}, + {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6"}, + {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c"}, + {file = "multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02"}, + {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4"}, + {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4"}, + {file = "multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5"}, + {file = "multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208"}, + {file = "multidict-6.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21"}, + {file = "multidict-6.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b"}, + {file = "multidict-6.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2"}, + {file = "multidict-6.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752"}, + {file = "multidict-6.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df"}, + {file = "multidict-6.4.3-cp39-cp39-win32.whl", hash = "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f"}, + {file = "multidict-6.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897"}, + {file = "multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9"}, + {file = "multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec"}, ] [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} +[[package]] +name = "multiprocess" +version = "0.70.16" +description = "better multiprocessing and multithreading in Python" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee"}, + {file = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}, + {file = "multiprocess-0.70.16-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37b55f71c07e2d741374998c043b9520b626a8dddc8b3129222ca4f1a06ef67a"}, + {file = "multiprocess-0.70.16-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba8c31889abf4511c7308a8c52bb4a30b9d590e7f58523302ba00237702ca054"}, + {file = "multiprocess-0.70.16-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41"}, + {file = "multiprocess-0.70.16-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a"}, + {file = "multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}, + {file = "multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a"}, + {file = "multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e"}, + {file = "multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435"}, + {file = "multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3"}, + {file = "multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1"}, +] + +[package.dependencies] +dill = ">=0.3.8" + [[package]] name = "mypy" version = "1.15.0" @@ -2991,14 +3336,14 @@ reports = ["lxml"] [[package]] name = "mypy-boto3-s3" -version = "1.37.0" -description = "Type annotations for boto3 S3 1.37.0 service generated with mypy-boto3-builder 8.9.2" +version = "1.38.0" +description = "Type annotations for boto3 S3 1.38.0 service generated with mypy-boto3-builder 8.10.1" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "mypy_boto3_s3-1.37.0-py3-none-any.whl", hash = "sha256:d2b702649d7ebb2bd2b8f574fd51b35fc2a2ec4a8efb590db5eb0d0d9f74be6f"}, - {file = "mypy_boto3_s3-1.37.0.tar.gz", hash = "sha256:bc6ec4cbbd8e0206143d9b1f24927e086a2467a2c6a641feb978599d75954e82"}, + {file = "mypy_boto3_s3-1.38.0-py3-none-any.whl", hash = "sha256:5cd9449df0ef6cf89e00e6fc9130a0ab641f703a23ab1d2146c394da058e8282"}, + {file = "mypy_boto3_s3-1.38.0.tar.gz", hash = "sha256:f8fe586e45123ffcd305a0c30847128f3931d888649e2b4c5a52f412183c840a"}, ] [package.dependencies] @@ -3006,14 +3351,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" 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"}, + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] @@ -3031,108 +3376,105 @@ markers = {main = "extra == \"all\""} [[package]] name = "networkx" -version = "3.2.1" +version = "3.4.2" description = "Python package for creating and manipulating graphs and networks" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, - {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, + {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, + {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, ] [package.extras] -default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] +example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] +extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] -name = "ninja" -version = "1.11.1.3" -description = "Ninja is a small build system with a focus on speed" +name = "numba" +version = "0.61.2" +description = "compiling Python code using LLVM" optional = true -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "ninja-1.11.1.3-py3-none-macosx_10_9_universal2.whl", hash = "sha256:2b4879ea3f1169f3d855182c57dcc84d1b5048628c8b7be0d702b81882a37237"}, - {file = "ninja-1.11.1.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bc3ebc8b2e47716149f3541742b5cd8e0b08f51013b825c05baca3e34854370d"}, - {file = "ninja-1.11.1.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a27e78ca71316c8654965ee94b286a98c83877bfebe2607db96897bbfe458af0"}, - {file = "ninja-1.11.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2883ea46b3c5079074f56820f9989c6261fcc6fd873d914ee49010ecf283c3b2"}, - {file = "ninja-1.11.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c4bdb9fd2d0c06501ae15abfd23407660e95659e384acd36e013b6dd7d8a8e4"}, - {file = "ninja-1.11.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:114ed5c61c8474df6a69ab89097a20749b769e2c219a452cb2fadc49b0d581b0"}, - {file = "ninja-1.11.1.3-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fa2247fce98f683bc712562d82b22b8a0a5c000738a13147ca2d1b68c122298"}, - {file = "ninja-1.11.1.3-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:a38c6c6c8032bed68b70c3b065d944c35e9f903342875d3a3218c1607987077c"}, - {file = "ninja-1.11.1.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:56ada5d33b8741d298836644042faddebc83ee669782d661e21563034beb5aba"}, - {file = "ninja-1.11.1.3-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:53409151da081f3c198bb0bfc220a7f4e821e022c5b7d29719adda892ddb31bb"}, - {file = "ninja-1.11.1.3-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:1ad2112c2b0159ed7c4ae3731595191b1546ba62316fc40808edecd0306fefa3"}, - {file = "ninja-1.11.1.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:28aea3c1c280cba95b8608d50797169f3a34280e3e9a6379b6e340f0c9eaeeb0"}, - {file = "ninja-1.11.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b6966f83064a88a51693073eea3decd47e08c3965241e09578ef7aa3a7738329"}, - {file = "ninja-1.11.1.3-py3-none-win32.whl", hash = "sha256:a4a3b71490557e18c010cbb26bd1ea9a0c32ee67e8f105e9731515b6e0af792e"}, - {file = "ninja-1.11.1.3-py3-none-win_amd64.whl", hash = "sha256:04d48d14ea7ba11951c156599ab526bdda575450797ff57c6fdf99b2554d09c7"}, - {file = "ninja-1.11.1.3-py3-none-win_arm64.whl", hash = "sha256:17978ad611d8ead578d83637f5ae80c2261b033db0b493a7ce94f88623f29e1b"}, - {file = "ninja-1.11.1.3.tar.gz", hash = "sha256:edfa0d2e9d7ead1635b03e40a32ad56cc8f56798b6e2e9848d8300b174897076"}, + {file = "numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a"}, + {file = "numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd"}, + {file = "numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642"}, + {file = "numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2"}, + {file = "numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9"}, + {file = "numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2"}, + {file = "numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b"}, + {file = "numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60"}, + {file = "numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18"}, + {file = "numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1"}, + {file = "numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2"}, + {file = "numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8"}, + {file = "numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546"}, + {file = "numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd"}, + {file = "numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18"}, + {file = "numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154"}, + {file = "numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140"}, + {file = "numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab"}, + {file = "numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e"}, + {file = "numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7"}, + {file = "numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d"}, ] -[package.extras] -test = ["coverage (>=4.2)", "importlib_metadata (>=2.0)", "pytest (>=6.0)", "pytest-cov (>=3)"] +[package.dependencies] +llvmlite = "==0.44.*" +numpy = ">=1.24,<2.3" [[package]] name = "numpy" -version = "2.0.2" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] @@ -3189,14 +3531,15 @@ files = [ [[package]] name = "nvidia-cudnn-cu12" -version = "8.9.2.26" +version = "9.1.0.70" description = "cuDNN runtime libraries" optional = true python-versions = ">=3" groups = ["main"] markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and extra == \"all\"" files = [ - {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"}, + {file = "nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f"}, + {file = "nvidia_cudnn_cu12-9.1.0.70-py3-none-win_amd64.whl", hash = "sha256:6278562929433d68365a07a4a1546c237ba2849852c0d4b2262a486e805b977a"}, ] [package.dependencies] @@ -3264,15 +3607,15 @@ nvidia-nvjitlink-cu12 = "*" [[package]] name = "nvidia-ml-py" -version = "12.570.86" +version = "12.575.51" description = "Python Bindings for the NVIDIA Management Library" optional = true python-versions = "*" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "nvidia_ml_py-12.570.86-py3-none-any.whl", hash = "sha256:58907de35a845abd13dcb227f18298f3b5dd94a72d04c9e594e77711e95c0b51"}, - {file = "nvidia_ml_py-12.570.86.tar.gz", hash = "sha256:0508d4a0c7b6d015cf574530b95a62ed4fc89da3b8b47e1aefe6777db170ec8b"}, + {file = "nvidia_ml_py-12.575.51-py3-none-any.whl", hash = "sha256:eb8641800d98ce40a22f479873f34b482e214a7e80349c63be51c3919845446e"}, + {file = "nvidia_ml_py-12.575.51.tar.gz", hash = "sha256:6490e93fea99eb4e966327ae18c6eec6256194c921f23459c8767aee28c54581"}, ] [[package]] @@ -3290,16 +3633,16 @@ files = [ [[package]] name = "nvidia-nvjitlink-cu12" -version = "12.8.93" +version = "12.9.41" description = "Nvidia JIT LTO Library" optional = true python-versions = ">=3" groups = ["main"] markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and extra == \"all\"" files = [ - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88"}, - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7"}, - {file = "nvidia_nvjitlink_cu12-12.8.93-py3-none-win_amd64.whl", hash = "sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f"}, + {file = "nvidia_nvjitlink_cu12-12.9.41-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:c3a2cd87cecf3f0ca5e5df97115ede3a81efec1d4b7e2ec89d13f66834042930"}, + {file = "nvidia_nvjitlink_cu12-12.9.41-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:631270891e78de08ebc669bb9ba4418b7899da9efb927fcf6fdff85c9507f54f"}, + {file = "nvidia_nvjitlink_cu12-12.9.41-py3-none-win_amd64.whl", hash = "sha256:d7980883fddf331adb635be475b9d7f07272273cd51f6da8adf487571f17da9e"}, ] [[package]] @@ -3317,14 +3660,14 @@ files = [ [[package]] name = "openai" -version = "1.76.0" +version = "1.77.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a"}, - {file = "openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2"}, + {file = "openai-1.77.0-py3-none-any.whl", hash = "sha256:07706e91eb71631234996989a8ea991d5ee56f0744ef694c961e0824d4f39218"}, + {file = "openai-1.77.0.tar.gz", hash = "sha256:897969f927f0068b8091b4b041d1f8175bcf124f7ea31bab418bf720971223bc"}, ] [package.dependencies] @@ -3344,100 +3687,50 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "outlines" -version = "0.1.14" +version = "0.0.46" description = "Probabilistic Generative Model Programming" optional = true -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "outlines-0.1.14-py3-none-any.whl", hash = "sha256:a5090d50c368ed078051de25686a53032cd9f1702528afc646c3dae9482598ce"}, - {file = "outlines-0.1.14.tar.gz", hash = "sha256:35f0c49fc7eedc64ec08e2d6fd434845cf63cc0c3fdeb5900ac7902d074e57be"}, + {file = "outlines-0.0.46-py3-none-any.whl", hash = "sha256:fea7b2ccd2dd82ddf11849a55ef14c4d7b72f12136a67d1b105b81639c8ca2b0"}, + {file = "outlines-0.0.46.tar.gz", hash = "sha256:cd272418a268e0a25b7189180dfdcf9fe1b99f50ac1dfb9ffd83c896c5b3fa3c"}, ] [package.dependencies] -airportsdata = "*" cloudpickle = "*" +datasets = "*" diskcache = "*" -genson = "*" interegular = "*" jinja2 = "*" jsonschema = "*" lark = "*" -nest_asyncio = "*" -numpy = "*" -outlines_core = "0.1.26" +nest-asyncio = "*" +numba = "*" +numpy = "<2.0.0" +pyairports = "*" pycountry = "*" pydantic = ">=2.0" referencing = "*" requests = "*" -torch = "*" tqdm = "*" -typing_extensions = "*" +typing-extensions = "*" [package.extras] -exllamav2 = ["exllamav2"] -llamacpp = ["datasets", "llama-cpp-python", "numpy (<2)", "transformers"] -mlxlm = ["datasets", "mlx-lm"] -openai = ["openai"] serve = ["fastapi", "pydantic (>=2.0)", "uvicorn", "vllm (>=0.3.0)"] -test = ["accelerate", "beartype (<0.16.0)", "coverage[toml] (>=5.1)", "datasets", "diff-cover", "exllamav2", "huggingface_hub", "jax", "llama-cpp-python", "mlx-lm (>=0.19.2) ; platform_machine == \"arm64\" and sys_platform == \"darwin\"", "openai (>=1.0.0)", "pillow", "pre-commit", "pytest", "pytest-benchmark", "pytest-cov", "pytest-mock", "responses", "transformers", "vllm ; sys_platform == \"linux\""] -transformers = ["accelerate", "datasets", "numpy (<2)", "transformers"] -vllm = ["numpy (<2)", "transformers", "vllm"] - -[[package]] -name = "outlines-core" -version = "0.1.26" -description = "Structured Text Generation in Rust" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"all\"" -files = [ - {file = "outlines_core-0.1.26-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6a962a7452e7ac170fa04d405342cadae2d28fafa5b1830cef7aa610257ed32f"}, - {file = "outlines_core-0.1.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15a3684fa29564da2db03934cf0097bef3e871f70d3af0ef2b52fdb886da2e09"}, - {file = "outlines_core-0.1.26-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64e01c0cfa9ba371634d7c3f6ea1862397cef98e4509fe98e3f57faa721a72d6"}, - {file = "outlines_core-0.1.26-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3c4196148e47f455f1ace78e329d5b97e531cbc406456d681592952adae7e17"}, - {file = "outlines_core-0.1.26-cp310-cp310-win32.whl", hash = "sha256:f38d290a7f6e5e12cbfcaee03269dfc0dbda49b360024b4279d1aba251fdc346"}, - {file = "outlines_core-0.1.26-cp310-cp310-win_amd64.whl", hash = "sha256:11ff56af56cb54c563b7f25d86cd9ee77f3fed825f1d4dccd9449bb1e4e89538"}, - {file = "outlines_core-0.1.26-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b6787b07b7c673fc3087d2b537719ecac8e03b10a47d032dd1926985c32885b0"}, - {file = "outlines_core-0.1.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e0ea28a76da31d25b6f53242bf13e1b59a0241badf82353c88f55e1cf81b128"}, - {file = "outlines_core-0.1.26-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8932044a3d9329be53a226118850638f85b4d7842f9b863d0a123f23de220cd"}, - {file = "outlines_core-0.1.26-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a84b7cd2fb6268bf990dd3d479ffb4fa0bace6f571cb85b15b6cdb44b84f5b69"}, - {file = "outlines_core-0.1.26-cp311-cp311-win32.whl", hash = "sha256:f19765c151abfc970996368080aeea6d2a19e927817fe4e2af6726e639be3de4"}, - {file = "outlines_core-0.1.26-cp311-cp311-win_amd64.whl", hash = "sha256:3f59aeccea21ed6ff3cf52102fd163f26d279821c20e5127ddd18d4ea4d0c8d2"}, - {file = "outlines_core-0.1.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f54633bca50055d42ea4d94ae06dcbe52d3d76a9b621b75723b1177d0d952953"}, - {file = "outlines_core-0.1.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9525321b48700dcaaabf60bcdc951e45f9357ba3fb3e1bfc81b662d7d4170e7c"}, - {file = "outlines_core-0.1.26-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f409f72c11f6ffadb57066950dd384d5388015028c1a1a615c9a64988dae3e"}, - {file = "outlines_core-0.1.26-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86a1bb46adc5cbf6dfd7a7fe4105e0e2a4c6e041732a053126b41c521a1f223"}, - {file = "outlines_core-0.1.26-cp312-cp312-win32.whl", hash = "sha256:19f462f6b00935708677ad27cb4df55e0e17f6ffe713ab750f5f2683b090f95d"}, - {file = "outlines_core-0.1.26-cp312-cp312-win_amd64.whl", hash = "sha256:9b36bff12779e58883747116893a17b3551bbd10865878b951b03a44d112229a"}, - {file = "outlines_core-0.1.26-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7b7849cf40028319ebb9d8ba0fe4c590ef5888eebe524a81b3af30aaa06ea21c"}, - {file = "outlines_core-0.1.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2f8641aab4a6bd84516907492ce82099503129da01b3c29c1dc9ad50320bae77"}, - {file = "outlines_core-0.1.26-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba56604efdbc5932c7a8a88c2b8b0d0c740ab883b0012fb5464a9736796802b"}, - {file = "outlines_core-0.1.26-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc8c87d89bd267356f8149c9066cbb98970425ec162997fbf195c3f1feb7009"}, - {file = "outlines_core-0.1.26-cp39-cp39-win32.whl", hash = "sha256:9d792a43ed9d8a4e1b38f4d83fe99db442d57aad4404c2edf98b710892eda47e"}, - {file = "outlines_core-0.1.26-cp39-cp39-win_amd64.whl", hash = "sha256:ad8564ecd7b64bcb840596c5049ff1c1a96346de494302ffcc0f2b188c15675e"}, - {file = "outlines_core-0.1.26.tar.gz", hash = "sha256:481c4301341e77cc8f1832d616784adb4d461b4fec65878e7c0d2cba7163a189"}, -] - -[package.dependencies] -interegular = "*" -jsonschema = "*" - -[package.extras] -test = ["accelerate", "asv", "beartype (<0.16.0)", "coverage[toml] (>=5.1)", "datasets", "diff-cover", "huggingface_hub", "numpy", "pillow", "pre-commit", "psutil", "pydantic", "pytest", "pytest-benchmark", "pytest-cov", "pytest-mock", "scipy", "setuptools-rust", "torch", "transformers"] +test = ["accelerate", "beartype (<0.16.0)", "coverage[toml] (>=5.1)", "diff-cover", "huggingface-hub", "llama-cpp-python", "mlx-lm", "openai (>=1.0.0)", "pre-commit", "pytest", "pytest-benchmark", "pytest-cov", "pytest-mock", "responses", "torch", "transformers", "vllm"] [[package]] name = "packaging" -version = "24.2" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "docs"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] @@ -3545,14 +3838,14 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pandas-stubs" -version = "2.2.2.240807" +version = "2.2.3.250308" description = "Type annotations for pandas" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e"}, - {file = "pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93"}, + {file = "pandas_stubs-2.2.3.250308-py3-none-any.whl", hash = "sha256:a377edff3b61f8b268c82499fdbe7c00fdeed13235b8b71d6a1dc347aeddc74d"}, + {file = "pandas_stubs-2.2.3.250308.tar.gz", hash = "sha256:3a6e9daf161f00b85c83772ed3d5cff9522028f07a94817472c07b91f46710fd"}, ] [package.dependencies] @@ -3594,7 +3887,7 @@ description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" groups = ["dev"] -markers = "sys_platform != \"win32\"" +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -3704,20 +3997,21 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" -groups = ["dev", "docs"] +python-versions = ">=3.9" +groups = ["main", "dev", "docs"] files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] +markers = {main = "extra == \"all\""} [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" @@ -3747,6 +4041,29 @@ files = [ {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, ] +[[package]] +name = "pooch" +version = "1.8.2" +description = "A friend to fetch your data files" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47"}, + {file = "pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10"}, +] + +[package.dependencies] +packaging = ">=20.0" +platformdirs = ">=2.5.0" +requests = ">=2.19.0" + +[package.extras] +progress = ["tqdm (>=4.41.0,<5.0.0)"] +sftp = ["paramiko (>=2.7.0)"] +xxhash = ["xxhash (>=1.4.3)"] + [[package]] name = "prometheus-client" version = "0.21.1" @@ -3765,15 +4082,15 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "7.0.2" +version = "7.1.0" description = "Instrument your FastAPI app with Prometheus metrics" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "prometheus_fastapi_instrumentator-7.0.2-py3-none-any.whl", hash = "sha256:975e39992acb7a112758ff13ba95317e6c54d1bbf605f9156f31ac9f2800c32d"}, - {file = "prometheus_fastapi_instrumentator-7.0.2.tar.gz", hash = "sha256:8a4d8fb13dbe19d2882ac6af9ce236e4e1f98dc48e3fa44fe88d8e23ac3c953f"}, + {file = "prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9"}, + {file = "prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e"}, ] [package.dependencies] @@ -3782,14 +4099,14 @@ starlette = ">=0.30.0,<1.0.0" [[package]] name = "prompt-toolkit" -version = "3.0.50" +version = "3.0.51" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, - {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, ] [package.dependencies] @@ -3797,130 +4114,130 @@ wcwidth = "*" [[package]] name = "propcache" -version = "0.3.0" +version = "0.3.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa44f64c37cc30c9f05932c740a8b40ce359f51882c70883cc95feac842da4d"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2383a17385d9800b6eb5855c2f05ee550f803878f344f58b6e194de08b96352c"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e7420211f5a65a54675fd860ea04173cde60a7cc20ccfbafcccd155225f8bc"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3302c5287e504d23bb0e64d2a921d1eb4a03fb93a0a0aa3b53de059f5a5d737d"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2e068a83552ddf7a39a99488bcba05ac13454fb205c847674da0352602082f"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d913d36bdaf368637b4f88d554fb9cb9d53d6920b9c5563846555938d5450bf"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ee1983728964d6070ab443399c476de93d5d741f71e8f6e7880a065f878e0b9"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36ca5e9a21822cc1746023e88f5c0af6fce3af3b85d4520efb1ce4221bed75cc"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9ecde3671e62eeb99e977f5221abcf40c208f69b5eb986b061ccec317c82ebd0"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d383bf5e045d7f9d239b38e6acadd7b7fdf6c0087259a84ae3475d18e9a2ae8b"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8cb625bcb5add899cb8ba7bf716ec1d3e8f7cdea9b0713fa99eadf73b6d4986f"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5fa159dcee5dba00c1def3231c249cf261185189205073bde13797e57dd7540a"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7080b0159ce05f179cfac592cda1a82898ca9cd097dacf8ea20ae33474fbb25"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed7161bccab7696a473fe7ddb619c1d75963732b37da4618ba12e60899fefe4f"}, - {file = "propcache-0.3.0-cp310-cp310-win32.whl", hash = "sha256:bf0d9a171908f32d54f651648c7290397b8792f4303821c42a74e7805bfb813c"}, - {file = "propcache-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:42924dc0c9d73e49908e35bbdec87adedd651ea24c53c29cac103ede0ea1d340"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9ddd49258610499aab83b4f5b61b32e11fce873586282a0e972e5ab3bcadee51"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2578541776769b500bada3f8a4eeaf944530516b6e90c089aa368266ed70c49e"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8074c5dd61c8a3e915fa8fc04754fa55cfa5978200d2daa1e2d4294c1f136aa"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b58229a844931bca61b3a20efd2be2a2acb4ad1622fc026504309a6883686fbf"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e45377d5d6fefe1677da2a2c07b024a6dac782088e37c0b1efea4cfe2b1be19b"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec5060592d83454e8063e487696ac3783cc48c9a329498bafae0d972bc7816c9"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15010f29fbed80e711db272909a074dc79858c6d28e2915704cfc487a8ac89c6"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a254537b9b696ede293bfdbc0a65200e8e4507bc9f37831e2a0318a9b333c85c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2b975528998de037dfbc10144b8aed9b8dd5a99ec547f14d1cb7c5665a43f075"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:19d36bb351ad5554ff20f2ae75f88ce205b0748c38b146c75628577020351e3c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6032231d4a5abd67c7f71168fd64a47b6b451fbcb91c8397c2f7610e67683810"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6985a593417cdbc94c7f9c3403747335e450c1599da1647a5af76539672464d3"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a1948df1bb1d56b5e7b0553c0fa04fd0e320997ae99689488201f19fa90d2e7"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8319293e85feadbbfe2150a5659dbc2ebc4afdeaf7d98936fb9a2f2ba0d4c35c"}, - {file = "propcache-0.3.0-cp311-cp311-win32.whl", hash = "sha256:63f26258a163c34542c24808f03d734b338da66ba91f410a703e505c8485791d"}, - {file = "propcache-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:cacea77ef7a2195f04f9279297684955e3d1ae4241092ff0cfcef532bb7a1c32"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c"}, - {file = "propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d"}, - {file = "propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626"}, - {file = "propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374"}, - {file = "propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf"}, - {file = "propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863"}, - {file = "propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:03c091bb752349402f23ee43bb2bff6bd80ccab7c9df6b88ad4322258d6960fc"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46ed02532cb66612d42ae5c3929b5e98ae330ea0f3900bc66ec5f4862069519b"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11ae6a8a01b8a4dc79093b5d3ca2c8a4436f5ee251a9840d7790dccbd96cb649"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df03cd88f95b1b99052b52b1bb92173229d7a674df0ab06d2b25765ee8404bce"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03acd9ff19021bd0567582ac88f821b66883e158274183b9e5586f678984f8fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd54895e4ae7d32f1e3dd91261df46ee7483a735017dc6f987904f194aa5fd14"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a67e5c04e3119594d8cfae517f4b9330c395df07ea65eab16f3d559b7068fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee25f1ac091def37c4b59d192bbe3a206298feeb89132a470325bf76ad122a1e"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58e6d2a5a7cb3e5f166fd58e71e9a4ff504be9dc61b88167e75f835da5764d07"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:be90c94570840939fecedf99fa72839aed70b0ced449b415c85e01ae67422c90"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49ea05212a529c2caffe411e25a59308b07d6e10bf2505d77da72891f9a05641"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:119e244ab40f70a98c91906d4c1f4c5f2e68bd0b14e7ab0a06922038fae8a20f"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:507c5357a8d8b4593b97fb669c50598f4e6cccbbf77e22fa9598aba78292b4d7"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8526b0941ec5a40220fc4dfde76aed58808e2b309c03e9fa8e2260083ef7157f"}, - {file = "propcache-0.3.0-cp39-cp39-win32.whl", hash = "sha256:7cedd25e5f678f7738da38037435b340694ab34d424938041aa630d8bac42663"}, - {file = "propcache-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:bf4298f366ca7e1ad1d21bbb58300a6985015909964077afd37559084590c929"}, - {file = "propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043"}, - {file = "propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, + {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, + {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, + {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, + {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, + {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, + {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, + {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, + {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, + {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, + {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, + {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, + {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, + {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, + {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, ] [[package]] name = "protobuf" -version = "6.30.1" +version = "6.30.2" description = "" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "protobuf-6.30.1-cp310-abi3-win32.whl", hash = "sha256:ba0706f948d0195f5cac504da156d88174e03218d9364ab40d903788c1903d7e"}, - {file = "protobuf-6.30.1-cp310-abi3-win_amd64.whl", hash = "sha256:ed484f9ddd47f0f1bf0648806cccdb4fe2fb6b19820f9b79a5adf5dcfd1b8c5f"}, - {file = "protobuf-6.30.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aa4f7dfaed0d840b03d08d14bfdb41348feaee06a828a8c455698234135b4075"}, - {file = "protobuf-6.30.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:47cd320b7db63e8c9ac35f5596ea1c1e61491d8a8eb6d8b45edc44760b53a4f6"}, - {file = "protobuf-6.30.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3083660225fa94748ac2e407f09a899e6a28bf9c0e70c75def8d15706bf85fc"}, - {file = "protobuf-6.30.1-cp39-cp39-win32.whl", hash = "sha256:554d7e61cce2aa4c63ca27328f757a9f3867bce8ec213bf09096a8d16bcdcb6a"}, - {file = "protobuf-6.30.1-cp39-cp39-win_amd64.whl", hash = "sha256:b510f55ce60f84dc7febc619b47215b900466e3555ab8cb1ba42deb4496d6cc0"}, - {file = "protobuf-6.30.1-py3-none-any.whl", hash = "sha256:3c25e51e1359f1f5fa3b298faa6016e650d148f214db2e47671131b9063c53be"}, - {file = "protobuf-6.30.1.tar.gz", hash = "sha256:535fb4e44d0236893d5cf1263a0f706f1160b689a7ab962e9da8a9ce4050b780"}, + {file = "protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103"}, + {file = "protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9"}, + {file = "protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b"}, + {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815"}, + {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d"}, + {file = "protobuf-6.30.2-cp39-cp39-win32.whl", hash = "sha256:524afedc03b31b15586ca7f64d877a98b184f007180ce25183d1a5cb230ee72b"}, + {file = "protobuf-6.30.2-cp39-cp39-win_amd64.whl", hash = "sha256:acec579c39c88bd8fbbacab1b8052c793efe83a0a5bd99db4a31423a25c0a0e2"}, + {file = "protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51"}, + {file = "protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048"}, ] [[package]] @@ -3955,7 +4272,7 @@ description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" groups = ["dev"] -markers = "sys_platform != \"win32\"" +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -3989,6 +4306,88 @@ files = [ {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, ] +[[package]] +name = "pyairports" +version = "2.1.1" +description = "Airport and other locations database" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "pyairports-2.1.1-py3-none-any.whl", hash = "sha256:3dfa0cc3e47696692ade92feccdc6b046968f2a75f5e30f65735d6db7251cb26"}, + {file = "pyairports-2.1.1.tar.gz", hash = "sha256:3d60a727fce4da81b9c6393ea8ae0b33d67b37ece344dffc863f749e3ad62bcd"}, +] + +[[package]] +name = "pyarrow" +version = "20.0.0" +description = "Python library for Apache Arrow" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "pyarrow-20.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c7dd06fd7d7b410ca5dc839cc9d485d2bc4ae5240851bcd45d85105cc90a47d7"}, + {file = "pyarrow-20.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d5382de8dc34c943249b01c19110783d0d64b207167c728461add1ecc2db88e4"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6415a0d0174487456ddc9beaead703d0ded5966129fa4fd3114d76b5d1c5ceae"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15aa1b3b2587e74328a730457068dc6c89e6dcbf438d4369f572af9d320a25ee"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5605919fbe67a7948c1f03b9f3727d82846c053cd2ce9303ace791855923fd20"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a5704f29a74b81673d266e5ec1fe376f060627c2e42c5c7651288ed4b0db29e9"}, + {file = "pyarrow-20.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:00138f79ee1b5aca81e2bdedb91e3739b987245e11fa3c826f9e57c5d102fb75"}, + {file = "pyarrow-20.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2d67ac28f57a362f1a2c1e6fa98bfe2f03230f7e15927aecd067433b1e70ce8"}, + {file = "pyarrow-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a8b029a07956b8d7bd742ffca25374dd3f634b35e46cc7a7c3fa4c75b297191"}, + {file = "pyarrow-20.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:24ca380585444cb2a31324c546a9a56abbe87e26069189e14bdba19c86c049f0"}, + {file = "pyarrow-20.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:95b330059ddfdc591a3225f2d272123be26c8fa76e8c9ee1a77aad507361cfdb"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f0fb1041267e9968c6d0d2ce3ff92e3928b243e2b6d11eeb84d9ac547308232"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ff87cc837601532cc8242d2f7e09b4e02404de1b797aee747dd4ba4bd6313f"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a3a5dcf54286e6141d5114522cf31dd67a9e7c9133d150799f30ee302a7a1ab"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a6ad3e7758ecf559900261a4df985662df54fb7fdb55e8e3b3aa99b23d526b62"}, + {file = "pyarrow-20.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6bb830757103a6cb300a04610e08d9636f0cd223d32f388418ea893a3e655f1c"}, + {file = "pyarrow-20.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96e37f0766ecb4514a899d9a3554fadda770fb57ddf42b63d80f14bc20aa7db3"}, + {file = "pyarrow-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3346babb516f4b6fd790da99b98bed9708e3f02e734c84971faccb20736848dc"}, + {file = "pyarrow-20.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:75a51a5b0eef32727a247707d4755322cb970be7e935172b6a3a9f9ae98404ba"}, + {file = "pyarrow-20.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:211d5e84cecc640c7a3ab900f930aaff5cd2702177e0d562d426fb7c4f737781"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba3cf4182828be7a896cbd232aa8dd6a31bd1f9e32776cc3796c012855e1199"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c3a01f313ffe27ac4126f4c2e5ea0f36a5fc6ab51f8726cf41fee4b256680bd"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a2791f69ad72addd33510fec7bb14ee06c2a448e06b649e264c094c5b5f7ce28"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4250e28a22302ce8692d3a0e8ec9d9dde54ec00d237cff4dfa9c1fbf79e472a8"}, + {file = "pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e"}, + {file = "pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a"}, + {file = "pyarrow-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b"}, + {file = "pyarrow-20.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a15532e77b94c61efadde86d10957950392999503b3616b2ffcef7621a002893"}, + {file = "pyarrow-20.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dd43f58037443af715f34f1322c782ec463a3c8a94a85fdb2d987ceb5658e061"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0d288143a8585806e3cc7c39566407aab646fb9ece164609dac1cfff45f6ae"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6953f0114f8d6f3d905d98e987d0924dabce59c3cda380bdfaa25a6201563b4"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:991f85b48a8a5e839b2128590ce07611fae48a904cae6cab1f089c5955b57eb5"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:97c8dc984ed09cb07d618d57d8d4b67a5100a30c3818c2fb0b04599f0da2de7b"}, + {file = "pyarrow-20.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9b71daf534f4745818f96c214dbc1e6124d7daf059167330b610fc69b6f3d3e3"}, + {file = "pyarrow-20.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8b88758f9303fa5a83d6c90e176714b2fd3852e776fc2d7e42a22dd6c2fb368"}, + {file = "pyarrow-20.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:30b3051b7975801c1e1d387e17c588d8ab05ced9b1e14eec57915f79869b5031"}, + {file = "pyarrow-20.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ca151afa4f9b7bc45bcc791eb9a89e90a9eb2772767d0b1e5389609c7d03db63"}, + {file = "pyarrow-20.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:4680f01ecd86e0dd63e39eb5cd59ef9ff24a9d166db328679e36c108dc993d4c"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4c8534e2ff059765647aa69b75d6543f9fef59e2cd4c6d18015192565d2b70"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1f8a47f4b4ae4c69c4d702cfbdfe4d41e18e5c7ef6f1bb1c50918c1e81c57b"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:a1f60dc14658efaa927f8214734f6a01a806d7690be4b3232ba526836d216122"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:204a846dca751428991346976b914d6d2a82ae5b8316a6ed99789ebf976551e6"}, + {file = "pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f3b117b922af5e4c6b9a9115825726cac7d8b1421c37c2b5e24fbacc8930612c"}, + {file = "pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e724a3fd23ae5b9c010e7be857f4405ed5e679db5c93e66204db1a69f733936a"}, + {file = "pyarrow-20.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:82f1ee5133bd8f49d31be1299dc07f585136679666b502540db854968576faf9"}, + {file = "pyarrow-20.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:1bcbe471ef3349be7714261dea28fe280db574f9d0f77eeccc195a2d161fd861"}, + {file = "pyarrow-20.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:a18a14baef7d7ae49247e75641fd8bcbb39f44ed49a9fc4ec2f65d5031aa3b96"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb497649e505dc36542d0e68eca1a3c94ecbe9799cb67b578b55f2441a247fbc"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11529a2283cb1f6271d7c23e4a8f9f8b7fd173f7360776b668e509d712a02eec"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fc1499ed3b4b57ee4e090e1cea6eb3584793fe3d1b4297bbf53f09b434991a5"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:db53390eaf8a4dab4dbd6d93c85c5cf002db24902dbff0ca7d988beb5c9dd15b"}, + {file = "pyarrow-20.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:851c6a8260ad387caf82d2bbf54759130534723e37083111d4ed481cb253cc0d"}, + {file = "pyarrow-20.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e22f80b97a271f0a7d9cd07394a7d348f80d3ac63ed7cc38b6d1b696ab3b2619"}, + {file = "pyarrow-20.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:9965a050048ab02409fb7cbbefeedba04d3d67f2cc899eff505cc084345959ca"}, + {file = "pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1"}, +] + +[package.extras] +test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] + [[package]] name = "pycountry" version = "24.6.1" @@ -4013,24 +4412,25 @@ files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -markers = {main = "(extra == \"examples\" or extra == \"all\") and platform_python_implementation != \"PyPy\"", dev = "implementation_name == \"pypy\""} +markers = {main = "(extra == \"examples\" or extra == \"all\") and platform_python_implementation != \"PyPy\" or extra == \"all\"", dev = "implementation_name == \"pypy\""} [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.4" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, + {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" +pydantic-core = "2.33.2" typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -4038,112 +4438,111 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [package.dependencies] @@ -4151,35 +4550,38 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.9.1" description = "Settings management using Pydantic" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"}, - {file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"}, + {file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"}, + {file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"}, ] [package.dependencies] pydantic = ">=2.7.0" python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" [package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pydantic-xml" -version = "2.14.3" +version = "2.16.0" description = "pydantic xml extension" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "pydantic_xml-2.14.3-py3-none-any.whl", hash = "sha256:4a0017f88bd7304695d9faa69b3127e67ea49064692113249ee9dd69ab8bd5aa"}, - {file = "pydantic_xml-2.14.3.tar.gz", hash = "sha256:6aa4bb20905d9e5809fd195a048be76788a73dcd611aef50cf364cdb24d3141e"}, + {file = "pydantic_xml-2.16.0-py3-none-any.whl", hash = "sha256:e1ecd513287e30070ce0a9f8c0e461187ebf5b18da79ca62f5dd4219fb93b68e"}, + {file = "pydantic_xml-2.16.0.tar.gz", hash = "sha256:64ae5d8538a23706471f0b2007c9252ef290dff40c216dbc3051c79030aaf03f"}, ] [package.dependencies] @@ -4207,14 +4609,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.14.3" +version = "10.15" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, - {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, + {file = "pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f"}, + {file = "pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7"}, ] [package.dependencies] @@ -4283,29 +4685,41 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, ] [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + [[package]] name = "pytz" -version = "2025.1" +version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] [[package]] @@ -4415,146 +4829,149 @@ pyyaml = "*" [[package]] name = "pyzmq" -version = "26.3.0" +version = "26.4.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ - {file = "pyzmq-26.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:1586944f4736515af5c6d3a5b150c7e8ca2a2d6e46b23057320584d6f2438f4a"}, - {file = "pyzmq-26.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa7efc695d1fc9f72d91bf9b6c6fe2d7e1b4193836ec530a98faf7d7a7577a58"}, - {file = "pyzmq-26.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd84441e4021cec6e4dd040550386cd9c9ea1d9418ea1a8002dbb7b576026b2b"}, - {file = "pyzmq-26.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9176856f36c34a8aa5c0b35ddf52a5d5cd8abeece57c2cd904cfddae3fd9acd3"}, - {file = "pyzmq-26.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:49334faa749d55b77f084389a80654bf2e68ab5191c0235066f0140c1b670d64"}, - {file = "pyzmq-26.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fd30fc80fe96efb06bea21667c5793bbd65c0dc793187feb39b8f96990680b00"}, - {file = "pyzmq-26.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b2eddfbbfb473a62c3a251bb737a6d58d91907f6e1d95791431ebe556f47d916"}, - {file = "pyzmq-26.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:70b3acb9ad729a53d4e751dace35404a024f188aad406013454216aba5485b4e"}, - {file = "pyzmq-26.3.0-cp310-cp310-win32.whl", hash = "sha256:c1bd75d692cd7c6d862a98013bfdf06702783b75cffbf5dae06d718fecefe8f2"}, - {file = "pyzmq-26.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d7165bcda0dbf203e5ad04d79955d223d84b2263df4db92f525ba370b03a12ab"}, - {file = "pyzmq-26.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:e34a63f71d2ecffb3c643909ad2d488251afeb5ef3635602b3448e609611a7ed"}, - {file = "pyzmq-26.3.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:2833602d9d42c94b9d0d2a44d2b382d3d3a4485be018ba19dddc401a464c617a"}, - {file = "pyzmq-26.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8270d104ec7caa0bdac246d31d48d94472033ceab5ba142881704350b28159c"}, - {file = "pyzmq-26.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c208a977843d18d3bd185f323e4eaa912eb4869cb230947dc6edd8a27a4e558a"}, - {file = "pyzmq-26.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eddc2be28a379c218e0d92e4a432805dcb0ca5870156a90b54c03cd9799f9f8a"}, - {file = "pyzmq-26.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c0b519fa2159c42272f8a244354a0e110d65175647e5185b04008ec00df9f079"}, - {file = "pyzmq-26.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1595533de3a80bf8363372c20bafa963ec4bf9f2b8f539b1d9a5017f430b84c9"}, - {file = "pyzmq-26.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bbef99eb8d18ba9a40f00e8836b8040cdcf0f2fa649684cf7a66339599919d21"}, - {file = "pyzmq-26.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:979486d444ca3c469cd1c7f6a619ce48ff08b3b595d451937db543754bfacb65"}, - {file = "pyzmq-26.3.0-cp311-cp311-win32.whl", hash = "sha256:4b127cfe10b4c56e4285b69fd4b38ea1d368099ea4273d8fb349163fce3cd598"}, - {file = "pyzmq-26.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:cf736cc1298ef15280d9fcf7a25c09b05af016656856dc6fe5626fd8912658dd"}, - {file = "pyzmq-26.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2dc46ec09f5d36f606ac8393303149e69d17121beee13c8dac25e2a2078e31c4"}, - {file = "pyzmq-26.3.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:c80653332c6136da7f4d4e143975e74ac0fa14f851f716d90583bc19e8945cea"}, - {file = "pyzmq-26.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e317ee1d4528a03506cb1c282cd9db73660a35b3564096de37de7350e7d87a7"}, - {file = "pyzmq-26.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:943a22ebb3daacb45f76a9bcca9a7b74e7d94608c0c0505da30af900b998ca8d"}, - {file = "pyzmq-26.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fc9e71490d989144981ea21ef4fdfaa7b6aa84aff9632d91c736441ce2f6b00"}, - {file = "pyzmq-26.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e281a8071a06888575a4eb523c4deeefdcd2f5fe4a2d47e02ac8bf3a5b49f695"}, - {file = "pyzmq-26.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:be77efd735bb1064605be8dec6e721141c1421ef0b115ef54e493a64e50e9a52"}, - {file = "pyzmq-26.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a4ac2ffa34f1212dd586af90f4ba894e424f0cabb3a49cdcff944925640f6ac"}, - {file = "pyzmq-26.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ba698c7c252af83b6bba9775035263f0df5f807f0404019916d4b71af8161f66"}, - {file = "pyzmq-26.3.0-cp312-cp312-win32.whl", hash = "sha256:214038aaa88e801e54c2ef0cfdb2e6df27eb05f67b477380a452b595c5ecfa37"}, - {file = "pyzmq-26.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:bad7fe0372e505442482ca3ccbc0d6f38dae81b1650f57a0aa6bbee18e7df495"}, - {file = "pyzmq-26.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:b7b578d604e79e99aa39495becea013fd043fa9f36e4b490efa951f3d847a24d"}, - {file = "pyzmq-26.3.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:fa85953df84beb7b8b73cb3ec3f5d92b62687a09a8e71525c6734e020edf56fd"}, - {file = "pyzmq-26.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:209d09f0ab6ddbcebe64630d1e6ca940687e736f443c265ae15bc4bfad833597"}, - {file = "pyzmq-26.3.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d35cc1086f1d4f907df85c6cceb2245cb39a04f69c3f375993363216134d76d4"}, - {file = "pyzmq-26.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b380e9087078ba91e45fb18cdd0c25275ffaa045cf63c947be0ddae6186bc9d9"}, - {file = "pyzmq-26.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6d64e74143587efe7c9522bb74d1448128fdf9897cc9b6d8b9927490922fd558"}, - {file = "pyzmq-26.3.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:efba4f53ac7752eea6d8ca38a4ddac579e6e742fba78d1e99c12c95cd2acfc64"}, - {file = "pyzmq-26.3.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:9b0137a1c40da3b7989839f9b78a44de642cdd1ce20dcef341de174c8d04aa53"}, - {file = "pyzmq-26.3.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a995404bd3982c089e57b428c74edd5bfc3b0616b3dbcd6a8e270f1ee2110f36"}, - {file = "pyzmq-26.3.0-cp313-cp313-win32.whl", hash = "sha256:240b1634b9e530ef6a277d95cbca1a6922f44dfddc5f0a3cd6c722a8de867f14"}, - {file = "pyzmq-26.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:fe67291775ea4c2883764ba467eb389c29c308c56b86c1e19e49c9e1ed0cbeca"}, - {file = "pyzmq-26.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:73ca9ae9a9011b714cf7650450cd9c8b61a135180b708904f1f0a05004543dce"}, - {file = "pyzmq-26.3.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:fea7efbd7e49af9d7e5ed6c506dfc7de3d1a628790bd3a35fd0e3c904dc7d464"}, - {file = "pyzmq-26.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4430c7cba23bb0e2ee203eee7851c1654167d956fc6d4b3a87909ccaf3c5825"}, - {file = "pyzmq-26.3.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:016d89bee8c7d566fad75516b4e53ec7c81018c062d4c51cd061badf9539be52"}, - {file = "pyzmq-26.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04bfe59852d76d56736bfd10ac1d49d421ab8ed11030b4a0332900691507f557"}, - {file = "pyzmq-26.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:1fe05bd0d633a0f672bb28cb8b4743358d196792e1caf04973b7898a0d70b046"}, - {file = "pyzmq-26.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:2aa1a9f236d5b835fb8642f27de95f9edcfd276c4bc1b6ffc84f27c6fb2e2981"}, - {file = "pyzmq-26.3.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:21399b31753bf321043ea60c360ed5052cc7be20739785b1dff1820f819e35b3"}, - {file = "pyzmq-26.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d015efcd96aca8882057e7e6f06224f79eecd22cad193d3e6a0a91ec67590d1f"}, - {file = "pyzmq-26.3.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18183cc3851b995fdc7e5f03d03b8a4e1b12b0f79dff1ec1da75069af6357a05"}, - {file = "pyzmq-26.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:da87e977f92d930a3683e10ba2b38bcc59adfc25896827e0b9d78b208b7757a6"}, - {file = "pyzmq-26.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf6db401f4957afbf372a4730c6d5b2a234393af723983cbf4bcd13d54c71e1a"}, - {file = "pyzmq-26.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03caa2ffd64252122139d50ec92987f89616b9b92c9ba72920b40e92709d5e26"}, - {file = "pyzmq-26.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fbf206e5329e20937fa19bd41cf3af06d5967f8f7e86b59d783b26b40ced755c"}, - {file = "pyzmq-26.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6fb539a6382a048308b409d8c66d79bf636eda1b24f70c78f2a1fd16e92b037b"}, - {file = "pyzmq-26.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7897b8c8bbbb2bd8cad887bffcb07aede71ef1e45383bd4d6ac049bf0af312a4"}, - {file = "pyzmq-26.3.0-cp38-cp38-win32.whl", hash = "sha256:91dead2daca698ae52ce70ee2adbb94ddd9b5f96877565fd40aa4efd18ecc6a3"}, - {file = "pyzmq-26.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:8c088e009a6d6b9f563336adb906e3a8d3fd64db129acc8d8fd0e9fe22b2dac8"}, - {file = "pyzmq-26.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2eaed0d911fb3280981d5495978152fab6afd9fe217fd16f411523665089cef1"}, - {file = "pyzmq-26.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7998b60ef1c105846fb3bfca494769fde3bba6160902e7cd27a8df8257890ee9"}, - {file = "pyzmq-26.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:96c0006a8d1d00e46cb44c8e8d7316d4a232f3d8f2ed43179d4578dbcb0829b6"}, - {file = "pyzmq-26.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e17cc198dc50a25a0f245e6b1e56f692df2acec3ccae82d1f60c34bfb72bbec"}, - {file = "pyzmq-26.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:92a30840f4f2a31f7049d0a7de5fc69dd03b19bd5d8e7fed8d0bde49ce49b589"}, - {file = "pyzmq-26.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f52eba83272a26b444f4b8fc79f2e2c83f91d706d693836c9f7ccb16e6713c31"}, - {file = "pyzmq-26.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:952085a09ff32115794629ba47f8940896d7842afdef1283332109d38222479d"}, - {file = "pyzmq-26.3.0-cp39-cp39-win32.whl", hash = "sha256:0240289e33e3fbae44a5db73e54e955399179332a6b1d47c764a4983ec1524c3"}, - {file = "pyzmq-26.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b2db7c82f08b8ce44c0b9d1153ce63907491972a7581e8b6adea71817f119df8"}, - {file = "pyzmq-26.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:2d3459b6311463c96abcb97808ee0a1abb0d932833edb6aa81c30d622fd4a12d"}, - {file = "pyzmq-26.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad03f4252d9041b0635c37528dfa3f44b39f46024ae28c8567f7423676ee409b"}, - {file = "pyzmq-26.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3dfb68cf7bf4cfdf34283a75848e077c5defa4907506327282afe92780084d"}, - {file = "pyzmq-26.3.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:356ec0e39c5a9cda872b65aca1fd8a5d296ffdadf8e2442b70ff32e73ef597b1"}, - {file = "pyzmq-26.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:749d671b0eec8e738bbf0b361168369d8c682b94fcd458c20741dc4d69ef5278"}, - {file = "pyzmq-26.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f950f17ae608e0786298340163cac25a4c5543ef25362dd5ddb6dcb10b547be9"}, - {file = "pyzmq-26.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4fc9903a73c25be9d5fe45c87faababcf3879445efa16140146b08fccfac017"}, - {file = "pyzmq-26.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c15b69af22030960ac63567e98ad8221cddf5d720d9cf03d85021dfd452324ef"}, - {file = "pyzmq-26.3.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2cf9ab0dff4dbaa2e893eb608373c97eb908e53b7d9793ad00ccbd082c0ee12f"}, - {file = "pyzmq-26.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ec332675f6a138db57aad93ae6387953763f85419bdbd18e914cb279ee1c451"}, - {file = "pyzmq-26.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:eb96568a22fe070590942cd4780950e2172e00fb033a8b76e47692583b1bd97c"}, - {file = "pyzmq-26.3.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:009a38241c76184cb004c869e82a99f0aee32eda412c1eb44df5820324a01d25"}, - {file = "pyzmq-26.3.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c22a12713707467abedc6d75529dd365180c4c2a1511268972c6e1d472bd63e"}, - {file = "pyzmq-26.3.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1614fcd116275d24f2346ffca4047a741c546ad9d561cbf7813f11226ca4ed2c"}, - {file = "pyzmq-26.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e2cafe7e9c7fed690e8ecf65af119f9c482923b5075a78f6f7629c63e1b4b1d"}, - {file = "pyzmq-26.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:14e0b81753424bd374075df6cc30b87f2c99e5f022501d97eff66544ca578941"}, - {file = "pyzmq-26.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:21c6ddb98557a77cfe3366af0c5600fb222a1b2de5f90d9cd052b324e0c295e8"}, - {file = "pyzmq-26.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc81d5d60c9d40e692de14b8d884d43cf67562402b931681f0ccb3ce6b19875"}, - {file = "pyzmq-26.3.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52b064fafef772d0f5dbf52d4c39f092be7bc62d9a602fe6e82082e001326de3"}, - {file = "pyzmq-26.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b72206eb041f780451c61e1e89dbc3705f3d66aaaa14ee320d4f55864b13358a"}, - {file = "pyzmq-26.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab78dc21c7b1e13053086bcf0b4246440b43b5409904b73bfd1156654ece8a1"}, - {file = "pyzmq-26.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0b42403ad7d1194dca9574cd3c56691c345f4601fa2d0a33434f35142baec7ac"}, - {file = "pyzmq-26.3.0.tar.gz", hash = "sha256:f1cd68b8236faab78138a8fc703f7ca0ad431b17a3fcac696358600d4e6243b3"}, + {file = "pyzmq-26.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918"}, + {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315"}, + {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d52d62edc96787f5c1dfa6c6ccff9b581cfae5a70d94ec4c8da157656c73b5b"}, + {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1410c3a3705db68d11eb2424d75894d41cff2f64d948ffe245dd97a9debfebf4"}, + {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7dacb06a9c83b007cc01e8e5277f94c95c453c5851aac5e83efe93e72226353f"}, + {file = "pyzmq-26.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6bab961c8c9b3a4dc94d26e9b2cdf84de9918931d01d6ff38c721a83ab3c0ef5"}, + {file = "pyzmq-26.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a5c09413b924d96af2aa8b57e76b9b0058284d60e2fc3730ce0f979031d162a"}, + {file = "pyzmq-26.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d489ac234d38e57f458fdbd12a996bfe990ac028feaf6f3c1e81ff766513d3b"}, + {file = "pyzmq-26.4.0-cp310-cp310-win32.whl", hash = "sha256:dea1c8db78fb1b4b7dc9f8e213d0af3fc8ecd2c51a1d5a3ca1cde1bda034a980"}, + {file = "pyzmq-26.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa59e1f5a224b5e04dc6c101d7186058efa68288c2d714aa12d27603ae93318b"}, + {file = "pyzmq-26.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:a651fe2f447672f4a815e22e74630b6b1ec3a1ab670c95e5e5e28dcd4e69bbb5"}, + {file = "pyzmq-26.4.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:bfcf82644c9b45ddd7cd2a041f3ff8dce4a0904429b74d73a439e8cab1bd9e54"}, + {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9bcae3979b2654d5289d3490742378b2f3ce804b0b5fd42036074e2bf35b030"}, + {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccdff8ac4246b6fb60dcf3982dfaeeff5dd04f36051fe0632748fc0aa0679c01"}, + {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4550af385b442dc2d55ab7717837812799d3674cb12f9a3aa897611839c18e9e"}, + {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f7ffe9db1187a253fca95191854b3fda24696f086e8789d1d449308a34b88"}, + {file = "pyzmq-26.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3709c9ff7ba61589b7372923fd82b99a81932b592a5c7f1a24147c91da9a68d6"}, + {file = "pyzmq-26.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f8f3c30fb2d26ae5ce36b59768ba60fb72507ea9efc72f8f69fa088450cff1df"}, + {file = "pyzmq-26.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:382a4a48c8080e273427fc692037e3f7d2851959ffe40864f2db32646eeb3cef"}, + {file = "pyzmq-26.4.0-cp311-cp311-win32.whl", hash = "sha256:d56aad0517d4c09e3b4f15adebba8f6372c5102c27742a5bdbfc74a7dceb8fca"}, + {file = "pyzmq-26.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:963977ac8baed7058c1e126014f3fe58b3773f45c78cce7af5c26c09b6823896"}, + {file = "pyzmq-26.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0c8e8cadc81e44cc5088fcd53b9b3b4ce9344815f6c4a03aec653509296fae3"}, + {file = "pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b"}, + {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905"}, + {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b"}, + {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63"}, + {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5"}, + {file = "pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b"}, + {file = "pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84"}, + {file = "pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f"}, + {file = "pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44"}, + {file = "pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be"}, + {file = "pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0"}, + {file = "pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3"}, + {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43"}, + {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6"}, + {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e"}, + {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771"}, + {file = "pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30"}, + {file = "pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86"}, + {file = "pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101"}, + {file = "pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637"}, + {file = "pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b"}, + {file = "pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08"}, + {file = "pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4"}, + {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a"}, + {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b"}, + {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d"}, + {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf"}, + {file = "pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c"}, + {file = "pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8"}, + {file = "pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364"}, + {file = "pyzmq-26.4.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:831cc53bf6068d46d942af52fa8b0b9d128fb39bcf1f80d468dc9a3ae1da5bfb"}, + {file = "pyzmq-26.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:51d18be6193c25bd229524cfac21e39887c8d5e0217b1857998dfbef57c070a4"}, + {file = "pyzmq-26.4.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:445c97854204119ae2232503585ebb4fa7517142f71092cb129e5ee547957a1f"}, + {file = "pyzmq-26.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:807b8f4ad3e6084412c0f3df0613269f552110fa6fb91743e3e306223dbf11a6"}, + {file = "pyzmq-26.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c01d109dd675ac47fa15c0a79d256878d898f90bc10589f808b62d021d2e653c"}, + {file = "pyzmq-26.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0a294026e28679a8dd64c922e59411cb586dad307661b4d8a5c49e7bbca37621"}, + {file = "pyzmq-26.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:22c8dd677274af8dfb1efd05006d6f68fb2f054b17066e308ae20cb3f61028cf"}, + {file = "pyzmq-26.4.0-cp38-cp38-win32.whl", hash = "sha256:14fc678b696bc42c14e2d7f86ac4e97889d5e6b94d366ebcb637a768d2ad01af"}, + {file = "pyzmq-26.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1ef0a536662bbbdc8525f7e2ef19e74123ec9c4578e0582ecd41aedc414a169"}, + {file = "pyzmq-26.4.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:a88643de8abd000ce99ca72056a1a2ae15881ee365ecb24dd1d9111e43d57842"}, + {file = "pyzmq-26.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a744ce209ecb557406fb928f3c8c55ce79b16c3eeb682da38ef5059a9af0848"}, + {file = "pyzmq-26.4.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9434540f333332224ecb02ee6278b6c6f11ea1266b48526e73c903119b2f420f"}, + {file = "pyzmq-26.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6c6f0a23e55cd38d27d4c89add963294ea091ebcb104d7fdab0f093bc5abb1c"}, + {file = "pyzmq-26.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6145df55dc2309f6ef72d70576dcd5aabb0fd373311613fe85a5e547c722b780"}, + {file = "pyzmq-26.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2ea81823840ef8c56e5d2f9918e4d571236294fea4d1842b302aebffb9e40997"}, + {file = "pyzmq-26.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc2abc385dc37835445abe206524fbc0c9e3fce87631dfaa90918a1ba8f425eb"}, + {file = "pyzmq-26.4.0-cp39-cp39-win32.whl", hash = "sha256:41a2508fe7bed4c76b4cf55aacfb8733926f59d440d9ae2b81ee8220633b4d12"}, + {file = "pyzmq-26.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4000e8255d6cbce38982e5622ebb90823f3409b7ffe8aeae4337ef7d6d2612a"}, + {file = "pyzmq-26.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f6919d9c120488246bdc2a2f96662fa80d67b35bd6d66218f457e722b3ff64"}, + {file = "pyzmq-26.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:98d948288ce893a2edc5ec3c438fe8de2daa5bbbd6e2e865ec5f966e237084ba"}, + {file = "pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f34f5c9e0203ece706a1003f1492a56c06c0632d86cb77bcfe77b56aacf27b"}, + {file = "pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80c9b48aef586ff8b698359ce22f9508937c799cc1d2c9c2f7c95996f2300c94"}, + {file = "pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f2a5b74009fd50b53b26f65daff23e9853e79aa86e0aa08a53a7628d92d44a"}, + {file = "pyzmq-26.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:61c5f93d7622d84cb3092d7f6398ffc77654c346545313a3737e266fc11a3beb"}, + {file = "pyzmq-26.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4478b14cb54a805088299c25a79f27eaf530564a7a4f72bf432a040042b554eb"}, + {file = "pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a28ac29c60e4ba84b5f58605ace8ad495414a724fe7aceb7cf06cd0598d04e1"}, + {file = "pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b03c1ceea27c6520124f4fb2ba9c647409b9abdf9a62388117148a90419494"}, + {file = "pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7731abd23a782851426d4e37deb2057bf9410848a4459b5ede4fe89342e687a9"}, + {file = "pyzmq-26.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0"}, + {file = "pyzmq-26.4.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:91c3ffaea475ec8bb1a32d77ebc441dcdd13cd3c4c284a6672b92a0f5ade1917"}, + {file = "pyzmq-26.4.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d9a78a52668bf5c9e7b0da36aa5760a9fc3680144e1445d68e98df78a25082ed"}, + {file = "pyzmq-26.4.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b70cab356ff8c860118b89dc86cd910c73ce2127eb986dada4fbac399ef644cf"}, + {file = "pyzmq-26.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acae207d4387780838192326b32d373bb286da0b299e733860e96f80728eb0af"}, + {file = "pyzmq-26.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f928eafd15794aa4be75463d537348b35503c1e014c5b663f206504ec1a90fe4"}, + {file = "pyzmq-26.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:552b0d2e39987733e1e9e948a0ced6ff75e0ea39ab1a1db2fc36eb60fd8760db"}, + {file = "pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd670a8aa843f2ee637039bbd412e0d7294a5e588e1ecc9ad98b0cdc050259a4"}, + {file = "pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d367b7b775a0e1e54a59a2ba3ed4d5e0a31566af97cc9154e34262777dab95ed"}, + {file = "pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112af16c406e4a93df2caef49f884f4c2bb2b558b0b5577ef0b2465d15c1abc"}, + {file = "pyzmq-26.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76c298683f82669cab0b6da59071f55238c039738297c69f187a542c6d40099"}, + {file = "pyzmq-26.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:49b6ca2e625b46f499fb081aaf7819a177f41eeb555acb05758aa97f4f95d147"}, + {file = "pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d"}, ] +markers = {main = "extra == \"all\""} [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "ray" -version = "2.43.0" +version = "2.46.0" description = "Ray provides a simple, universal API for building distributed applications." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "ray-2.43.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:18626fff368451a37a76b33d52a70f05b20a211eb0867d860721c8e86cb6955a"}, - {file = "ray-2.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7c4fdec59a14d6b2939d91fee6efc84b614a6722c3be0b27fa371e3f563255f"}, - {file = "ray-2.43.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:57381c54f200e6c0203d5f70ac6f882b13cc1a80faf336518787a39a6d6f65d0"}, - {file = "ray-2.43.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:1872983a285a85b776bf311c809d559f8482909a2d39ead5f2ac69cfe3aa8544"}, - {file = "ray-2.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:f61b9a644197f7049cb8688e2ea9db49d486f0cbef433f7a7b7349bfbf8dec19"}, - {file = "ray-2.43.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:fde8a81280f07af983bc3769c9941db5db273ce10e92abb3348e41bed023d735"}, - {file = "ray-2.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e93c32ad0cb67f1f7da76fac409d87d5cd5ea3eb03b836830e9ef5cc810bc2c0"}, - {file = "ray-2.43.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:476ec3e1fa2464ddd5f049c0f2758ff9dfecc21fb8df4266f1df01b2780c6653"}, - {file = "ray-2.43.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:78c3bdbf182b4d019fa9a8aabd55c39bf705bb630aea064f768f305fc472d1eb"}, - {file = "ray-2.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2b0fa0272ade67bad2e83d7de996795bfb4f10f4b895476b95fbfda6d3c3ed6"}, - {file = "ray-2.43.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:5121fdf4bcbcb0fda3b9b71164dd6c8fcc79a2e258022a2a3957e401018913fb"}, - {file = "ray-2.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f26f7b72da04c3c4422269c31b067abd15cb38424b7012d812ddfb2c77462ea"}, - {file = "ray-2.43.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:73770d4c8a989730985ff2b4292129249e28c1e29e84589470c9ba1ae91ca832"}, - {file = "ray-2.43.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:b45f478d29ce5df3fc19861df64fef9ed5c25f1e83fa10028d33fadefdeca095"}, - {file = "ray-2.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c3a9880112a8d561280a34e8ef9471070f81ca467e08b669e5e77a85e173c9c"}, - {file = "ray-2.43.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d0e26f9db91a5b3343f0858eb256255b35c7e97fc6bf97065f5744ad7e8cc297"}, - {file = "ray-2.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8cfdec2bf61c48690f6cb4325f1673e0d1d00dea925b59b631196e67f426a0e"}, - {file = "ray-2.43.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:87e0cac85189dc519af1b6ef2abd654804c488c932caadd658b90caaa65f8dae"}, - {file = "ray-2.43.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:5edf3da18041e0b6170c62e08480ab050f0766361ff3f6e11257cc04ab46e592"}, - {file = "ray-2.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:889964d09ae048f5bf56a54d2477779f44d780b35dfadaf927da20e4703a6f67"}, + {file = "ray-2.46.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:719244b84df79502e5f09497f256618d94d78d66fbaf229422008a0568d3a0ff"}, + {file = "ray-2.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4378a86919e6643238a1094f711b87fa8dc1a18b998d4190f69ab33c64a22a8c"}, + {file = "ray-2.46.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:396b912a4dbf64966e2fdfca9facbcafe57b792ca4842ac5ae17507fdbdfe89f"}, + {file = "ray-2.46.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c12850608c57c8afd9613a9f757d77663c50d4bd4e77ba2f181425052520c01a"}, + {file = "ray-2.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc953aa4879c7a77893f921905df5cf65227cafd94fbc8273bec65ea393eacdd"}, + {file = "ray-2.46.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:942ba51de6f9cd7fb2ed17618181af48ce6b9517743d3235d846ec32295eca76"}, + {file = "ray-2.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af84f3ed0854bb6de28192ca9e0a3bfa1eb34d69f118ae6348522198896480c8"}, + {file = "ray-2.46.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:81c8ce8b7ba33cb607ec78f5eb2555470e3046bb317732d8282e8189bb58ccbd"}, + {file = "ray-2.46.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:d4ddedc3f4d48df564bcee7b131c98c9f898fef0a57483f4ba335f47f951a62f"}, + {file = "ray-2.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:130415c4d231830156f37ce70acbdb5fdee10f6886adc4e85bdc4533d51c24c6"}, + {file = "ray-2.46.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:d1f37ead29299637144726f809c2e0ff958dd9c0e75930ef614156d6a0a3a57f"}, + {file = "ray-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7a064acfeee7f0677d9e3f25daef9c59593559faea764b44a3e2c5331d5d832"}, + {file = "ray-2.46.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:006cbe1a8fdc37664114aa218773100ee891399785e256c202e48958d2dac167"}, + {file = "ray-2.46.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:5cec1edda93f618ffd2301f81d5398037f03fa9b16825e7e4d8a00ae7a9a4381"}, + {file = "ray-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d3160f8d187baaea91a86d16a9fd81136cf8607419c94b7a74d66fce774b5c2"}, + {file = "ray-2.46.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b2fc2c43ea0a37521193c61ef9a27b6fca8dbab116a58a52fd44344cd73e1ece"}, + {file = "ray-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4296dd8c0174256a04ee4b54abe013b6802a45fb85fb7cfdb1375231965d6d4d"}, + {file = "ray-2.46.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:808daece1f12bd8924b9c6382a0f98da6f5c6886cfb271ed8d89407a89413cd5"}, + {file = "ray-2.46.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:a5a28c0a311d2c3221dcf729c40898a6df82466bb5af21e81be0453e09856adf"}, + {file = "ray-2.46.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:e0ec198c16d0e9af7f03242ef7ad7d548eee37a918193917278a124ddd57410a"}, + {file = "ray-2.46.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e31568818973efa4f8ce18b82bce03089395a62ac9fe639e94d755959f607fe9"}, + {file = "ray-2.46.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7c44a98cb24f4905e898d05b787cbe9f267a9f66c1e1f8cda50814f8b3673be2"}, + {file = "ray-2.46.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:91ea998a49578b1450cbef60705f6ece8622a262a3d764d5c99ba89b741de5d0"}, + {file = "ray-2.46.0-cp39-cp39-win_amd64.whl", hash = "sha256:018e98c9745eae53b53ad14fef1ca1c43bb64c39c3cceb9e6d4517729396003b"}, ] [package.dependencies] -aiosignal = "*" click = ">=7.0" filelock = "*" -frozenlist = "*" jsonschema = "*" msgpack = ">=1.0.0,<2.0.0" packaging = "*" @@ -4566,13 +4983,13 @@ requests = "*" adag = ["cupy-cuda12x ; sys_platform != \"darwin\""] air = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "fsspec", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "numpy (>=1.20)", "opencensus", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] all = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "cupy-cuda12x ; sys_platform != \"darwin\"", "dm-tree", "fastapi", "fsspec", "grpcio", "grpcio (!=1.56.0) ; sys_platform == \"darwin\"", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "gymnasium (==1.0.0)", "lz4", "memray ; sys_platform != \"win32\"", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk", "ormsgpack (==1.7.0)", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyOpenSSL", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "pyyaml", "requests", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] -all-cpp = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "cupy-cuda12x ; sys_platform != \"darwin\"", "dm-tree", "fastapi", "fsspec", "grpcio", "grpcio (!=1.56.0) ; sys_platform == \"darwin\"", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "gymnasium (==1.0.0)", "lz4", "memray ; sys_platform != \"win32\"", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk", "ormsgpack (==1.7.0)", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyOpenSSL", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "pyyaml", "ray-cpp (==2.43.0)", "requests", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] +all-cpp = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "cupy-cuda12x ; sys_platform != \"darwin\"", "dm-tree", "fastapi", "fsspec", "grpcio", "grpcio (!=1.56.0) ; sys_platform == \"darwin\"", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "gymnasium (==1.0.0)", "lz4", "memray ; sys_platform != \"win32\"", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk", "ormsgpack (==1.7.0)", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyOpenSSL", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "pyyaml", "ray-cpp (==2.46.0)", "requests", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] cgraph = ["cupy-cuda12x ; sys_platform != \"darwin\""] client = ["grpcio", "grpcio (!=1.56.0) ; sys_platform == \"darwin\""] -cpp = ["ray-cpp (==2.43.0)"] +cpp = ["ray-cpp (==2.46.0)"] data = ["fsspec", "numpy (>=1.20)", "pandas (>=1.3)", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)"] default = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "opencensus", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "virtualenv (>=20.0.24,!=20.21.1)"] -llm = ["aiohttp (>=3.7)", "aiohttp-cors", "async-timeout", "asyncache (>=0.3.1)", "boto3", "colorful", "fastapi", "fsspec", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "jsonref (>=1.1.0)", "numpy (>=1.20)", "opencensus", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "vllm (>=0.7.2)", "watchfiles"] +llm = ["aiohttp (>=3.7)", "aiohttp-cors", "async-timeout ; python_version < \"3.11\"", "colorful", "fastapi", "fsspec", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "jsonref (>=1.1.0)", "jsonschema", "ninja", "numpy (>=1.20)", "opencensus", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "typer", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "vllm (>=0.8.5)", "watchfiles"] observability = ["memray ; sys_platform != \"win32\"", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk"] rllib = ["dm-tree", "fsspec", "gymnasium (==1.0.0)", "lz4", "ormsgpack (==1.7.0)", "pandas", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pyyaml", "requests", "scipy", "tensorboardX (>=1.9)"] serve = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "opencensus", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] @@ -4725,115 +5142,126 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rpds-py" -version = "0.23.1" +version = "0.24.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "rpds_py-0.23.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2a54027554ce9b129fc3d633c92fa33b30de9f08bc61b32c053dc9b537266fed"}, - {file = "rpds_py-0.23.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5ef909a37e9738d146519657a1aab4584018746a18f71c692f2f22168ece40c"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ee9d6f0b38efb22ad94c3b68ffebe4c47865cdf4b17f6806d6c674e1feb4246"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7356a6da0562190558c4fcc14f0281db191cdf4cb96e7604c06acfcee96df15"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9441af1d25aed96901f97ad83d5c3e35e6cd21a25ca5e4916c82d7dd0490a4fa"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d8abf7896a91fb97e7977d1aadfcc2c80415d6dc2f1d0fca5b8d0df247248f3"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b08027489ba8fedde72ddd233a5ea411b85a6ed78175f40285bd401bde7466d"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fee513135b5a58f3bb6d89e48326cd5aa308e4bcdf2f7d59f67c861ada482bf8"}, - {file = "rpds_py-0.23.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:35d5631ce0af26318dba0ae0ac941c534453e42f569011585cb323b7774502a5"}, - {file = "rpds_py-0.23.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a20cb698c4a59c534c6701b1c24a968ff2768b18ea2991f886bd8985ce17a89f"}, - {file = "rpds_py-0.23.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e9c206a1abc27e0588cf8b7c8246e51f1a16a103734f7750830a1ccb63f557a"}, - {file = "rpds_py-0.23.1-cp310-cp310-win32.whl", hash = "sha256:d9f75a06ecc68f159d5d7603b734e1ff6daa9497a929150f794013aa9f6e3f12"}, - {file = "rpds_py-0.23.1-cp310-cp310-win_amd64.whl", hash = "sha256:f35eff113ad430b5272bbfc18ba111c66ff525828f24898b4e146eb479a2cdda"}, - {file = "rpds_py-0.23.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b79f5ced71efd70414a9a80bbbfaa7160da307723166f09b69773153bf17c590"}, - {file = "rpds_py-0.23.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9e799dac1ffbe7b10c1fd42fe4cd51371a549c6e108249bde9cd1200e8f59b4"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721f9c4011b443b6e84505fc00cc7aadc9d1743f1c988e4c89353e19c4a968ee"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f88626e3f5e57432e6191cd0c5d6d6b319b635e70b40be2ffba713053e5147dd"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:285019078537949cecd0190f3690a0b0125ff743d6a53dfeb7a4e6787af154f5"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92f5654157de1379c509b15acec9d12ecf6e3bc1996571b6cb82a4302060447"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e768267cbe051dd8d1c5305ba690bb153204a09bf2e3de3ae530de955f5b5580"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5334a71f7dc1160382d45997e29f2637c02f8a26af41073189d79b95d3321f1"}, - {file = "rpds_py-0.23.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6adb81564af0cd428910f83fa7da46ce9ad47c56c0b22b50872bc4515d91966"}, - {file = "rpds_py-0.23.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cafa48f2133d4daa028473ede7d81cd1b9f9e6925e9e4003ebdf77010ee02f35"}, - {file = "rpds_py-0.23.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fced9fd4a07a1ded1bac7e961ddd9753dd5d8b755ba8e05acba54a21f5f1522"}, - {file = "rpds_py-0.23.1-cp311-cp311-win32.whl", hash = "sha256:243241c95174b5fb7204c04595852fe3943cc41f47aa14c3828bc18cd9d3b2d6"}, - {file = "rpds_py-0.23.1-cp311-cp311-win_amd64.whl", hash = "sha256:11dd60b2ffddba85715d8a66bb39b95ddbe389ad2cfcf42c833f1bcde0878eaf"}, - {file = "rpds_py-0.23.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c"}, - {file = "rpds_py-0.23.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35"}, - {file = "rpds_py-0.23.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b"}, - {file = "rpds_py-0.23.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef"}, - {file = "rpds_py-0.23.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad"}, - {file = "rpds_py-0.23.1-cp312-cp312-win32.whl", hash = "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057"}, - {file = "rpds_py-0.23.1-cp312-cp312-win_amd64.whl", hash = "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165"}, - {file = "rpds_py-0.23.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935"}, - {file = "rpds_py-0.23.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64"}, - {file = "rpds_py-0.23.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8"}, - {file = "rpds_py-0.23.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957"}, - {file = "rpds_py-0.23.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93"}, - {file = "rpds_py-0.23.1-cp313-cp313-win32.whl", hash = "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd"}, - {file = "rpds_py-0.23.1-cp313-cp313-win_amd64.whl", hash = "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70"}, - {file = "rpds_py-0.23.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731"}, - {file = "rpds_py-0.23.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e"}, - {file = "rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6"}, - {file = "rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b"}, - {file = "rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5"}, - {file = "rpds_py-0.23.1-cp313-cp313t-win32.whl", hash = "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7"}, - {file = "rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d"}, - {file = "rpds_py-0.23.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:09cd7dbcb673eb60518231e02874df66ec1296c01a4fcd733875755c02014b19"}, - {file = "rpds_py-0.23.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6760211eee3a76316cf328f5a8bd695b47b1626d21c8a27fb3b2473a884d597"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e680c1518733b73c994361e4b06441b92e973ef7d9449feec72e8ee4f713da"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae28144c1daa61366205d32abd8c90372790ff79fc60c1a8ad7fd3c8553a600e"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c698d123ce5d8f2d0cd17f73336615f6a2e3bdcedac07a1291bb4d8e7d82a05a"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98b257ae1e83f81fb947a363a274c4eb66640212516becaff7bef09a5dceacaa"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c9ff044eb07c8468594d12602291c635da292308c8c619244e30698e7fc455a"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7938c7b0599a05246d704b3f5e01be91a93b411d0d6cc62275f025293b8a11ce"}, - {file = "rpds_py-0.23.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e9cb79ecedfc156c0692257ac7ed415243b6c35dd969baa461a6888fc79f2f07"}, - {file = "rpds_py-0.23.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7b77e07233925bd33fc0022b8537774423e4c6680b6436316c5075e79b6384f4"}, - {file = "rpds_py-0.23.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a970bfaf130c29a679b1d0a6e0f867483cea455ab1535fb427566a475078f27f"}, - {file = "rpds_py-0.23.1-cp39-cp39-win32.whl", hash = "sha256:4233df01a250b3984465faed12ad472f035b7cd5240ea3f7c76b7a7016084495"}, - {file = "rpds_py-0.23.1-cp39-cp39-win_amd64.whl", hash = "sha256:c617d7453a80e29d9973b926983b1e700a9377dbe021faa36041c78537d7b08c"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c1f8afa346ccd59e4e5630d5abb67aba6a9812fddf764fd7eb11f382a345f8cc"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fad784a31869747df4ac968a351e070c06ca377549e4ace94775aaa3ab33ee06"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5a96fcac2f18e5a0a23a75cd27ce2656c66c11c127b0318e508aab436b77428"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e77febf227a1dc3220159355dba68faa13f8dca9335d97504abf428469fb18b"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26bb3e8de93443d55e2e748e9fd87deb5f8075ca7bc0502cfc8be8687d69a2ec"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db7707dde9143a67b8812c7e66aeb2d843fe33cc8e374170f4d2c50bd8f2472d"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eedaaccc9bb66581d4ae7c50e15856e335e57ef2734dbc5fd8ba3e2a4ab3cb6"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28358c54fffadf0ae893f6c1050e8f8853e45df22483b7fff2f6ab6152f5d8bf"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:633462ef7e61d839171bf206551d5ab42b30b71cac8f10a64a662536e057fdef"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a98f510d86f689fcb486dc59e6e363af04151e5260ad1bdddb5625c10f1e95f8"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e0397dd0b3955c61ef9b22838144aa4bef6f0796ba5cc8edfc64d468b93798b4"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:75307599f0d25bf6937248e5ac4e3bde5ea72ae6618623b86146ccc7845ed00b"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3614d280bf7aab0d3721b5ce0e73434acb90a2c993121b6e81a1c15c665298ac"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e5963ea87f88bddf7edd59644a35a0feecf75f8985430124c253612d4f7d27ae"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad76f44f70aac3a54ceb1813ca630c53415da3a24fd93c570b2dfb4856591017"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c6ae11e6e93728d86aafc51ced98b1658a0080a7dd9417d24bfb955bb09c3c2"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc869af5cba24d45fb0399b0cfdbcefcf6910bf4dee5d74036a57cf5264b3ff4"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76b32eb2ab650a29e423525e84eb197c45504b1c1e6e17b6cc91fcfeb1a4b1d"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4263320ed887ed843f85beba67f8b2d1483b5947f2dc73a8b068924558bfeace"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7f9682a8f71acdf59fd554b82b1c12f517118ee72c0f3944eda461606dfe7eb9"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:754fba3084b70162a6b91efceee8a3f06b19e43dac3f71841662053c0584209a"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:a1c66e71ecfd2a4acf0e4bd75e7a3605afa8f9b28a3b497e4ba962719df2be57"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8d67beb6002441faef8251c45e24994de32c4c8686f7356a1f601ad7c466f7c3"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a1e17d8dc8e57d8e0fd21f8f0f0a5211b3fa258b2e444c2053471ef93fe25a00"}, - {file = "rpds_py-0.23.1.tar.gz", hash = "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707"}, + {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, + {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, + {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, + {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, + {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, + {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, + {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, + {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, + {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, + {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45"}, + {file = "rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103"}, + {file = "rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f"}, + {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, ] [[package]] @@ -4870,6 +5298,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -4878,6 +5307,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -4886,6 +5316,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -4894,6 +5325,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -4902,6 +5334,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -4936,14 +5369,14 @@ files = [ [[package]] name = "s3transfer" -version = "0.11.4" +version = "0.12.0" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d"}, - {file = "s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679"}, + {file = "s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18"}, + {file = "s3transfer-0.12.0.tar.gz", hash = "sha256:8ac58bc1989a3fdb7c7f3ee0918a66b160d038a147c7b5db1500930a607e9a1c"}, ] [package.dependencies] @@ -4991,6 +5424,127 @@ tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] torch = ["safetensors[numpy]", "torch (>=1.10)"] +[[package]] +name = "scikit-learn" +version = "1.6.1" +description = "A set of python modules for machine learning and data mining" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}, + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b"}, + {file = "scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8"}, + {file = "scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86"}, + {file = "scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97"}, + {file = "scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422"}, + {file = "scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b"}, + {file = "scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e"}, +] + +[package.dependencies] +joblib = ">=1.2.0" +numpy = ">=1.19.5" +scipy = ">=1.6.0" +threadpoolctl = ">=3.1.0" + +[package.extras] +benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.17.1)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)", "towncrier (>=24.8.0)"] +examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==2.5.6)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.5.1)", "scikit-image (>=0.17.2)"] + +[[package]] +name = "scipy" +version = "1.15.2" +description = "Fundamental algorithms for scientific computing in Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9"}, + {file = "scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5"}, + {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ecf797d2d798cf7c838c6d98321061eb3e72a74710e6c40540f0e8087e3b499e"}, + {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:9b18aa747da280664642997e65aab1dd19d0c3d17068a04b3fe34e2559196cb9"}, + {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3"}, + {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d"}, + {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58"}, + {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa"}, + {file = "scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8bf5cb4a25046ac61d38f8d3c3426ec11ebc350246a4642f2f315fe95bda655"}, + {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e"}, + {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0"}, + {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40"}, + {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462"}, + {file = "scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20"}, + {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e"}, + {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8"}, + {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11"}, + {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53"}, + {file = "scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb"}, + {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27"}, + {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0"}, + {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32"}, + {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d"}, + {file = "scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af"}, + {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274"}, + {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776"}, + {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828"}, + {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28"}, + {file = "scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db"}, + {file = "scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.5" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.16.5)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "sentencepiece" version = "0.2.0" @@ -5079,16 +5633,78 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "soundfile" +version = "0.13.1" +description = "An audio library based on libsndfile, CFFI and NumPy" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445"}, + {file = "soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33"}, + {file = "soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593"}, + {file = "soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb"}, + {file = "soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618"}, + {file = "soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5"}, + {file = "soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9"}, + {file = "soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b"}, +] + +[package.dependencies] +cffi = ">=1.0" +numpy = "*" + +[[package]] +name = "soxr" +version = "0.5.0.post1" +description = "High quality, one-dimensional sample-rate conversion library" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "soxr-0.5.0.post1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:7406d782d85f8cf64e66b65e6b7721973de8a1dc50b9e88bc2288c343a987484"}, + {file = "soxr-0.5.0.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa0a382fb8d8e2afed2c1642723b2d2d1b9a6728ff89f77f3524034c8885b8c9"}, + {file = "soxr-0.5.0.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b01d3efb95a2851f78414bcd00738b0253eec3f5a1e5482838e965ffef84969"}, + {file = "soxr-0.5.0.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc049b0a151a65aa75b92f0ac64bb2dba785d16b78c31c2b94e68c141751d6d"}, + {file = "soxr-0.5.0.post1-cp310-cp310-win_amd64.whl", hash = "sha256:97f269bc26937c267a2ace43a77167d0c5c8bba5a2b45863bb6042b5b50c474e"}, + {file = "soxr-0.5.0.post1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6fb77b626773a966e3d8f6cb24f6f74b5327fa5dc90f1ff492450e9cdc03a378"}, + {file = "soxr-0.5.0.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:39e0f791ba178d69cd676485dbee37e75a34f20daa478d90341ecb7f6d9d690f"}, + {file = "soxr-0.5.0.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f0b558f445ba4b64dbcb37b5f803052eee7d93b1dbbbb97b3ec1787cb5a28eb"}, + {file = "soxr-0.5.0.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca6903671808e0a6078b0d146bb7a2952b118dfba44008b2aa60f221938ba829"}, + {file = "soxr-0.5.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:c4d8d5283ed6f5efead0df2c05ae82c169cfdfcf5a82999c2d629c78b33775e8"}, + {file = "soxr-0.5.0.post1-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:fef509466c9c25f65eae0ce1e4b9ac9705d22c6038c914160ddaf459589c6e31"}, + {file = "soxr-0.5.0.post1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:4704ba6b13a3f1e41d12acf192878384c1c31f71ce606829c64abdf64a8d7d32"}, + {file = "soxr-0.5.0.post1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd052a66471a7335b22a6208601a9d0df7b46b8d087dce4ff6e13eed6a33a2a1"}, + {file = "soxr-0.5.0.post1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3f16810dd649ab1f433991d2a9661e9e6a116c2b4101039b53b3c3e90a094fc"}, + {file = "soxr-0.5.0.post1-cp312-abi3-win_amd64.whl", hash = "sha256:b1be9fee90afb38546bdbd7bde714d1d9a8c5a45137f97478a83b65e7f3146f6"}, + {file = "soxr-0.5.0.post1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:c5af7b355959061beb90a1d73c4834ece4549f07b708f8c73c088153cec29935"}, + {file = "soxr-0.5.0.post1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e1dda616fc797b1507b65486f3116ed2c929f13c722922963dd419d64ada6c07"}, + {file = "soxr-0.5.0.post1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94de2812368e98cb42b4eaeddf8ee1657ecc19bd053f8e67b9b5aa12a3592012"}, + {file = "soxr-0.5.0.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8e9c980637e03d3f345a4fd81d56477a58c294fb26205fa121bc4eb23d9d01"}, + {file = "soxr-0.5.0.post1-cp39-cp39-win_amd64.whl", hash = "sha256:7e71b0b0db450f36de70f1047505231db77a713f8c47df9342582ae8a4b828f2"}, + {file = "soxr-0.5.0.post1.tar.gz", hash = "sha256:7092b9f3e8a416044e1fa138c8172520757179763b85dc53aa9504f4813cff73"}, +] + +[package.dependencies] +numpy = "*" + +[package.extras] +docs = ["linkify-it-py", "myst-parser", "sphinx", "sphinx-book-theme"] +test = ["pytest"] + [[package]] name = "sse-starlette" -version = "2.2.1" +version = "2.3.4" description = "SSE plugin for Starlette" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99"}, - {file = "sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419"}, + {file = "sse_starlette-2.3.4-py3-none-any.whl", hash = "sha256:b8100694f3f892b133d0f7483acb7aacfcf6ed60f863b31947664b6dc74e529f"}, + {file = "sse_starlette-2.3.4.tar.gz", hash = "sha256:0ffd6bed217cdbb74a84816437c609278003998b4991cd2e6872d0b35130e4d5"}, ] [package.dependencies] @@ -5119,16 +5735,62 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] +[[package]] +name = "standard-aifc" +version = "3.13.0" +description = "Standard library aifc redistribution. \"dead battery\"." +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"all\" and python_version >= \"3.13\"" +files = [ + {file = "standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"}, + {file = "standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"}, +] + +[package.dependencies] +audioop-lts = {version = "*", markers = "python_version >= \"3.13\""} +standard-chunk = {version = "*", markers = "python_version >= \"3.13\""} + +[[package]] +name = "standard-chunk" +version = "3.13.0" +description = "Standard library chunk redistribution. \"dead battery\"." +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"all\" and python_version >= \"3.13\"" +files = [ + {file = "standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"}, + {file = "standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"}, +] + +[[package]] +name = "standard-sunau" +version = "3.13.0" +description = "Standard library sunau redistribution. \"dead battery\"." +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"all\" and python_version >= \"3.13\"" +files = [ + {file = "standard_sunau-3.13.0-py3-none-any.whl", hash = "sha256:53af624a9529c41062f4c2fd33837f297f3baa196b0cfceffea6555654602622"}, + {file = "standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908"}, +] + +[package.dependencies] +audioop-lts = {version = "*", markers = "python_version >= \"3.13\""} + [[package]] name = "starlette" -version = "0.46.1" +version = "0.46.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"}, - {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"}, + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, ] [package.dependencies] @@ -5139,15 +5801,15 @@ full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart [[package]] name = "sympy" -version = "1.13.3" +version = "1.14.0" description = "Computer algebra system (CAS) in Python" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73"}, - {file = "sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9"}, + {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, + {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, ] [package.dependencies] @@ -5157,18 +5819,16 @@ mpmath = ">=1.1.0,<1.4" dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] [[package]] -name = "tbb" -version = "2021.13.1" -description = "Intel® oneAPI Threading Building Blocks (oneTBB)" +name = "threadpoolctl" +version = "3.6.0" +description = "threadpoolctl" optional = true -python-versions = "*" +python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"all\" and platform_system == \"Windows\"" +markers = "extra == \"all\"" files = [ - {file = "tbb-2021.13.1-py2.py3-none-manylinux1_i686.whl", hash = "sha256:bb5bdea0c0e9e6ad0739e7a8796c2635ce9eccca86dd48c426cd8027ac70fb1d"}, - {file = "tbb-2021.13.1-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:d916359dc685579d09e4b344241550afc1cc034f7f5ec7234c258b6680912d70"}, - {file = "tbb-2021.13.1-py3-none-win32.whl", hash = "sha256:00f5e5a70051650ddd0ab6247c0549521968339ec21002e475cd23b1cbf46d66"}, - {file = "tbb-2021.13.1-py3-none-win_amd64.whl", hash = "sha256:cbf024b2463fdab3ebe3fa6ff453026358e6b903839c80d647e08ad6d0796ee9"}, + {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, + {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, ] [[package]] @@ -5316,46 +5976,45 @@ files = [ [[package]] name = "torch" -version = "2.3.0" +version = "2.4.0" description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" optional = true python-versions = ">=3.8.0" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "torch-2.3.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:d8ea5a465dbfd8501f33c937d1f693176c9aef9d1c1b0ca1d44ed7b0a18c52ac"}, - {file = "torch-2.3.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09c81c5859a5b819956c6925a405ef1cdda393c9d8a01ce3851453f699d3358c"}, - {file = "torch-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:1bf023aa20902586f614f7682fedfa463e773e26c58820b74158a72470259459"}, - {file = "torch-2.3.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:758ef938de87a2653bba74b91f703458c15569f1562bf4b6c63c62d9c5a0c1f5"}, - {file = "torch-2.3.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:493d54ee2f9df100b5ce1d18c96dbb8d14908721f76351e908c9d2622773a788"}, - {file = "torch-2.3.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:bce43af735c3da16cc14c7de2be7ad038e2fbf75654c2e274e575c6c05772ace"}, - {file = "torch-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:729804e97b7cf19ae9ab4181f91f5e612af07956f35c8b2c8e9d9f3596a8e877"}, - {file = "torch-2.3.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:d24e328226d8e2af7cf80fcb1d2f1d108e0de32777fab4aaa2b37b9765d8be73"}, - {file = "torch-2.3.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:b0de2bdc0486ea7b14fc47ff805172df44e421a7318b7c4d92ef589a75d27410"}, - {file = "torch-2.3.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a306c87a3eead1ed47457822c01dfbd459fe2920f2d38cbdf90de18f23f72542"}, - {file = "torch-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9b98bf1a3c8af2d4c41f0bf1433920900896c446d1ddc128290ff146d1eb4bd"}, - {file = "torch-2.3.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:dca986214267b34065a79000cee54232e62b41dff1ec2cab9abc3fc8b3dee0ad"}, - {file = "torch-2.3.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:20572f426965dd8a04e92a473d7e445fa579e09943cc0354f3e6fef6130ce061"}, - {file = "torch-2.3.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e65ba85ae292909cde0dde6369826d51165a3fc8823dc1854cd9432d7f79b932"}, - {file = "torch-2.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:5515503a193781fd1b3f5c474e89c9dfa2faaa782b2795cc4a7ab7e67de923f6"}, - {file = "torch-2.3.0-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:6ae9f64b09516baa4ef890af0672dc981c20b1f0d829ce115d4420a247e88fba"}, - {file = "torch-2.3.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cd0dc498b961ab19cb3f8dbf0c6c50e244f2f37dbfa05754ab44ea057c944ef9"}, - {file = "torch-2.3.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e05f836559251e4096f3786ee99f4a8cbe67bc7fbedba8ad5e799681e47c5e80"}, - {file = "torch-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:4fb27b35dbb32303c2927da86e27b54a92209ddfb7234afb1949ea2b3effffea"}, - {file = "torch-2.3.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:760f8bedff506ce9e6e103498f9b1e9e15809e008368594c3a66bf74a8a51380"}, + {file = "torch-2.4.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:4ed94583e244af51d6a8d28701ca5a9e02d1219e782f5a01dd401f90af17d8ac"}, + {file = "torch-2.4.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c4ca297b7bd58b506bfd6e78ffd14eb97c0e7797dcd7965df62f50bb575d8954"}, + {file = "torch-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:2497cbc7b3c951d69b276ca51fe01c2865db67040ac67f5fc20b03e41d16ea4a"}, + {file = "torch-2.4.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:685418ab93730efbee71528821ff54005596970dd497bf03c89204fb7e3f71de"}, + {file = "torch-2.4.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e743adadd8c8152bb8373543964551a7cb7cc20ba898dc8f9c0cdbe47c283de0"}, + {file = "torch-2.4.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:7334325c0292cbd5c2eac085f449bf57d3690932eac37027e193ba775703c9e6"}, + {file = "torch-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:97730014da4c57ffacb3c09298c6ce05400606e890bd7a05008d13dd086e46b1"}, + {file = "torch-2.4.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:f169b4ea6dc93b3a33319611fcc47dc1406e4dd539844dcbd2dec4c1b96e166d"}, + {file = "torch-2.4.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:997084a0f9784d2a89095a6dc67c7925e21bf25dea0b3d069b41195016ccfcbb"}, + {file = "torch-2.4.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:bc3988e8b36d1e8b998d143255d9408d8c75da4ab6dd0dcfd23b623dfb0f0f57"}, + {file = "torch-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3374128bbf7e62cdaed6c237bfd39809fbcfaa576bee91e904706840c3f2195c"}, + {file = "torch-2.4.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:91aaf00bfe1ffa44dc5b52809d9a95129fca10212eca3ac26420eb11727c6288"}, + {file = "torch-2.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cc30457ea5489c62747d3306438af00c606b509d78822a88f804202ba63111ed"}, + {file = "torch-2.4.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a046491aaf96d1215e65e1fa85911ef2ded6d49ea34c8df4d0638879f2402eef"}, + {file = "torch-2.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:688eec9240f3ce775f22e1e1a5ab9894f3d5fe60f3f586deb7dbd23a46a83916"}, + {file = "torch-2.4.0-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:3af4de2a618fb065e78404c4ba27a818a7b7957eaeff28c6c66ce7fb504b68b8"}, + {file = "torch-2.4.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:618808d3f610d5f180e47a697d4ec90b810953bb1e020f424b2ac7fb0884b545"}, + {file = "torch-2.4.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ed765d232d23566052ba83632ec73a4fccde00b4c94ad45d63b471b09d63b7a7"}, + {file = "torch-2.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2feb98ac470109472fb10dfef38622a7ee08482a16c357863ebc7bc7db7c8f7"}, + {file = "torch-2.4.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:8940fc8b97a4c61fdb5d46a368f21f4a3a562a17879e932eb51a5ec62310cb31"}, ] [package.dependencies] filelock = "*" fsspec = "*" jinja2 = "*" -mkl = {version = ">=2021.1.1,<=2021.4.0", markers = "platform_system == \"Windows\""} networkx = "*" nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cudnn-cu12 = {version = "9.1.0.70", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} @@ -5363,12 +6022,57 @@ nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \" nvidia-nccl-cu12 = {version = "2.20.5", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} sympy = "*" -triton = {version = "2.3.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.12\""} +triton = {version = "3.0.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\""} typing-extensions = ">=4.8.0" [package.extras] opt-einsum = ["opt-einsum (>=3.3)"] -optree = ["optree (>=0.9.1)"] +optree = ["optree (>=0.11.0)"] + +[[package]] +name = "torchvision" +version = "0.19.0" +description = "image and video datasets and models for torch deep learning" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "torchvision-0.19.0-1-cp310-cp310-win_amd64.whl", hash = "sha256:6ed066aae5c50465d7c4761357aefe5dbd2eb7075a33ab8c14b352fc2353ad4c"}, + {file = "torchvision-0.19.0-1-cp311-cp311-win_amd64.whl", hash = "sha256:6b1bce2e4c003d890a18f14ff289528707d918e38539ff890ef02aa31dae1b56"}, + {file = "torchvision-0.19.0-1-cp312-cp312-win_amd64.whl", hash = "sha256:13aee7a46e049c8c1e7d35a0394b0587a7e62ff3d1a822cd2bbbacb675ac4a09"}, + {file = "torchvision-0.19.0-1-cp38-cp38-win_amd64.whl", hash = "sha256:2acc436d043d4f81b3bc6929cbfa4ef1cdae4d8a0b04ec72ec30a497e9a38179"}, + {file = "torchvision-0.19.0-1-cp39-cp39-win_amd64.whl", hash = "sha256:b5f70f5a8bd9c8b00a076bf466b39b5cd679ef62587c47cc048adb04d9c5f155"}, + {file = "torchvision-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec874ef85dcb24c69e600f6e276af892c80cde3ffdaeb7275efda463242bc2a8"}, + {file = "torchvision-0.19.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:106842b1e475b14d9a04ee0d6f5477d43100e3bb78e9d31e37422384d0d84179"}, + {file = "torchvision-0.19.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d467d434005fd05a227a2ba7af4c591bb67e6d4a97bbd06eda8da83f43e9fd07"}, + {file = "torchvision-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:f77ac31f7337d0f6f4b58e65582c6c93b9d9eeec7dfd7478896b5cdc19a2d60d"}, + {file = "torchvision-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbf3aa71a3899244fc884303ed3c4604a160824fefac77e82317a5463efc1d9b"}, + {file = "torchvision-0.19.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ec4162dc71d9db7f0b51d0f92491929c1419605ff436e1305e50de13504a1c30"}, + {file = "torchvision-0.19.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:4e6aa4fa3f0bc3599fa071c149e651a3e6bdd67c9161794478f9f91471c406a2"}, + {file = "torchvision-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac5525d5cc09e425b5cf5752ecf66eefbbbd8c8cd945198ce35eb01a694e6069"}, + {file = "torchvision-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c09ef8ed184fa877f6251b620226e74f682b8f1d6b341456428d4955b8d9c670"}, + {file = "torchvision-0.19.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:02f1dd5cfc897957535b41b0258ec452d30de044e20c2de2c75869f7708e7656"}, + {file = "torchvision-0.19.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:be0f27a28b8e9f2ae98a31af34a4bdd2a5bf154d92bd73a5797c8d2156fb3ab6"}, + {file = "torchvision-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6ba7756f75c80212e51d3576f85ea204589e0c16efdb9b835dd677bc8929a67"}, + {file = "torchvision-0.19.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:854e967a16a9409e941b5bbe5aa357b23f7158bccb9de35ae20fd4945f05ecd1"}, + {file = "torchvision-0.19.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d9afb8a3c3ce99a161a64c2a3b91cb545632a72118053cbfb84e87a02a8dcd02"}, + {file = "torchvision-0.19.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:079a696e0b2cb52e4be30afa8e9b3d7d280f02a2b5ffedd7e821fa1efd1a5a8d"}, + {file = "torchvision-0.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:aaa338ff3a55a8c0f94e0e64eff6fe2af1fc933a95fd43812760e72ea66e986b"}, + {file = "torchvision-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd1279571d4b68d5a53d9b7a35aedf91c4cb1e0b08099f6a1effa7b25b8c95e7"}, + {file = "torchvision-0.19.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d54b5e19b7ebebca7d0b08497b4c6335264cad04c94c05fa35988d9e9eed0c4"}, + {file = "torchvision-0.19.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5f9a598dcf82bdfc8e4436ce74763b3877dabec3b33f94613b94ede13e3e4dee"}, + {file = "torchvision-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:ec1281c10402234d470bfd4d53663d81f4364f293b2f8fe24d4a7a1adc78c90c"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=5.3.0,<8.3.dev0 || >=8.4.dev0" +torch = "2.4.0" + +[package.extras] +gdown = ["gdown (>=4.7.3)"] +scipy = ["scipy"] [[package]] name = "tornado" @@ -5431,62 +6135,65 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "transformers" -version = "4.49.0" +version = "4.51.3" description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" optional = true python-versions = ">=3.9.0" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "transformers-4.49.0-py3-none-any.whl", hash = "sha256:6b4fded1c5fee04d384b1014495b4235a2b53c87503d7d592423c06128cbbe03"}, - {file = "transformers-4.49.0.tar.gz", hash = "sha256:7e40e640b5b8dc3f48743f5f5adbdce3660c82baafbd3afdfc04143cdbd2089e"}, + {file = "transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83"}, + {file = "transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409"}, ] [package.dependencies] filelock = "*" -huggingface-hub = ">=0.26.0,<1.0" +huggingface-hub = ">=0.30.0,<1.0" numpy = ">=1.17" packaging = ">=20.0" pyyaml = ">=5.1" regex = "!=2019.12.17" requests = "*" -safetensors = ">=0.4.1" +safetensors = ">=0.4.3" tokenizers = ">=0.21,<0.22" tqdm = ">=4.27" [package.extras] accelerate = ["accelerate (>=0.26.0)"] agents = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "datasets (!=2.5.0)", "diffusers", "opencv-python", "sentencepiece (>=0.1.91,!=0.1.92)", "torch (>=2.0)"] -all = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "av", "codecarbon (>=2.8.1)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "librosa", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "torchaudio", "torchvision"] +all = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "av", "codecarbon (>=2.8.1)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.3.2,<0.4)", "librosa", "num2words", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "torchaudio", "torchvision"] audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] benchmark = ["optimum-benchmark (>=0.3.0)"] codecarbon = ["codecarbon (>=2.8.1)"] deepspeed = ["accelerate (>=0.26.0)", "deepspeed (>=0.9.3)"] -deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk (<=3.8.1)", "optuna", "parameterized", "protobuf", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] -dev = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "av", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "libcst", "librosa", "nltk (<=3.8.1)", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] -dev-tensorflow = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "libcst", "librosa", "nltk (<=3.8.1)", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-rich", "pytest-timeout", "pytest-xdist", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.21,<0.22)", "urllib3 (<2.0.0)"] -dev-torch = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "libcst", "librosa", "nltk (<=3.8.1)", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.26.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk (<=3.8.1)", "optuna", "parameterized", "protobuf", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] +dev = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "av", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "kernels (>=0.3.2,<0.4)", "libcst", "librosa", "nltk (<=3.8.1)", "num2words", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] +dev-tensorflow = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "libcst", "librosa", "nltk (<=3.8.1)", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.21,<0.22)", "urllib3 (<2.0.0)"] +dev-torch = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.26.0)", "beautifulsoup4", "codecarbon (>=2.8.1)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "kernels (>=0.3.2,<0.4)", "libcst", "librosa", "nltk (<=3.8.1)", "num2words", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rich", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm (<=1.0.11)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] flax = ["flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "optax (>=0.0.8,<=0.1.4)", "scipy (<1.13.0)"] flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] ftfy = ["ftfy"] -integrations = ["optuna", "ray[tune] (>=2.7.0)", "sigopt"] +hf-xet = ["hf-xet"] +hub-kernels = ["kernels (>=0.3.2,<0.4)"] +integrations = ["kernels (>=0.3.2,<0.4)", "optuna", "ray[tune] (>=2.7.0)", "sigopt"] ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"] modelcreation = ["cookiecutter (==1.7.3)"] natten = ["natten (>=0.14.6,<0.15.0)"] +num2words = ["num2words"] onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"] onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"] optuna = ["optuna"] -quality = ["GitPython (<3.1.19)", "datasets (!=2.5.0)", "isort (>=5.5.4)", "libcst", "rich", "ruff (==0.5.1)", "urllib3 (<2.0.0)"] +quality = ["GitPython (<3.1.19)", "datasets (!=2.5.0)", "isort (>=5.5.4)", "libcst", "rich", "ruff (==0.11.2)", "urllib3 (<2.0.0)"] ray = ["ray[tune] (>=2.7.0)"] retrieval = ["datasets (!=2.5.0)", "faiss-cpu"] -ruff = ["ruff (==0.5.1)"] +ruff = ["ruff (==0.11.2)"] sagemaker = ["sagemaker (>=2.31.0)"] sentencepiece = ["protobuf", "sentencepiece (>=0.1.91,!=0.1.92)"] serving = ["fastapi", "pydantic", "starlette", "uvicorn"] sigopt = ["sigopt"] sklearn = ["scikit-learn"] speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] -testing = ["GitPython (<3.1.19)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk (<=3.8.1)", "parameterized", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] +testing = ["GitPython (<3.1.19)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk (<=3.8.1)", "parameterized", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-asyncio", "pytest-order", "pytest-rerunfailures", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.11.2)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] tf = ["keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"] tf-cpu = ["keras (>2.9,<2.16)", "keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow-cpu (>2.9,<2.16)", "tensorflow-probability (<0.24)", "tensorflow-text (<2.16)", "tf2onnx"] tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] @@ -5496,25 +6203,24 @@ tokenizers = ["tokenizers (>=0.21,<0.22)"] torch = ["accelerate (>=0.26.0)", "torch (>=2.0)"] torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] torch-vision = ["Pillow (>=10.0.1,<=15.0)", "torchvision"] -torchhub = ["filelock", "huggingface-hub (>=0.26.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "tqdm (>=4.27)"] +torchhub = ["filelock", "huggingface-hub (>=0.30.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.21,<0.22)", "torch (>=2.0)", "tqdm (>=4.27)"] video = ["av"] vision = ["Pillow (>=10.0.1,<=15.0)"] [[package]] name = "triton" -version = "2.3.0" +version = "3.0.0" description = "A language and compiler for custom Deep Learning operations" optional = true python-versions = "*" groups = ["main"] -markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version <= \"3.11\" and extra == \"all\"" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.13\" and extra == \"all\"" files = [ - {file = "triton-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ce4b8ff70c48e47274c66f269cce8861cf1dc347ceeb7a67414ca151b1822d8"}, - {file = "triton-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c3d9607f85103afdb279938fc1dd2a66e4f5999a58eb48a346bd42738f986dd"}, - {file = "triton-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:218d742e67480d9581bafb73ed598416cc8a56f6316152e5562ee65e33de01c0"}, - {file = "triton-2.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381ec6b3dac06922d3e4099cfc943ef032893b25415de295e82b1a82b0359d2c"}, - {file = "triton-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:038e06a09c06a164fef9c48de3af1e13a63dc1ba3c792871e61a8e79720ea440"}, - {file = "triton-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8f636e0341ac348899a47a057c3daea99ea7db31528a225a3ba4ded28ccc65"}, + {file = "triton-3.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1efef76935b2febc365bfadf74bcb65a6f959a9872e5bddf44cc9e0adce1e1a"}, + {file = "triton-3.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ce8520437c602fb633f1324cc3871c47bee3b67acf9756c1a66309b60e3216c"}, + {file = "triton-3.0.0-1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e509deb77f1c067d8640725ef00c5cbfcb2052a1a3cb6a6d343841f92624eb"}, + {file = "triton-3.0.0-1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bcbf3b1c48af6a28011a5c40a5b3b9b5330530c3827716b5fbf6d7adcc1e53e9"}, + {file = "triton-3.0.0-1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6e5727202f7078c56f91ff13ad0c1abab14a0e7f2c87e91b12b6f64f3e8ae609"}, ] [package.dependencies] @@ -5522,19 +6228,19 @@ filelock = "*" [package.extras] build = ["cmake (>=3.20)", "lit"] -tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)", "torch"] -tutorials = ["matplotlib", "pandas", "tabulate", "torch"] +tests = ["autopep8", "flake8", "isort", "llnl-hatchet", "numpy", "pytest", "scipy (>=1.7.1)"] +tutorials = ["matplotlib", "pandas", "tabulate"] [[package]] name = "types-awscrt" -version = "0.24.2" +version = "0.26.1" description = "Type annotations and code completion for awscrt" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "types_awscrt-0.24.2-py3-none-any.whl", hash = "sha256:345ab84a4f75b26bfb816b249657855824a4f2d1ce5b58268c549f81fce6eccc"}, - {file = "types_awscrt-0.24.2.tar.gz", hash = "sha256:5826baf69ad5d29c76be49fc7df00222281fa31b14f99e9fb4492d71ec98fea5"}, + {file = "types_awscrt-0.26.1-py3-none-any.whl", hash = "sha256:176d320a26990efc057d4bf71396e05be027c142252ac48cc0d87aaea0704280"}, + {file = "types_awscrt-0.26.1.tar.gz", hash = "sha256:aca96f889b3745c0e74f42f08f277fed3bf6e9baa2cf9b06a36f78d77720e504"}, ] [[package]] @@ -5551,14 +6257,14 @@ files = [ [[package]] name = "types-pytz" -version = "2025.1.0.20250318" +version = "2025.2.0.20250326" description = "Typing stubs for pytz" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "types_pytz-2025.1.0.20250318-py3-none-any.whl", hash = "sha256:04dba4907c5415777083f9548693c6d9f80ec53adcaff55a38526a3f8ddcae04"}, - {file = "types_pytz-2025.1.0.20250318.tar.gz", hash = "sha256:97e0e35184c6fe14e3a5014512057f2c57bb0c6582d63c1cfcc4809f82180449"}, + {file = "types_pytz-2025.2.0.20250326-py3-none-any.whl", hash = "sha256:3c397fd1b845cd2b3adc9398607764ced9e578a98a5d1fbb4a9bc9253edfb162"}, + {file = "types_pytz-2025.2.0.20250326.tar.gz", hash = "sha256:deda02de24f527066fc8d6a19e284ab3f3ae716a42b4adb6b40e75e408c08d36"}, ] [[package]] @@ -5578,14 +6284,14 @@ types-urllib3 = "*" [[package]] name = "types-s3transfer" -version = "0.11.4" +version = "0.12.0" description = "Type annotations and code completion for s3transfer" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "types_s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:2a76d92c07d4a3cb469e5343b2e7560e0b8078b2e03696a65407b8c44c861b61"}, - {file = "types_s3transfer-0.11.4.tar.gz", hash = "sha256:05fde593c84270f19fd053f0b1e08f5a057d7c5f036b9884e68fb8cd3041ac30"}, + {file = "types_s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:101bbc5b7f00b71512374df881f480fc6bf63c948b5098ab024bf3370fbfb0e8"}, + {file = "types_s3transfer-0.12.0.tar.gz", hash = "sha256:f8f59201481e904362873bf0be3267f259d60ad946ebdfcb847d092a1fa26f98"}, ] [[package]] @@ -5602,38 +6308,53 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "dev"] 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"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" -version = "2025.1" +version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, - {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] name = "urllib3" -version = "2.3.0" +version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["main", "docs"] files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] @@ -5644,14 +6365,15 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.2" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"all\" or sys_platform != \"emscripten\"" files = [ - {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, - {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, + {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, + {file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"}, ] [package.dependencies] @@ -5724,69 +6446,74 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", [[package]] name = "vllm" -version = "0.5.0.post1" +version = "0.5.5" description = "A high-throughput and memory-efficient inference and serving engine for LLMs" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "vllm-0.5.0.post1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:88e22d26aaeb8f721622f2544dec66aaba86d4658097b12d95aa83332291fc3b"}, - {file = "vllm-0.5.0.post1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:fe6e6c84e6653444904cb4a8c37f394fcfa3cff1b19ffbcbc5d3fc5f56b2129b"}, - {file = "vllm-0.5.0.post1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:331aec6e79174a7cf1d9f87d84c6743df5f6a3a1a2d82ca504e4f4e8cc518716"}, - {file = "vllm-0.5.0.post1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b17ffe3a99eeebdad536383b9cd21e0a23ee7759646574b97cf0f68b8f4e5ebf"}, - {file = "vllm-0.5.0.post1.tar.gz", hash = "sha256:e4a19889d95f6896ca89655f64ffc9361f44e66a8e9f086748aa989031571a23"}, + {file = "vllm-0.5.5-cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:64fb5b8a3300af3006a3b9ba4eacdf5f4bd2a85887a695e6587684447c49eb8e"}, + {file = "vllm-0.5.5.tar.gz", hash = "sha256:14abe33243939e18cdc437abfc78543aac62e11a43f66d36ec855a3e40c797dd"}, ] [package.dependencies] aiohttp = "*" -cmake = ">=3.21" fastapi = "*" filelock = ">=3.10.4" -lm-format-enforcer = "0.10.1" -ninja = "*" -numpy = "*" +gguf = "0.9.1" +importlib-metadata = "*" +librosa = "*" +lm-format-enforcer = "0.10.6" +msgspec = "*" +numpy = "<2.0.0" nvidia-ml-py = "*" -openai = "*" -outlines = ">=0.0.43" +openai = ">=1.0" +outlines = ">=0.0.43,<0.1" pillow = "*" prometheus-client = ">=0.18.0" prometheus-fastapi-instrumentator = ">=7.0.0" +protobuf = "*" psutil = "*" py-cpuinfo = "*" -pydantic = ">=2.0" +pydantic = ">=2.8" +pyzmq = "*" ray = ">=2.9" requests = "*" sentencepiece = "*" +soundfile = "*" tiktoken = ">=0.6.0" tokenizers = ">=0.19.1" -torch = "2.3.0" -transformers = ">=4.40.0" -typing-extensions = "*" +torch = "2.4.0" +torchvision = "0.19" +tqdm = "*" +transformers = ">=4.43.2" +typing-extensions = ">=4.10" uvicorn = {version = "*", extras = ["standard"]} -vllm-flash-attn = "2.5.9" -xformers = "0.0.26.post1" +vllm-flash-attn = {version = "2.6.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +xformers = {version = "0.0.27.post2", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} [package.extras] tensorizer = ["tensorizer (>=2.9.0)"] [[package]] name = "vllm-flash-attn" -version = "2.5.9" +version = "2.6.1" description = "Forward-only flash-attn" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\"" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and extra == \"all\"" files = [ - {file = "vllm_flash_attn-2.5.9-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:3e33af65880e99c47ff636f465d45c08e5e4c2eedbc06c9970599a6408768944"}, - {file = "vllm_flash_attn-2.5.9-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:88a63365db204859131fd8b3a0e85ea3b8b726f4be5ddbf2c6211a94a2cf4258"}, - {file = "vllm_flash_attn-2.5.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5ed55fe0a07d80210ac14975c6329fb5e1afafb3c680ca9e334395e4d5265831"}, - {file = "vllm_flash_attn-2.5.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:384845ae339946cb46e11cdfb5fadda1c0e519d2312b595e04da0df67a6d5383"}, + {file = "vllm_flash_attn-2.6.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f4570a3ab09178877e00d530a5019d8b3b56b7eabe2ec6b86b696dcc33c3bacb"}, + {file = "vllm_flash_attn-2.6.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:2ce0a1ca09bc07dca4b59a56f0db98f5f6a93f8392377fbfd2979e639bc91e88"}, + {file = "vllm_flash_attn-2.6.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:e8c054964382125c6ab34274055417d8341516b9de53c92a8015fabb51d9962c"}, + {file = "vllm_flash_attn-2.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b9a6addea5a829ef05d64cf0dbebaa01e38cbb91a771293cdf6d2f6251600ea"}, + {file = "vllm_flash_attn-2.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:ef99311ff2b4d836e0d3073bcb56df97b334dacd1976d67b3ca5dcdf2ffbfb38"}, ] [package.dependencies] -torch = "2.3.0" +torch = "2.4.0" [[package]] name = "watchdog" @@ -5833,84 +6560,84 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "watchfiles" -version = "1.0.4" +version = "1.0.5" description = "Simple, modern and high performance file watching and code reload in python." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, - {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"}, - {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"}, - {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"}, - {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"}, - {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"}, - {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"}, - {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"}, - {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"}, - {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"}, - {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"}, - {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"}, - {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"}, - {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"}, - {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"}, - {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"}, - {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"}, - {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"}, - {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"}, - {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"}, - {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"}, - {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"}, - {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"}, - {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"}, - {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"}, - {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"}, - {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"}, - {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"}, - {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"}, - {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"}, - {file = "watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21"}, - {file = "watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3"}, - {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf"}, - {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a"}, - {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b"}, - {file = "watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27"}, - {file = "watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"}, - {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb"}, - {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42"}, - {file = "watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"}, + {file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"}, + {file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"}, + {file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"}, + {file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"}, + {file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"}, + {file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"}, + {file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"}, + {file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382"}, + {file = "watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18"}, + {file = "watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"}, + {file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"}, ] [package.dependencies] @@ -6039,14 +6766,14 @@ files = [ [[package]] name = "widgetsnbextension" -version = "4.0.13" +version = "4.0.14" description = "Jupyter interactive widgets for Jupyter Notebook" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, - {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, + {file = "widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575"}, + {file = "widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af"}, ] [[package]] @@ -6067,27 +6794,29 @@ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [[package]] name = "xformers" -version = "0.0.26.post1" +version = "0.0.27.post2" description = "XFormers: A collection of composable Transformer building blocks." optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\"" +markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and extra == \"all\"" files = [ - {file = "xformers-0.0.26.post1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:1fe0f9a3dddb7f6175c2e34de2f318d6742eec28c068b8334b093016b2c9c2c8"}, - {file = "xformers-0.0.26.post1-cp310-cp310-win_amd64.whl", hash = "sha256:e34b8dd6982077bee0c8eb2db8bc1513177201bfe0af890a4db42d8d31c966a5"}, - {file = "xformers-0.0.26.post1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0d35615870b9237077aec51802a5a821f9e9d2708730c8914d5cff301fb29558"}, - {file = "xformers-0.0.26.post1-cp311-cp311-win_amd64.whl", hash = "sha256:d05c547b4ba603fc8e21fad03a342138eaaece35fe0a1692e6ee0d061ddd21ac"}, - {file = "xformers-0.0.26.post1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:f3afa385266264d5f713e75ad1b31de99add9a719f2f461f98e032c259170cc9"}, - {file = "xformers-0.0.26.post1-cp38-cp38-win_amd64.whl", hash = "sha256:e96e2c1a11e84a6aac8386a304e3e338057c95029c82b221bf6aa1658aeacdf0"}, - {file = "xformers-0.0.26.post1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:16dcfa801450a03f148d6d5f7ba2b5540a27c52517946570fe0e1f75238d73a1"}, - {file = "xformers-0.0.26.post1-cp39-cp39-win_amd64.whl", hash = "sha256:cf267bac8f4454db1056e4e6ff9e4df1e02b2b31d8116d50913af15fa6ae7132"}, - {file = "xformers-0.0.26.post1.tar.gz", hash = "sha256:1d14b5f999ede649198379b0470ebdd25007ba224ae336ef958124158a6de8b1"}, + {file = "xformers-0.0.27.post2-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:7d4bd4876559fdc3cccbcaea717d3a28ca7f2fb09ab84cac3d859449ba247830"}, + {file = "xformers-0.0.27.post2-cp310-cp310-win_amd64.whl", hash = "sha256:cd476c23fea18aa7298753c031c4827a544d919ee542cec771c01bc824d0127d"}, + {file = "xformers-0.0.27.post2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:373a1c5d65dab89ea058d17a37c07e5da5ff3b9bebf61425e26a0a5293b0d4a9"}, + {file = "xformers-0.0.27.post2-cp311-cp311-win_amd64.whl", hash = "sha256:7a26febb550b642c7da9864b6a46ccb89461571d27cfef54f7a252f03060d07c"}, + {file = "xformers-0.0.27.post2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:3500f5ff3614aa18762fddc945e4057e7225e23f35986c6666cc49d6cb1c8ee7"}, + {file = "xformers-0.0.27.post2-cp312-cp312-win_amd64.whl", hash = "sha256:0454ba745babf43500320b1d171ef4e44dec38f279fab9df1710ca5ce44a749c"}, + {file = "xformers-0.0.27.post2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ab39ba95a7529c412c3d81e7817e2b42c31980b72e1937055fd4ba4503a34b3d"}, + {file = "xformers-0.0.27.post2-cp38-cp38-win_amd64.whl", hash = "sha256:5a83aa75a7208c359b2755af318e97e70855dbae6cdf998227f6dee220a56203"}, + {file = "xformers-0.0.27.post2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f9fca3af140a8649678e946d55e32c8be59917c4b7d0e96437c057a4bbe30e11"}, + {file = "xformers-0.0.27.post2-cp39-cp39-win_amd64.whl", hash = "sha256:d555ef6d3722941cd785cf4b7c6039b06e8a0ca1360ff661200cf043393ca5a2"}, + {file = "xformers-0.0.27.post2.tar.gz", hash = "sha256:5c3bcefabf29532cac7eb7556fb684be209698955b004aac069742e49373bdb3"}, ] [package.dependencies] numpy = "*" -torch = "2.3.0" +torch = "2.4.0" [[package]] name = "xmltodict" @@ -6101,102 +6830,258 @@ files = [ {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, ] +[[package]] +name = "xxhash" +version = "3.5.0" +description = "Python binding for xxHash" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"all\"" +files = [ + {file = "xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212"}, + {file = "xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442"}, + {file = "xxhash-3.5.0-cp310-cp310-win32.whl", hash = "sha256:61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da"}, + {file = "xxhash-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9"}, + {file = "xxhash-3.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6"}, + {file = "xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1"}, + {file = "xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839"}, + {file = "xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da"}, + {file = "xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58"}, + {file = "xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3"}, + {file = "xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00"}, + {file = "xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e"}, + {file = "xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8"}, + {file = "xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e"}, + {file = "xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2"}, + {file = "xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6"}, + {file = "xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c"}, + {file = "xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637"}, + {file = "xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43"}, + {file = "xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b"}, + {file = "xxhash-3.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6e5f70f6dca1d3b09bccb7daf4e087075ff776e3da9ac870f86ca316736bb4aa"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e76e83efc7b443052dd1e585a76201e40b3411fe3da7af4fe434ec51b2f163b"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33eac61d0796ca0591f94548dcfe37bb193671e0c9bcf065789b5792f2eda644"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ec70a89be933ea49222fafc3999987d7899fc676f688dd12252509434636622"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86b8e7f703ec6ff4f351cfdb9f428955859537125904aa8c963604f2e9d3e7"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0adfbd36003d9f86c8c97110039f7539b379f28656a04097e7434d3eaf9aa131"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:63107013578c8a730419adc05608756c3fa640bdc6abe806c3123a49fb829f43"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:683b94dbd1ca67557850b86423318a2e323511648f9f3f7b1840408a02b9a48c"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5d2a01dcce81789cf4b12d478b5464632204f4c834dc2d064902ee27d2d1f0ee"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:a9d360a792cbcce2fe7b66b8d51274ec297c53cbc423401480e53b26161a290d"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f0b48edbebea1b7421a9c687c304f7b44d0677c46498a046079d445454504737"}, + {file = "xxhash-3.5.0-cp37-cp37m-win32.whl", hash = "sha256:7ccb800c9418e438b44b060a32adeb8393764da7441eb52aa2aa195448935306"}, + {file = "xxhash-3.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c3bc7bf8cb8806f8d1c9bf149c18708cb1c406520097d6b0a73977460ea03602"}, + {file = "xxhash-3.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74752ecaa544657d88b1d1c94ae68031e364a4d47005a90288f3bab3da3c970f"}, + {file = "xxhash-3.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dee1316133c9b463aa81aca676bc506d3f80d8f65aeb0bba2b78d0b30c51d7bd"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:602d339548d35a8579c6b013339fb34aee2df9b4e105f985443d2860e4d7ffaa"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:695735deeddfb35da1677dbc16a083445360e37ff46d8ac5c6fcd64917ff9ade"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1030a39ba01b0c519b1a82f80e8802630d16ab95dc3f2b2386a0b5c8ed5cbb10"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5bc08f33c4966f4eb6590d6ff3ceae76151ad744576b5fc6c4ba8edd459fdec"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160e0c19ee500482ddfb5d5570a0415f565d8ae2b3fd69c5dcfce8a58107b1c3"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f1abffa122452481a61c3551ab3c89d72238e279e517705b8b03847b1d93d738"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d5e9db7ef3ecbfc0b4733579cea45713a76852b002cf605420b12ef3ef1ec148"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:23241ff6423378a731d84864bf923a41649dc67b144debd1077f02e6249a0d54"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:82b833d5563fefd6fceafb1aed2f3f3ebe19f84760fdd289f8b926731c2e6e91"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a80ad0ffd78bef9509eee27b4a29e56f5414b87fb01a888353e3d5bda7038bd"}, + {file = "xxhash-3.5.0-cp38-cp38-win32.whl", hash = "sha256:50ac2184ffb1b999e11e27c7e3e70cc1139047e7ebc1aa95ed12f4269abe98d4"}, + {file = "xxhash-3.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:392f52ebbb932db566973693de48f15ce787cabd15cf6334e855ed22ea0be5b3"}, + {file = "xxhash-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc8cdd7f33d57f0468b0614ae634cc38ab9202c6957a60e31d285a71ebe0301"}, + {file = "xxhash-3.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0c48b6300cd0b0106bf49169c3e0536408dfbeb1ccb53180068a18b03c662ab"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe1a92cfbaa0a1253e339ccec42dbe6db262615e52df591b68726ab10338003f"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33513d6cc3ed3b559134fb307aae9bdd94d7e7c02907b37896a6c45ff9ce51bd"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eefc37f6138f522e771ac6db71a6d4838ec7933939676f3753eafd7d3f4c40bc"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a606c8070ada8aa2a88e181773fa1ef17ba65ce5dd168b9d08038e2a61b33754"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42eca420c8fa072cc1dd62597635d140e78e384a79bb4944f825fbef8bfeeef6"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:604253b2143e13218ff1ef0b59ce67f18b8bd1c4205d2ffda22b09b426386898"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6e93a5ad22f434d7876665444a97e713a8f60b5b1a3521e8df11b98309bff833"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7a46e1d6d2817ba8024de44c4fd79913a90e5f7265434cef97026215b7d30df6"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:30eb2efe6503c379b7ab99c81ba4a779748e3830241f032ab46bd182bf5873af"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c8aa771ff2c13dd9cda8166d685d7333d389fae30a4d2bb39d63ab5775de8606"}, + {file = "xxhash-3.5.0-cp39-cp39-win32.whl", hash = "sha256:5ed9ebc46f24cf91034544b26b131241b699edbfc99ec5e7f8f3d02d6eb7fba4"}, + {file = "xxhash-3.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:220f3f896c6b8d0316f63f16c077d52c412619e475f9372333474ee15133a558"}, + {file = "xxhash-3.5.0-cp39-cp39-win_arm64.whl", hash = "sha256:a7b1d8315d9b5e9f89eb2933b73afae6ec9597a258d52190944437158b49d38e"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b4154c00eb22e4d543f472cfca430e7962a0f1d0f3778334f2e08a7ba59363c"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d30bbc1644f726b825b3278764240f449d75f1a8bdda892e641d4a688b1494ae"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fa0b72f2423e2aa53077e54a61c28e181d23effeaafd73fcb9c494e60930c8e"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13de2b76c1835399b2e419a296d5b38dc4855385d9e96916299170085ef72f57"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0691bfcc4f9c656bcb96cc5db94b4d75980b9d5589f2e59de790091028580837"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:297595fe6138d4da2c8ce9e72a04d73e58725bb60f3a19048bc96ab2ff31c692"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc1276d369452040cbb943300dc8abeedab14245ea44056a2943183822513a18"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2061188a1ba352fc699c82bff722f4baacb4b4b8b2f0c745d2001e56d0dfb514"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c384c434021e4f62b8d9ba0bc9467e14d394893077e2c66d826243025e1f81"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e6a4dd644d72ab316b580a1c120b375890e4c52ec392d4aef3c63361ec4d77d1"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:531af8845aaadcadf951b7e0c1345c6b9c68a990eeb74ff9acd8501a0ad6a1c9"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ce379bcaa9fcc00f19affa7773084dd09f5b59947b3fb47a1ceb0179f91aaa1"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd1b2281d01723f076df3c8188f43f2472248a6b63118b036e641243656b1b0f"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c770750cc80e8694492244bca7251385188bc5597b6a39d98a9f30e8da984e0"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b150b8467852e1bd844387459aa6fbe11d7f38b56e901f9f3b3e6aba0d660240"}, + {file = "xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f"}, +] + [[package]] name = "yarl" -version = "1.18.3" +version = "1.20.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, - {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, - {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, - {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, - {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, - {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, - {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, - {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, - {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, - {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, - {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, - {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, - {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, + {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, + {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, + {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, + {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, + {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, + {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, + {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, + {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, + {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, + {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"}, + {file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"}, + {file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"}, + {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, + {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" -propcache = ">=0.2.0" +propcache = ">=0.2.1" [[package]] name = "zipp" From 709627c25d99317e33f43afa898ef0240f135a94 Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 8 May 2025 21:48:00 -0700 Subject: [PATCH 24/25] Dependency updates and type fixes --- poetry.lock | 702 +++++------------------------ pyproject.toml | 15 +- rigging/generator/transformers_.py | 33 +- rigging/generator/vllm_.py | 20 +- 4 files changed, 144 insertions(+), 626 deletions(-) diff --git a/poetry.lock b/poetry.lock index b424654..7f3a871 100644 --- a/poetry.lock +++ b/poetry.lock @@ -368,54 +368,20 @@ files = [ [package.extras] test = ["tox"] -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] - -[[package]] -name = "backrefs" -version = "5.8" -description = "A wrapper around re and regex that adds additional back references." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, - {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, - {file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"}, - {file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"}, - {file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"}, - {file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"}, -] - -[package.extras] -extras = ["regex"] - [[package]] name = "boto3" -version = "1.38.11" +version = "1.38.12" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "boto3-1.38.11-py3-none-any.whl", hash = "sha256:dc3ff3a382d33d1e7e4141a5c63d084faf90657d6008ef917a3e64cfcbbb47e0"}, - {file = "boto3-1.38.11.tar.gz", hash = "sha256:ecf141395b8ede64dbc975c62a55fbb2231fc3e50904d65c088bc83ae98595cf"}, + {file = "boto3-1.38.12-py3-none-any.whl", hash = "sha256:9939b65b0bf04781f531245f110dd0ada6825f06cf9b95350efb830b9f69d214"}, + {file = "boto3-1.38.12.tar.gz", hash = "sha256:ca06315fdb20821fc1084a7b08557556eed97cb917a30ff19d8524b495383889"}, ] [package.dependencies] -botocore = ">=1.38.11,<1.39.0" +botocore = ">=1.38.12,<1.39.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.12.0,<0.13.0" @@ -424,14 +390,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.38.11" -description = "Type annotations for boto3 1.38.11 generated with mypy-boto3-builder 8.11.0" +version = "1.38.12" +description = "Type annotations for boto3 1.38.12 generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "boto3_stubs-1.38.11-py3-none-any.whl", hash = "sha256:cec7bc6d47be6ae2d77e7716c1521f4248cb484d83af0c4b0a50b8dd45ee9254"}, - {file = "boto3_stubs-1.38.11.tar.gz", hash = "sha256:427aea4639fb629b42d418e13972dcd156a113da042909207872eb801a8f09c3"}, + {file = "boto3_stubs-1.38.12-py3-none-any.whl", hash = "sha256:0f39ac6bdbaa5488e6480e654dfe31e832e44813fd45798324eea2924b6f3d60"}, + {file = "boto3_stubs-1.38.12.tar.gz", hash = "sha256:409498ca6c9e4ce9f32e11535217b97fac5cd413e560339f4001c026ebb79952"}, ] [package.dependencies] @@ -488,7 +454,7 @@ bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime ( bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"] billing = ["mypy-boto3-billing (>=1.38.0,<1.39.0)"] billingconductor = ["mypy-boto3-billingconductor (>=1.38.0,<1.39.0)"] -boto3 = ["boto3 (==1.38.11)"] +boto3 = ["boto3 (==1.38.12)"] braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"] budgets = ["mypy-boto3-budgets (>=1.38.0,<1.39.0)"] ce = ["mypy-boto3-ce (>=1.38.0,<1.39.0)"] @@ -852,14 +818,14 @@ xray = ["mypy-boto3-xray (>=1.38.0,<1.39.0)"] [[package]] name = "botocore" -version = "1.38.11" +version = "1.38.12" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "botocore-1.38.11-py3-none-any.whl", hash = "sha256:f5cb7a292774d4a212347475d95668bb96c1fe322e71c19bf871fb06503597a5"}, - {file = "botocore-1.38.11.tar.gz", hash = "sha256:44c5cb042fefedbe0b0ae4c3f96aed03f4cab6973d4317410c8d7c6d32807aef"}, + {file = "botocore-1.38.12-py3-none-any.whl", hash = "sha256:bcea44f3fe3a5bc18030656b8d32013d8b2d76b54433f591500a14bcac2e94ee"}, + {file = "botocore-1.38.12.tar.gz", hash = "sha256:86c459de3e39b418f4eb81e88c23fba02995496141db73816e7f65cb8b04408b"}, ] [package.dependencies] @@ -872,14 +838,14 @@ crt = ["awscrt (==0.23.8)"] [[package]] name = "botocore-stubs" -version = "1.38.11" +version = "1.38.12" description = "Type annotations and code completion for botocore" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "botocore_stubs-1.38.11-py3-none-any.whl", hash = "sha256:962c9eaeb7d1ae2287007d72eb2785cc92e58b10f53dbd749361556c7fa8e96c"}, - {file = "botocore_stubs-1.38.11.tar.gz", hash = "sha256:5898dd855ae152725d60ff48e1f400fc23d18fb0ff0b9bd4a45458af2c446ebe"}, + {file = "botocore_stubs-1.38.12-py3-none-any.whl", hash = "sha256:e25cda287d65f9460cce4f3489e3d9842a8920688cc8d0790bc0b5ed7ee5bc10"}, + {file = "botocore_stubs-1.38.12.tar.gz", hash = "sha256:d8656b6be20208fbbfd42fdee81b8c5374c8ae317a0046df6c155140a606a57e"}, ] [package.dependencies] @@ -888,56 +854,13 @@ types-awscrt = "*" [package.extras] botocore = ["botocore"] -[[package]] -name = "cairocffi" -version = "1.7.1" -description = "cffi-based cairo bindings for Python" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f"}, - {file = "cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b"}, -] - -[package.dependencies] -cffi = ">=1.1.0" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["numpy", "pikepdf", "pytest", "ruff"] -xcb = ["xcffib (>=1.4.0)"] - -[[package]] -name = "cairosvg" -version = "2.7.1" -description = "A Simple SVG Converter based on Cairo" -optional = false -python-versions = ">=3.5" -groups = ["docs"] -files = [ - {file = "CairoSVG-2.7.1-py3-none-any.whl", hash = "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b"}, - {file = "CairoSVG-2.7.1.tar.gz", hash = "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0"}, -] - -[package.dependencies] -cairocffi = "*" -cssselect2 = "*" -defusedxml = "*" -pillow = "*" -tinycss2 = "*" - -[package.extras] -doc = ["sphinx", "sphinx-rtd-theme"] -test = ["flake8", "isort", "pytest"] - [[package]] name = "certifi" version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, @@ -949,7 +872,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -1030,7 +953,7 @@ version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, @@ -1132,7 +1055,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -1160,7 +1083,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -1321,26 +1244,6 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] -[[package]] -name = "cssselect2" -version = "0.8.0" -description = "CSS selectors for Python ElementTree" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e"}, - {file = "cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a"}, -] - -[package.dependencies] -tinycss2 = "*" -webencodings = "*" - -[package.extras] -doc = ["furo", "sphinx"] -test = ["pytest", "ruff"] - [[package]] name = "datasets" version = "3.6.0" @@ -1434,18 +1337,6 @@ files = [ ] markers = {main = "extra == \"all\""} -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["docs"] -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 = "dill" version = "0.3.8" @@ -1792,39 +1683,6 @@ files = [ numpy = ">=1.17" tqdm = ">=4.27" -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "griffe" -version = "1.7.3" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75"}, - {file = "griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b"}, -] - -[package.dependencies] -colorama = ">=0.4" - [[package]] name = "h11" version = "0.16.0" @@ -2019,7 +1877,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -2249,7 +2107,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -2650,14 +2508,14 @@ pyyaml = "*" [[package]] name = "logfire-api" -version = "3.14.1" +version = "3.15.0" description = "Shim for the Logfire SDK which does nothing unless Logfire is installed" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "logfire_api-3.14.1-py3-none-any.whl", hash = "sha256:61f786457d712b4f0bf99486b67ce33d7a6576e77056e1d91862df353cb5f4ed"}, - {file = "logfire_api-3.14.1.tar.gz", hash = "sha256:513708709d843c36bcd4a909f2da589d7ff23970a225b76a11499090c82101e8"}, + {file = "logfire_api-3.15.0-py3-none-any.whl", hash = "sha256:1c928bfdae2b03085719a12d6d4269ed53bb7dc02d18ca6de43002d15e0ee117"}, + {file = "logfire_api-3.15.0.tar.gz", hash = "sha256:eabd4a673a4953ab1402db4457c12270d2e0d00ad449873161dd809b2cd6fd66"}, ] [[package]] @@ -2679,29 +2537,13 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] -[[package]] -name = "markdown" -version = "3.8" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, - {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, -] - -[package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] -testing = ["coverage", "pyyaml"] - [[package]] name = "markupsafe" version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -2783,14 +2625,14 @@ traitlets = "*" [[package]] name = "mcp" -version = "1.7.1" +version = "1.8.0" description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "mcp-1.7.1-py3-none-any.whl", hash = "sha256:f7e6108977db6d03418495426c7ace085ba2341b75197f8727f96f9cfd30057a"}, - {file = "mcp-1.7.1.tar.gz", hash = "sha256:eb4f1f53bd717f75dda8a1416e00804b831a8f3c331e23447a03b78f04b43a6e"}, + {file = "mcp-1.8.0-py3-none-any.whl", hash = "sha256:889d9d3b4f12b7da59e7a3933a0acadae1fce498bfcd220defb590aa291a1334"}, + {file = "mcp-1.8.0.tar.gz", hash = "sha256:263dfb700540b726c093f0c3e043f66aded0730d0b51f04eb0a3eb90055fe49b"}, ] [package.dependencies] @@ -2809,186 +2651,6 @@ cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] rich = ["rich (>=13.9.4)"] ws = ["websockets (>=15.0.1)"] -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -groups = ["docs"] -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, - {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -jinja2 = ">=2.11.1" -markdown = ">=3.3.6" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -mkdocs-get-deps = ">=0.2.0" -packaging = ">=20.5" -pathspec = ">=0.11.1" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.1" -description = "Automatically link across pages in MkDocs." -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f"}, - {file = "mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079"}, -] - -[package.dependencies] -Markdown = ">=3.3" -markupsafe = ">=2.0.1" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, - {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, -] - -[package.dependencies] -mergedeep = ">=1.3.4" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" - -[[package]] -name = "mkdocs-material" -version = "9.6.12" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs_material-9.6.12-py3-none-any.whl", hash = "sha256:92b4fbdc329e4febc267ca6e2c51e8501fa97b2225c5f4deb4d4e43550f8e61e"}, - {file = "mkdocs_material-9.6.12.tar.gz", hash = "sha256:add6a6337b29f9ea7912cb1efc661de2c369060b040eb5119855d794ea85b473"}, -] - -[package.dependencies] -babel = ">=2.10,<3.0" -backrefs = ">=5.7.post1,<6.0" -cairosvg = {version = ">=2.6,<3.0", optional = true, markers = "extra == \"imaging\""} -colorama = ">=0.4,<1.0" -jinja2 = ">=3.1,<4.0" -markdown = ">=3.2,<4.0" -mkdocs = ">=1.6,<2.0" -mkdocs-material-extensions = ">=1.3,<2.0" -paginate = ">=0.5,<1.0" -pillow = {version = ">=10.2,<11.0", optional = true, markers = "extra == \"imaging\""} -pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -requests = ">=2.26,<3.0" - -[package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -description = "Extension pack for Python Markdown and MkDocs Material." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, - {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, -] - -[[package]] -name = "mkdocs-section-index" -version = "0.3.10" -description = "MkDocs plugin to allow clickable sections that lead to an index page" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776"}, - {file = "mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8"}, -] - -[package.dependencies] -mkdocs = ">=1.2" - -[[package]] -name = "mkdocstrings" -version = "0.25.2" -description = "Automatic documentation from sources, for MkDocs." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocstrings-0.25.2-py3-none-any.whl", hash = "sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc"}, - {file = "mkdocstrings-0.25.2.tar.gz", hash = "sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc"}, -] - -[package.dependencies] -click = ">=7.0" -Jinja2 = ">=2.11.1" -Markdown = ">=3.3" -MarkupSafe = ">=1.1" -mkdocs = ">=1.4" -mkdocs-autorefs = ">=0.3.1" -platformdirs = ">=2.2.0" -pymdown-extensions = ">=6.3" - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=0.5.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "1.10.9" -description = "A Python handler for mkdocstrings." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "mkdocstrings_python-1.10.9-py3-none-any.whl", hash = "sha256:cbe98710a6757dfd4dff79bf36cb9731908fb4c69dd2736b15270ae7a488243d"}, - {file = "mkdocstrings_python-1.10.9.tar.gz", hash = "sha256:f344aaa47e727d8a2dc911e063025e58e2b7fb31a41110ccc3902aa6be7ca196"}, -] - -[package.dependencies] -griffe = ">=0.49" -mkdocs-autorefs = ">=1.0" -mkdocstrings = ">=0.25" - [[package]] name = "mpmath" version = "1.3.0" @@ -3660,14 +3322,14 @@ files = [ [[package]] name = "openai" -version = "1.77.0" +version = "1.78.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-1.77.0-py3-none-any.whl", hash = "sha256:07706e91eb71631234996989a8ea991d5ee56f0744ef694c961e0824d4f39218"}, - {file = "openai-1.77.0.tar.gz", hash = "sha256:897969f927f0068b8091b4b041d1f8175bcf124f7ea31bab418bf720971223bc"}, + {file = "openai-1.78.0-py3-none-any.whl", hash = "sha256:1ade6a48cd323ad8a7715e7e1669bb97a17e1a5b8a916644261aaef4bf284778"}, + {file = "openai-1.78.0.tar.gz", hash = "sha256:254aef4980688468e96cbddb1f348ed01d274d02c64c6c69b0334bf001fb62b3"}, ] [package.dependencies] @@ -3727,28 +3389,12 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] -[[package]] -name = "paginate" -version = "0.5.7" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, - {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, -] - -[package.extras] -dev = ["pytest", "tox"] -lint = ["black"] - [[package]] name = "pandas" version = "2.2.3" @@ -3868,18 +3514,6 @@ files = [ qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["docopt", "pytest"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -3900,9 +3534,10 @@ ptyprocess = ">=0.5" name = "pillow" version = "10.4.0" description = "Python Imaging Library (Fork)" -optional = false +optional = true python-versions = ">=3.8" -groups = ["main", "docs"] +groups = ["main"] +markers = "extra == \"all\"" files = [ {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, @@ -3985,7 +3620,6 @@ files = [ {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] -markers = {main = "extra == \"all\""} [package.extras] docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] @@ -4001,7 +3635,7 @@ version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -4407,7 +4041,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -4598,7 +4232,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["dev", "docs"] +groups = ["dev"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -4607,25 +4241,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pymdown-extensions" -version = "10.15" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f"}, - {file = "pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7"}, -] - -[package.dependencies] -markdown = ">=3.6" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.19.1)"] - [[package]] name = "pytest" version = "8.3.5" @@ -4674,7 +4289,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] 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"}, @@ -4755,7 +4370,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -4812,21 +4427,6 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -optional = false -python-versions = ">=3.6" -groups = ["docs"] -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[package.dependencies] -pyyaml = "*" - [[package]] name = "pyzmq" version = "26.4.0" @@ -5124,7 +4724,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -5298,7 +4898,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -5307,7 +4906,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -5316,7 +4914,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -5325,7 +4922,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -5334,7 +4930,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -5342,29 +4937,30 @@ files = [ [[package]] name = "ruff" -version = "0.1.15" +version = "0.10.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.10.0-py3-none-linux_armv6l.whl", hash = "sha256:46a2aa0eaae5048e5f804f0be9489d8a661633e23277b7293089e70d5c1a35c4"}, + {file = "ruff-0.10.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:775a6bc61af9dd0a2e1763406522a137e62aabb743d8b43ed95f019cdd1526c7"}, + {file = "ruff-0.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8b03e6fcd39d20f0004f9956f0ed5eadc404d3a299f9d9286323884e3b663730"}, + {file = "ruff-0.10.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:621101d1af80248827f2409a78c8177c8319986a57b4663613b9c72f8617bfcd"}, + {file = "ruff-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2dfe85cb6bfbd4259801e7d4982f2a72bdbd5749dc73a09d68a6dbf77f2209a"}, + {file = "ruff-0.10.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43ac3879a20c22fdc57e559f0bb27f0c71828656841d0b42d3505b1e5b3a83c8"}, + {file = "ruff-0.10.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ef5e3aac421bbc62f8a7aab21edd49a359ed42205f7a5091a74386bca1efa293"}, + {file = "ruff-0.10.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f4f62d7fac8b748fce67ad308116b4d4cc1a9f964b4804fc5408fbd06e13ba9"}, + {file = "ruff-0.10.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9f6205c5b0d626f98da01a0e75b724a64c21c554bba24b12522c9e9ba6a04"}, + {file = "ruff-0.10.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46a97f3d55f68464c48d1e929a8582c7e5bb80ac73336bbc7b0da894d8e6cd9e"}, + {file = "ruff-0.10.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0b811197d0dc96c13d610f8cfdc56030b405bcff5c2f10eab187b329da0ca4a"}, + {file = "ruff-0.10.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a13a3fda0870c1c964b47ff5d73805ae80d2a9de93ee2d185d453b8fddf85a84"}, + {file = "ruff-0.10.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6ceb8d9f062e90ddcbad929f6136edf764bbf6411420a07e8357602ea28cd99f"}, + {file = "ruff-0.10.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c41d07d573617ed2f287ea892af2446fd8a8d877481e8e1ba6928e020665d240"}, + {file = "ruff-0.10.0-py3-none-win32.whl", hash = "sha256:76e2de0cbdd587e373cd3b4050d2c45babdd7014c1888a6f121c29525c748a15"}, + {file = "ruff-0.10.0-py3-none-win_amd64.whl", hash = "sha256:f943acdecdcc6786a8d1dad455dd9f94e6d57ccc115be4993f9b52ef8316027a"}, + {file = "ruff-0.10.0-py3-none-win_arm64.whl", hash = "sha256:935a943bdbd9ff0685acd80d484ea91088e27617537b5f7ef8907187d19d28d0"}, + {file = "ruff-0.10.0.tar.gz", hash = "sha256:fa1554e18deaf8aa097dbcfeafaf38b17a2a1e98fdc18f50e62e8a836abee392"}, ] [[package]] @@ -5482,59 +5078,59 @@ tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc ( [[package]] name = "scipy" -version = "1.15.2" +version = "1.15.3" description = "Fundamental algorithms for scientific computing in Python" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"all\"" files = [ - {file = "scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9"}, - {file = "scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5"}, - {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ecf797d2d798cf7c838c6d98321061eb3e72a74710e6c40540f0e8087e3b499e"}, - {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:9b18aa747da280664642997e65aab1dd19d0c3d17068a04b3fe34e2559196cb9"}, - {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3"}, - {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d"}, - {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58"}, - {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa"}, - {file = "scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8bf5cb4a25046ac61d38f8d3c3426ec11ebc350246a4642f2f315fe95bda655"}, - {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e"}, - {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0"}, - {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40"}, - {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462"}, - {file = "scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20"}, - {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e"}, - {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8"}, - {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11"}, - {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53"}, - {file = "scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb"}, - {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27"}, - {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0"}, - {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32"}, - {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d"}, - {file = "scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af"}, - {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274"}, - {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776"}, - {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828"}, - {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28"}, - {file = "scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db"}, - {file = "scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}, + {file = "scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}, + {file = "scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}, + {file = "scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}, + {file = "scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}, + {file = "scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}, + {file = "scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}, ] [package.dependencies] @@ -5542,7 +5138,7 @@ numpy = ">=1.23.5,<2.5" [package.extras] dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] -doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.16.5)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] @@ -5615,7 +5211,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -5879,25 +5475,6 @@ requests = ">=2.26.0" [package.extras] blobfile = ["blobfile (>=2)"] -[[package]] -name = "tinycss2" -version = "1.4.0" -description = "A tiny CSS parser" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, - {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, -] - -[package.dependencies] -webencodings = ">=0.4" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["pytest", "ruff"] - [[package]] name = "tokenizers" version = "0.21.1" @@ -6351,7 +5928,7 @@ version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, @@ -6515,49 +6092,6 @@ files = [ [package.dependencies] torch = "2.4.0" -[[package]] -name = "watchdog" -version = "6.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -files = [ - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, - {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, - {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, - {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, - {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - [[package]] name = "watchfiles" version = "1.0.5" @@ -6655,18 +6189,6 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = false -python-versions = "*" -groups = ["docs"] -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - [[package]] name = "websockets" version = "13.1" @@ -7111,4 +6633,4 @@ tracing = [] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "4c7ad8761314553da361ee2d487b858a4fb914a75dcb56178dc093a0551712e0" +content-hash = "1b6ebf9f66fd10e8ece2d7d5f14c0f4ee6ca8d8e6a6132b7086a667cdcd0ed1e" diff --git a/pyproject.toml b/pyproject.toml index 6674f41..039acea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ logfire-api = "^3.1.1" jsonpath-ng = "^1.7.0" ruamel-yaml = "^0.18.10" jsonref = "^1.1.0" +mcp = "^1.5.0" vllm = { version = "^0.5.0", optional = true } transformers = { version = "^4.41.0", optional = true } @@ -37,7 +38,6 @@ click = { version = "^8.1.7", optional = true } httpx = { version = "^0.27.0", optional = true } aiodocker = { version = "^0.22.2", optional = true } websockets = { version = "^13.0", optional = true } -mcp = "^1.5.0" [tool.poetry.extras] tracing = ["logfire"] @@ -56,8 +56,8 @@ all = [ [tool.poetry.group.dev.dependencies] ipykernel = "^6.27.1" -mypy = "^1.8.0" -ruff = "^0.1.14" +mypy = "^1.15.0" +ruff = "^0.10.0" pytest = "^8.0.0" pandas-stubs = "^2.2.1.240316" coverage = "^7.5.1" @@ -66,15 +66,6 @@ pytest-asyncio = "^0.23.7" types-colorama = "^0.4.15.20240311" types-requests = "2.31.0.6" -[tool.poetry.group.docs.dependencies] -mkdocs = "^1.6.0" -mkdocs-material = { extras = ["imaging"], version = "^9.5.20" } -mkdocstrings = "^0.25.0" -mkdocstrings-python = "^1.10.0" -mkdocs-section-index = "^0.3.9" -pymdown-extensions = "^10.8.1" -pygments = "^2.18.0" - # Build [build-system] diff --git a/rigging/generator/transformers_.py b/rigging/generator/transformers_.py index 03ad0ce..6c9999c 100644 --- a/rigging/generator/transformers_.py +++ b/rigging/generator/transformers_.py @@ -2,11 +2,10 @@ import typing as t import torch -import transformers # type: ignore [import-untyped] -from transformers import ( +import transformers # type: ignore [import-untyped, unused-ignore] +from transformers import ( # type: ignore [attr-defined] AutoModelForCausalLM, AutoTokenizer, - PreTrainedTokenizer, TextGenerationPipeline, ) @@ -85,7 +84,7 @@ def llm(self) -> AutoModelForCausalLM: return self._llm @property - def tokenizer(self) -> PreTrainedTokenizer: + def tokenizer(self) -> AutoTokenizer: """The underlying `AutoTokenizer` instance.""" if self._tokenizer is None: self._tokenizer = AutoTokenizer.from_pretrained(self.model) @@ -95,19 +94,19 @@ def tokenizer(self) -> PreTrainedTokenizer: def pipeline(self) -> TextGenerationPipeline: """The underlying `TextGenerationPipeline` instance.""" if self._pipeline is None: - self._pipeline = transformers.pipeline( + self._pipeline = transformers.pipeline( # type: ignore [attr-defined, assignment] "text-generation", return_full_text=False, - model=self.llm, - tokenizer=self.tokenizer, + model=self.llm, # type: ignore [arg-type] + tokenizer=self.tokenizer, # type: ignore [arg-type] ) - return self._pipeline + return self._pipeline # type: ignore [return-value] @classmethod def from_obj( cls, - model: str, - tokenizer: PreTrainedTokenizer, + model: t.Any, + tokenizer: AutoTokenizer, *, pipeline: TextGenerationPipeline | None = None, params: GenerateParams | None = None, @@ -124,9 +123,9 @@ def from_obj( The TransformersGenerator instance. """ instance = cls(model=model, params=params or GenerateParams()) - instance._llm = model # noqa: SLF001 - instance._tokenizer = tokenizer # noqa: SLF001 - instance._pipeline = pipeline # noqa: SLF001 + instance._llm = model + instance._tokenizer = tokenizer + instance._pipeline = pipeline return instance def load(self) -> "TransformersGenerator": @@ -176,8 +175,8 @@ async def generate_messages( generated = [o.to_generated_message() for o in outputs] for i, (in_messages, out_message) in enumerate(zip(messages, generated, strict=False)): - trace_messages(in_messages, f"Messages {i+1}/{len(in_messages)}") - trace_messages([out_message], f"Response {i+1}/{len(in_messages)}") + trace_messages(in_messages, f"Messages {i + 1}/{len(in_messages)}") + trace_messages([out_message], f"Response {i + 1}/{len(in_messages)}") return generated @@ -189,7 +188,7 @@ async def generate_texts( generated = self._generate(texts, params) for i, (text, response) in enumerate(zip(texts, generated, strict=False)): - trace_str(text, f"Text {i+1}/{len(texts)}") - trace_str(response, f"Generated {i+1}/{len(texts)}") + trace_str(text, f"Text {i + 1}/{len(texts)}") + trace_str(response, f"Generated {i + 1}/{len(texts)}") return generated diff --git a/rigging/generator/vllm_.py b/rigging/generator/vllm_.py index 944b2f5..e13ebf4 100644 --- a/rigging/generator/vllm_.py +++ b/rigging/generator/vllm_.py @@ -91,7 +91,7 @@ def from_obj( The VLLMGenerator instance. """ generator = cls(model=model, params=params or GenerateParams()) - generator._llm = llm # noqa: SLF001 + generator._llm = llm return generator def load(self) -> "VLLMGenerator": @@ -158,17 +158,23 @@ async def generate_messages( params: t.Sequence[GenerateParams], ) -> t.Sequence[GeneratedMessage]: message_dicts = [[m.to_openai_spec() for m in _messages] for _messages in messages] - texts = self.llm.get_tokenizer().apply_chat_template( + tokenizer = self.llm.get_tokenizer() + if not hasattr(tokenizer, "apply_chat_template"): + raise RuntimeError( + "The tokenizer does not support the apply_chat_template method.", + ) + + texts = tokenizer.apply_chat_template( message_dicts, add_generation_prompt=True, tokenize=False, ) - generated_texts = self._generate(texts, params=params) + generated_texts = self._generate(t.cast("list[str]", texts), params=params) generated = [g.to_generated_message() for g in generated_texts] for i, (in_messages, out_message) in enumerate(zip(messages, generated, strict=False)): - trace_messages(in_messages, f"Messages {i+1}/{len(in_messages)}") - trace_messages([out_message], f"Response {i+1}/{len(in_messages)}") + trace_messages(in_messages, f"Messages {i + 1}/{len(in_messages)}") + trace_messages([out_message], f"Response {i + 1}/{len(in_messages)}") return generated @@ -180,7 +186,7 @@ async def generate_texts( generated = self._generate(list(texts), params=params) for i, (text, response) in enumerate(zip(texts, generated, strict=False)): - trace_str(text, f"Text {i+1}/{len(texts)}") - trace_str(response, f"Generated {i+1}/{len(texts)}") + trace_str(text, f"Text {i + 1}/{len(texts)}") + trace_str(response, f"Generated {i + 1}/{len(texts)}") return generated From b4fff186e9c28a31cdbe769192d97beca0f763af Mon Sep 17 00:00:00 2001 From: monoxgas Date: Thu, 8 May 2025 22:11:18 -0700 Subject: [PATCH 25/25] Linting fixes --- pyproject.toml | 4 ++- rigging/__init__.py | 68 +++++++++++++++++------------------ rigging/chat.py | 30 +++++++--------- rigging/completion.py | 8 ++--- rigging/data.py | 4 +-- rigging/error.py | 2 +- rigging/generator/__init__.py | 10 +++--- rigging/generator/base.py | 12 +++---- rigging/generator/http.py | 4 +-- rigging/interact.py | 2 +- rigging/message.py | 9 ++--- rigging/model.py | 8 ++--- rigging/parsing.py | 6 ++-- rigging/tool/__init__.py | 2 +- rigging/tool/base.py | 20 +++++------ rigging/tool/mcp.py | 8 ++--- rigging/watchers.py | 4 +-- tests/test_chat_pipeline.py | 16 ++++----- tests/test_prompt.py | 12 ------- tests/test_tool.py | 11 +++--- tests/test_watchers.py | 16 ++++----- 21 files changed, 115 insertions(+), 141 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 039acea..1b9f162 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,9 @@ strict = true target-version = "py310" line-length = 100 extend-exclude = [ - "*.ipynb", # jupyter notebooks + "*.ipynb", # jupyter notebooks + "examples/*", # example files + ".github/*", # github files ] [tool.ruff.lint] diff --git a/rigging/__init__.py b/rigging/__init__.py index dca33f2..03ff1fa 100644 --- a/rigging/__init__.py +++ b/rigging/__init__.py @@ -42,52 +42,52 @@ __version__ = VERSION __all__ = [ - "get_generator", - "Message", - "MessageDict", - "Messages", - "ContentText", - "ContentImageUrl", - "ContentAudioInput", - "Tool", - "Model", - "attr", - "element", - "wrapped", "Chat", "ChatPipeline", - "Generator", - "GenerateParams", - "GeneratedMessage", - "GeneratedText", - "chat", - "complete", "Completion", "CompletionPipeline", - "register_generator", - "prompt", - "Prompt", + "ContentAudioInput", + "ContentImageUrl", + "ContentText", "Ctx", - "data", - "watchers", - "model", - "error", - "parsing", - "tool", - "tool_method", - "logging", - "await_", - "interact", - "ThenChatCallback", + "GenerateParams", + "GeneratedMessage", + "GeneratedText", + "Generator", "MapChatCallback", - "ThenCompletionCallback", "MapCompletionCallback", + "Message", + "MessageDict", + "Messages", + "Model", "PipelineStep", - "PipelineStepGenerator", "PipelineStepContextManager", + "PipelineStepGenerator", + "Prompt", + "ThenChatCallback", + "ThenCompletionCallback", + "Tool", + "attr", + "await_", + "chat", + "complete", + "data", + "element", + "error", "generator", + "get_generator", + "interact", + "logging", "mcp", + "model", + "parsing", + "prompt", + "register_generator", "robopages", + "tool", + "tool_method", + "watchers", + "wrapped", ] from loguru import logger diff --git a/rigging/chat.py b/rigging/chat.py index 7b48bba..1ab091b 100644 --- a/rigging/chat.py +++ b/rigging/chat.py @@ -217,7 +217,7 @@ def message_dicts(self) -> list[MessageDict]: """ return [ t.cast( - MessageDict, + "MessageDict", m.model_dump(include={"role", "content_parts"}, exclude_none=True), ) for m in self.all @@ -409,7 +409,7 @@ def inject_tool_prompt( tool_system_prompt = tool_description_prompt_part( definitions, - t.cast(t.Literal["xml", "json-in-xml"], mode), + t.cast("t.Literal['xml', 'json-in-xml']", mode), ) return self.inject_system_content(tool_system_prompt) @@ -524,8 +524,7 @@ def __call__( self, chat: Chat, /, - ) -> t.Awaitable[Chat | None]: - ... + ) -> t.Awaitable[Chat | None]: ... @runtime_checkable @@ -534,8 +533,7 @@ def __call__( self, chat: Chat, /, - ) -> "PipelineStepGenerator | PipelineStepContextManager | t.Awaitable[PipelineStepGenerator | PipelineStepContextManager | None]": - ... + ) -> "PipelineStepGenerator | PipelineStepContextManager | t.Awaitable[PipelineStepGenerator | PipelineStepContextManager | None]": ... ThenChatCallback = _ThenChatCallback | _ThenChatStepCallback @@ -550,8 +548,7 @@ def __call__( self, chats: list[Chat], /, - ) -> t.Awaitable[list[Chat]]: - ... + ) -> t.Awaitable[list[Chat]]: ... @runtime_checkable @@ -560,8 +557,7 @@ def __call__( self, chats: list[Chat], /, - ) -> "PipelineStepGenerator | PipelineStepContextManager | t.Awaitable[PipelineStepGenerator | PipelineStepContextManager]": - ... + ) -> "PipelineStepGenerator | PipelineStepContextManager | t.Awaitable[PipelineStepGenerator | PipelineStepContextManager]": ... MapChatCallback = _MapChatCallback | _MapChatStepCallback @@ -658,9 +654,10 @@ def depth(self) -> int: This is useful for setting constraints on recursion depth. """ depth = 0 - while self.parent is not None: + current = self + while current.parent is not None: depth += 1 - self = self.parent + current = current.parent return depth @@ -852,8 +849,7 @@ def add( and self.chat.all[-1].role == message_list[0].role and ( merge_strategy == "all" - or merge_strategy == "only-user-role" - and self.chat.all[-1].role == "user" + or (merge_strategy == "only-user-role" and self.chat.all[-1].role == "user") ) ): self.chat.all[-1].content_parts += message_list[0].content_parts @@ -1263,7 +1259,7 @@ async def _then_tools(self, chat: Chat) -> PipelineStepContextManager | None: # Parse the actual tool calls - tool_calls: (list[ApiToolCall] | list[XmlToolCall] | list[JsonInXmlToolCall] | None) = None + tool_calls: list[ApiToolCall] | list[XmlToolCall] | list[JsonInXmlToolCall] | None = None if self.tool_mode == "api": tool_calls = chat.last.tool_calls if self.tool_mode == "xml": @@ -1435,7 +1431,7 @@ async def complete() -> None: ) generator = t.cast( - PipelineStepGenerator, + "PipelineStepGenerator", await exit_stack.enter_async_context(aclosing(result)), ) async for step in generator: @@ -1675,7 +1671,7 @@ async def _step( # noqa: PLR0915, PLR0912 if inspect.isasyncgen(chats_or_generator): generator = t.cast( - PipelineStepGenerator, + "PipelineStepGenerator", await exit_stack.enter_async_context( aclosing(chats_or_generator), ), diff --git a/rigging/completion.py b/rigging/completion.py index 6122298..a073aa0 100644 --- a/rigging/completion.py +++ b/rigging/completion.py @@ -263,7 +263,7 @@ def __init__( ExhuastedMaxRounds is implicitly included. """ - self.on_failed: "FailMode" = "raise" + self.on_failed: FailMode = "raise" """How to handle failures in the pipeline unless overriden in calls.""" # (callback, all_text, max_rounds) @@ -747,7 +747,7 @@ async def _run( # noqa: PLR0912 raise inbound # noqa: TRY301 inbounds = [inbound for inbound in _inbounds if isinstance(inbound, GeneratedText)] - except Exception as e: # noqa: BLE001 + except Exception as e: if on_failed == "raise" or not any( isinstance(e, t) for t in self.errors_to_fail_on ): @@ -768,14 +768,14 @@ async def _run( # noqa: PLR0912 state.processor.send(inbound.text) continue except StopIteration as stop: - output = t.cast(str, stop.value) + output = t.cast("str", stop.value) except CompletionExhaustedMaxRoundsError as exhausted: if on_failed == "raise": raise output = exhausted.completion failed = True error = exhausted - except Exception as e: # noqa: BLE001 + except Exception as e: if on_failed == "raise" or not any( isinstance(e, t) for t in self.errors_to_fail_on ): diff --git a/rigging/data.py b/rigging/data.py index 21071dc..f3ac787 100644 --- a/rigging/data.py +++ b/rigging/data.py @@ -297,7 +297,7 @@ def elastic_data_to_chats( while all(hasattr(data, attr) for attr in ("keys", "__getitem__")) and "hits" in data: data = data["hits"] - objects = t.cast(t.Sequence[t.Mapping[str, t.Any]], data) + objects = t.cast("t.Sequence[t.Mapping[str, t.Any]]", data) if not isinstance(objects, t.Sequence): raise TypeError( f"Expected to find a sequence of objects (optionally under hits), found: {type(data)}", @@ -341,7 +341,7 @@ async def elastic_to_chats( A pandas DataFrame containing the chat data. """ data = await client.search(index=index, query=query, size=max_results, **kwargs) - return elastic_data_to_chats(t.cast(dict[str, t.Any], data)) + return elastic_data_to_chats(t.cast("dict[str, t.Any]", data)) async def s3_bucket_exists(client: S3Client, bucket: str) -> bool: diff --git a/rigging/error.py b/rigging/error.py index 350c1de..cc0f5b5 100644 --- a/rigging/error.py +++ b/rigging/error.py @@ -122,7 +122,7 @@ def _raise_as(func: t.Callable[P, R]) -> t.Callable[P, R]: def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: try: return func(*args, **kwargs) - except Exception as e: # noqa: BLE001 + except Exception as e: error = error_type(message) raise error from e diff --git a/rigging/generator/__init__.py b/rigging/generator/__init__.py index 225836d..8acf3a8 100644 --- a/rigging/generator/__init__.py +++ b/rigging/generator/__init__.py @@ -54,19 +54,19 @@ def get_transformers_lazy() -> type[Generator]: register_generator("transformers", get_transformers_lazy) __all__ = [ - "get_generator", - "Generator", "GenerateParams", "GeneratedMessage", "GeneratedText", + "Generator", + "HTTPGenerator", + "LiteLLMGenerator", "StopReason", "Usage", "chat", "complete", "get_generator", - "register_generator", + "get_generator", "get_identifier", - "LiteLLMGenerator", - "HTTPGenerator", + "register_generator", # TODO: We can't add VLLM and Transformers here because they are lazy loaded ] diff --git a/rigging/generator/base.py b/rigging/generator/base.py index 8795421..da42c88 100644 --- a/rigging/generator/base.py +++ b/rigging/generator/base.py @@ -663,7 +663,7 @@ def get_identifier(generator: Generator, params: GenerateParams | None = None) - return identifier -def get_generator(identifier: str, *, params: GenerateParams | None = None) -> Generator: # noqa: PLR0912 +def get_generator(identifier: str, *, params: GenerateParams | None = None) -> Generator: """ Get a generator by an identifier string. Uses LiteLLM by default. @@ -708,24 +708,24 @@ def get_generator(identifier: str, *, params: GenerateParams | None = None) -> G if "!" in identifier: try: provider, model = identifier.split("!") - except Exception as e: # noqa: BLE001 + except Exception as e: raise InvalidModelSpecifiedError(identifier) from e if provider not in g_providers: raise InvalidModelSpecifiedError(identifier) if not isinstance(g_providers[provider], type): - lazy_generator = t.cast(LazyGenerator, g_providers[provider]) + lazy_generator = t.cast("LazyGenerator", g_providers[provider]) g_providers[provider] = lazy_generator() - generator_cls = t.cast(type[Generator], g_providers[provider]) + generator_cls = t.cast("type[Generator]", g_providers[provider]) kwargs = {} if "," in model: try: model, kwargs_str = model.split(",", 1) kwargs = dict(arg.split("=", 1) for arg in kwargs_str.split(",")) - except Exception as e: # noqa: BLE001 + except Exception as e: raise InvalidModelSpecifiedError(identifier) from e # See if any of the kwargs would apply to the cls constructor directly @@ -753,7 +753,7 @@ def get_generator(identifier: str, *, params: GenerateParams | None = None) -> G try: merged_params = GenerateParams(**kwargs).merge_with(params) - except Exception as e: # noqa: BLE001 + except Exception as e: raise InvalidModelSpecifiedError(identifier) from e return generator_cls(model=model, params=merged_params, **init_kwargs) diff --git a/rigging/generator/http.py b/rigging/generator/http.py index 4c366f5..da5a107 100644 --- a/rigging/generator/http.py +++ b/rigging/generator/http.py @@ -33,12 +33,12 @@ def _to_str(v: str | dict[str, t.Any]) -> str: def _to_dict(v: str | dict[str, t.Any]) -> dict[str, t.Any]: - return t.cast(dict[str, t.Any], json.loads(v)) if isinstance(v, str) else v + return t.cast("dict[str, t.Any]", json.loads(v)) if isinstance(v, str) else v def _to_dict_or_str(v: str) -> dict[str, t.Any] | str: try: - return t.cast(dict[str, t.Any], json.loads(v)) + return t.cast("dict[str, t.Any]", json.loads(v)) except json.JSONDecodeError: return v diff --git a/rigging/interact.py b/rigging/interact.py index f65a2a1..dca97f8 100644 --- a/rigging/interact.py +++ b/rigging/interact.py @@ -87,7 +87,7 @@ async def interact( else: pipeline.add(user_input) - print("") + print() animation_task = asyncio.create_task(_animate()) chat = await pipeline.run() diff --git a/rigging/message.py b/rigging/message.py index edc55bb..def10a6 100644 --- a/rigging/message.py +++ b/rigging/message.py @@ -273,7 +273,7 @@ def from_file( raise ValueError( f"Could not determine format for file '{file}', please provide one", ) - format = t.cast(ContentAudioFormat, mimetype.split("/")[-1]) # noqa: A001 + format = t.cast("ContentAudioFormat", mimetype.split("/")[-1]) # noqa: A001 encoded = base64.b64encode(file.read_bytes()).decode() return cls(input_audio=cls.Audio(data=encoded, format=format, transcript=transcript)) @@ -904,12 +904,7 @@ def from_model( @classmethod def fit_as_list( cls, - messages: t.Sequence[MessageDict] - | t.Sequence["Message"] - | MessageDict - | "Message" - | Content - | str, + messages: "t.Sequence[MessageDict] | t.Sequence[Message] | MessageDict | Message | Content | str", ) -> list["Message"]: """Helper function to convert various common types to a strict list of Message objects.""" if isinstance(messages, (Message, dict, str, *ContentTypes)): diff --git a/rigging/model.py b/rigging/model.py index cc7a198..e75e75d 100644 --- a/rigging/model.py +++ b/rigging/model.py @@ -7,7 +7,7 @@ import inspect import re import typing as t -from xml.etree import ElementTree as ET # noqa: N817 +from xml.etree import ElementTree as ET import typing_extensions as te import xmltodict # type: ignore [import-untyped] @@ -63,7 +63,7 @@ def __get__(self, _: t.Any, owner: t.Any) -> str: # ... # if "[" in cls.__name__: - return t.cast(str, parent.__xml_tag__) + return t.cast("str", parent.__xml_tag__) return to_xml_tag(cls.__name__) @@ -236,7 +236,7 @@ def xml_example(cls) -> str: schema = cls.model_json_schema() properties = schema["properties"] - structure = {cls.__xml_tag__: {field: None for field in properties}} + structure = {cls.__xml_tag__: dict.fromkeys(properties)} xml_string = xmltodict.unparse( structure, pretty=True, @@ -244,7 +244,7 @@ def xml_example(cls) -> str: indent=" ", short_empty_elements=True, ) - return t.cast(str, xml_string) # Bad type hints in xmltodict + return t.cast("str", xml_string) # Bad type hints in xmltodict @classmethod def ensure_valid(cls) -> None: diff --git a/rigging/parsing.py b/rigging/parsing.py index d58ba12..dc7e4f8 100644 --- a/rigging/parsing.py +++ b/rigging/parsing.py @@ -129,14 +129,14 @@ def try_parse_many( MissingModelError: If a model type is missing and `fail_on_missing` is True. Exception: If the model is malformed and `fail_on_missing` is True. """ - model: "ModelT" - parsed: list[tuple["ModelT", slice]] = [] + model: ModelT + parsed: list[tuple[ModelT, slice]] = [] try: for model_class in types: for model, slice_ in model_class.from_text(text): parsed.append((model, slice_)) - except Exception: # noqa: BLE001 + except Exception: if fail_on_missing: raise diff --git a/rigging/tool/__init__.py b/rigging/tool/__init__.py index 26877fb..d21b676 100644 --- a/rigging/tool/__init__.py +++ b/rigging/tool/__init__.py @@ -9,8 +9,8 @@ __all__ = [ "Tool", - "robopages", "mcp", + "robopages", "tool", "tool_method", ] diff --git a/rigging/tool/base.py b/rigging/tool/base.py index 79a962b..55e4844 100644 --- a/rigging/tool/base.py +++ b/rigging/tool/base.py @@ -235,7 +235,7 @@ def model(self) -> type[Model]: if self._signature else make_from_schema(self.parameters_schema, "params") ) - except Exception as e: # noqa: BLE001 + except Exception as e: raise ToolDefinitionError( f"Failed to create model for tool '{self.name}'. " "This is likely due to constraints on arguments when the `xml` tool mode is used.", @@ -258,7 +258,7 @@ def json_definition(self) -> JsonInXmlToolDefinition: parameters=json.dumps(self.parameters_schema), ) - async def handle_tool_call( + async def handle_tool_call( # noqa: PLR0912 self, tool_call: ApiToolCall | XmlToolCall | JsonInXmlToolCall, ) -> tuple["Message", bool]: @@ -305,7 +305,7 @@ async def handle_tool_call( + tool_call_parameters + self.model.xml_end_tag(), ) - except Exception as e: # noqa: BLE001 + except Exception as e: raise ValueError( f"Failed to parse parameters from:\n{tool_call_parameters}", ) from e @@ -334,7 +334,7 @@ async def handle_tool_call( result: t.Any = self.fn(**kwargs) # type: ignore [call-arg] if inspect.isawaitable(result): result = await result - except Exception as e: # noqa: BLE001 + except Exception as e: if self.catch is True or ( not isinstance(self.catch, bool) and isinstance(e, tuple(self.catch)) ): @@ -410,16 +410,14 @@ def tool( description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, truncate: int | None = None, -) -> t.Callable[[t.Callable[P, R]], Tool[P, R]]: - ... +) -> t.Callable[[t.Callable[P, R]], Tool[P, R]]: ... @t.overload def tool( func: t.Callable[P, R], /, -) -> Tool[P, R]: - ... +) -> Tool[P, R]: ... def tool( @@ -513,16 +511,14 @@ def tool_method( description: str | None = None, catch: bool | t.Iterable[type[Exception]] = False, truncate: int | None = None, -) -> t.Callable[[t.Callable[t.Concatenate[t.Any, P], R]], ToolMethod[P, R]]: - ... +) -> t.Callable[[t.Callable[t.Concatenate[t.Any, P], R]], ToolMethod[P, R]]: ... @t.overload def tool_method( func: t.Callable[t.Concatenate[t.Any, P], R], /, -) -> ToolMethod[P, R]: - ... +) -> ToolMethod[P, R]: ... def tool_method( diff --git a/rigging/tool/mcp.py b/rigging/tool/mcp.py index 7fa286b..7bf25ce 100644 --- a/rigging/tool/mcp.py +++ b/rigging/tool/mcp.py @@ -126,10 +126,10 @@ async def __aenter__(self) -> "MCPClient": try: if self.transport == "stdio": self._session = await self._connect_via_stdio( - t.cast(StdioConnection, self.connection), + t.cast("StdioConnection", self.connection), ) elif self.transport == "sse": - self._session = await self._connect_via_sse(t.cast(SSEConnection, self.connection)) + self._session = await self._connect_via_sse(t.cast("SSEConnection", self.connection)) else: raise TypeError( # noqa: TRY301 f"Unsupported transport: {self.transport}. Must be 'stdio' or 'sse'", @@ -183,7 +183,6 @@ def mcp( ) ``` """ - ... @t.overload @@ -218,8 +217,7 @@ def mcp( ) ``` """ - ... def mcp(transport: Transport, **connection: t.Any) -> MCPClient: - return MCPClient(transport, t.cast(StdioConnection | SSEConnection, connection)) + return MCPClient(transport, t.cast("StdioConnection | SSEConnection", connection)) diff --git a/rigging/watchers.py b/rigging/watchers.py index 52346ce..915a627 100644 --- a/rigging/watchers.py +++ b/rigging/watchers.py @@ -42,7 +42,7 @@ async def _write_chats_to_jsonl(chats: "list[Chat]") -> None: Path.unlink(file) replaced = True - with file.open("a") as f: + with file.open("a") as f: # noqa: ASYNC230 for chat in chats: f.write(chat.model_dump_json(exclude_none=True) + "\n") @@ -71,7 +71,7 @@ async def _write_messages_to_jsonl(chats: "list[Chat]") -> None: Path.unlink(file) replaced = True - with file.open("a") as f: + with file.open("a") as f: # noqa: ASYNC230 for chat in flatten_chats(chats): f.write(json.dumps(chat) + "\n") diff --git a/tests/test_chat_pipeline.py b/tests/test_chat_pipeline.py index 3622c65..96f6d69 100644 --- a/tests/test_chat_pipeline.py +++ b/tests/test_chat_pipeline.py @@ -17,7 +17,7 @@ # ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_basic_chat_pipeline() -> None: generator = FixedGenerator( model="fixed", @@ -35,7 +35,7 @@ async def test_basic_chat_pipeline() -> None: assert not chat.failed -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_chat_until_parsed_as_basic() -> None: generator = FixedGenerator( model="fixed", @@ -56,7 +56,7 @@ async def test_chat_until_parsed_as_basic() -> None: assert not chat.failed -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_max_depth_limit() -> None: max_depth = 3 call_count = 0 @@ -79,7 +79,7 @@ async def recursive_callback(chat: Chat) -> PipelineStepContextManager: assert call_count == max_depth + 1 # One initial call + max_depth recursive calls -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_pipeline_steps() -> None: should_continue = True @@ -115,7 +115,7 @@ async def callback(chat: Chat) -> PipelineStepContextManager | None: assert all_steps[0].chats[0].last.content == "test response" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_chat_pipeline_error_handling() -> None: generator = FailingGenerator(model="failing", params=GenerateParams()) @@ -147,7 +147,7 @@ async def test_chat_pipeline_error_handling() -> None: assert len(chat_skip) == 0 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_parsing_with_recovery() -> None: response_sequence = [ "Invalid response", # First response that fails to parse @@ -174,7 +174,7 @@ def get_next_response(self: CallbackGenerator, messages: t.Sequence[Message]) -> assert len(chat.all) > 2 # Should have more than just the initial Q&A due to recovery attempts -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_map_callback() -> None: generator = FixedGenerator(model="fixed", text="Response 1", params=GenerateParams()) @@ -196,7 +196,7 @@ async def double_chats(chats: list[Chat]) -> list[Chat]: assert chats[1].last.content == "Modified: Response 1" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_watch_callback() -> None: generator = FixedGenerator(model="fixed", text="Response", params=GenerateParams()) diff --git a/tests/test_prompt.py b/tests/test_prompt.py index eb9ef42..5c446c9 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -15,7 +15,6 @@ def test_prompt_render_docstring_parse() -> None: @rg.prompt async def foo(name: str) -> str: """Say hello.""" - ... assert foo.docstring == "Say hello." @@ -23,7 +22,6 @@ async def foo(name: str) -> str: async def bar(name: str) -> str: """ Say hello.""" - ... assert bar.docstring == "Say hello." @@ -34,7 +32,6 @@ async def baz(name: str) -> str: hello. """ - ... assert baz.docstring == "Say hello." @@ -43,7 +40,6 @@ def test_basic_prompt_render() -> None: @rg.prompt async def hello(name: str) -> str: """Say hello.""" - ... rendered = hello.render("Alice") assert rendered == dedent( @@ -63,7 +59,6 @@ def test_prompt_render_with_docstring_variables() -> None: @rg.prompt async def greet(name: str, greeting: str = "Hello") -> str: """Say '{{ greeting }}' to {{ name }}.""" - ... rendered = greet.render("Bob") assert rendered == dedent( @@ -85,7 +80,6 @@ class Person(rg.Model): @rg.prompt async def create_person(name: str, age: int) -> Person: """Create a person.""" - ... rendered = create_person.render("Alice", 30) assert rendered == dedent( @@ -110,7 +104,6 @@ def test_prompt_render_with_list_output() -> None: @rg.prompt async def generate_numbers(count: int) -> list[int]: """Generate a list of numbers.""" - ... rendered = generate_numbers.render(5) assert rendered == dedent( @@ -130,7 +123,6 @@ def test_prompt_render_with_tuple_output() -> None: @rg.prompt async def create_user(username: str) -> tuple[str, int]: """Create a new user.""" - ... rendered = create_user.render("johndoe") assert rendered == dedent( @@ -152,7 +144,6 @@ def test_prompt_render_with_tuple_output_ctx() -> None: @rg.prompt async def create_user(username: str) -> tuple[Annotated[str, rg.Ctx(tag="id")], int]: """Create a new user.""" - ... rendered = create_user.render("johndoe") assert rendered == dedent( @@ -180,7 +171,6 @@ class User: @rg.prompt async def register_user(username: str, email: str, age: int) -> User: """Register a new user: {{ username}}.""" - ... rendered = register_user.render("johndoe", "johndoe@example.com", 25) assert rendered == dedent( @@ -206,7 +196,6 @@ def test_prompt_render_with_chat_return() -> None: @rg.prompt async def foo(input_: str) -> Chat: """Do something.""" - ... rendered = foo.render("bar") assert rendered == dedent( @@ -228,7 +217,6 @@ class User: @rg.prompt async def register_user(username: str, email: str, age: int) -> User: """Register a new user: {{ username }}.""" - ... rendered = register_user.render("johndoe", "john@email.com", 30) assert rendered == dedent( diff --git a/tests/test_tool.py b/tests/test_tool.py index e3bb657..7919893 100644 --- a/tests/test_tool.py +++ b/tests/test_tool.py @@ -196,7 +196,7 @@ def config_function(name: str, version: int, features: list[str] = []) -> dict[s class TestToolHandleCall: """Test suite for tool call handling.""" - @pytest.fixture() + @pytest.fixture def sample_tool(self) -> Tool[..., t.Any]: def calculator(a: int, b: int, operation: str = "add") -> int: """Perform math operations.""" @@ -210,7 +210,7 @@ def calculator(a: int, b: int, operation: str = "add") -> int: return Tool.from_callable(calculator) - @pytest.mark.asyncio() + @pytest.mark.asyncio async def test_handle_api_tool_call(self, sample_tool: Tool[..., t.Any]) -> None: """Test handling API format tool calls.""" from rigging.tool.api import ApiFunctionCall, ApiToolCall @@ -231,7 +231,7 @@ async def test_handle_api_tool_call(self, sample_tool: Tool[..., t.Any]) -> None assert message.tool_call_id == "call123" assert message.content == "15" - @pytest.mark.asyncio() + @pytest.mark.asyncio async def test_handle_xml_tool_call(self, sample_tool: Tool[..., t.Any]) -> None: """Test handling XML format tool calls.""" tool_call = XmlToolCall( @@ -252,7 +252,7 @@ async def test_handle_xml_tool_call(self, sample_tool: Tool[..., t.Any]) -> None assert message.role == "user" assert message.content == '8' - @pytest.mark.asyncio() + @pytest.mark.asyncio async def test_handle_json_xml_tool_call(self, sample_tool: Tool[..., t.Any]) -> None: """Test handling JSON-in-XML format tool calls.""" tool_call = JsonInXmlToolCall( @@ -312,7 +312,6 @@ def test_prompt_integration() -> None: @rg.prompt async def generate_greeting(name: str, formal: bool = False) -> str: # type: ignore [empty-body] """Generate a greeting for the user.""" - ... tool = Tool.from_callable(generate_greeting) @@ -355,7 +354,7 @@ def process_settings(settings: UserSettings) -> dict[str, t.Any]: Tool.from_callable(process_settings).xml_definition # noqa: B018 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_tool_error_catching() -> None: """Test that errors in tool functions are caught and reported.""" diff --git a/tests/test_watchers.py b/tests/test_watchers.py index f662e1a..ce18453 100644 --- a/tests/test_watchers.py +++ b/tests/test_watchers.py @@ -11,7 +11,7 @@ # ruff: noqa: S101, PLR2004, ARG001, PT011, SLF001, FBT001, FBT002, N803 -@pytest.fixture() +@pytest.fixture def sample_chats() -> list[Chat]: chat1 = Chat( messages=[ @@ -28,7 +28,7 @@ def sample_chats() -> list[Chat]: return [chat1, chat2] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_write_chats_to_jsonl(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" watcher = rg.watchers.write_chats_to_jsonl(output_file) @@ -46,12 +46,12 @@ async def test_write_chats_to_jsonl(tmp_path: Path, sample_chats: list[Chat]) -> for i, line in enumerate(lines): saved_chat = json.loads(line) original_chat = sample_chats[i] - for i, message in enumerate(saved_chat["messages"]): - assert message["role"] == original_chat.messages[i].role - assert message["content"] == original_chat.messages[i].content + for k, message in enumerate(saved_chat["messages"]): + assert message["role"] == original_chat.messages[k].role + assert message["content"] == original_chat.messages[k].content -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_write_chats_to_jsonl_append(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" watcher = rg.watchers.write_chats_to_jsonl(output_file) @@ -67,7 +67,7 @@ async def test_write_chats_to_jsonl_append(tmp_path: Path, sample_chats: list[Ch assert len(lines) == 2 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_write_chats_to_jsonl_replace(tmp_path: Path, sample_chats: list[Chat]) -> None: output_file = tmp_path / "chats.jsonl" @@ -147,7 +147,7 @@ def put_object(self, Bucket: str, Key: str, Body: str) -> None: self.buckets[Bucket][Key] = Body -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_write_chats_to_s3(sample_chats: list[Chat]) -> None: s3_mock_client = MockS3Client()

?Y^ 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 list[str]: - """Provide famous authors.""" - -print(await get_authors()) - -# ['William Shakespeare', 'J.K. Rowling', 'Jane Austen'] -``` - -Rigging is built by [**dreadnode**](https://dreadnode.io) where we use it daily. - -## Installation - -We publish every version to Pypi: -```bash -pip install rigging -``` - -If you want all the extras (vLLM, transformers, examples), just specify the `all` extra: -```bash -pip install rigging[all] -``` - -If you want to build from source: -```bash -cd rigging/ -poetry install -``` - -## Migration Guides - -- **[Migrating from v1.x to v2.x](topics/migrations.md#migrating-from-v1x-to-v2x)** - -## Getting Started - -Rigging is a flexible library built on other flexible libraries. As such it might take a bit to warm -up to it's interfaces provided the many ways you can accomplish your goals. However, the code is well documented -and topic pages and source are a great places to step in/out of as you explore. - -??? tip "IDE Setup" - - Rigging has been built with full type support which provides clear guidance on what - methods return what types, and when they return those types. It's recommended that you - operate in a development environment which can take advantage of this information. - Rigging will almost "fall" into place and you won't be guessing about - objects as you work. - -### Basic Chats - -Let's start with a very basic generation example that doesn't include any parsing features, continuations, etc. -You want to chat with a model and collect it's response. - -We first need to get a [generator][rigging.generator.Generator] object. We'll use -[`get_generator`][rigging.generator.get_generator] which will resolve an identifier string -to the underlying generator class object. - -??? note "API Keys" - - The default Rigging generator is [LiteLLM][rigging.generator.LiteLLMGenerator], which - wraps a large number of providers and models. We assume for these examples that you - have API tokens set as environment variables for these models. You can refer to the - [LiteLLM docs](https://docs.litellm.ai/docs/) for supported providers and their key format. - If you'd like, you can change any of the model IDs we use and/or add `,api_key=[sk-1234]` to the - end of any of the generator IDs to specify them inline. - -```py hl_lines="3" -import rigging as rg # (1)! - -generator = rg.get_generator("claude-3-sonnet-20240229") # (2)! -pipeline = generator.chat( - [ - {"role": "system", "content": "You are a wizard harry."}, - {"role": "user", "content": "Say hello!"}, - ] -) -chat = await pipeline.run() # (3)! -print(chat.all) -# [ -# Message(role='system', parts=[], content='You are a wizard harry.'), -# Message(role='user', parts=[], content='Say hello!'), -# ] -``` - -1. You'll see us use this shorthand import syntax throughout our code, it's - totally optional but makes things look nice. -2. This is actually shorthand for `litellm!anthropic/claude-3-sonnet-20240229`, where `litellm` - is the provider. We just default to that generator and you don't have to be explicit. You - can find more information about this in the [generators](topics/generators.md) docs. -3. From version 2 onwards, Rigging is fully async. You can use `await` to trigger generation - and get your results, or use [`await_`][rigging.util.await_]. - - -Generators have an easy [`chat()`][rigging.generator.Generator.chat] method which you'll -use to initiate the conversations. You can supply messages in many different forms from -dictionary objects, full [`Message`][rigging.message.Message] classes, or a simple `str` -which will be converted to a user message. - -```py hl_lines="4-9" -import rigging as rg - -generator = rg.get_generator("claude-3-sonnet-20240229") -pipeline = generator.chat( # (1)! - [ - {"role": "system", "content": "You are a wizard harry."}, - {"role": "user", "content": "Say hello!"}, - ] -) -chat = await pipeline.run() -print(chat.all) -# [ -# Message(role='system', parts=[], content='You are a wizard harry.'), -# Message(role='user', parts=[], content='Say hello!'), -# Message(role='assistant', parts=[], content='Hello! How can I help you today?'), -# ] -``` - -1. [`generator.chat`][rigging.generator.Generator.chat] is actually just a helper for - [`chat(generator, ...)`][rigging.generator.chat], they do the same thing. - -??? note "ChatPipeline vs Chat" - - You'll notice we name the result of `chat()` as `pipeline`. The naming might be confusing, - but chats go through 2 phases. We first stage them into a pipeline, where we operate - and prepare them before we actually trigger generation with `run()`. - - Calling `.chat()` doesn't trigger any generation, but calling any of these run methods will: - - - [rigging.chat.ChatPipeline.run][] - - [rigging.chat.ChatPipeline.run_many][] - - [rigging.chat.ChatPipeline.run_batch][] - - [rigging.chat.ChatPipeline.run_over][] - -In this case, we have nothing additional we want to add to our chat pipeline, and we are only interested -in generating exactly one response message. We simply call [`.run()`][rigging.chat.ChatPipeline.run] to -execute the generation process and collect our final [`Chat`][rigging.chat.Chat] object. - -```py hl_lines="10-11" -import rigging as rg - -generator = rg.get_generator("claude-3-sonnet-20240229") -pipeline = generator.chat( - [ - {"role": "system", "content": "You are a wizard harry."}, - {"role": "user", "content": "Say hello!"}, - ] -) -chat = await pipeline.run() -print(chat.all) -# [ -# Message(role='system', parts=[], content='You are a wizard harry.'), -# Message(role='user', parts=[], content='Say hello!'), -# Message(role='assistant', parts=[], content='Hello! How can I help you today?'), -# ] -``` - -View more about Chat objects and their properties [over here][rigging.chat.Chat]. In general, chats -give you access to exactly what messages were passed into a model, and what came out the other side. - -### Prompts - -Operating chat pipelines manually is very flexible, but can feel a bit verbose. Rigging supports -the concept of "prompt functions" where you to define the interaction with an LLM as a python function -signature, and convert that to a callable object which abstracts the pipeline away from you. - -=== "From ID" - - ```py - import rigging as rg - - @rg.prompt(generator_id="claude-3-sonnet-20240229") - async def say_hello(name: str) -> rg.Chat: - """Say hello to {{ name }}""" - - chat = await say_hello("Harry") - ``` - -=== "From Generator" - - ```py - import rigging as rg - - generator = rg.get_generator("claude-3-sonnet-20240229") - - @generator.prompt - async def say_hello(name: str) -> rg.Chat: - """Say hello to {{ name }}""" - - chat = await say_hello("Harry") - ``` - -=== "From Pipeline" - - ```py - import rigging as rg - - generator = rg.get_generator("claude-3-sonnet-20240229") - pipeline = generator.chat([ - {"role": "system", "content": "Talk like a pirate."} - ]) - - @pipeline.prompt - async def say_hello(name: str) -> rg.Chat: - """Say hello to {{ name }}""" - - chat = await say_hello("Harry") - ``` - -Prompts are very powerful. You can take control over any of the inputs in your docstring, gather -outputs as structured objects, lists, dataclasses, and collect the underlying Chat object, etc. - -Check out [Prompt Functions](topics/prompt-functions.md) for more information. - - -### Tools - -Tools exposed to LLMs are super simple with Rigging. You can define a python function and make -it available straight in the chat pipeline. - -```py -import rigging as rg - -def add_numbers(x: float, y: float) -> float: - return x + y - -chat = ( - await - rg.get_generator("gpt-4o-mini") - .chat("What is 1337 + 42?") - .using(add_numbers) - .run() -) - -print(chat.conversation) - -# [user]: What is 1337 + 42? -# -# [assistant]: -# |- add_numbers({"x":1337,"y":42}) -# -# [tool]: 1379 -# -# [assistant]: 1337 + 42 equals 1379. -``` - -You can add as many tools as you'd like, document them and their parameters, and we support complex -argument types like pydantic models and dataclasses. Your function can return standard objects to -cast into strings, [`Message`][rigging.message.Message] objects, or even content parts for -multi-modal generation ([`ContentImageUrl`][rigging.message.ContentImageUrl]) - -Check out [Tools](topics/tools.md) for more information. - -### Tools + Prompts - -You can combine prompts and tools to achieve "multi-agent" behavior: - -```py -import rigging as rg -from typing import Annotated - -Joke = Annotated[str, rg.Ctx("joke")] - -@rg.prompt(generator_id="gpt-4o-mini") -async def generate_jokes(count: int) -> list[Joke]: - "Write {{count}} short hilarious jokes." - - -@rg.prompt(generator_id="gpt-4o", tools=[generate_jokes]) -async def write_joke() -> Joke: - """ - Generate some jokes, then choose the best. - You must return just a single joke. - """ - -joke = await write_joke() -``` - -Underneath the `generate_jokes` prompt will be presented as an available tool when `gpt-4o` is working -on tasks, and rigging with handle all the inference and type processing for you. - -### Conversations - -Both [`ChatPipeline`][rigging.chat.ChatPipeline] and [`Chat`][rigging.chat.Chat] objects provide freedom -for forking off the current state of messages, or continuing a stream of messages after generation has occured. - -In general: - -- [`ChatPipeline.fork`][rigging.chat.ChatPipeline.fork] will clone the current chat pipeline and let you maintain - both the new and original object for continued processing. -- [`Chat.fork`][rigging.chat.Chat.fork] will produce a fresh `ChatPipeline` from all the messages prior to the - previous generation (useful for "going back" in time). -- [`Chat.continue_`][rigging.chat.Chat.continue_] is similar to `fork` (actually a wrapper) which tells `fork` to - include the generated messages as you move on (useful for "going forward" in time). - -In other words, the abstraction of going back and forth in a "conversation" would be continuously calling -[`Chat.continue_`][rigging.chat.Chat.continue_] after each round of generation. - -```py -import rigging as rg - -generator = rg.get_generator("gpt-3.5-turbo") -chat = generator.chat("Hello, how are you?") - -# We can fork before generation has occured -specific = await chat.fork("Be specific please.").run() -poetic = await chat.fork("Be as poetic as possible").with_(temperature=1.5).run() # (1)! - -# We can also continue after generation -next_chat = poetic.continue_("That's good, tell me a joke") # (2)! - -update = await next_chat.run() -``` - -1. In this case the temperature change will only be applied to the poetic path because `fork` has - created a clone of our chat pipeline. -2. For convience, we can usually just pass `str` objects in place of full messages, which underneath - will be converted to a [`Message`][rigging.message.Message] object with the `user` role. - -### Basic Parsing - -Now let's assume we want to ask the model for a piece of information, and we want to make sure -this item conforms to a pre-defined structure. Underneath rigging uses [Pydantic XML](https://pydantic-xml.readthedocs.io/) -which itself is built on [Pydantic](https://docs.pydantic.dev/). We'll cover more about -constructing models in a [later section](topics/models.md), but don't stress the details for now. - -??? note "XML vs JSON" - - Rigging is opinionated with regard to using XML to weave unstructured data with structured contents - as the underlying LLM generates text responses, at least when it comes to raw text content. If you want - to take advantage of structured JSON parsing provided by model providers or inference tools, - [`APITools`](topics/tools.md) are a good alternative. - - You can read more about XML tag use from [Anthropic](https://docs.anthropic.com/claude/docs/use-xml-tags) - who have done extensive research with their models. - -To begin, let's define a `FunFact` model which we'll have the LLM fill in. Rigging exposes a -[`Model`][rigging.model.Model] base class which you should inherit from when defining structured -inputs. This is a lightweight wrapper around pydantic-xml's [`BaseXMLModel`](`https://pydantic-xml.readthedocs.io/en/latest/pages/api.html#pydantic_xml.BaseXmlModel`) -with some added features and functionality to make it easy for Rigging to manage. However, everything -these models support (for the most part) is also supported in Rigging. - -```py hl_lines="3-4" -import rigging as rg - -class FunFact(rg.Model): - fact: str # (1)! - -chat = await rg.get_generator('gpt-3.5-turbo').chat( - f"Provide a fun fact between {FunFact.xml_example()} tags." -).run() - -fun_fact = chat.last.parse(FunFact) - -print(fun_fact.fact) -# The Eiffel Tower can be 15 cm taller during the summer due to the expansion of the iron in the heat. -``` - -1. This is what pydantic XML refers to as a "primitive" class as it is simply and single - typed value placed between the tags. See more about primitive types, elements, and attributes in the - [Pydantic XML Docs](https://pydantic-xml.readthedocs.io/en/latest/pages/quickstart.html#primitives) - -We need to show the target LLM how to format it's response, so we'll use the -[`.xml_example()`][rigging.model.Model.xml_example] class method which all models -support. By default this will simple emit empty XML tags of our model: - -```xml -Provide a fun fact between tags. -``` - -??? note "Customizing Model Tags" - - Tags for a model are auto-generated based on the name of the class. You are free - to override these by passing `tag=[value]` into your class definition like this: - - ```py - class LongNameForThing(rg.Model, tag="short"): - ... - ``` - -We wrap up the generation and extract our parsed object by calling [`.parse()`][rigging.message.Message.parse] -on the [last message][rigging.chat.Chat.last] of our generated chat. This will process the contents -of the message, extract the first matching model which parses successfully, and return it to us as a python -object. - -```py hl_lines="10" -import rigging as rg - -class FunFact(rg.Model): - fact: str - -chat = await rg.get_generator('gpt-3.5-turbo').chat( - f"Provide a fun fact between {FunFact.xml_example()} tags." -).run() - -fun_fact = chat.last.parse(FunFact) - -print(fun_fact.fact) # (1)! -# The Eiffel Tower can be 15 cm taller during the summer due to the expansion of the iron in the heat. -``` - -1. Because we've defined `FunFact` as a class, the result if `.parse()` is typed to that object. In our - code, all the properties of fact will be available just like we created the object directly. - -Notice that we don't have to worry about the model being verbose in it's response, as we've communicated -that the text between the `#!xml ` tags is the relevent place to put it's answer. - -### Strict Parsing - -In the example above, we don't handle the case where the model fails to properly conform to our -desired output structure. If the last message content is invalid in some way, our call to `parse` -will result in an exception from rigging. Rigging is designed at it's core to manage this process, -and we have a few options: - -1. We can extend our chat pipeline with [`.until_parsed_as()`][rigging.chat.ChatPipeline] which will cause the - [`run()`][rigging.chat.ChatPipeline.run] function to internally check if parsing is succeeding - before returning the chat back to you. -2. We can make the parsing optional by switching to [`.try_parse()`][rigging.message.Message.try_parse]. The type - of the return value with automatically switch to `#!python FunFact | None` and you can handle cases - where parsing failed. - -=== "Option 1 - Until Parsed As" - - ```py hl_lines="5" - chat = ( - await - rg.get_generator('gpt-3.5-turbo') - .chat(f"Provide a fun fact between {FunFact.xml_example()} tags.") - .until_parsed_as(FunFact) - .run() - ) - - fun_fact = chat.last.parse(FunFact) # This call should never fail - - print(fun_fact or "Failed to get fact") - ``` - - !!! note "Double Parsing" - - We still have to call [`.parse()`][rigging.message.Message.parse] on the message despite - using [`.until_parsed_as()`][rigging.chat.ChatPipeline.until_parsed_as]. This is - a limitation of type hinting as we'd have to turn every `ChatPipeline` and `Chat` into a generic - which could carry types forward. It's a small price for big code complexity savings. However, - the use of [`.until_parsed_as()`][rigging.chat.ChatPipeline.until_parsed_as] **will** cause - the generated messages to have parsed models in their [`.parts`][rigging.message.Message.parts]. - So if you don't need to access the typed object immediately, you can be confident serializing - the chat object and the model will be there when you need it. - - !!! note "Max Rounds Concept" - - When control is passed into a chat pipeline with [`.until_parsed_as()`][rigging.chat.ChatPipeline.until_parsed_as], - a callback is registered internally to operate during generation. When model output is received, the - callback will attempt to parse, and if it fails, it will re-trigger generation with or without context depending - on the [`attempt_recovery`][rigging.chat.ChatPipeline.until_parsed_as] parameter. This process will repeat - until the model produces a valid output or the maximum number of "rounds" is reached. - - Often you might find yourself constantly getting [`ExhaustedMaxRoundsError`][rigging.error.ExhaustedMaxRoundsError] - exceptions. This is usually a sign that the LLM doesn't have enough information about the desired output, or - complexity in your model is too high. You have a few options for gracefull handling these situations: - - 1. You can adjust the `max_rounds` as needed and try using `attempt_recovery`. - 2. Pass `allow_failed` to your [`run()`][rigging.chat.ChatPipeline.run] - method and check the [`.failed`][rigging.chat.Chat.failed] property after generation - 3. Use an external callback like [`.then()`][rigging.chat.ChatPipeline.then] to - get more external control over the process. - -=== "Option 2 - Try Parse" - - ```py hl_lines="5" - chat = await rg.get_generator('gpt-3.5-turbo').chat( - f"Provide a fun fact between {FunFact.xml_example()} tags." - ).run() - - fun_fact = chat.last.try_parse(FunFact) # fun_fact might now be None - - print(fun_fact or "Failed to get fact") - ``` - -### Parsing Multiple Models - -Assuming we wanted to extend our example to produce a set of interesting facts, we have a couple of options: - -1. Simply use [`run_many()`][rigging.chat.ChatPipeline.run_many] and generate N examples individually -2. Rework our code slightly and let the model provide us multiple facts at once. - -=== "Option 1 - Multiple Generations" - - ```py - chats = await rg.get_generator('gpt-3.5-turbo').chat( - f"Provide a fun fact between {FunFact.xml_example()} tags." - ).run_many(3) - - for chat in chats: - print(chat.last.parse(FunFact).fact) - ``` - -=== "Option 2 - Inline Set" - - ```py - chat = await rg.get_generator('gpt-3.5-turbo').chat( - f"Provide a 3 fun facts each between {FunFact.xml_example()} tags." - ).run() - - for fun_fact in chat.last.parse_set(FunFact): - print(fun_fact.fact) - ``` - -### Parsing with Prompts - -The use of [`Prompt`][rigging.prompt.Prompt] functions can make parsing even easier. We can refactor -our previous example and have rigging parse out FunFacts directly for us: - -=== "Multiple Generations" - - ```py - import rigging as rg - - class FunFact(rg.Model): - fact: str - - @rg.prompt(generator_id="gpt-3.5-turbo") - def get_fun_fact() -> FunFact: - """Provide a fun fact.""" - - fun_facts = await get_fun_fact.run_many(3) - ``` - -=== "Inline Set" - - ```py - import rigging as rg - - class FunFact(rg.Model): - fact: str - - @rg.prompt(generator_id="gpt-3.5-turbo") - def get_fun_facts(count: int = 3) -> list[FunFact]: - """Provide fun facts.""" - - fun_facts = await get_fun_facts() - ``` - -### Keep Going - -Check out the **[topics section](topics/workflow.md)** for more in-depth explanations and examples. diff --git a/docs/install.mdx b/docs/install.mdx new file mode 100644 index 0000000..423dbc4 --- /dev/null +++ b/docs/install.mdx @@ -0,0 +1,49 @@ +--- +title: "Installation" +description: "Installation and Setup" +public: true +--- + +We publish every version to Pypi: + + +```bash pip +pip install -U rigging +``` + +```bash uv +uv pip install -U rigging +``` + +```bash poetry +poetry add rigging +``` + + +If you want all the extras (vLLM, transformers, examples), just specify the `all` extra: + + +```bash pip +pip install -U rigging[all] +``` + +```bash uv +uv pip install -U rigging[all] +``` + +```bash poetry +poetry add rigging[all] +``` + + +If you want to build from source: + +```bash +cd rigging/ +poetry install +``` + +## Migration Guides + +- **[Migrating from v2 -> v3](topics/migrations.md#migrating-from-v2x-to-v3x)** +- **[Migrating from v1 -> v2](topics/migrations.md#migrating-from-v1x-to-v2x)** diff --git a/docs/intro.mdx b/docs/intro.mdx new file mode 100644 index 0000000..4a76d13 --- /dev/null +++ b/docs/intro.mdx @@ -0,0 +1,454 @@ +--- +title: "Introduction" +description: "How to get started with Rigging" +public: true +--- + +Rigging is a lightweight LLM framework to make using language models in production code as simple and effective as possible. Here are the highlights: + +- [**Structured Pydantic models**](/topics/data-models) can be used interchangably with unstructured text output. +- LiteLLM as the default generator giving you [**instant access to a huge array of models**](/topics/generators). +- Define prompts as python functions with **type hints and docstrings**. +- Simple [**tool use**](/topics/tools), even for models which don't support them at the API. +- Store different models and configs as **simple connection strings** just like databases. +- Integrated **tracing** support with [Logfire](https://logfire.pydantic.dev/docs/) to track activity. +- Chat templating, forking, continuations, generation parameter overloads, stripping segments, etc. +- Async batching and fast iterations for [**large scale generation**](/topics/iterating-and-batching). +- Metadata, callbacks, and data format conversions. +- Modern python with type hints, async support, pydantic validation, serialization, etc. + +```python +import rigging as rg + +@rg.prompt(generator_id="gpt-4") +async def get_authors(count: int = 3) -> list[str]: + """Provide famous authors.""" + +print(await get_authors()) + +# ['William Shakespeare', 'J.K. Rowling', 'Jane Austen'] +``` + +## Getting Started + +### Basic Chats + +Let's start with a very basic generation example that doesn't include any parsing features, continuations, etc. You want to chat with a model and collect it's response. + +We first need to get a `Generator` object. We'll use `get_generator` which will resolve an identifier string to the underlying generator class object. + +```python {3} +import rigging as rg + +generator = rg.get_generator("claude-3-sonnet-20240229") +pipeline = generator.chat( + [ + {"role": "system", "content": "You are a wizard harry."}, + {"role": "user", "content": "Say hello!"}, + ] +) +chat = await pipeline.run() +print(chat.all) +# [ +# Message(role='system', parts=[], content='You are a wizard harry.'), +# Message(role='user', parts=[], content='Say hello!'), +# ] +``` + + +The default Rigging generator is LiteLLM, which wraps a large number of providers and models. We assume for these examples that you have API tokens set as environment variables for these models. You can refer to the [LiteLLM docs](https://docs.litellm.ai/docs/) for supported providers and their key format. If you'd like, you can change any of the model IDs we use and/or add `,api_key=[sk-1234]` to the end of any of the generator IDs to specify them inline. + + +1. You'll see us use this shorthand import syntax throughout our code, it's totally optional but makes things look nice. +2. This is actually shorthand for `litellm!anthropic/claude-3-sonnet-20240229`, where `litellm` is the provider. We just default to that generator and you don't have to be explicit. You can find more information about this in the [generators](topics/generators.md) docs. +3. From version 2 onwards, Rigging is fully async. You can use `await` to trigger generation and get your results, or use `await_`. + +Generators have an easy `chat()` method which you'll use to initiate the conversations. You can supply messages in many different forms from dictionary objects, full `Message`rigging.message.Message] classes, or a simple `str` which will be converted to a user message. + +```python {4-9} +import rigging as rg + +generator = rg.get_generator("claude-3-sonnet-20240229") +pipeline = generator.chat( # (1)! + [ + {"role": "system", "content": "You are a wizard harry."}, + {"role": "user", "content": "Say hello!"}, + ] +) +chat = await pipeline.run() +print(chat.all) +# [ +# Message(role='system', parts=[], content='You are a wizard harry.'), +# Message(role='user', parts=[], content='Say hello!'), +# Message(role='assistant', parts=[], content='Hello! How can I help you today?'), +# ] +``` + +*1. `generator.chat` is actually just a helper for `chat(generator, ...)`, they do the same thing.* + + +**ChatPipeline vs Chat** + +You'll notice we name the result of `chat()` as `pipeline`. The naming might be confusing, +but chats go through 2 phases. We first stage them into a pipeline, where we operate +and prepare them before we actually trigger generation with `run()`. + +Calling `.chat()` doesn't trigger any generation, but calling any of these run methods will: + +- `rigging.chat.ChatPipeline.run` +- `rigging.chat.ChatPipeline.run_many` +- `rigging.chat.ChatPipeline.run_batch` +- `rigging.chat.ChatPipeline.run_over` + + +In this case, we have nothing additional we want to add to our chat pipeline, and we are only interested in generating exactly one response message. We simply call `.run()` to execute the generation process and collect our final `Chat` object. + +```python {10-11} +import rigging as rg + +generator = rg.get_generator("claude-3-sonnet-20240229") +pipeline = generator.chat( + [ + {"role": "system", "content": "You are a wizard harry."}, + {"role": "user", "content": "Say hello!"}, + ] +) +chat = await pipeline.run() +print(chat.all) +# [ +# Message(role='system', parts=[], content='You are a wizard harry.'), +# Message(role='user', parts=[], content='Say hello!'), +# Message(role='assistant', parts=[], content='Hello! How can I help you today?'), +# ] +``` + +View more about Chat objects and their properties over here. In general, chats give you access to exactly what messages were passed into a model, and what came out the other side. + + +**IDE Setup** + +Rigging has been built with full type support which provides clear guidance on what methods return what types, and when they return those types. It's recommended that you operate in a development environment which can take advantage of this information. Rigging will almost "fall" into place and you won't be guessing about objects as you work. + + +### Prompts + +Operating chat pipelines manually is very flexible, but can feel a bit verbose. Rigging supports the concept of "prompt functions" where you to define the interaction with an LLM as a python function signature, and convert that to a callable object which abstracts the pipeline away from you. + + +```python From a Generator ID +import rigging as rg + +@rg.prompt(generator_id="claude-3-sonnet-20240229") +async def say_hello(name: str) -> rg.Chat: + """Say hello to {{ name }}""" + +chat = await say_hello("Harry") +``` + +```python From Generator +import rigging as rg + +generator = rg.get_generator("claude-3-sonnet-20240229") + +@generator.prompt +async def say_hello(name: str) -> rg.Chat: + """Say hello to {{ name }}""" + +chat = await say_hello("Harry") +``` + +```python From Pipeline +import rigging as rg + +generator = rg.get_generator("claude-3-sonnet-20240229") +pipeline = generator.chat([ + {"role": "system", "content": "Talk like a pirate."} +]) + +@pipeline.prompt +async def say_hello(name: str) -> rg.Chat: + """Say hello to {{ name }}""" + +chat = await say_hello("Harry") +``` + + +Prompts are very powerful. You can take control over any of the inputs in your docstring, gather +outputs as structured objects, lists, dataclasses, and collect the underlying Chat object, etc. + +Check out [Prompt Functions](topics/prompt-functions.md) for more information. + +### Conversations + +Both `ChatPipeline` and `Chat` objects provide freedom +for forking off the current state of messages, or continuing a stream of messages after generation has occured. + +In general: + +- `ChatPipeline.fork` will clone the current chat pipeline and let you maintain both the new and original object for continued processing. +- `Chat.fork` will produce a fresh `ChatPipeline` from all the messages prior to the previous generation (useful for "going back" in time). +- `Chat.continue_` is similar to `fork` (actually a wrapper) which tells `fork` to include the generated messages as you move on (useful for "going forward" in time). + +In other words, the abstraction of going back and forth in a "conversation" would be continuously calling `Chat.continue_` after each round of generation. + +```python +import rigging as rg + +generator = rg.get_generator("gpt-3.5-turbo") +chat = generator.chat("Hello, how are you?") + +# We can fork before generation has occured +specific = await chat.fork("Be specific please.").run() +poetic = await chat.fork("Be as poetic as possible").with_(temperature=1.5).run() # (1)! + +# We can also continue after generation +next_chat = poetic.continue_("That's good, tell me a joke") # (2)! + +update = await next_chat.run() +``` + +1. In this case the temperature change will only be applied to the poetic path because `fork` has created a clone of our chat pipeline. +2. For convience, we can usually just pass `str` objects in place of full messages, which underneath will be converted to a `Message` object with the `user` role. + +### Basic Parsing + +Now let's assume we want to ask the model for a piece of information, and we want to make sure this item conforms to a pre-defined structure. Underneath rigging uses [Pydantic XML](https://pydantic-xml.readthedocs.io/) which itself is built on [Pydantic](https://docs.pydantic.dev/). We'll cover more about constructing models in a [later section](topics/models.md), but don't stress the details for now. + + +**XML vs JSON** + +Rigging is opinionated with regard to using XML to weave unstructured data with structured contents as the underlying LLM generates text responses, at least when it comes to raw text content. If you want to take advantage of structured JSON parsing provided by model providers or inference tools, [**Tools**](/topics/tools) are a great way to do that. + +You can read more about XML tag use from [Anthropic](https://docs.anthropic.com/claude/docs/use-xml-tags) who have done extensive research with their models. + + +To begin, let's define a `FunFact` model which we'll have the LLM fill in. Rigging exposes a `Model` base class which you should inherit from when defining structured inputs. This is a lightweight wrapper around pydantic-xml's [`BaseXMLModel`](`https://pydantic-xml.readthedocs.io/en/latest/pages/api.html#pydantic_xml.BaseXmlModel`) with some added features and functionality to make it easy for Rigging to manage. However, everything these models support (for the most part) is also supported in Rigging. + +```python {3-4} +import rigging as rg + +class FunFact(rg.Model): # (1)! + fact: str + +chat = await rg.get_generator('gpt-3.5-turbo').chat( + f"Provide a fun fact between {FunFact.xml_example()} tags." +).run() + +fun_fact = chat.last.parse(FunFact) + +print(fun_fact.fact) +# The Eiffel Tower can be 15 cm taller during the summer due to the expansion of the iron in the heat. +``` + +*1. This is what pydantic XML refers to as a "primitive" class as it is simply and single typed value placed between the tags. See more about primitive types, elements, and attributes in the [Pydantic XML Docs](https://pydantic-xml.readthedocs.io/en/latest/pages/quickstart.html#primitives)* + +We need to show the target LLM how to format it's response, so we'll use the `.xml_example()` class method which all models support. By default this will simple emit empty XML tags of our model: + +```xml +Provide a fun fact between tags. +``` + +### Customizing Model Tags + +Tags for a model are auto-generated based on the name of the class. You are free +to override these by passing `tag=[value]` into your class definition like this: + +```python +class LongNameForThing(rg.Model, tag="short"): + ... +``` + +We wrap up the generation and extract our parsed object by calling `.parse()` on the last message of our generated chat. This will process the contents of the message, extract the first matching model which parses successfully, and return it to us as a python object. + +```python {10} +import rigging as rg + +class FunFact(rg.Model): + fact: str + +chat = await rg.get_generator('gpt-3.5-turbo').chat( + f"Provide a fun fact between {FunFact.xml_example()} tags." +).run() + +fun_fact = chat.last.parse(FunFact) # (1)! + +print(fun_fact.fact) +# The Eiffel Tower can be 15 cm taller during the summer due to the expansion of the iron in the heat. +``` + +*1. Because we've defined `FunFact` as a class, the result if `.parse()` is typed to that object. In our code, all the properties of fact will be available just like we created the object directly.* + +Notice that we don't have to worry about the model being verbose in it's response, as we've communicated that the text between the `` tags is the relevent place to put it's answer. + +### Strict Parsing + +In the example above, we don't handle the case where the model fails to properly conform to our desired output structure. If the last message content is invalid in some way, our call to `parse` will result in an exception from rigging. Rigging is designed at it's core to manage this process, and we have a few options: + +1. We can extend our chat pipeline with `.until_parsed_as()` which will cause the `run()` function to internally check if parsing is succeeding before returning the chat back to you. +2. We can make the parsing optional by switching to `.try_parse()`. The type of the return value with automatically switch to `#!python FunFact | None` and you can handle cases where parsing failed. + + +```python Until Parsed As {5} +chat = ( + await + rg.get_generator('gpt-3.5-turbo') + .chat(f"Provide a fun fact between {FunFact.xml_example()} tags.") + .until_parsed_as(FunFact) + .run() +) + +fun_fact = chat.last.parse(FunFact) # This call should never fail + +print(fun_fact or "Failed to get fact") +``` + +```python Try Parse {5} +chat = await rg.get_generator('gpt-3.5-turbo').chat( + f"Provide a fun fact between {FunFact.xml_example()} tags." +).run() + +fun_fact = chat.last.try_parse(FunFact) # fun_fact might now be None + +print(fun_fact or "Failed to get fact") +``` + + + +**Double Parsing** + +We still have to call `.parse()` on the message despite using `.until_parsed_as()`. This is a limitation of type hinting as we'd have to turn every `ChatPipeline` and `Chat` into a generic which could carry types forward. It's a small price for big code complexity savings. However, the use of `.until_parsed_as()` **will** cause the generated messages to have parsed models in their `.parts`. So if you don't need to access the typed object immediately, you can be confident serializing the chat object and the model will be there when you need it. + + +### Max Depth Concept + +When control is passed into a chat pipeline with `.until_parsed_as()`, a `.then()` callback is registered internally to operate during generation. When model output is received, the callback will attempt to parse, and if it fails, it will re-trigger generation. This process will repeat until the model produces a valid output or the maximum "depth" is reached. + +Often you might find yourself constantly getting `MaxDepthError` exceptions. This is usually a sign that the LLM doesn't have enough information about the desired output, or complexity in your model is too high. You have a few options for gracefull handling these situations: + +1. You can adjust the `max_depth` to a higher value. This will allow the model to try more times before failing. +2. Pass `allow_failed` to your `run()` method and check the `.failed` property after generation +3. Use an custom callback with `.then()` to get more external control over the process. + +### Parsing Multiple Models + +Assuming we wanted to extend our example to produce a set of interesting facts, we have a couple of options: + +1. Simply use `run_many()` and generate N examples individually +2. Rework our code slightly and let the model provide us multiple facts at once. + + +```python Multiple Generations +chats = await rg.get_generator('gpt-3.5-turbo').chat( + f"Provide a fun fact between {FunFact.xml_example()} tags." +).run_many(3) + +for chat in chats: + print(chat.last.parse(FunFact).fact) +``` + +```python Inline Set +chat = await rg.get_generator('gpt-3.5-turbo').chat( + f"Provide a 3 fun facts each between {FunFact.xml_example()} tags." +).run() + +for fun_fact in chat.last.parse_set(FunFact): + print(fun_fact.fact) +``` + + +### Parsing with Prompts + +The use of `Prompt` functions can make parsing even easier. We can refactor our previous example and have rigging parse out FunFacts directly for us: + + +```python Multiple Generations +import rigging as rg + +class FunFact(rg.Model): + fact: str + +@rg.prompt(generator_id="gpt-3.5-turbo") +def get_fun_fact() -> FunFact: + """Provide a fun fact.""" + +fun_facts = await get_fun_fact.run_many(3) +``` + +```python Inline Set +import rigging as rg + +class FunFact(rg.Model): + fact: str + +@rg.prompt(generator_id="gpt-3.5-turbo") +def get_fun_facts(count: int = 3) -> list[FunFact]: + """Provide fun facts.""" + +fun_facts = await get_fun_facts() +``` + + +### Tools + +Tools exposed to LLMs are super simple with Rigging. You can define a python function and make it available straight in the chat pipeline. + +```python +import rigging as rg + +def add_numbers(x: float, y: float) -> float: + return x + y + +chat = ( + await + rg.get_generator("gpt-4o-mini") + .chat("What is 1337 + 42?") + .using(add_numbers) + .run() +) + +print(chat.conversation) + +# [user]: What is 1337 + 42? +# +# [assistant]: +# |- add_numbers({"x":1337,"y":42}) +# +# [tool]: 1379 +# +# [assistant]: 1337 + 42 equals 1379. +``` + +You can add as many tools as you'd like, document them and their parameters, and we support complex argument types like pydantic models and dataclasses. Your function can return standard objects to cast into strings, `Message` objects, or even content parts for multi-modal generation (`ContentImageUrl`) + +Check out [Tools](/topics/tools) for more information. + +### Tools + Prompts + +You can combine prompts and tools to achieve "multi-agent" behavior: + +```python +import rigging as rg +from typing import Annotated + +Joke = Annotated[str, rg.Ctx("joke")] + +@rg.prompt(generator_id="gpt-4o-mini") +async def generate_jokes(count: int) -> list[Joke]: + "Write {{count}} short hilarious jokes." + + +@rg.prompt(generator_id="gpt-4o", tools=[generate_jokes]) +async def write_joke() -> Joke: + """ + Generate some jokes, then choose the best. + You must return just a single joke. + """ + +joke = await write_joke() +``` + +Underneath the `generate_jokes` prompt will be presented as an available tool when `gpt-4o` is working on tasks, and rigging with handle all the inference and type processing for you. + +### Keep Going + +Check out the **[topics section](topics/workflow)** for more in-depth explanations and examples. \ No newline at end of file diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css deleted file mode 100644 index 48f54b6..0000000 --- a/docs/stylesheets/extra.css +++ /dev/null @@ -1,77 +0,0 @@ -/* Main color overrides */ -[data-md-color-scheme="slate"] { - --md-primary-fg-color: #EAEAEA; - --md-primary-fg-color--dark: var(--md-primary-fg-color); - --md-accent-fg-color: rgb(149, 133, 227); - - --md-primary-color: #EAEAEA; - --md-primary-bg-color: #191919; - --md-default-bg-color: #191919; - - --md-default-fg-color: hsla(0, 0%, 100%, 0.90); - --md-default-fg-color--light: hsla(0, 0%, 100%, 0.70); - --md-default-fg-color--lighter: hsla(0, 0%, 100%, 0.60); - --md-default-fg-color--lightest: hsla(0, 0%, 100%, 0.40); - - --md-footer-bg-color: hsla(0, 0%, 10%, 0.87); - --md-footer-bg-color--dark: hsla(0, 0%, 8%, 1); - - --md-typeset-a-color: var(--md-accent-fg-color); - - --md-code-hl-number-color: rgb(231, 107, 93); - --md-code-hl-special-color: hsla(340, 83%, 66%, 1); - --md-code-hl-function-color: hsla(291, 57%, 65%, 1); - --md-code-hl-constant-color: hsla(250, 62%, 70%, 1); - --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1); - --md-code-hl-string-color: var(--md-accent-fg-color); - --md-code-hl-name-color: var(--md-default-fg-color--light); - --md-code-hl-operator-color: var(--md-default-fg-color--light); - --md-code-hl-punctuation-color: var(--md-default-fg-color--light); - --md-code-hl-comment-color: rgb(55, 161, 108); - --md-code-hl-generic-color: var(--md-default-fg-color--light); - --md-code-hl-variable-color: var(--md-default-fg-color--light); -} - -/* Indentation. */ -div.doc-contents:not(.first) { - padding-left: 25px; - border-left: .05rem solid var(--md-typeset-table-color); -} - -/* Mark external links as such. */ -a.external::after, -a.autorefs-external::after { - /* https://primer.style/octicons/arrow-up-right-24 */ - mask-image: url('data:image/svg+xml,'); - -webkit-mask-image: url('data:image/svg+xml,'); - content: ' '; - - display: inline-block; - vertical-align: middle; - position: relative; - - height: 1em; - width: 1em; - background-color: currentColor; -} - -a.external:hover::after, -a.autorefs-external:hover::after { - background-color: var(--md-accent-fg-color); -} - -/* Fancier color for operators such as * and |. */ -.doc-signature .o { - color: var(--md-code-hl-special-color); -} - -/* Fancier color for constants such as None, True, and False. */ -.doc-signature .kc { - color: var(--md-code-hl-constant-color); -} - -/* Fancier color for built-in types (only useful when cross-references are used). */ -.doc-signature .n>a[href^="https://docs.python.org/"][href*="/functions.html#"], -.doc-signature .n>a[href^="https://docs.python.org/"][href*="/stdtypes.html#"] { - color: var(--md-code-hl-constant-color); -} \ No newline at end of file diff --git a/docs/topics/callbacks-and-mapping.md b/docs/topics/callbacks-and-mapping.md deleted file mode 100644 index b1d1183..0000000 --- a/docs/topics/callbacks-and-mapping.md +++ /dev/null @@ -1,338 +0,0 @@ -# Callbacks and Mapping - -Rigging is designed to give control over how the generation process works, and what occurs after. -In fact, higher level functions like [`.using()`][rigging.chat.ChatPipeline.using] -and [`.until_parsed_as()`][rigging.chat.ChatPipeline] leverage a generic callback system -underneath to guide generation. Let's walk through them. - -## Watch Callbacks - -Pipelines, Prompts, and Generators hold a list of passive callbacks which will be passed -[`Chat`][rigging.chat.Chat] or [`Completion`][rigging.completion.Completion] objects -as they are generated. - -Watch callbacks are useful for logging, monitoring, or other passive actions that don't -directly affect the generation process. Register them with any of the following: - -- [`Generator.watch()`][rigging.generator.Generator.watch] -- [`ChatPipeline.watch()`][rigging.chat.ChatPipeline.watch] -- [`CompletionPipeline.watch()`][rigging.completion.CompletionPipeline.watch] -- [`Prompt.watch()`][rigging.prompt.Prompt.watch] - -We also provide various helpers in the `rigging.watch` module for writing to -files or databases like elastic. - -```py -import rigging as rg - -log_to_file = rg.watchers.write_chats_to_jsonl("chats.jsonl") - -pipeline = ( - rg.get_generator("gpt-3.5-turbo") - .chat("Explain why the sky is blue") - .watch(log_to_file) -) - -chat = await pipeline.run_many(5) -``` - -## Until Callbacks - -If you want to gain control over the generation process before it completes, -you can use the [`ChatPipeline.until()`][rigging.chat.ChatPipeline.until] or -[`CompletionPipeline.until()`][rigging.completion.CompletionPipeline.until] methods. - -These allow you to register a callback function which participates in generation and -can decide whether generation should proceed, and exactly how it does so. For chat interfaces, these -functions also get fine control over the contents of the chat while callbacks are resolving. This -is how we can provide feedback to an LLM model during generation like validation errors when -parsing fails ([`attempt_recovery`][rigging.chat.ChatPipeline.until]). - -```py -import rigging as rg - -class Joke(rg.Model): - content: str - -def involves_a_cat(message: rg.Message) -> tuple[bool, list[rg.Message]]: - if "cat" not in message.content.lower(): - return True, [message, rg.Message("user", "Please include a cat in your joke")] # (1)! - return False, [message] - -chat = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat(f"Tell me a joke about an animal between {Joke.xml_tags()} tags.") - .until_parsed_as(Joke) - .until(involves_a_cat, drop_dialog=False) # (2)! - .run() -) - -print(chat.conversation) -# [user]: Tell me a joke about an animal between tags. -# [assistant]: Why did the duck go to the doctor? Because he was feeling a little down! -# [user]: Please include a cat in your joke -# [assistant]: Why was the cat sitting on the computer? Because it wanted to keep an eye on the mouse! - -print(chat.last.parse(Joke)) -# Joke(content='Why was the cat sitting on the computer? Because it wanted to keep an eye on the mouse!') -``` - -1. Returning `True` from this callback tells Rigging to go back to the generator with the supplied - messages and rerun the generation step. Whether you're appended messages are used is dependent - on the `attempt_recovery=True` on [`ChatPipeline.until()`][rigging.chat.ChatPipeline.until]. In - this instance our request to include a cat will be appending to the intermediate messages while - generation completes. We can essentially provide feedback to the model about how it should attempt - to satisfy the callback function. -2. Our use of `drop_dialog=False` here allows us to see the intermediate steps of resolving - our callbacks in the final Chat. It's up to you whether you want these intermediate messages - included or not. The default is to drop them once the callbacks resolve. - -??? "Using .until on CompletionPipeline" - - The interface for a `CompletionPipeline` is very similar to `ChatPipeline`, except that you - are only allowed to make a statement about whether generation should retry. You are not - currently allowed to inject additional text as intermediate context while your callback - is attempting to resolve. - -### Allowing Failures - -If you want to allow the generation process to avoid raising an exception when the maximum -rounds is exhausted, you can configure [`on_failed`][rigging.chat.FailMode] on the pipeline, or pass it directly -to various [run methods][rigging.chat.ChatPipeline.run_many] of a `ChatPipeline` or `CompletionPipeline`. - -for single runs, pass `allow_failed=True` to [`.run()`][rigging.chat.ChatPipeline.run]. - -This breaks any guarantees about the validity of final chat objects, but you can check their status -with the [`Chat.failed`][rigging.chat.Chat.failed] or [`Completion.failed`][rigging.completion.Completion.failed] properties. - -In the case of `on_failed='skip'`, the final outputs of any run method could be anywhere -from an empty list to a complete list of the requested batch/many. - -=== "Allowing Failures" - - ```py hl_lines="11" - import rigging as rg - - class ValidName(rg.Model): - ... - - chat = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat(f"Provide a fake name between {ValidName.xml_tags()} tags.") - .until_parsed_as(ValidName) - .run(allow_failed=True) - ) - - if chat.failed: - print("Failed to generate a valid name.") - else: - print(chat.last.parse(ValidName)) - ``` - -=== "Including Failures" - - ```py hl_lines="11" - import rigging as rg - - class ValidName(rg.Model): - ... - - chats = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat(f"Provide a fake name between {ValidName.xml_tags()} tags.") - .until_parsed_as(ValidName) - .run_many(3, on_failed="include") - ) - - for chat in chats: - if chat.failed: - print("Failed to generate a valid name.") - else: - print(chat.last.parse(ValidName)) - ``` - -=== "Skipping Failures" - - ```py hl_lines="13" - import rigging as rg - - class ValidName(rg.Model): - ... - - count = 5 - - chats = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat(f"Provide a fake name between {ValidName.xml_tags()} tags.") - .until_parsed_as(ValidName) - .run_many(count, on_failed="skip") - ) - - successful = len(chats) - print(f"Generated {successful} valid names out of {count} attempts.") - ``` - -### Defining Failures - -By default Rigging will catch [`ExhaustedMaxRoundsError`][rigging.error.ExhaustedMaxRoundsError] and -treat those exceptions as a soft failure you can configure with `on_failed`. However, you can also -add different exceptions to a pipeline with [`.catch()`][rigging.chat.ChatPipeline.catch] which will be -caught and treated as soft failures. - -For instance, some APIs might raise exceptions if you cross some threshold for content moderation, and -you don't want these exceptions to interupt large scale pipelines. - -```py -import litellm -import rigging as rg - -pipeline = ( - rg.get_generator("gpt-3.5-turbo") - .chat("Tell me about great sci-fi books.") - .catch(litellm.APIError, on_failed="include") # (1)! -) - -chats = await pipeline.run_many(3) -``` - -1. Here we're adding a custom exception to the pipeline that will be caught and treated as a soft failure. - In the case of litellm raising an APIError, those chats will be marked as failed and included in the - final output. You can access the raised error with the [`Chat.error`][rigging.chat.Chat.error] property. - -## Then Callbacks - -You might prefer to have your callbacks execute after generation completes, and operate on -the Chat/Completion objects from there. This is functionally very similar to -[`ChatPipeline.until()`][rigging.chat.ChatPipeline.until] and might be preferred -to expose more of the parsing internals to your code as opposed to the opaque nature -of other callback types. Use the [`ChatPipeline.then()`][rigging.chat.ChatPipeline.then] -to register any number of callbacks before executing [`ChatPipeline.run()`][rigging.chat.ChatPipeline.run]. - -!!! tip "Branching Chats" - - A common use case for `.then()` is to branch the conversation based on the output of the - of previous generations. You can continue to chain `.then()` and `.run()` calls to create - a set of generations that collapse back to the final call when they complete. - -=== "Using .then()" - - ```py - import rigging as rg - - async def check_animal(chat: rg.Chat) -> rg.Chat | None: - for animal in ["cat", "dog", "cow", "mouse", "elephant", "chicken"]: - if animal in chat.last.content.lower(): - pipeline = chat.continue_(f"Why did you pick {animal}?") - return await pipeline.meta(questioned=True).run() - - pipeline = rg.get_generator("gpt-3.5-turbo").chat("Tell me a joke about an animal.") - pipeline = pipeline.then(check_animal) - chats = await pipeline.run_many(3) - - for i, chat in enumerate(chats): - questioned = chat.metadata.get("questioned", False) - print(f"--- Chat {i+1} (?: {questioned}) ---") - print(chat.conversation) - print() - ``` - -=== "Output" - - ``` - --- Chat 1 (?: True) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the cat sit on the computer? To keep an eye on the mouse! - - [user]: Why did you pick cat? - - [assistant]: Because they are purr-fect for computer-related jokes! - - --- Chat 2 (?: False) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the duck go to the doctor? Because he was feeling a little "fowl"! - - --- Chat 3 (?: True) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the chicken join a band? Because it had the drumsticks! - - [user]: Why did you pick chicken? - - [assistant]: Because chickens are always up for a good cluck! - ``` - -## Map Callbacks - -Rigging also allows you to map process a group of Chats all at once. This is particularly -useful for instances of uses of [`.run_many()`][rigging.chat.ChatPipeline.run_many] and -[`.run_batch()`][rigging.chat.ChatPipeline.run_batch]. - -You also might want to take certain actions depending on the state of a set of Chats -all at once. For instance, attempting re-generation if a certain % of Chats didn't -meet some criteria. - -!!! note "Ordering" - - `map()` callbacks are always executed before `then()` callbacks. Order is preserved - based on when they were installed into the `ChatPipeline`. - -=== "Using .map()" - - ```py - import rigging as rg - - async def check_animal(chats: list[rg.Chat]) -> list[rg.Chat]: - return [ - await chat.continue_(f"Why did you pick that animal?").meta(questioned=True).run() - if any(a in chat.last.content.lower() for a in ["cat", "dog", "cow", "mouse", "elephant", "chicken"]) - else chat - for chat in chats - ] - - chats = ( - await - rg.get_generator("gpt-3.5-turbo") - .chat("Tell me a joke about an animal.") - .map(check_animal) - .run_many(3) - ) - - for i, chat in enumerate(chats): - questioned = chat.metadata.get("questioned", False) - print(f"--- Chat {i+1} (?: {questioned}) ---") - print(chat.conversation) - print() - ``` - -=== "Output" - - ``` - --- Chat 1 (?: True) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the duck cross the road? To prove he wasn't chicken! - - [user]: Why did you pick that animal? - - [assistant]: I chose a duck because they're known for their sense of humor and whimsical nature! Plus, who doesn't love a good duck joke? - - --- Chat 2 (?: True) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the chicken join a band? Because it had the drumsticks! - - [user]: Why did you pick that animal? - - [assistant]: I chose a chicken because they are often associated with funny jokes and puns due to their quirky and comedic behavior. Plus, who doesn't love a good chicken joke? - - --- Chat 3 (?: False) --- - [user]: Tell me a joke about an animal. - - [assistant]: Why did the duck go to the doctor? Because he was feeling a little down in the dumps! - ``` \ No newline at end of file diff --git a/docs/topics/chats-and-messages.md b/docs/topics/chats-and-messages.mdx similarity index 58% rename from docs/topics/chats-and-messages.md rename to docs/topics/chats-and-messages.mdx index 6fc072f..273f130 100644 --- a/docs/topics/chats-and-messages.md +++ b/docs/topics/chats-and-messages.mdx @@ -1,14 +1,14 @@ -# Chats and Messages +--- +title: "Chats and Messages" +description: "Chats and Messages are how Rigging represents the conversation with a model." +public: true +--- -[`Chat`][rigging.chat.Chat] objects hold a sequence of [`Message`][rigging.message.Message] -objects pre and post generation. This is the most common way that we interact with LLMs, -and the interface of both these and [`ChatPipeline`][rigging.chat.ChatPipeline]'s are -very flexible objects that let you tune the generation process, gather structured outputs, -validate parsing, perform text replacements, serialize and deserialize, fork conversations, etc. +`Chat` objects hold a sequence of `Message` objects pre and post generation. This is the most common way that we interact with LLMs, and the interface of both these and `ChatPipeline`'s are very flexible objects that let you tune the generation process, gather structured outputs, validate parsing, perform text replacements, serialize and deserialize, fork conversations, etc. ## Basic Usage -```py +```python import rigging as rg generator = rg.get_generator("claude-2.1") @@ -48,12 +48,9 @@ print(chat.conversation) ## Templating (apply) -You can use both [`ChatPipeline.apply()`][rigging.chat.ChatPipeline.apply] and [`ChatPipeline.apply_to_all()`][rigging.chat.ChatPipeline.apply_to_all] -to swap values prefixed with `$` characters inside message contents for fast templating support. +You can use both `ChatPipeline.apply()`and `ChatPipeline.apply_to_all()` to swap values prefixed with `$` characters inside message contents for fast templating support. This functionality uses [string.Template.safe_substitute](https://docs.python.org/3/library/string.html#string.Template.safe_substitute) underneath. -This functionality uses [string.Template.safe_substitute](https://docs.python.org/3/library/string.html#string.Template.safe_substitute) underneath. - -```py +```python import rigging as rg template = ( @@ -71,17 +68,11 @@ for country in ["France", "Germany"]: ## Parsed Parts -Message objects hold all of their parsed [`ParsedMessagePart`][rigging.message.ParsedMessagePart]'s inside their -[`.parts`][rigging.chat.Message.parts] property. These parts maintain both the instance of the parsed Rigging -model object and a [`.slice_`][rigging.message.ParsedMessagePart.slice_] property that defines exactly -where in the message content they are located. +Message objects hold all of their parsed `ParsedMessagePart`'s inside their `.parts` property. These parts maintain both the instance of the parsed Rigging model object and a `.slice_` property that defines exactly where in the message content they are located. -Every time parsing occurs, these parts are re-synced by using [`.to_pretty_xml()`][rigging.model.Model.to_pretty_xml] -on the model, and stitching the clean content back into the message, fixing any other slices which might -have been affected by the operation, and ordering the [`.parts`][rigging.chat.Message.parts] property based on where -they occur in the message content. +Every time parsing occurs, these parts are re-synced by using `.to_pretty_xml()` on the model, and stitching the clean content back into the message, fixing any other slices which might have been affected by the operation, and ordering the `.parts` property based on where they occur in the message content. -```py +```python import rigging as rg from pydantic import StringConstraints from typing import Annotated @@ -110,19 +101,13 @@ print(message.content[message.parts[0].slice_]) #