diff --git a/pyproject.toml b/pyproject.toml index d473983b9..e188343db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,8 @@ openai = ["openai>=1.102.0"] [dependency-groups] dev = [ "dirty-equals>=0.9.0", + # Dependency to support Docket memory:// brokers - only necessary until next fakeredis release + "fakeredis[lua] @ git+https://github.com/cunla/fakeredis-py.git@ad50a0de8d6dce554fb629ec284bc4ccbc6a7f12", "fastmcp[openai]", # add optional dependencies for fastmcp dev "fastapi>=0.115.12", diff --git a/src/fastmcp/dependencies.py b/src/fastmcp/dependencies.py index 12b80e029..8d88cfbf8 100644 --- a/src/fastmcp/dependencies.py +++ b/src/fastmcp/dependencies.py @@ -7,4 +7,6 @@ from docket import Depends -__all__ = ["Depends"] +from fastmcp.server.dependencies import CurrentContext, CurrentDocket, CurrentWorker + +__all__ = ["CurrentContext", "CurrentDocket", "CurrentWorker", "Depends"] diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py index d96fc0a77..b4471d93f 100644 --- a/src/fastmcp/server/dependencies.py +++ b/src/fastmcp/server/dependencies.py @@ -4,10 +4,11 @@ import inspect from collections.abc import AsyncGenerator, Callable from contextlib import AsyncExitStack, asynccontextmanager +from contextvars import ContextVar from functools import lru_cache -from typing import TYPE_CHECKING, Any, get_type_hints +from typing import TYPE_CHECKING, Any, cast, get_type_hints -from docket.dependencies import _Depends, get_dependency_parameters +from docket.dependencies import Dependency, _Depends, get_dependency_parameters from mcp.server.auth.middleware.auth_context import ( get_access_token as _sdk_get_access_token, ) @@ -20,10 +21,20 @@ from fastmcp.utilities.types import is_class_member_of_type if TYPE_CHECKING: + from docket import Docket + from docket.worker import Worker + from fastmcp.server.context import Context +# ContextVars for tracking Docket infrastructure +_current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None) # type: ignore[assignment] +_current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None) # type: ignore[assignment] + __all__ = [ "AccessToken", + "CurrentContext", + "CurrentDocket", + "CurrentWorker", "get_access_token", "get_context", "get_http_headers", @@ -126,6 +137,9 @@ async def _resolve_fastmcp_dependencies( - A cache for resolved dependencies - An AsyncExitStack for managing context manager lifetimes + The Docket instance (for CurrentDocket dependency) is managed separately + by the server's lifespan and made available via ContextVar. + Note: This does NOT set up Docket's Execution context. If user code needs Docket-specific dependencies like TaskArgument(), TaskKey(), etc., those will fail with clear errors about missing context. @@ -235,6 +249,139 @@ def get_context() -> Context: return context +class _CurrentContext(Dependency): + """Internal dependency class for CurrentContext.""" + + async def __aenter__(self) -> Context: + return get_context() + + +def CurrentContext() -> Context: + """Get the current FastMCP Context instance. + + This dependency provides access to the active FastMCP Context for the + current MCP operation (tool/resource/prompt call). + + Returns: + A dependency that resolves to the active Context instance + + Raises: + RuntimeError: If no active context found (during resolution) + + Example: + ```python + from fastmcp.dependencies import CurrentContext + + @mcp.tool() + async def log_progress(ctx: Context = CurrentContext()) -> str: + ctx.report_progress(50, 100, "Halfway done") + return "Working" + ``` + """ + return cast("Context", _CurrentContext()) + + +class _CurrentDocket(Dependency): + """Internal dependency class for CurrentDocket.""" + + async def __aenter__(self) -> Docket: + import fastmcp + + # Check if experimental flag is enabled + if not fastmcp.settings.experimental.enable_docket: + raise RuntimeError( + "Docket support is not enabled. " + "Set FASTMCP_EXPERIMENTAL_ENABLE_DOCKET=true to enable experimental Docket support." + ) + + # Get Docket from ContextVar (set by _docket_lifespan) + docket = _current_docket.get() + if docket is None: + raise RuntimeError( + "No Docket instance found. This should not happen when " + "FASTMCP_EXPERIMENTAL_ENABLE_DOCKET is enabled." + ) + + return docket + + +def CurrentDocket() -> Docket: + """Get the current Docket instance managed by FastMCP. + + This dependency provides access to the Docket instance that FastMCP + automatically creates when experimental Docket support is enabled. + + Requires: + - FASTMCP_EXPERIMENTAL_ENABLE_DOCKET=true + + Returns: + A dependency that resolves to the active Docket instance + + Raises: + RuntimeError: If experimental flag not enabled (during resolution) + + Example: + ```python + from fastmcp.dependencies import CurrentDocket + + @mcp.tool() + async def schedule_task(docket: Docket = CurrentDocket()) -> str: + await docket.add(some_function)(arg1, arg2) + return "Scheduled" + ``` + """ + return cast("Docket", _CurrentDocket()) + + +class _CurrentWorker(Dependency): + """Internal dependency class for CurrentWorker.""" + + async def __aenter__(self) -> Worker: + import fastmcp + + if not fastmcp.settings.experimental.enable_docket: + raise RuntimeError( + "Docket support is not enabled. " + "Set FASTMCP_EXPERIMENTAL_ENABLE_DOCKET=true to enable experimental Docket support." + ) + + worker = _current_worker.get() + if worker is None: + raise RuntimeError( + "No Worker instance found. This should not happen when " + "FASTMCP_EXPERIMENTAL_ENABLE_DOCKET is enabled." + ) + + return worker + + +def CurrentWorker() -> Worker: + """Get the current Docket Worker instance managed by FastMCP. + + This dependency provides access to the Worker instance that FastMCP + automatically creates when experimental Docket support is enabled. + + Requires: + - FASTMCP_EXPERIMENTAL_ENABLE_DOCKET=true + + Returns: + A dependency that resolves to the active Worker instance + + Raises: + RuntimeError: If experimental flag not enabled (during resolution) + + Example: + ```python + from fastmcp.dependencies import CurrentWorker + + @mcp.tool() + async def check_worker_status(worker: Worker = CurrentWorker()) -> str: + return f"Worker: {worker.name}" + ``` + """ + return cast("Worker", _CurrentWorker()) + + def get_http_request() -> Request: from mcp.server.lowlevel.server import request_ctx diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index ea6e6b70b..e09b90a0e 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -382,13 +382,66 @@ def icons(self) -> list[mcp.types.Icon]: else: return list(self._mcp_server.icons) + @asynccontextmanager + async def _docket_lifespan( + self, user_lifespan_result: LifespanResultT + ) -> AsyncIterator[LifespanResultT]: + """Manage Docket instance and Worker when experimental support is enabled. + + Args: + user_lifespan_result: The result from the user's lifespan function + + Yields: + User's lifespan result (Docket is managed via ContextVar, not lifespan result) + """ + from fastmcp import settings + from fastmcp.server.dependencies import _current_docket, _current_worker + + if not settings.experimental.enable_docket: + # Docket support not enabled, pass through user lifespan result + yield user_lifespan_result + return + + # Import Docket components + from docket import Docket, Worker + + # Create Docket instance with memory:// URL + async with Docket(url="memory://") as docket: + # Set Docket in ContextVar so CurrentDocket can access it + docket_token = _current_docket.set(docket) + try: + # Create and start Worker, then task group for run_forever() + async with ( + Worker(docket) as worker, + anyio.create_task_group() as tg, + ): + # Set Worker in ContextVar so CurrentWorker can access it + worker_token = _current_worker.set(worker) + try: + # Start worker as background task + tg.start_soon(worker.run_forever) + + try: + yield user_lifespan_result + finally: + # Cancel task group when exiting (cancels worker) + tg.cancel_scope.cancel() + finally: + _current_worker.reset(worker_token) + finally: + # Reset ContextVar + _current_docket.reset(docket_token) + @asynccontextmanager async def _lifespan_manager(self) -> AsyncIterator[None]: if self._lifespan_result_set: yield return - async with self._lifespan(self) as lifespan_result: + async with ( + self._lifespan(self) as user_lifespan_result, + self._docket_lifespan(user_lifespan_result) as lifespan_result, + ): self._lifespan_result = lifespan_result self._lifespan_result_set = True diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index ca84cb5a2..0f0ab4fe7 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -82,6 +82,19 @@ class ExperimentalSettings(BaseSettings): ), ] = False + enable_docket: Annotated[ + bool, + Field( + description=inspect.cleandoc( + """ + Enable experimental Docket support for background task execution. + When enabled, FastMCP will create a Docket instance with a Worker + to process background tasks. + """ + ), + ), + ] = False + class Settings(BaseSettings): """FastMCP settings.""" diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py index de2ccb790..208ae3106 100644 --- a/tests/server/test_dependencies.py +++ b/tests/server/test_dependencies.py @@ -7,9 +7,11 @@ from fastmcp import FastMCP from fastmcp.client import Client -from fastmcp.dependencies import Depends +from fastmcp.dependencies import CurrentContext, Depends from fastmcp.server.context import Context +HUZZAH = "huzzah!" + class Connection: """Test connection that tracks whether it's currently open.""" @@ -147,6 +149,37 @@ async def my_tool( assert len(tool.inputSchema["properties"]) == 2 +async def test_current_context_dependency(mcp: FastMCP): + """Test that CurrentContext dependency provides access to FastMCP Context.""" + + @mcp.tool() + def use_context(ctx: Context = CurrentContext()) -> str: + assert isinstance(ctx, Context) + return HUZZAH + + async with Client(mcp) as client: + result = await client.call_tool("use_context", {}) + assert HUZZAH in str(result) + + +async def test_current_context_and_legacy_context_coexist(mcp: FastMCP): + """Test that CurrentContext dependency and legacy Context injection work together.""" + + @mcp.tool() + def use_both_contexts( + legacy_ctx: Context, + dep_ctx: Context = CurrentContext(), + ) -> str: + assert isinstance(legacy_ctx, Context) + assert isinstance(dep_ctx, Context) + assert legacy_ctx is dep_ctx + return HUZZAH + + async with Client(mcp) as client: + result = await client.call_tool("use_both_contexts", {}) + assert HUZZAH in str(result) + + async def test_backward_compat_context_still_works(mcp: FastMCP): """Test that existing Context injection via type annotation still works.""" diff --git a/tests/server/test_server_docket.py b/tests/server/test_server_docket.py new file mode 100644 index 000000000..bd995cee0 --- /dev/null +++ b/tests/server/test_server_docket.py @@ -0,0 +1,195 @@ +"""Tests for experimental Docket integration in FastMCP.""" + +import asyncio +from contextlib import asynccontextmanager + +import pytest +from docket import Docket +from docket.worker import Worker + +from fastmcp import FastMCP +from fastmcp.client import Client +from fastmcp.dependencies import CurrentDocket, CurrentWorker +from fastmcp.exceptions import ToolError +from fastmcp.server.dependencies import get_context +from fastmcp.utilities.tests import temporary_settings + +HUZZAH = "huzzah!" + + +@pytest.fixture(autouse=True) +def enable_docket(): + """Enable experimental Docket support for all tests in this suite.""" + with temporary_settings(experimental__enable_docket=True): + yield + + +async def test_docket_disabled(): + """Verify that Docket errors when experimental flag is disabled.""" + with temporary_settings(experimental__enable_docket=False): + mcp = FastMCP("test-server") + + @mcp.tool() + def needs_docket(docket: Docket = CurrentDocket()) -> str: + return f"Got docket: {type(docket).__name__}" + + async with Client(mcp) as client: + with pytest.raises(ToolError, match="Failed to resolve dependency"): + await client.call_tool("needs_docket", {}) + + +async def test_current_docket_with_flag_enabled(): + """CurrentDocket dependency works when experimental flag is enabled.""" + mcp = FastMCP("test-server") + + @mcp.tool() + def check_docket(docket: Docket = CurrentDocket()) -> str: + assert isinstance(docket, Docket) + return HUZZAH + + async with Client(mcp) as client: + result = await client.call_tool("check_docket", {}) + assert HUZZAH in str(result) + + +async def test_current_worker_with_flag_enabled(): + """CurrentWorker dependency works when experimental flag is enabled.""" + mcp = FastMCP("test-server") + + @mcp.tool() + def check_worker( + worker: Worker = CurrentWorker(), + docket: Docket = CurrentDocket(), + ) -> str: + assert isinstance(worker, Worker) + assert worker.docket is docket + return HUZZAH + + async with Client(mcp) as client: + result = await client.call_tool("check_worker", {}) + assert HUZZAH in str(result) + + +async def test_worker_executes_background_tasks(): + """Verify that the Docket Worker is running and executes tasks.""" + executed_tasks = [] + mcp = FastMCP("test-server") + + @mcp.tool() + async def schedule_work( + task_name: str, + docket: Docket = CurrentDocket(), + worker: Worker = CurrentWorker(), + ) -> str: + """Schedule a background task and wait for it to complete.""" + + async def background_task(name: str): + """Simple background task that records its execution.""" + executed_tasks.append(name) + + # Schedule the task + await docket.add(background_task)(task_name) + + # Run worker until it finishes all queued tasks + await worker.run_until_finished() + + return f"Scheduled {task_name}" + + async with Client(mcp) as client: + result = await client.call_tool("schedule_work", {"task_name": "test-task"}) + assert "Scheduled test-task" in str(result) + assert "test-task" in executed_tasks + + +async def test_current_docket_in_resource(): + """CurrentDocket works in resources when flag is enabled.""" + mcp = FastMCP("test-server") + + @mcp.resource("docket://info") + def get_docket_info(docket: Docket = CurrentDocket()) -> str: + assert isinstance(docket, Docket) + return HUZZAH + + async with Client(mcp) as client: + result = await client.read_resource("docket://info") + assert HUZZAH in str(result) + + +async def test_current_docket_in_prompt(): + """CurrentDocket works in prompts when flag is enabled.""" + mcp = FastMCP("test-server") + + @mcp.prompt() + def task_prompt(task_type: str, docket: Docket = CurrentDocket()) -> str: + assert isinstance(docket, Docket) + return HUZZAH + + async with Client(mcp) as client: + result = await client.get_prompt("task_prompt", {"task_type": "background"}) + assert HUZZAH in str(result) + + +async def test_current_docket_in_resource_template(): + """CurrentDocket works in resource templates when flag is enabled.""" + mcp = FastMCP("test-server") + + @mcp.resource("docket://tasks/{task_id}") + def get_task_status(task_id: str, docket: Docket = CurrentDocket()) -> str: + assert isinstance(docket, Docket) + return HUZZAH + + async with Client(mcp) as client: + result = await client.read_resource("docket://tasks/123") + assert HUZZAH in str(result) + + +async def test_concurrent_calls_maintain_isolation(): + """Multiple concurrent calls each get the same Docket instance.""" + mcp = FastMCP("test-server") + docket_ids = [] + + @mcp.tool() + def capture_docket_id(call_num: int, docket: Docket = CurrentDocket()) -> str: + docket_ids.append((call_num, id(docket))) + return HUZZAH + + async with Client(mcp) as client: + results = await asyncio.gather( + client.call_tool("capture_docket_id", {"call_num": 1}), + client.call_tool("capture_docket_id", {"call_num": 2}), + client.call_tool("capture_docket_id", {"call_num": 3}), + ) + + for result in results: + assert HUZZAH in str(result) + + # All calls should see the same Docket instance + assert len(docket_ids) == 3 + first_id = docket_ids[0][1] + assert all(docket_id == first_id for _, docket_id in docket_ids) + + +async def test_user_lifespan_still_works_with_docket(): + """User-provided lifespan works correctly alongside Docket.""" + lifespan_entered = False + + @asynccontextmanager + async def custom_lifespan(server: FastMCP): + nonlocal lifespan_entered + lifespan_entered = True + yield {"custom_data": "test_value"} + + mcp = FastMCP("test-server", lifespan=custom_lifespan) + + @mcp.tool() + def check_both(docket: Docket = CurrentDocket()) -> str: + assert isinstance(docket, Docket) + ctx = get_context() + lifespan_data = ctx.request_context.lifespan_context + assert lifespan_data.get("custom_data") == "test_value" + return HUZZAH + + async with Client(mcp) as client: + assert lifespan_entered + result = await client.call_tool("check_both", {}) + assert HUZZAH in str(result) diff --git a/uv.lock b/uv.lock index 13b521f45..340c4fd97 100644 --- a/uv.lock +++ b/uv.lock @@ -567,6 +567,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, ] +[[package]] +name = "fakeredis" +version = "2.32.1" +source = { git = "https://github.com/cunla/fakeredis-py.git?rev=ad50a0de8d6dce554fb629ec284bc4ccbc6a7f12#ad50a0de8d6dce554fb629ec284bc4ccbc6a7f12" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + [[package]] name = "fancycompleter" version = "0.11.1" @@ -624,6 +639,7 @@ openai = [ [package.dev-dependencies] dev = [ { name = "dirty-equals" }, + { name = "fakeredis", extra = ["lua"] }, { name = "fastapi" }, { name = "fastmcp", extra = ["openai"] }, { name = "inline-snapshot", extra = ["dirty-equals"] }, @@ -673,6 +689,7 @@ provides-extras = ["openai"] [package.metadata.requires-dev] dev = [ { name = "dirty-equals", specifier = ">=0.9.0" }, + { name = "fakeredis", extras = ["lua"], git = "https://github.com/cunla/fakeredis-py.git?rev=ad50a0de8d6dce554fb629ec284bc4ccbc6a7f12" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "fastmcp", extras = ["openai"] }, { name = "inline-snapshot", extras = ["dirty-equals"], specifier = ">=0.27.2" }, @@ -1062,6 +1079,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, ] +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" }, + { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" }, + { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" }, + { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, + { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, + { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, + { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, + { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -2138,6 +2229,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "sse-starlette" version = "3.0.2"