mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
feat(fireworks): migrate to fireworks-ai 1.x SDK (#37581)
Closes #37172 --- Bumps `langchain-fireworks` to the rewritten `fireworks-ai` 1.x SDK (currently 1.2.0a*; Stainless-generated, pure-httpx, no `grpcio`/`protobuf`/`googleapis-common-protos`). The motivating bug is a startup crash in self-hosted LangGraph environments that also import `langchain-google-vertexai`. Importing `fireworks-ai` 0.19.x eagerly loads vendored grpcio protobuf modules under `fireworks.control_plane.generated.protos_grpcio.*`, which register `google/rpc/status.proto`, `google/api/*.proto`, and `google/longrunning/*.proto` in the default protobuf descriptor pool. When `langchain-google-vertexai` later triggers `google.api_core.exceptions` → `grpc_status.rpc_status` → `google.rpc.status_pb2`, the pool already holds a byte-different descriptor for `google/rpc/status.proto` and startup dies with: ``` TypeError: Couldn't build proto file into descriptor pool: duplicate file name google/rpc/status.proto ``` Fleet has been pinning around this by routing Fireworks through `ChatOpenAI` against the OpenAI-compat endpoint, which works for inference but means Fireworks `ModelProfile` data never loads — so Kimi K2.6's ~262k context window goes unrecognized and summarization triggers below limit. The 1.x SDK does not vendor protobuf at all. The control-plane gRPC code path is gone; chat inference goes over httpx. Verified locally that `import langchain_fireworks` and `from langchain_fireworks import ChatFireworks` load zero `_pb2` / `google.*` modules. ## What changed in `ChatFireworks` - Imports switch from `fireworks.client` to the top-level `fireworks` package. - Async path now `await client.chat.completions.create(...)`; the 0.x `acreate` shim is no longer used. - Error classes remapped to the 1.x hierarchy. `InvalidRequestError` → `BadRequestError`. `BadGatewayError` and `ServiceUnavailableError` no longer exist (1.x maps all `>=500` to `InternalServerError`) and were dropped from the retryable set with no loss of coverage. `FireworksContextOverflowError`'s parent class becomes `BadRequestError`. - `stream_options` is moved into the SDK's `extra_body` because the Stainless-generated `create()` signature does not model it as a typed kwarg. Top-level `stream_options` is preserved as a caller convenience; if a caller supplies both `extra_body["stream_options"]` and a top-level value, `extra_body` wins and the discarded value is logged. - The 0.x `(connect, read)` tuple form of `request_timeout` is normalized to an `httpx.Timeout` so existing user code keeps working. - The SDK's built-in retry layer is suppressed via `max_retries=0` on client construction so retries remain owned by `create_base_retry_decorator` and surface through the LangChain `run_manager`. ## Lifecycle methods Adds `close()` and `aclose()` on `ChatFireworks`. The 1.x `AsyncFireworks` client defaults to `httpx_aiohttp.HttpxAiohttpClient`, whose underlying aiohttp `ClientSession` is created lazily on first request. Sync-only paths therefore never open a session — which fixes the "Unclosed client session" warnings from #37172 at the source. Callers using async paths can now release the connector deterministically rather than relying on GC after the event loop has stopped. An autouse fixture in the integration `conftest.py` calls `aclose()` between tests to silence the corresponding `Unclosed connector` warning that surfaces under `pytest-asyncio`. ## Relation to #37227 Supersedes #37227. That PR monkey-patched `fireworks._util.is_running_in_async_context` and `fireworks.client.api_client.is_running_in_async_context` to suppress the 0.x SDK's eager `aiohttp.ClientSession` creation in async contexts. Both module paths are removed in 1.x; the SDK's lazy-session behavior makes the suppression unnecessary, and the explicit `aclose()` provides the cleaner long-term lifecycle hook. Thanks to @keenborder786 for surfacing the failure mode. ## Installation note `fireworks-ai` 1.x is currently published as an alpha (`1.2.0a*`); a stable 1.x is not yet out. `pip install langchain-fireworks` / `uv pip install langchain-fireworks` will need `--pre` (or `--prerelease=allow`) until Fireworks GAs 1.x. The `pyproject.toml` adds `[tool.uv] prerelease = "allow"` so the in-repo dev environment resolves cleanly. The package version is bumped to `1.4.0` — the public surface (`ChatFireworks`, `Fireworks`, `FireworksEmbeddings`) is unchanged; the breakage is confined to internal error classes and the transitive SDK.
This commit is contained in:
@@ -15,15 +15,14 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fireworks.client import AsyncFireworks, Fireworks # type: ignore[import-untyped]
|
from fireworks import (
|
||||||
from fireworks.client.error import ( # type: ignore[import-untyped]
|
|
||||||
APITimeoutError,
|
APITimeoutError,
|
||||||
BadGatewayError,
|
AsyncFireworks,
|
||||||
|
BadRequestError,
|
||||||
|
Fireworks,
|
||||||
FireworksError,
|
FireworksError,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
InvalidRequestError,
|
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
ServiceUnavailableError,
|
|
||||||
)
|
)
|
||||||
from langchain_core.callbacks import (
|
from langchain_core.callbacks import (
|
||||||
AsyncCallbackManagerForLLMRun,
|
AsyncCallbackManagerForLLMRun,
|
||||||
@@ -94,6 +93,7 @@ from pydantic import (
|
|||||||
BaseModel,
|
BaseModel,
|
||||||
ConfigDict,
|
ConfigDict,
|
||||||
Field,
|
Field,
|
||||||
|
PrivateAttr,
|
||||||
SecretStr,
|
SecretStr,
|
||||||
model_validator,
|
model_validator,
|
||||||
)
|
)
|
||||||
@@ -410,20 +410,19 @@ def _convert_chunk_to_message_chunk(
|
|||||||
class _RetryableHTTPStatusError(FireworksError):
|
class _RetryableHTTPStatusError(FireworksError):
|
||||||
"""Internal marker for 5xx `httpx.HTTPStatusError` responses.
|
"""Internal marker for 5xx `httpx.HTTPStatusError` responses.
|
||||||
|
|
||||||
The Fireworks SDK maps a subset of status codes (500, 502, 503) to typed
|
The 1.x SDK wraps every status response into a typed `APIStatusError`
|
||||||
exceptions but lets others (504, 507-511, Cloudflare-edge 520-599)
|
subclass, so this path is defense-in-depth: it only fires when a raw
|
||||||
propagate as raw `httpx.HTTPStatusError`. Promoting those to this marker
|
`httpx.HTTPStatusError` escapes the SDK (e.g., a custom `http_client` or
|
||||||
inside `_call` keeps the retryable set expressible as a list of classes
|
monkey-patched transport raises one directly). Promoting it here keeps the
|
||||||
for `create_base_retry_decorator`, preserving parity with `ChatMistralAI`.
|
retryable set expressible as a list of classes for
|
||||||
|
`create_base_retry_decorator`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
_RETRYABLE_ERRORS: tuple[type[BaseException], ...] = (
|
_RETRYABLE_ERRORS: tuple[type[BaseException], ...] = (
|
||||||
APITimeoutError,
|
APITimeoutError,
|
||||||
BadGatewayError,
|
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
ServiceUnavailableError,
|
|
||||||
httpx.TimeoutException,
|
httpx.TimeoutException,
|
||||||
httpx.TransportError,
|
httpx.TransportError,
|
||||||
_RetryableHTTPStatusError,
|
_RetryableHTTPStatusError,
|
||||||
@@ -438,14 +437,16 @@ def _promote_http_status_error(exc: httpx.HTTPStatusError) -> NoReturn:
|
|||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
class FireworksContextOverflowError(InvalidRequestError, ContextOverflowError):
|
class FireworksContextOverflowError(BadRequestError, ContextOverflowError):
|
||||||
"""`InvalidRequestError` raised when input exceeds Fireworks's context limit."""
|
"""`BadRequestError` raised when input exceeds Fireworks's context limit."""
|
||||||
|
|
||||||
|
|
||||||
def _handle_fireworks_invalid_request(e: InvalidRequestError) -> NoReturn:
|
def _handle_fireworks_invalid_request(e: BadRequestError) -> NoReturn:
|
||||||
"""Promote prompt-too-long errors to `FireworksContextOverflowError`."""
|
"""Promote prompt-too-long errors to `FireworksContextOverflowError`."""
|
||||||
if "prompt is too long" in str(e):
|
if "prompt is too long" in str(e):
|
||||||
raise FireworksContextOverflowError(str(e)) from e
|
raise FireworksContextOverflowError(
|
||||||
|
str(e), response=e.response, body=e.body
|
||||||
|
) from e
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@@ -461,14 +462,13 @@ def _create_retry_decorator(
|
|||||||
) -> Callable[[Any], Any]:
|
) -> Callable[[Any], Any]:
|
||||||
"""Return a tenacity retry decorator for Fireworks SDK calls.
|
"""Return a tenacity retry decorator for Fireworks SDK calls.
|
||||||
|
|
||||||
Retries are implemented here because the pinned Fireworks SDK 0.x does
|
Retries live here rather than in the SDK so each attempt is visible to the
|
||||||
not honor its own `_max_retries` attribute on completion resources.
|
LangChain `run_manager.on_retry` callback. The SDK's own retry layer is
|
||||||
|
suppressed via `max_retries=0` on the client; see `validate_environment`.
|
||||||
"""
|
"""
|
||||||
# `max_retries` counts retries *after* the initial attempt.
|
# `max_retries` counts retries *after* the initial attempt.
|
||||||
# `create_base_retry_decorator` forwards its `max_retries` to
|
# `create_base_retry_decorator` forwards its `max_retries` to
|
||||||
# `stop_after_attempt`, which counts total attempts — so offset by 1.
|
# `stop_after_attempt`, which counts total attempts — so offset by 1.
|
||||||
# Note: this diverges from `ChatMistralAI`, which passes the raw value;
|
|
||||||
# the fireworks field docstring is the source of truth here.
|
|
||||||
# `None` and `0` both mean "single attempt, no retries".
|
# `None` and `0` both mean "single attempt, no retries".
|
||||||
attempts = (llm.max_retries + 1) if llm.max_retries else 1
|
attempts = (llm.max_retries + 1) if llm.max_retries else 1
|
||||||
return create_base_retry_decorator(
|
return create_base_retry_decorator(
|
||||||
@@ -478,6 +478,36 @@ def _create_retry_decorator(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_sdk_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Move fields the 1.x SDK does not model into `extra_body`.
|
||||||
|
|
||||||
|
The Stainless-generated `chat.completions.create` signature has a fixed set
|
||||||
|
of typed parameters. Fireworks accepts additional fields on the wire (notably
|
||||||
|
`stream_options.include_usage`) that the SDK schema does not declare. The
|
||||||
|
SDK exposes `extra_body` precisely for this — merge anything that looks
|
||||||
|
extra-body-shaped into it so it lands in the JSON request body.
|
||||||
|
|
||||||
|
If a caller supplies both `extra_body={"stream_options": ...}` and a
|
||||||
|
top-level `stream_options=...`, the value already in `extra_body` wins
|
||||||
|
(callers using `extra_body` are presumed to want explicit control); the
|
||||||
|
discarded top-level value is logged.
|
||||||
|
"""
|
||||||
|
extra_body = dict(kwargs.pop("extra_body", None) or {})
|
||||||
|
top_level_stream_options = kwargs.pop("stream_options", None)
|
||||||
|
if top_level_stream_options is not None:
|
||||||
|
if "stream_options" in extra_body:
|
||||||
|
logger.warning(
|
||||||
|
"Both `extra_body['stream_options']` and a top-level "
|
||||||
|
"`stream_options` were supplied; using `extra_body`'s value "
|
||||||
|
"and discarding the top-level value.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
extra_body["stream_options"] = top_level_stream_options
|
||||||
|
if extra_body:
|
||||||
|
kwargs["extra_body"] = extra_body
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
def _completion_with_retry(
|
def _completion_with_retry(
|
||||||
llm: ChatFireworks,
|
llm: ChatFireworks,
|
||||||
run_manager: CallbackManagerForLLMRun | None = None,
|
run_manager: CallbackManagerForLLMRun | None = None,
|
||||||
@@ -485,6 +515,7 @@ def _completion_with_retry(
|
|||||||
) -> Any:
|
) -> Any:
|
||||||
"""Retry the sync completion call, including stream setup."""
|
"""Retry the sync completion call, including stream setup."""
|
||||||
retry_decorator = _create_retry_decorator(llm, run_manager=run_manager)
|
retry_decorator = _create_retry_decorator(llm, run_manager=run_manager)
|
||||||
|
kwargs = _prepare_sdk_kwargs(kwargs)
|
||||||
|
|
||||||
@retry_decorator
|
@retry_decorator
|
||||||
def _call() -> Any:
|
def _call() -> Any:
|
||||||
@@ -517,12 +548,17 @@ async def _acompletion_with_retry(
|
|||||||
) -> Any:
|
) -> Any:
|
||||||
"""Retry the async completion call, including stream setup."""
|
"""Retry the async completion call, including stream setup."""
|
||||||
retry_decorator = _create_retry_decorator(llm, run_manager=run_manager)
|
retry_decorator = _create_retry_decorator(llm, run_manager=run_manager)
|
||||||
|
kwargs = _prepare_sdk_kwargs(kwargs)
|
||||||
|
|
||||||
@retry_decorator
|
@retry_decorator
|
||||||
async def _call() -> Any:
|
async def _call() -> Any:
|
||||||
if kwargs.get("stream"):
|
if kwargs.get("stream"):
|
||||||
try:
|
try:
|
||||||
result = llm.async_client.acreate(**kwargs)
|
# 1.x async `create()` is a coroutine that resolves to an
|
||||||
|
# `AsyncStream` when `stream=True`. Await it, then advance the
|
||||||
|
# async iterator once inside the retry boundary so transport
|
||||||
|
# errors surface here rather than at first downstream consumer.
|
||||||
|
result = await llm.async_client.create(**kwargs)
|
||||||
agen = result.__aiter__()
|
agen = result.__aiter__()
|
||||||
first = await agen.__anext__()
|
first = await agen.__anext__()
|
||||||
except StopAsyncIteration:
|
except StopAsyncIteration:
|
||||||
@@ -531,7 +567,7 @@ async def _acompletion_with_retry(
|
|||||||
_promote_http_status_error(e)
|
_promote_http_status_error(e)
|
||||||
return _aprepend_chunk(first, agen)
|
return _aprepend_chunk(first, agen)
|
||||||
try:
|
try:
|
||||||
return await llm.async_client.acreate(**kwargs)
|
return await llm.async_client.create(**kwargs)
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
_promote_http_status_error(e)
|
_promote_http_status_error(e)
|
||||||
|
|
||||||
@@ -549,11 +585,6 @@ async def _aprepend_chunk(first: Any, rest: AsyncIterator[Any]) -> AsyncIterator
|
|||||||
yield item
|
yield item
|
||||||
|
|
||||||
|
|
||||||
# This is basically a copy and replace for ChatFireworks, except
|
|
||||||
# - I needed to gut out tiktoken and some of the token estimation logic
|
|
||||||
# (not sure how important it is)
|
|
||||||
# - Environment variable is different
|
|
||||||
# we should refactor into some OpenAI-like class in the future
|
|
||||||
class ChatFireworks(BaseChatModel):
|
class ChatFireworks(BaseChatModel):
|
||||||
"""`Fireworks` Chat large language models API.
|
"""`Fireworks` Chat large language models API.
|
||||||
|
|
||||||
@@ -598,8 +629,28 @@ class ChatFireworks(BaseChatModel):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
client: Any = Field(default=None, exclude=True)
|
client: Any = Field(default=None, exclude=True)
|
||||||
|
"""Internal `fireworks.Fireworks().chat.completions` resource.
|
||||||
|
|
||||||
|
Constructed with `max_retries=0` so retries are owned by
|
||||||
|
`_create_retry_decorator` (which surfaces each attempt to the LangChain
|
||||||
|
`run_manager`). Callers reaching for this directly should set their own
|
||||||
|
retry layer.
|
||||||
|
"""
|
||||||
|
|
||||||
async_client: Any = Field(default=None, exclude=True)
|
async_client: Any = Field(default=None, exclude=True)
|
||||||
|
"""Internal `fireworks.AsyncFireworks().chat.completions` resource.
|
||||||
|
|
||||||
|
Constructed with `max_retries=0`; see `client`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_sdk_client: Any = PrivateAttr(default=None)
|
||||||
|
"""Owning `fireworks.Fireworks` instance, retained so `close()` can call
|
||||||
|
into the underlying HTTPX client. The 1.x SDK does not expose lifecycle
|
||||||
|
methods on the `chat.completions` resource itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_async_sdk_client: Any = PrivateAttr(default=None)
|
||||||
|
"""Owning `fireworks.AsyncFireworks` instance; see `_sdk_client`."""
|
||||||
|
|
||||||
model_name: str = Field(alias="model")
|
model_name: str = Field(alias="model")
|
||||||
"""Model name to use."""
|
"""Model name to use."""
|
||||||
@@ -720,18 +771,58 @@ class ChatFireworks(BaseChatModel):
|
|||||||
msg = "n must be 1 when streaming."
|
msg = "n must be 1 when streaming."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
client_params = {
|
api_key = self.fireworks_api_key.get_secret_value()
|
||||||
"api_key": self.fireworks_api_key.get_secret_value(),
|
base_url = self.fireworks_api_base
|
||||||
"base_url": self.fireworks_api_base,
|
# 0.x accepted a `(connect, read)` tuple. 1.x's SDK only accepts a
|
||||||
"timeout": self.request_timeout,
|
# float, `httpx.Timeout`, or `None` — normalize so existing user code
|
||||||
}
|
# keeps working.
|
||||||
|
if isinstance(self.request_timeout, tuple):
|
||||||
|
connect, read = self.request_timeout
|
||||||
|
timeout: Any = httpx.Timeout(read, connect=connect)
|
||||||
|
else:
|
||||||
|
timeout = self.request_timeout
|
||||||
|
# `langchain-fireworks` owns retry/backoff via `_create_retry_decorator`
|
||||||
|
# so the LangChain `run_manager` sees each attempt. Suppress the
|
||||||
|
# SDK's built-in retry layer to avoid double-retrying.
|
||||||
if not self.client:
|
if not self.client:
|
||||||
self.client = Fireworks(**client_params).chat.completions
|
self._sdk_client = Fireworks(
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=base_url,
|
||||||
|
timeout=timeout,
|
||||||
|
max_retries=0,
|
||||||
|
)
|
||||||
|
self.client = self._sdk_client.chat.completions
|
||||||
if not self.async_client:
|
if not self.async_client:
|
||||||
self.async_client = AsyncFireworks(**client_params).chat.completions
|
self._async_sdk_client = AsyncFireworks(
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=base_url,
|
||||||
|
timeout=timeout,
|
||||||
|
max_retries=0,
|
||||||
|
)
|
||||||
|
self.async_client = self._async_sdk_client.chat.completions
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close the underlying sync HTTP client.
|
||||||
|
|
||||||
|
After calling, sync invocations on this model will raise. Async
|
||||||
|
invocations remain available until `aclose()` is also called. Safe to
|
||||||
|
call multiple times.
|
||||||
|
"""
|
||||||
|
if self._sdk_client is not None:
|
||||||
|
self._sdk_client.close()
|
||||||
|
|
||||||
|
async def aclose(self) -> None:
|
||||||
|
"""Close the underlying async HTTP client.
|
||||||
|
|
||||||
|
Releases the aiohttp-backed connector that the 1.x SDK uses by
|
||||||
|
default. Without this, transient `ChatFireworks` instances can leak
|
||||||
|
an `Unclosed connector` warning at GC if the event loop has already
|
||||||
|
stopped. Safe to call multiple times.
|
||||||
|
"""
|
||||||
|
if self._async_sdk_client is not None:
|
||||||
|
await self._async_sdk_client.close()
|
||||||
|
|
||||||
def _resolve_model_profile(self) -> ModelProfile | None:
|
def _resolve_model_profile(self) -> ModelProfile | None:
|
||||||
return _get_default_model_profile(self.model_name) or None
|
return _get_default_model_profile(self.model_name) or None
|
||||||
|
|
||||||
@@ -808,7 +899,7 @@ class ChatFireworks(BaseChatModel):
|
|||||||
stream = _completion_with_retry(
|
stream = _completion_with_retry(
|
||||||
self, run_manager=run_manager, messages=message_dicts, **params
|
self, run_manager=run_manager, messages=message_dicts, **params
|
||||||
)
|
)
|
||||||
except InvalidRequestError as e:
|
except BadRequestError as e:
|
||||||
_handle_fireworks_invalid_request(e)
|
_handle_fireworks_invalid_request(e)
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
if not isinstance(chunk, dict):
|
if not isinstance(chunk, dict):
|
||||||
@@ -858,7 +949,7 @@ class ChatFireworks(BaseChatModel):
|
|||||||
response = _completion_with_retry(
|
response = _completion_with_retry(
|
||||||
self, run_manager=run_manager, messages=message_dicts, **params
|
self, run_manager=run_manager, messages=message_dicts, **params
|
||||||
)
|
)
|
||||||
except InvalidRequestError as e:
|
except BadRequestError as e:
|
||||||
_handle_fireworks_invalid_request(e)
|
_handle_fireworks_invalid_request(e)
|
||||||
return self._create_chat_result(response)
|
return self._create_chat_result(response)
|
||||||
|
|
||||||
@@ -923,7 +1014,7 @@ class ChatFireworks(BaseChatModel):
|
|||||||
stream = await _acompletion_with_retry(
|
stream = await _acompletion_with_retry(
|
||||||
self, run_manager=run_manager, messages=message_dicts, **params
|
self, run_manager=run_manager, messages=message_dicts, **params
|
||||||
)
|
)
|
||||||
except InvalidRequestError as e:
|
except BadRequestError as e:
|
||||||
_handle_fireworks_invalid_request(e)
|
_handle_fireworks_invalid_request(e)
|
||||||
async for chunk in stream:
|
async for chunk in stream:
|
||||||
if not isinstance(chunk, dict):
|
if not isinstance(chunk, dict):
|
||||||
@@ -976,7 +1067,7 @@ class ChatFireworks(BaseChatModel):
|
|||||||
response = await _acompletion_with_retry(
|
response = await _acompletion_with_retry(
|
||||||
self, run_manager=run_manager, messages=message_dicts, **params
|
self, run_manager=run_manager, messages=message_dicts, **params
|
||||||
)
|
)
|
||||||
except InvalidRequestError as e:
|
except BadRequestError as e:
|
||||||
_handle_fireworks_invalid_request(e)
|
_handle_fireworks_invalid_request(e)
|
||||||
return self._create_chat_result(response)
|
return self._create_chat_result(response)
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ classifiers = [
|
|||||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||||
]
|
]
|
||||||
|
|
||||||
version = "1.3.1"
|
version = "1.4.0"
|
||||||
requires-python = ">=3.10.0,<4.0.0"
|
requires-python = ">=3.10.0,<4.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"langchain-core>=1.4.0,<2.0.0",
|
"langchain-core>=1.4.0,<2.0.0",
|
||||||
"fireworks-ai>=0.13.0,<1.0.0",
|
"fireworks-ai>=1.2.0a71,<2.0.0",
|
||||||
"openai>=2.0.0,<3.0.0",
|
"openai>=2.0.0,<3.0.0",
|
||||||
"requests>=2.0.0,<3.0.0",
|
"requests>=2.0.0,<3.0.0",
|
||||||
"aiohttp>=3.9.1,<4.0.0",
|
"aiohttp>=3.9.1,<4.0.0",
|
||||||
@@ -64,6 +64,11 @@ typing = [
|
|||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
constraint-dependencies = ["pygments>=2.20.0"] # CVE-2026-4539
|
constraint-dependencies = ["pygments>=2.20.0"] # CVE-2026-4539
|
||||||
|
# `fireworks-ai` 1.x is currently published as a prerelease (1.2.0a*).
|
||||||
|
# Allow uv to resolve to those prereleases until a stable 1.x ships.
|
||||||
|
# End users installing `langchain-fireworks` via `pip` / `uv pip install` must
|
||||||
|
# pass `--pre` (or `--prerelease=allow`) until then.
|
||||||
|
prerelease = "allow"
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
langchain-core = { path = "../../core", editable = true }
|
langchain-core = { path = "../../core", editable = true }
|
||||||
|
|||||||
52
libs/partners/fireworks/tests/integration_tests/conftest.py
Normal file
52
libs/partners/fireworks/tests/integration_tests/conftest.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Shared fixtures for `ChatFireworks` integration tests.
|
||||||
|
|
||||||
|
The 1.x `fireworks-ai` SDK defaults to an aiohttp-backed httpx transport for
|
||||||
|
`AsyncFireworks`. Each test constructs its own `ChatFireworks`, which opens a
|
||||||
|
TCP connector lazily on first call. Without explicit cleanup, the connector is
|
||||||
|
finalized by GC *after* `pytest-asyncio` has stopped the event loop, producing
|
||||||
|
an `Unclosed connector` warning at teardown.
|
||||||
|
|
||||||
|
This conftest tracks every `ChatFireworks` instance created during a test and
|
||||||
|
calls `aclose()` on it before the loop closes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import gc
|
||||||
|
import weakref
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from langchain_fireworks import ChatFireworks
|
||||||
|
|
||||||
|
# `ChatFireworks` (a Pydantic `BaseModel`) is not hashable, so a `WeakSet`
|
||||||
|
# does not work; track via weak references keyed by `id()`.
|
||||||
|
_live_models: dict[int, weakref.ref[ChatFireworks]] = {}
|
||||||
|
_original_init = ChatFireworks.__init__
|
||||||
|
|
||||||
|
|
||||||
|
def _tracking_init(self: ChatFireworks, *args: Any, **kwargs: Any) -> None:
|
||||||
|
_original_init(self, *args, **kwargs)
|
||||||
|
_live_models[id(self)] = weakref.ref(self)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def _close_chat_fireworks_clients() -> AsyncIterator[None]:
|
||||||
|
"""Close every `ChatFireworks` created during the test.
|
||||||
|
|
||||||
|
Yields control to the test, then walks the live-instance map and awaits
|
||||||
|
each model's `aclose()` while the event loop is still alive.
|
||||||
|
"""
|
||||||
|
ChatFireworks.__init__ = _tracking_init # type: ignore[method-assign]
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
ChatFireworks.__init__ = _original_init # type: ignore[method-assign]
|
||||||
|
for ref in list(_live_models.values()):
|
||||||
|
model = ref()
|
||||||
|
if model is not None:
|
||||||
|
await model.aclose()
|
||||||
|
_live_models.clear()
|
||||||
|
gc.collect()
|
||||||
@@ -7,12 +7,12 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
from fireworks.client.error import ( # type: ignore[import-untyped]
|
from fireworks import (
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
|
BadRequestError,
|
||||||
FireworksError,
|
FireworksError,
|
||||||
InvalidRequestError,
|
InternalServerError,
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
ServiceUnavailableError,
|
|
||||||
)
|
)
|
||||||
from langchain_core.exceptions import ContextOverflowError
|
from langchain_core.exceptions import ContextOverflowError
|
||||||
from langchain_core.messages import (
|
from langchain_core.messages import (
|
||||||
@@ -46,6 +46,17 @@ def _make_model(**kwargs: Any) -> ChatFireworks:
|
|||||||
return ChatFireworks(**defaults) # type: ignore[arg-type]
|
return ChatFireworks(**defaults) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def _api_error(cls: type, msg: str, status_code: int) -> Exception:
|
||||||
|
"""Construct a 1.x SDK `APIStatusError` subclass with a synthetic response.
|
||||||
|
|
||||||
|
Stainless-generated SDK errors require `message`, `response`, and `body`,
|
||||||
|
so this helper keeps the test setup readable.
|
||||||
|
"""
|
||||||
|
request = httpx.Request("POST", "https://api.fireworks.ai/inference/v1")
|
||||||
|
response = httpx.Response(status_code=status_code, request=request)
|
||||||
|
return cls(msg, response=response, body=None)
|
||||||
|
|
||||||
|
|
||||||
_STREAM_CHUNKS: list[dict[str, Any]] = [
|
_STREAM_CHUNKS: list[dict[str, Any]] = [
|
||||||
{
|
{
|
||||||
"choices": [{"delta": {"role": "assistant", "content": ""}, "index": 0}],
|
"choices": [{"delta": {"role": "assistant", "content": ""}, "index": 0}],
|
||||||
@@ -481,8 +492,8 @@ def test_completion_with_retry_retries_on_retryable_error() -> None:
|
|||||||
llm = _make_llm(max_retries=2)
|
llm = _make_llm(max_retries=2)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = [
|
mock_client.create.side_effect = [
|
||||||
RateLimitError("rate limited"),
|
_api_error(RateLimitError, "rate limited", 429),
|
||||||
ServiceUnavailableError("unavailable"),
|
_api_error(InternalServerError, "unavailable", 503),
|
||||||
_success_response(),
|
_success_response(),
|
||||||
]
|
]
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
@@ -497,7 +508,7 @@ def test_completion_with_retry_does_not_retry_non_retryable() -> None:
|
|||||||
"""Non-retryable errors propagate after a single attempt."""
|
"""Non-retryable errors propagate after a single attempt."""
|
||||||
llm = _make_llm(max_retries=3)
|
llm = _make_llm(max_retries=3)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = AuthenticationError("bad key")
|
mock_client.create.side_effect = _api_error(AuthenticationError, "bad key", 401)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(AuthenticationError):
|
with pytest.raises(AuthenticationError):
|
||||||
@@ -510,7 +521,7 @@ def test_completion_with_retry_respects_max_retries_none() -> None:
|
|||||||
"""`max_retries=None` disables retries."""
|
"""`max_retries=None` disables retries."""
|
||||||
llm = _make_llm(max_retries=None)
|
llm = _make_llm(max_retries=None)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = RateLimitError("rate limited")
|
mock_client.create.side_effect = _api_error(RateLimitError, "rate limited", 429)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(RateLimitError):
|
with pytest.raises(RateLimitError):
|
||||||
@@ -523,7 +534,7 @@ def test_completion_with_retry_exhausts_and_raises() -> None:
|
|||||||
"""When every attempt fails, the last error is re-raised."""
|
"""When every attempt fails, the last error is re-raised."""
|
||||||
llm = _make_llm(max_retries=2)
|
llm = _make_llm(max_retries=2)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = RateLimitError("rate limited")
|
mock_client.create.side_effect = _api_error(RateLimitError, "rate limited", 429)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(RateLimitError):
|
with pytest.raises(RateLimitError):
|
||||||
@@ -545,7 +556,7 @@ def test_completion_with_retry_streaming_retries_on_setup() -> None:
|
|||||||
|
|
||||||
def _failing_gen() -> Any:
|
def _failing_gen() -> Any:
|
||||||
msg = "rate limited"
|
msg = "rate limited"
|
||||||
raise RateLimitError(msg)
|
raise _api_error(RateLimitError, msg, 429)
|
||||||
yield # pragma: no cover
|
yield # pragma: no cover
|
||||||
|
|
||||||
return _failing_gen()
|
return _failing_gen()
|
||||||
@@ -640,7 +651,7 @@ def test_completion_with_retry_max_retries_zero_is_single_attempt() -> None:
|
|||||||
"""`max_retries=0` disables retries (same as `None`)."""
|
"""`max_retries=0` disables retries (same as `None`)."""
|
||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = RateLimitError("rate limited")
|
mock_client.create.side_effect = _api_error(RateLimitError, "rate limited", 429)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(RateLimitError):
|
with pytest.raises(RateLimitError):
|
||||||
@@ -674,7 +685,7 @@ def test_chat_fireworks_invoke_routes_through_retry() -> None:
|
|||||||
llm = _make_llm(max_retries=2)
|
llm = _make_llm(max_retries=2)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = [
|
mock_client.create.side_effect = [
|
||||||
RateLimitError("rate limited"),
|
_api_error(RateLimitError, "rate limited", 429),
|
||||||
_success_response(),
|
_success_response(),
|
||||||
]
|
]
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
@@ -691,13 +702,13 @@ async def test_acompletion_with_retry_streaming_retries_on_setup() -> None:
|
|||||||
llm = _make_llm(max_retries=1)
|
llm = _make_llm(max_retries=1)
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
|
|
||||||
def _acreate(**_kwargs: Any) -> Any:
|
async def _create(**_kwargs: Any) -> Any:
|
||||||
calls["n"] += 1
|
calls["n"] += 1
|
||||||
if calls["n"] == 1:
|
if calls["n"] == 1:
|
||||||
|
|
||||||
async def _failing_agen() -> Any:
|
async def _failing_agen() -> Any:
|
||||||
msg = "rate limited"
|
msg = "rate limited"
|
||||||
raise RateLimitError(msg)
|
raise _api_error(RateLimitError, msg, 429)
|
||||||
yield # pragma: no cover
|
yield # pragma: no cover
|
||||||
|
|
||||||
return _failing_agen()
|
return _failing_agen()
|
||||||
@@ -709,7 +720,7 @@ async def test_acompletion_with_retry_streaming_retries_on_setup() -> None:
|
|||||||
return _good_agen()
|
return _good_agen()
|
||||||
|
|
||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
agen = await _acompletion_with_retry(llm, messages=[], stream=True)
|
agen = await _acompletion_with_retry(llm, messages=[], stream=True)
|
||||||
@@ -734,14 +745,18 @@ async def test_acompletion_with_retry_streaming_accepts_async_iterable_only_resu
|
|||||||
|
|
||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
mock_async.acreate = MagicMock(return_value=_AsyncIterableOnlyStream())
|
|
||||||
|
async def _create(**_kwargs: Any) -> Any:
|
||||||
|
return _AsyncIterableOnlyStream()
|
||||||
|
|
||||||
|
mock_async.create = MagicMock(side_effect=_create)
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
agen = await _acompletion_with_retry(llm, messages=[], stream=True)
|
agen = await _acompletion_with_retry(llm, messages=[], stream=True)
|
||||||
chunks = [c async for c in agen]
|
chunks = [c async for c in agen]
|
||||||
|
|
||||||
assert [c["id"] for c in chunks] == [0, 1]
|
assert [c["id"] for c in chunks] == [0, 1]
|
||||||
assert mock_async.acreate.call_count == 1
|
assert mock_async.create.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_achat_fireworks_ainvoke_routes_through_retry() -> None:
|
async def test_achat_fireworks_ainvoke_routes_through_retry() -> None:
|
||||||
@@ -749,15 +764,15 @@ async def test_achat_fireworks_ainvoke_routes_through_retry() -> None:
|
|||||||
llm = _make_llm(max_retries=2)
|
llm = _make_llm(max_retries=2)
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
|
|
||||||
async def _acreate(**_kwargs: Any) -> dict[str, Any]:
|
async def _create(**_kwargs: Any) -> dict[str, Any]:
|
||||||
calls["n"] += 1
|
calls["n"] += 1
|
||||||
if calls["n"] == 1:
|
if calls["n"] == 1:
|
||||||
msg = "rate limited"
|
msg = "rate limited"
|
||||||
raise RateLimitError(msg)
|
raise _api_error(RateLimitError, msg, 429)
|
||||||
return _success_response()
|
return _success_response()
|
||||||
|
|
||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
result = await llm.ainvoke("hello")
|
result = await llm.ainvoke("hello")
|
||||||
@@ -773,14 +788,14 @@ async def test_acompletion_with_retry_retries_on_retryable_error() -> None:
|
|||||||
|
|
||||||
call_count = {"n": 0}
|
call_count = {"n": 0}
|
||||||
|
|
||||||
async def _acreate(**_kwargs: Any) -> dict[str, Any]:
|
async def _create(**_kwargs: Any) -> dict[str, Any]:
|
||||||
call_count["n"] += 1
|
call_count["n"] += 1
|
||||||
if call_count["n"] < 3:
|
if call_count["n"] < 3:
|
||||||
msg = "rate limited"
|
msg = "rate limited"
|
||||||
raise RateLimitError(msg)
|
raise _api_error(RateLimitError, msg, 429)
|
||||||
return _success_response()
|
return _success_response()
|
||||||
|
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
result = await _acompletion_with_retry(llm, messages=[])
|
result = await _acompletion_with_retry(llm, messages=[])
|
||||||
@@ -794,15 +809,15 @@ async def test_acompletion_with_retry_does_not_retry_non_retryable() -> None:
|
|||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
call_count = {"n": 0}
|
call_count = {"n": 0}
|
||||||
|
|
||||||
async def _acreate(**_kwargs: Any) -> dict[str, Any]:
|
async def _create(**_kwargs: Any) -> dict[str, Any]:
|
||||||
call_count["n"] += 1
|
call_count["n"] += 1
|
||||||
msg = "bad input"
|
msg = "bad input"
|
||||||
raise InvalidRequestError(msg)
|
raise _api_error(BadRequestError, msg, 400)
|
||||||
|
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
with pytest.raises(InvalidRequestError):
|
with pytest.raises(BadRequestError):
|
||||||
await _acompletion_with_retry(llm, messages=[HumanMessage(content="hi")])
|
await _acompletion_with_retry(llm, messages=[HumanMessage(content="hi")])
|
||||||
assert call_count["n"] == 1
|
assert call_count["n"] == 1
|
||||||
|
|
||||||
@@ -813,7 +828,7 @@ async def test_acompletion_with_retry_retries_on_5xx_http_status_error() -> None
|
|||||||
call_count = {"n": 0}
|
call_count = {"n": 0}
|
||||||
response_504 = httpx.Response(status_code=504, request=httpx.Request("POST", "x"))
|
response_504 = httpx.Response(status_code=504, request=httpx.Request("POST", "x"))
|
||||||
|
|
||||||
async def _acreate(**_kwargs: Any) -> dict[str, Any]:
|
async def _create(**_kwargs: Any) -> dict[str, Any]:
|
||||||
call_count["n"] += 1
|
call_count["n"] += 1
|
||||||
if call_count["n"] == 1:
|
if call_count["n"] == 1:
|
||||||
msg = "504"
|
msg = "504"
|
||||||
@@ -823,7 +838,7 @@ async def test_acompletion_with_retry_retries_on_5xx_http_status_error() -> None
|
|||||||
return _success_response()
|
return _success_response()
|
||||||
|
|
||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
result = await _acompletion_with_retry(llm, messages=[])
|
result = await _acompletion_with_retry(llm, messages=[])
|
||||||
@@ -835,7 +850,7 @@ async def test_acompletion_with_retry_raises_on_empty_stream() -> None:
|
|||||||
"""Async empty streams surface as a descriptive `FireworksError`."""
|
"""Async empty streams surface as a descriptive `FireworksError`."""
|
||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
|
|
||||||
def _acreate(**_kwargs: Any) -> Any:
|
async def _create(**_kwargs: Any) -> Any:
|
||||||
async def _empty_agen() -> Any:
|
async def _empty_agen() -> Any:
|
||||||
if False:
|
if False:
|
||||||
yield # pragma: no cover
|
yield # pragma: no cover
|
||||||
@@ -844,7 +859,7 @@ async def test_acompletion_with_retry_raises_on_empty_stream() -> None:
|
|||||||
return _empty_agen()
|
return _empty_agen()
|
||||||
|
|
||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
with pytest.raises(FireworksError, match="empty stream"):
|
with pytest.raises(FireworksError, match="empty stream"):
|
||||||
@@ -939,7 +954,7 @@ class TestStreamUsage:
|
|||||||
model.client.create.return_value = iter(list(_STREAM_CHUNKS))
|
model.client.create.return_value = iter(list(_STREAM_CHUNKS))
|
||||||
list(model.stream("Hello"))
|
list(model.stream("Hello"))
|
||||||
call_kwargs = model.client.create.call_args[1]
|
call_kwargs = model.client.create.call_args[1]
|
||||||
assert call_kwargs["stream_options"] == {"include_usage": True}
|
assert call_kwargs["extra_body"]["stream_options"] == {"include_usage": True}
|
||||||
|
|
||||||
def test_stream_options_not_passed_when_disabled(self) -> None:
|
def test_stream_options_not_passed_when_disabled(self) -> None:
|
||||||
model = _make_model(stream_usage=False)
|
model = _make_model(stream_usage=False)
|
||||||
@@ -948,6 +963,7 @@ class TestStreamUsage:
|
|||||||
list(model.stream("Hello"))
|
list(model.stream("Hello"))
|
||||||
call_kwargs = model.client.create.call_args[1]
|
call_kwargs = model.client.create.call_args[1]
|
||||||
assert "stream_options" not in call_kwargs
|
assert "stream_options" not in call_kwargs
|
||||||
|
assert "extra_body" not in call_kwargs
|
||||||
|
|
||||||
def test_user_stream_options_in_model_kwargs_wins(self) -> None:
|
def test_user_stream_options_in_model_kwargs_wins(self) -> None:
|
||||||
"""User-provided stream_options via model_kwargs overrides the default."""
|
"""User-provided stream_options via model_kwargs overrides the default."""
|
||||||
@@ -957,7 +973,34 @@ class TestStreamUsage:
|
|||||||
model.client.create.return_value = iter(list(_STREAM_CHUNKS))
|
model.client.create.return_value = iter(list(_STREAM_CHUNKS))
|
||||||
list(model.stream("Hello"))
|
list(model.stream("Hello"))
|
||||||
call_kwargs = model.client.create.call_args[1]
|
call_kwargs = model.client.create.call_args[1]
|
||||||
assert call_kwargs["stream_options"] == custom
|
assert call_kwargs["extra_body"]["stream_options"] == custom
|
||||||
|
|
||||||
|
def test_extra_body_stream_options_wins_over_top_level(
|
||||||
|
self, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""`extra_body['stream_options']` wins over a top-level value.
|
||||||
|
|
||||||
|
When both are supplied, the `extra_body` value is preserved and a
|
||||||
|
warning is logged.
|
||||||
|
"""
|
||||||
|
explicit = {"include_usage": False}
|
||||||
|
model = _make_model(
|
||||||
|
model_kwargs={
|
||||||
|
"stream_options": {"include_usage": True},
|
||||||
|
"extra_body": {"stream_options": explicit},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
model.client = MagicMock()
|
||||||
|
model.client.create.return_value = iter(list(_STREAM_CHUNKS))
|
||||||
|
with caplog.at_level("WARNING", logger="langchain_fireworks.chat_models"):
|
||||||
|
list(model.stream("Hello"))
|
||||||
|
call_kwargs = model.client.create.call_args[1]
|
||||||
|
assert call_kwargs["extra_body"]["stream_options"] == explicit
|
||||||
|
assert "stream_options" not in call_kwargs
|
||||||
|
assert any(
|
||||||
|
"extra_body" in rec.message and "discarding" in rec.message
|
||||||
|
for rec in caplog.records
|
||||||
|
)
|
||||||
|
|
||||||
def test_usage_only_chunk_emits_usage_metadata(self) -> None:
|
def test_usage_only_chunk_emits_usage_metadata(self) -> None:
|
||||||
"""The final empty-choices + usage chunk propagates as usage_metadata."""
|
"""The final empty-choices + usage chunk propagates as usage_metadata."""
|
||||||
@@ -981,10 +1024,13 @@ class TestStreamUsage:
|
|||||||
for c in _STREAM_CHUNKS:
|
for c in _STREAM_CHUNKS:
|
||||||
yield c
|
yield c
|
||||||
|
|
||||||
model.async_client.acreate = MagicMock(return_value=_aiter())
|
async def _create(**_kwargs: Any) -> Any:
|
||||||
|
return _aiter()
|
||||||
|
|
||||||
|
model.async_client.create = MagicMock(side_effect=_create)
|
||||||
[chunk async for chunk in model.astream("Hello")]
|
[chunk async for chunk in model.astream("Hello")]
|
||||||
call_kwargs = model.async_client.acreate.call_args[1]
|
call_kwargs = model.async_client.create.call_args[1]
|
||||||
assert call_kwargs["stream_options"] == {"include_usage": True}
|
assert call_kwargs["extra_body"]["stream_options"] == {"include_usage": True}
|
||||||
|
|
||||||
async def test_astream_usage_only_chunk_emits_usage_metadata(self) -> None:
|
async def test_astream_usage_only_chunk_emits_usage_metadata(self) -> None:
|
||||||
model = _make_model()
|
model = _make_model()
|
||||||
@@ -994,7 +1040,10 @@ class TestStreamUsage:
|
|||||||
for c in _STREAM_CHUNKS:
|
for c in _STREAM_CHUNKS:
|
||||||
yield c
|
yield c
|
||||||
|
|
||||||
model.async_client.acreate = MagicMock(return_value=_aiter())
|
async def _create(**_kwargs: Any) -> Any:
|
||||||
|
return _aiter()
|
||||||
|
|
||||||
|
model.async_client.create = MagicMock(side_effect=_create)
|
||||||
chunks = [chunk async for chunk in model.astream("Hello")]
|
chunks = [chunk async for chunk in model.astream("Hello")]
|
||||||
usage_chunks = [c for c in chunks if c.usage_metadata]
|
usage_chunks = [c for c in chunks if c.usage_metadata]
|
||||||
assert len(usage_chunks) == 1
|
assert len(usage_chunks) == 1
|
||||||
@@ -1155,7 +1204,9 @@ def test_context_overflow_error_invoke_sync() -> None:
|
|||||||
"""Prompt-too-long errors surface as `ContextOverflowError` on invoke."""
|
"""Prompt-too-long errors surface as `ContextOverflowError` on invoke."""
|
||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = InvalidRequestError(_CONTEXT_OVERFLOW_MESSAGE)
|
mock_client.create.side_effect = _api_error(
|
||||||
|
BadRequestError, _CONTEXT_OVERFLOW_MESSAGE, 400
|
||||||
|
)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(ContextOverflowError) as exc_info:
|
with pytest.raises(ContextOverflowError) as exc_info:
|
||||||
@@ -1170,10 +1221,10 @@ async def test_context_overflow_error_invoke_async() -> None:
|
|||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
|
|
||||||
async def _acreate(**_kwargs: Any) -> dict[str, Any]:
|
async def _create(**_kwargs: Any) -> dict[str, Any]:
|
||||||
raise InvalidRequestError(_CONTEXT_OVERFLOW_MESSAGE)
|
raise _api_error(BadRequestError, _CONTEXT_OVERFLOW_MESSAGE, 400)
|
||||||
|
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
with pytest.raises(ContextOverflowError) as exc_info:
|
with pytest.raises(ContextOverflowError) as exc_info:
|
||||||
@@ -1187,7 +1238,9 @@ def test_context_overflow_error_stream_sync() -> None:
|
|||||||
"""Prompt-too-long errors surface as `ContextOverflowError` on stream."""
|
"""Prompt-too-long errors surface as `ContextOverflowError` on stream."""
|
||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = InvalidRequestError(_CONTEXT_OVERFLOW_MESSAGE)
|
mock_client.create.side_effect = _api_error(
|
||||||
|
BadRequestError, _CONTEXT_OVERFLOW_MESSAGE, 400
|
||||||
|
)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(ContextOverflowError) as exc_info:
|
with pytest.raises(ContextOverflowError) as exc_info:
|
||||||
@@ -1202,14 +1255,14 @@ async def test_context_overflow_error_stream_async() -> None:
|
|||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_async = MagicMock()
|
mock_async = MagicMock()
|
||||||
|
|
||||||
def _acreate(**_kwargs: Any) -> Any:
|
async def _create(**_kwargs: Any) -> Any:
|
||||||
async def _failing_agen() -> Any:
|
async def _failing_agen() -> Any:
|
||||||
raise InvalidRequestError(_CONTEXT_OVERFLOW_MESSAGE)
|
raise _api_error(BadRequestError, _CONTEXT_OVERFLOW_MESSAGE, 400)
|
||||||
yield # pragma: no cover
|
yield # pragma: no cover
|
||||||
|
|
||||||
return _failing_agen()
|
return _failing_agen()
|
||||||
|
|
||||||
mock_async.acreate = _acreate
|
mock_async.create = _create
|
||||||
llm.async_client = mock_async
|
llm.async_client = mock_async
|
||||||
|
|
||||||
with pytest.raises(ContextOverflowError) as exc_info:
|
with pytest.raises(ContextOverflowError) as exc_info:
|
||||||
@@ -1221,27 +1274,95 @@ async def test_context_overflow_error_stream_async() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_context_overflow_error_backwards_compatibility() -> None:
|
def test_context_overflow_error_backwards_compatibility() -> None:
|
||||||
"""`ContextOverflowError` is also catchable as `InvalidRequestError`."""
|
"""`ContextOverflowError` is also catchable as `BadRequestError`."""
|
||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = InvalidRequestError(_CONTEXT_OVERFLOW_MESSAGE)
|
mock_client.create.side_effect = _api_error(
|
||||||
|
BadRequestError, _CONTEXT_OVERFLOW_MESSAGE, 400
|
||||||
|
)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(InvalidRequestError) as exc_info:
|
with pytest.raises(BadRequestError) as exc_info:
|
||||||
llm.invoke([HumanMessage(content="test")])
|
llm.invoke([HumanMessage(content="test")])
|
||||||
|
|
||||||
assert isinstance(exc_info.value, InvalidRequestError)
|
assert isinstance(exc_info.value, BadRequestError)
|
||||||
assert isinstance(exc_info.value, ContextOverflowError)
|
assert isinstance(exc_info.value, ContextOverflowError)
|
||||||
|
|
||||||
|
|
||||||
def test_unrelated_invalid_request_error_not_promoted() -> None:
|
def test_unrelated_invalid_request_error_not_promoted() -> None:
|
||||||
"""Unrelated `InvalidRequestError`s should not be wrapped."""
|
"""Unrelated `BadRequestError`s should not be wrapped."""
|
||||||
llm = _make_llm(max_retries=0)
|
llm = _make_llm(max_retries=0)
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.create.side_effect = InvalidRequestError("some other bad request")
|
mock_client.create.side_effect = _api_error(
|
||||||
|
BadRequestError, "some other bad request", 400
|
||||||
|
)
|
||||||
llm.client = mock_client
|
llm.client = mock_client
|
||||||
|
|
||||||
with pytest.raises(InvalidRequestError) as exc_info:
|
with pytest.raises(BadRequestError) as exc_info:
|
||||||
llm.invoke([HumanMessage(content="test")])
|
llm.invoke([HumanMessage(content="test")])
|
||||||
|
|
||||||
assert not isinstance(exc_info.value, ContextOverflowError)
|
assert not isinstance(exc_info.value, ContextOverflowError)
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_overflow_error_carries_response_metadata() -> None:
|
||||||
|
"""Promoted `FireworksContextOverflowError` preserves `response`/`body`.
|
||||||
|
|
||||||
|
Downstream catchers that introspect `.response.status_code` rely on this.
|
||||||
|
"""
|
||||||
|
llm = _make_llm(max_retries=0)
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.create.side_effect = _api_error(
|
||||||
|
BadRequestError, _CONTEXT_OVERFLOW_MESSAGE, 400
|
||||||
|
)
|
||||||
|
llm.client = mock_client
|
||||||
|
|
||||||
|
with pytest.raises(FireworksContextOverflowError) as exc_info:
|
||||||
|
llm.invoke([HumanMessage(content="test")])
|
||||||
|
|
||||||
|
assert exc_info.value.response.status_code == 400
|
||||||
|
assert exc_info.value.body is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_sdk_clients_constructed_with_max_retries_zero(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""`Fireworks` / `AsyncFireworks` must be built with `max_retries=0`.
|
||||||
|
|
||||||
|
Retries are owned by `_create_retry_decorator`; if this kwarg is lost,
|
||||||
|
every retryable failure would be retried by both layers.
|
||||||
|
"""
|
||||||
|
sync_mock = MagicMock()
|
||||||
|
async_mock = MagicMock()
|
||||||
|
monkeypatch.setattr("langchain_fireworks.chat_models.Fireworks", sync_mock)
|
||||||
|
monkeypatch.setattr("langchain_fireworks.chat_models.AsyncFireworks", async_mock)
|
||||||
|
|
||||||
|
ChatFireworks(model=MODEL_NAME, api_key="fake-key") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
assert sync_mock.call_args.kwargs["max_retries"] == 0
|
||||||
|
assert async_mock.call_args.kwargs["max_retries"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_timeout_tuple_normalized_to_httpx_timeout(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""0.x's `(connect, read)` tuple still works after the 1.x migration.
|
||||||
|
|
||||||
|
1.x's SDK only accepts `float | httpx.Timeout | None`. The validator
|
||||||
|
normalizes the legacy tuple so existing user code keeps working.
|
||||||
|
"""
|
||||||
|
sync_mock = MagicMock()
|
||||||
|
async_mock = MagicMock()
|
||||||
|
monkeypatch.setattr("langchain_fireworks.chat_models.Fireworks", sync_mock)
|
||||||
|
monkeypatch.setattr("langchain_fireworks.chat_models.AsyncFireworks", async_mock)
|
||||||
|
|
||||||
|
ChatFireworks(
|
||||||
|
model=MODEL_NAME,
|
||||||
|
api_key="fake-key", # type: ignore[arg-type]
|
||||||
|
timeout=(5.0, 30.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
forwarded = sync_mock.call_args.kwargs["timeout"]
|
||||||
|
assert isinstance(forwarded, httpx.Timeout)
|
||||||
|
assert forwarded.connect == 5.0
|
||||||
|
assert forwarded.read == 30.0
|
||||||
|
assert async_mock.call_args.kwargs["timeout"] == forwarded
|
||||||
|
|||||||
153
libs/partners/fireworks/uv.lock
generated
153
libs/partners/fireworks/uv.lock
generated
@@ -10,6 +10,9 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11' and platform_python_implementation != 'PyPy'",
|
"python_full_version < '3.11' and platform_python_implementation != 'PyPy'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[options]
|
||||||
|
prerelease-mode = "allow"
|
||||||
|
|
||||||
[manifest]
|
[manifest]
|
||||||
constraints = [{ name = "pygments", specifier = ">=2.20.0" }]
|
constraints = [{ name = "pygments", specifier = ">=2.20.0" }]
|
||||||
|
|
||||||
@@ -402,18 +405,21 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fireworks-ai"
|
name = "fireworks-ai"
|
||||||
version = "0.15.15"
|
version = "1.2.0a71"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "distro" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "httpx-sse" },
|
{ name = "httpx-aiohttp" },
|
||||||
{ name = "httpx-ws" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/20/5b/7ed59473cd8420a44d11d15594b00c3395e9af726f7c5a632171b02ffdc8/fireworks_ai-0.15.15.tar.gz", hash = "sha256:d558e02df06844cb33344d33ecfb1c619a5e82d2ec4d8f51a0a45b7de5d3f4a0", size = 91475, upload-time = "2025-06-20T21:11:27.957Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/81/b8/45bb8470cd05a32d1c5c6d60aa436a5eb8ce8649d4cb7409d478c67c5ede/fireworks_ai-1.2.0a71.tar.gz", hash = "sha256:e2e8bf8d46461433e4c44e077c13d14d7e433dec79cd97039d8893726dad8dd4", size = 365604, upload-time = "2026-05-19T22:39:53.58Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/e7/319a4ce37bed682741bc8ebbb84b7983da3d8cd7ac069d86b52a37d79f2e/fireworks_ai-0.15.15-py3-none-any.whl", hash = "sha256:1047b8e575a536898a827b089b0022c1fab207940f9773b90fa357ebf942f5c9", size = 112831, upload-time = "2025-06-20T21:11:26.701Z" },
|
{ url = "https://files.pythonhosted.org/packages/a3/8a/7d2b5e8474ae91a57b6cfe80917ec9d53d5bda3cdb56e5c6b2b1a3739609/fireworks_ai-1.2.0a71-py3-none-any.whl", hash = "sha256:ff7bab761bb233223d45f1b58b3927bf7729fbd1d42e975289fb8259ee4a7b69", size = 438000, upload-time = "2026-05-19T22:39:52.201Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -560,27 +566,16 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx-sse"
|
name = "httpx-aiohttp"
|
||||||
version = "0.4.1"
|
version = "0.1.12"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httpx-ws"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "aiohttp" },
|
||||||
{ name = "httpcore" },
|
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "wsproto" },
|
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/dc/d7941f8a598e636779c1f34daa3fd88849b36d440dd3522ceba90915abc6/httpx_ws-0.8.0.tar.gz", hash = "sha256:a4dbe99509751d9498ead8564a455e0e8d27501560e19864d249b8b9766b24ad", size = 25488, upload-time = "2025-09-20T14:23:40.69Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/63/2c/b894861cecf030fb45675ea24aa55b5722e97c602a163d872fca66c5a6d8/httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c", size = 275945, upload-time = "2025-12-12T10:12:15.283Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/65/36bec7fe24c61e7e700a5e08210e8f10cf1cf78eb75491316125c958ca76/httpx_ws-0.8.0-py3-none-any.whl", hash = "sha256:42f650a63c5d9b4940e0375e4bee001b644620e06c87d192692ce6015e17f8e2", size = 14986, upload-time = "2025-09-20T14:23:41.409Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/8d/85c9701e9af72ca132a1783e2a54364a90c6da832304416a30fc11196ab2/httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141", size = 6367, upload-time = "2025-12-12T10:12:14.018Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -759,7 +754,7 @@ typing = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langchain-fireworks"
|
name = "langchain-fireworks"
|
||||||
version = "1.3.1"
|
version = "1.4.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
@@ -797,7 +792,7 @@ typing = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiohttp", specifier = ">=3.9.1,<4.0.0" },
|
{ name = "aiohttp", specifier = ">=3.9.1,<4.0.0" },
|
||||||
{ name = "fireworks-ai", specifier = ">=0.13.0,<1.0.0" },
|
{ name = "fireworks-ai", specifier = ">=1.2.0a71,<2.0.0" },
|
||||||
{ name = "langchain-core", editable = "../../core" },
|
{ name = "langchain-core", editable = "../../core" },
|
||||||
{ name = "openai", specifier = ">=2.0.0,<3.0.0" },
|
{ name = "openai", specifier = ">=2.0.0,<3.0.0" },
|
||||||
{ name = "requests", specifier = ">=2.0.0,<3.0.0" },
|
{ name = "requests", specifier = ">=2.0.0,<3.0.0" },
|
||||||
@@ -1350,104 +1345,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "12.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -2258,18 +2155,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wsproto"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "h11" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xxhash"
|
name = "xxhash"
|
||||||
version = "3.6.0"
|
version = "3.6.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user