diff --git a/pyproject.toml b/pyproject.toml index 52ece028..c15ed3e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [project] name = "sap-cloud-sdk" - -version = "0.49.0" +version = "0.49.1" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/agentgateway/__init__.py b/src/sap_cloud_sdk/agentgateway/__init__.py index a8c0c850..03b0444d 100644 --- a/src/sap_cloud_sdk/agentgateway/__init__.py +++ b/src/sap_cloud_sdk/agentgateway/__init__.py @@ -54,6 +54,7 @@ from sap_cloud_sdk.agentgateway._models import ( AuthResult, + CacheOptions, MCPTool, MCPToolFilter, Agent, @@ -78,6 +79,7 @@ "ClientConfig", # Data models "AuthResult", + "CacheOptions", "MCPTool", "MCPToolFilter", "Agent", diff --git a/src/sap_cloud_sdk/agentgateway/_models.py b/src/sap_cloud_sdk/agentgateway/_models.py index 2ce30c79..ea1a695e 100644 --- a/src/sap_cloud_sdk/agentgateway/_models.py +++ b/src/sap_cloud_sdk/agentgateway/_models.py @@ -1,8 +1,18 @@ """Data models for Agent Gateway MCP tools.""" +from __future__ import annotations + import json from dataclasses import dataclass, field -from typing import Any +from typing import TYPE_CHECKING, Any + +from sap_cloud_sdk.agentgateway.config import ( + DEFAULT_MAX_MCP_TOOLS_CACHE_SIZE, + DEFAULT_MCP_TOOLS_CACHE_TTL_SECONDS, +) + +if TYPE_CHECKING: + from sap_cloud_sdk.agentgateway._tools_cache import MCPToolsCache @dataclass @@ -213,3 +223,50 @@ class MCPToolFilter: names: list[str] = field(default_factory=list) ord_ids: list[str] = field(default_factory=list) + + +class CacheOptions: + """Options for caching the result of list_mcp_tools. + + Pass an instance to list_mcp_tools(cache=...) to enable result caching. + The same instance can be reused across calls — cache state is stored on + it. Call evict() to force a fresh fetch on the next call. + + Args: + ttl: Cache lifetime in seconds. Defaults to 600 s. + max_size: Maximum number of distinct cached entries (keyed by filter + combo + auth type). Oldest entry is evicted when the limit is + exceeded. Defaults to 32. + + Example: + ```python + from sap_cloud_sdk.agentgateway import CacheOptions + + cache = CacheOptions(ttl=300) + tools = await agw_client.list_mcp_tools(cache=cache) + + # Later — force a fresh fetch (e.g. after a tool was added): + cache.evict() + tools = await agw_client.list_mcp_tools(cache=cache) + ``` + + Note: + Cache is in-process only. It is not shared across client instances, + processes, or Kubernetes pods. Two concurrent calls that both miss + the cache will both fetch independently — the last writer wins, no + data corruption occurs. + """ + + def __init__( + self, + ttl: float = DEFAULT_MCP_TOOLS_CACHE_TTL_SECONDS, + max_size: int = DEFAULT_MAX_MCP_TOOLS_CACHE_SIZE, + ) -> None: + self.ttl = ttl + self.max_size = max_size + self._cache: MCPToolsCache | None = None + + def evict(self) -> None: + """Clear all cached tool list entries. Forces a fresh fetch on the next call.""" + if self._cache is not None: + self._cache.evict() diff --git a/src/sap_cloud_sdk/agentgateway/_tools_cache.py b/src/sap_cloud_sdk/agentgateway/_tools_cache.py new file mode 100644 index 00000000..08d29355 --- /dev/null +++ b/src/sap_cloud_sdk/agentgateway/_tools_cache.py @@ -0,0 +1,89 @@ +"""Result cache for MCP tool lists. + +Caches list[MCPTool] per (filter, auth-type) key to avoid redundant MCP +session round-trips during agentic loops. Bounded by max_size with LRU +eviction; each entry has a monotonic TTL. + +Thread safety: +CPython GIL makes individual OrderedDict operations atomic, but compound +check-then-set is not. Two concurrent coroutines for the same key may both +miss and both fetch; the race produces redundant tool-list requests, not +data corruption. This matches the accepted behaviour in _token_cache.py. +""" + +import logging +import time +from collections import OrderedDict +from dataclasses import dataclass + +from sap_cloud_sdk.agentgateway._models import CacheOptions, MCPTool, MCPToolFilter + +logger = logging.getLogger(__name__) + + +@dataclass +class _CachedToolList: + tools: list[MCPTool] + expires_at: float # time.monotonic() value + + def is_valid(self) -> bool: + return time.monotonic() < self.expires_at + + +def _make_cache_key(filter: MCPToolFilter | None, user_scoped: bool) -> str: + """Build a stable string key from filter options and auth type.""" + ord_ids = "|".join(sorted(filter.ord_ids)) if filter and filter.ord_ids else "" + names = "|".join(sorted(filter.names)) if filter and filter.names else "" + auth = "user" if user_scoped else "system" + return f"{auth}:ord={ord_ids}:names={names}" + + +class MCPToolsCache: + """TTL + LRU cache for MCP tool list results. + + Keyed by (filter combo, auth type). Entries expire after `options.ttl` + seconds. When the number of entries exceeds `options.max_size`, the + least-recently-used entry is evicted. + + Callers hold a reference to their CacheOptions instance and call + evict() to invalidate all entries. + """ + + def __init__(self) -> None: + self._entries: OrderedDict[str, _CachedToolList] = OrderedDict() + + def get( + self, + filter: MCPToolFilter | None, + user_scoped: bool, + ) -> list[MCPTool] | None: + """Return cached tools for the given filter/auth combo, or None if miss/expired.""" + key = _make_cache_key(filter, user_scoped) + entry = self._entries.get(key) + if entry and entry.is_valid(): + self._entries.move_to_end(key) + return entry.tools + if entry: + del self._entries[key] + return None + + def set( + self, + tools: list[MCPTool], + filter: MCPToolFilter | None, + user_scoped: bool, + options: CacheOptions, + ) -> None: + """Store tools under the given filter/auth key, evicting LRU if at capacity.""" + key = _make_cache_key(filter, user_scoped) + expires_at = time.monotonic() + options.ttl + self._entries[key] = _CachedToolList(tools=tools, expires_at=expires_at) + self._entries.move_to_end(key) + while len(self._entries) > options.max_size: + evicted, _ = self._entries.popitem(last=False) + logger.debug("MCP tools cache full — evicted key '%s'", evicted) + + def evict(self) -> None: + """Clear all cached entries. Forces a fresh fetch on the next call.""" + self._entries.clear() + logger.debug("MCP tools cache evicted") diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index 51203dd5..30c2086f 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -34,11 +34,13 @@ Agent, AgentCardFilter, AuthResult, + CacheOptions, MCPTool, MCPToolFilter, ) from sap_cloud_sdk.core._tenant import _validate_tenant_subdomain from sap_cloud_sdk.agentgateway._token_cache import _GatewayUrlCache, _TokenCache +from sap_cloud_sdk.agentgateway._tools_cache import MCPToolsCache from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError from sap_cloud_sdk.core.telemetry import Module, Operation, record_metrics @@ -375,6 +377,7 @@ async def list_mcp_tools( self, user_token: str | Callable[[], str] | None = None, filter: MCPToolFilter | None = None, + cache: CacheOptions | None = None, ) -> list[MCPTool]: """List all MCP tools from MCP servers. @@ -395,6 +398,11 @@ async def list_mcp_tools( If provided, uses user-scoped auth instead of system auth. filter: Optional filter to narrow results by tool name or ORD ID. If None or empty, all tools are included. + cache: Optional caching options. When provided, tool lists are cached + in-process for ``cache.ttl`` seconds (default 600 s). Distinct filter + and auth-type combinations are cached independently, up to + ``cache.max_size`` entries (LRU eviction). Call ``cache.evict()`` to + clear all entries and force a fresh fetch on the next call. Returns: List of MCPTool objects from all MCP servers. @@ -419,9 +427,32 @@ async def list_mcp_tools( ord_ids=["sap.s4:apiAccess:salesOrder:v1"], ) ) + + # With caching — avoids redundant MCP round-trips: + from sap_cloud_sdk.agentgateway import CacheOptions + cache = CacheOptions(ttl=300) + tools = await agw_client.list_mcp_tools(cache=cache) + + # Force a fresh fetch (e.g. after a tool was added on the server): + cache.evict() + tools = await agw_client.list_mcp_tools(cache=cache) ``` """ try: + user_scoped = bool(user_token) + + if cache is not None: + if cache._cache is None: + cache._cache = MCPToolsCache() + tools_cache: MCPToolsCache | None = cache._cache + cache_opts: CacheOptions | None = cache + cached = tools_cache.get(filter, user_scoped) + if cached is not None: + return cached + else: + tools_cache = None + cache_opts = None + if user_token: auth = await self.get_user_auth(user_token) else: @@ -434,32 +465,41 @@ async def list_mcp_tools( "Customer agent credentials detected at '%s'", credentials_path ) credentials = load_customer_credentials(credentials_path) - return await get_mcp_tools_customer( + tools = await get_mcp_tools_customer( credentials, auth.access_token, self._config.timeout, filter=filter, ) + if tools_cache is not None and cache_opts is not None: + tools_cache.set(tools, filter, user_scoped, cache_opts) + return tools # Check for transparent mode if detect_transparent_credentials(): logger.info(_LOG_TRANSPARENT_MODE) credentials = load_customer_credentials_from_env() - return await get_mcp_tools_customer( + tools = await get_mcp_tools_customer( credentials, auth.access_token, self._config.timeout, filter=filter, ) + if tools_cache is not None and cache_opts is not None: + tools_cache.set(tools, filter, user_scoped, cache_opts) + return tools # LoB flow - requires tenant_subdomain tenant = self._resolve_tenant_subdomain() - return await get_mcp_tools_lob( + tools = await get_mcp_tools_lob( tenant, auth.access_token, self._config.timeout, filter=filter, ) + if tools_cache is not None and cache_opts is not None: + tools_cache.set(tools, filter, user_scoped, cache_opts) + return tools except AgentGatewaySDKError: raise diff --git a/src/sap_cloud_sdk/agentgateway/config.py b/src/sap_cloud_sdk/agentgateway/config.py index 17495dbd..829fb463 100644 --- a/src/sap_cloud_sdk/agentgateway/config.py +++ b/src/sap_cloud_sdk/agentgateway/config.py @@ -7,6 +7,8 @@ DEFAULT_TOKEN_EXPIRY_BUFFER_SECONDS = 30.0 DEFAULT_MAX_SYSTEM_TOKEN_CACHE_SIZE = 32 DEFAULT_MAX_USER_TOKEN_CACHE_SIZE = 256 +DEFAULT_MCP_TOOLS_CACHE_TTL_SECONDS = 600.0 +DEFAULT_MAX_MCP_TOOLS_CACHE_SIZE = 32 @dataclass diff --git a/src/sap_cloud_sdk/agentgateway/user-guide.md b/src/sap_cloud_sdk/agentgateway/user-guide.md index c578bff3..3f20b2f0 100644 --- a/src/sap_cloud_sdk/agentgateway/user-guide.md +++ b/src/sap_cloud_sdk/agentgateway/user-guide.md @@ -95,6 +95,36 @@ agents = await agw_client.list_agent_cards( ) ``` +### Caching Tool Lists + +In agentic loops, `list_mcp_tools()` can be called repeatedly. By default every call opens fresh MCP sessions — expensive for a tool list that rarely changes. Pass a `CacheOptions` instance to cache results in-process. + +```python +from sap_cloud_sdk.agentgateway import CacheOptions, create_client + +agw_client = create_client(tenant_subdomain="my-tenant") +cache = CacheOptions(ttl=300) # cache for 5 minutes + +# First call fetches from network and stores in cache +tools = await agw_client.list_mcp_tools(cache=cache) + +# Subsequent calls within TTL return immediately — no network round-trip +tools = await agw_client.list_mcp_tools(cache=cache) + +# Force a fresh fetch (e.g. after a tool was added on the server): +cache.evict() +tools = await agw_client.list_mcp_tools(cache=cache) +``` + +The cache is scoped to the `CacheOptions` instance — different instances don't share state. Distinct filter and auth-type combinations are cached as independent entries, up to `max_size` entries total (LRU eviction when the limit is hit). + +```python +# Custom TTL and size cap +cache = CacheOptions(ttl=600, max_size=10) +``` + +The cache is **in-process only** — not shared across client instances, processes, or Kubernetes pods. + ### LangChain Integration Convert MCP tools to LangChain `StructuredTool` objects for use with LangChain agents: @@ -221,6 +251,7 @@ class AgentGatewayClient: self, user_token: str | Callable[[], str] | None = None, filter: MCPToolFilter | None = None, + cache: CacheOptions | None = None, ) -> list[MCPTool] async def call_mcp_tool( @@ -281,6 +312,21 @@ Both fields default to empty lists. `names` is applied after fetching; `ord_ids` > Both filter classes use AND semantics: if both fields are set, a result must match all of them to be included. +### CacheOptions + +```python +from sap_cloud_sdk.agentgateway import CacheOptions + +CacheOptions( + ttl=600.0, # cache lifetime in seconds; default 600 + max_size=32, # max distinct cached entries (LRU eviction); default 32 +) +``` + +- `ttl`: How long a cached tool list is considered valid. After expiry the next call fetches fresh from the network. +- `max_size`: Cap on how many distinct entries (filter + auth-type combinations) are held in memory. When exceeded, the least-recently-used entry is evicted. +- `.evict()`: Clears all entries immediately, forcing a fresh fetch on the next call. + ### Data Models ```python diff --git a/src/sap_cloud_sdk/aicore/user-guide.md b/src/sap_cloud_sdk/aicore/user-guide.md index ad37e300..095a9204 100644 --- a/src/sap_cloud_sdk/aicore/user-guide.md +++ b/src/sap_cloud_sdk/aicore/user-guide.md @@ -131,7 +131,7 @@ before the cached OAuth token expires — so agents never see a 401 at all. ```python from sap_cloud_sdk.aicore import set_aicore_config, watch_aicore_config -set_aicore_config() # load credentials at startup +set_aicore_config() # load credentials at startup watch_aicore_config() # proactive reload on secret rotation ``` @@ -158,7 +158,7 @@ directly, bypassing the SDK's reactive handler. Two options: from sap_cloud_sdk.aicore import set_aicore_config, watch_aicore_config set_aicore_config() -watch_aicore_config() # ADD THIS — no other changes needed +watch_aicore_config() # ADD THIS — no other changes needed ``` **Option B — also add reactive reload for ChatLiteLLM:** @@ -171,7 +171,7 @@ from sap_cloud_sdk.aicore import ( ) set_aicore_config() -patch_litellm_for_credential_rotation() # patches litellm.completion globally +patch_litellm_for_credential_rotation() # patches litellm.completion globally watch_aicore_config() ``` diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index de1b41fd..152f4a92 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -117,6 +117,7 @@ to check a toggle for the current request: ```python from sap_cloud_sdk.core.runtime_context import is_feature_enabled + @app.route("/") async def handler(request): if is_feature_enabled("my-feature"): diff --git a/tests/agentgateway/unit/test_tools_cache.py b/tests/agentgateway/unit/test_tools_cache.py new file mode 100644 index 00000000..0ef0f395 --- /dev/null +++ b/tests/agentgateway/unit/test_tools_cache.py @@ -0,0 +1,189 @@ +"""Unit tests for MCPToolsCache.""" + +import time +from unittest.mock import patch + +import pytest + +from sap_cloud_sdk.agentgateway._models import CacheOptions, MCPTool, MCPToolFilter +from sap_cloud_sdk.agentgateway._tools_cache import MCPToolsCache, _make_cache_key + + +def _tool(name: str) -> MCPTool: + return MCPTool( + name=name, + server_name="test-server", + description="desc", + input_schema={}, + url="https://example.com/mcp", + ) + + +TOOLS_A = [_tool("tool-a")] +TOOLS_B = [_tool("tool-b"), _tool("tool-c")] +DEFAULT_OPTIONS = CacheOptions() + + +class TestCacheKey: + def test_system_and_user_produce_different_keys(self): + assert _make_cache_key(None, False) != _make_cache_key(None, True) + + def test_filter_ord_ids_included_in_key(self): + f = MCPToolFilter(ord_ids=["sap.s4:v1", "sap.crm:v2"]) + key = _make_cache_key(f, False) + assert "sap.s4:v1" in key + assert "sap.crm:v2" in key + + def test_filter_ord_ids_sorted_for_stability(self): + f1 = MCPToolFilter(ord_ids=["b", "a"]) + f2 = MCPToolFilter(ord_ids=["a", "b"]) + assert _make_cache_key(f1, False) == _make_cache_key(f2, False) + + def test_filter_names_sorted_for_stability(self): + f1 = MCPToolFilter(names=["z", "a"]) + f2 = MCPToolFilter(names=["a", "z"]) + assert _make_cache_key(f1, False) == _make_cache_key(f2, False) + + def test_none_filter_and_empty_filter_same_key(self): + assert _make_cache_key(None, False) == _make_cache_key(MCPToolFilter(), False) + + def test_different_filters_different_keys(self): + f1 = MCPToolFilter(names=["get-order"]) + f2 = MCPToolFilter(names=["create-order"]) + assert _make_cache_key(f1, False) != _make_cache_key(f2, False) + + +class TestCacheHitAndMiss: + def test_miss_on_empty_cache(self): + c = MCPToolsCache() + assert c.get(None, False) is None + + def test_hit_after_set(self): + c = MCPToolsCache() + c.set(TOOLS_A, None, False, DEFAULT_OPTIONS) + result = c.get(None, False) + assert result == TOOLS_A + + def test_miss_for_different_filter(self): + c = MCPToolsCache() + c.set(TOOLS_A, None, False, DEFAULT_OPTIONS) + assert c.get(MCPToolFilter(names=["other"]), False) is None + + def test_miss_for_different_auth_type(self): + c = MCPToolsCache() + c.set(TOOLS_A, None, False, DEFAULT_OPTIONS) + assert c.get(None, True) is None + + def test_independent_entries_for_different_filters(self): + c = MCPToolsCache() + f1 = MCPToolFilter(names=["tool-a"]) + f2 = MCPToolFilter(names=["tool-b"]) + c.set(TOOLS_A, f1, False, DEFAULT_OPTIONS) + c.set(TOOLS_B, f2, False, DEFAULT_OPTIONS) + assert c.get(f1, False) == TOOLS_A + assert c.get(f2, False) == TOOLS_B + + +class TestTTLExpiry: + def test_expired_entry_returns_none(self): + c = MCPToolsCache() + options = CacheOptions(ttl=1.0) + c.set(TOOLS_A, None, False, options) + with patch("sap_cloud_sdk.agentgateway._tools_cache.time") as mock_time: + mock_time.monotonic.return_value = time.monotonic() + 2.0 + assert c.get(None, False) is None + + def test_valid_entry_within_ttl_is_returned(self): + c = MCPToolsCache() + options = CacheOptions(ttl=600.0) + c.set(TOOLS_A, None, False, options) + assert c.get(None, False) == TOOLS_A + + def test_expired_entry_is_removed_from_cache(self): + c = MCPToolsCache() + options = CacheOptions(ttl=1.0) + c.set(TOOLS_A, None, False, options) + with patch("sap_cloud_sdk.agentgateway._tools_cache.time") as mock_time: + mock_time.monotonic.return_value = time.monotonic() + 2.0 + c.get(None, False) + assert len(c._entries) == 0 + + +class TestLruEviction: + def test_lru_entry_evicted_when_full(self): + options = CacheOptions(max_size=2) + c = MCPToolsCache() + f1 = MCPToolFilter(names=["a"]) + f2 = MCPToolFilter(names=["b"]) + f3 = MCPToolFilter(names=["c"]) + + c.set(TOOLS_A, f1, False, options) + c.set(TOOLS_A, f2, False, options) + # f1 is now LRU — adding f3 should evict it + c.set(TOOLS_A, f3, False, options) + + assert c.get(f1, False) is None # evicted + assert c.get(f2, False) == TOOLS_A + assert c.get(f3, False) == TOOLS_A + + def test_get_promotes_entry_to_mru(self): + options = CacheOptions(max_size=2) + c = MCPToolsCache() + f1 = MCPToolFilter(names=["a"]) + f2 = MCPToolFilter(names=["b"]) + f3 = MCPToolFilter(names=["c"]) + + c.set(TOOLS_A, f1, False, options) + c.set(TOOLS_A, f2, False, options) + # Access f1 to make it MRU; f2 becomes LRU + c.get(f1, False) + c.set(TOOLS_A, f3, False, options) + + assert c.get(f1, False) == TOOLS_A # promoted — not evicted + assert c.get(f2, False) is None # evicted + + +class TestEvict: + def test_evict_clears_all_entries(self): + c = MCPToolsCache() + c.set(TOOLS_A, None, False, DEFAULT_OPTIONS) + c.set(TOOLS_B, None, True, DEFAULT_OPTIONS) + c.evict() + assert c.get(None, False) is None + assert c.get(None, True) is None + + def test_evict_on_empty_cache_is_noop(self): + c = MCPToolsCache() + c.evict() # should not raise + assert len(c._entries) == 0 + + def test_set_after_evict_works(self): + c = MCPToolsCache() + c.set(TOOLS_A, None, False, DEFAULT_OPTIONS) + c.evict() + c.set(TOOLS_B, None, False, DEFAULT_OPTIONS) + assert c.get(None, False) == TOOLS_B + + +class TestCacheOptionsEvict: + def test_evict_before_first_use_is_noop(self): + cache = CacheOptions() + cache.evict() # _cache is None — should not raise + + def test_evict_clears_entries_via_cache_options(self): + cache = CacheOptions() + cache._cache = MCPToolsCache() + cache._cache.set(TOOLS_A, None, False, cache) + cache.evict() + assert cache._cache.get(None, False) is None + + def test_cache_options_defaults(self): + cache = CacheOptions() + assert cache.ttl == 600.0 + assert cache.max_size == 32 + assert cache._cache is None + + def test_cache_options_custom_values(self): + cache = CacheOptions(ttl=120.0, max_size=5) + assert cache.ttl == 120.0 + assert cache.max_size == 5 diff --git a/uv.lock b/uv.lock index 8a116925..736e0817 100644 --- a/uv.lock +++ b/uv.lock @@ -1013,9 +1013,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" }, { url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" }, { url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/8fd452fd81adb9ec79c8275c1375702ab0fd6bee4952da12eaa09b9508d8/greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", size = 623515, upload-time = "2026-05-20T14:09:07.853Z" }, { url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bc/c318aa9f3ffc77320fddcee3d892be957b42e2ff947198d9450b004f3a38/greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", size = 418439, upload-time = "2026-05-20T14:01:38.446Z" }, { url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" }, { url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" }, { url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" }, @@ -1023,9 +1021,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, - { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, - { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, @@ -1033,9 +1029,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, @@ -1043,9 +1037,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" }, { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" }, { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, @@ -1053,18 +1045,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, - { url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" }, { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" }, { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, - { url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" }, { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, - { url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" }, { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, @@ -1072,9 +1060,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, - { url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" }, { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" }, { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, @@ -3939,7 +3925,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.47.0" +version = "0.49.1" source = { editable = "." } dependencies = [ { name = "cryptography" },