diff --git a/CHANGELOG.md b/CHANGELOG.md index 59698e4df..042ce98f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Enabled extended thinking for `claude-opus-4.7`, `claude-opus-4.8`, `gpt-5.4`, `gpt-5.5`, and `gpt-5.6`. Selecting one of these models with a thinking level now activates the provider's reasoning/extended-thinking budget; previously it was silently skipped for these newer models. - Sandbox environments now automatically allow and authenticate their repository's git platform (GitLab/GitHub) for git operations over HTTPS in the sandbox. DAIV injects an `Authorization: Basic` credential at the egress proxy, built from a short-lived token — the same project-scoped ephemeral clone token for GitLab, and a `contents:write` installation token for GitHub — so `git fetch`/clone/push of the repository works without configuring a per-environment host rule. Because DAIV pushes from inside the sandbox, this applies even to **Network Off** environments when a push token exists: the environment is opened solely for its git platform (everything else stays blocked) so publishing always works, while token-less eval/benchmark runs stay fully network-isolated. The rule is applied at runtime (never stored on the environment) and takes precedence over a user-defined rule for the same host. - Added a per-environment **network egress policy** for sandbox environments, configurable from the environment form when network is enabled. Restrict outbound traffic to an allow-list of hosts (glob patterns), attach per-host HTTP credentials (header name + value, encrypted at rest and never rendered back), scope each host to **all methods** or **read-only** (`GET, HEAD, OPTIONS`), and set the `default` (deny/allow) reachability for unlisted hosts. The proxy always TLS-inspects every reachable host, so per-host credentials and method rules are always enforced. Credentials are injected by the daiv-sandbox egress proxy and never enter the sandbox container. The egress proxy is mandatory for network-enabled sessions: if the sandbox deployment has no egress proxy configured, a network-enabled run is rejected rather than falling back to unrestricted network. - Added `release_orphan_queued_threads` management command to recover `QUEUED` activities left behind on threads with no active sibling (rare TOCTOU loss). diff --git a/daiv/accounts/context_processors.py b/daiv/accounts/context_processors.py index 40e88204c..235b5457d 100644 --- a/daiv/accounts/context_processors.py +++ b/daiv/accounts/context_processors.py @@ -14,7 +14,9 @@ NAV_SECTION_MCP_GLOBAL = "mcp_servers_global" SECTION_URL_NAMES: dict[str, set[str]] = { - "dashboard": {"dashboard"}, + # The admin Manager Lens is a console surface under Dashboard — keep the Dashboard + # sidebar item highlighted when it is open. + "dashboard": {"dashboard", "manager_lens"}, "sessions": { "session_list", "session_new", diff --git a/daiv/accounts/locale/pt/LC_MESSAGES/django.po b/daiv/accounts/locale/pt/LC_MESSAGES/django.po index 1c8ebb76e..befc969f9 100644 --- a/daiv/accounts/locale/pt/LC_MESSAGES/django.po +++ b/daiv/accounts/locale/pt/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-17 11:20+0100\n" +"POT-Creation-Date: 2026-07-17 08:39-0500\n" "PO-Revision-Date: 2026-04-04 22:34+0100\n" "Last-Translator: \n" "Language-Team: Portuguese \n" @@ -95,6 +95,68 @@ msgstr "Feed" msgid "what happened" msgstr "o que aconteceu" +msgid "Org impact" +msgstr "Impacto da organização" + +msgid "Code Velocity" +msgstr "Velocidade de código" + +msgid "team members" +msgstr "membros da equipa" + +msgid "merges into default branches" +msgstr "merges nos ramos predefinidos" + +msgid "Lines changed" +msgstr "Linhas alteradas" + +#, python-format +msgid "Net change: %(net)s lines" +msgstr "Variação líquida: %(net)s linhas" + +msgid "DAIV attribution" +msgstr "Atribuição ao DAIV" + +msgid "MRs involving DAIV" +msgstr "MRs com participação do DAIV" + +#, python-format +msgid "DAIV: %(n)s MRs" +msgstr "DAIV: %(n)s MRs" + +#, python-format +msgid "Human: %(n)s MRs" +msgstr "Humano: %(n)s MRs" + +msgid "Commits authored by DAIV" +msgstr "Commits da autoria do DAIV" + +#, python-format +msgid "DAIV: %(n)s commits" +msgstr "DAIV: %(n)s commits" + +#, python-format +msgid "Human: %(n)s commits" +msgstr "Humano: %(n)s commits" + +msgid "Computing org impact…" +msgstr "A calcular o impacto da organização…" + +msgid "Console view" +msgstr "Vista da consola" + +msgid "Personal" +msgstr "Pessoal" + +msgid "Manager Lens" +msgstr "Vista de gestor" + +msgid "Manager Lens." +msgstr "Vista de gestor." + +msgid "Review console." +msgstr "Consola de revisão." + msgid "New" msgstr "Novo" @@ -155,6 +217,21 @@ msgstr "Terminar sessão" msgid "No users found." msgstr "Nenhum utilizador encontrado." +msgid "Time range" +msgstr "Intervalo de tempo" + +msgid "7 days" +msgstr "7 dias" + +msgid "30 days" +msgstr "30 dias" + +msgid "90 days" +msgstr "90 dias" + +msgid "All time" +msgstr "Desde sempre" + msgid "Welcome to DAIV" msgstr "Bem-vindo ao DAIV" @@ -199,21 +276,6 @@ msgstr "Pode autenticar-se com um código único enviado para o seu email, ou us msgid "Open navigation" msgstr "Abrir navegação" -msgid "Time range" -msgstr "Intervalo de tempo" - -msgid "7 days" -msgstr "7 dias" - -msgid "30 days" -msgstr "30 dias" - -msgid "90 days" -msgstr "90 dias" - -msgid "All time" -msgstr "Desde sempre" - msgid "Primary" msgstr "Principal" diff --git a/daiv/accounts/templates/accounts/_console_body.html b/daiv/accounts/templates/accounts/_console_body.html index 80d4afd4f..0dfb4205f 100644 --- a/daiv/accounts/templates/accounts/_console_body.html +++ b/daiv/accounts/templates/accounts/_console_body.html @@ -1,10 +1,12 @@ {% load i18n %} -{# Review Console main column — three stacked placeholder regions (Story 2.1). - Structural skeletons only; Hero/Queue/Feed content and the "you have N" count - are wired by later stories (Epic 3 / Epic 4 / Story 2.3). This partial carries - NO job-creation control (FR-3). Reused verbatim as the HTMX body fragment. - Each region is marked aria-busy while its content is pending; the decorative - shimmer skeletons are aria-hidden so assistive tech is not read empty rows. #} +{% comment %} +Review Console main column — three stacked placeholder regions (Story 2.1). +Structural skeletons only; Hero/Queue/Feed content and the "you have N" count +are wired by later stories (Epic 3 / Epic 4 / Story 2.3). This partial carries +NO job-creation control (FR-3). Reused verbatim as the HTMX body fragment. +Each region is marked aria-busy while its content is pending; the decorative +shimmer skeletons are aria-hidden so assistive tech is not read empty rows. +{% endcomment %}
{# ── Hero ─────────────────────────────────────────────────────────────── #}
diff --git a/daiv/accounts/templates/accounts/_manager_lens_toggle.html b/daiv/accounts/templates/accounts/_manager_lens_toggle.html new file mode 100644 index 000000000..03c171a8c --- /dev/null +++ b/daiv/accounts/templates/accounts/_manager_lens_toggle.html @@ -0,0 +1,44 @@ +{% load i18n %} +{% comment %} +Admin-only, NON-PERSISTENT console surface toggle (Personal | Manager Lens). +Rendered in the top-bar left slot of BOTH dashboard.html and manager_lens.html; +pass the current surface in via {% include ... with surface="personal|manager" %}. + +- Gated with user.is_admin so non-admins get NO toggle in the DOM (absent, not + shown-then-blocked, AC2). The route itself is denied server-side by + AdminRequiredMixin (AC6) as belt-and-braces. +- The two links carry BOTH hx-get (calm partial swap of the console main + column) and a plain href (no-JS fallback; the full page is deep-link safe). +- Stateless: nothing is written (no session key, no user field), so the next + fresh landing is always /dashboard/ and the surface never persists (AC4). +- Alpine keeps the active/aria-current state in sync after an HTMX swap (the + toggle sits OUTSIDE the swapped #console-main); a hard load is already correct + because Alpine seeds `surface` from the server-passed value. Teal = active/DO + (never brand/violet). Touch targets are >= 44px and it is not hover-only. +{% endcomment %} +{% if user.is_admin %} + +{% endif %} diff --git a/daiv/accounts/templates/accounts/dashboard.html b/daiv/accounts/templates/accounts/dashboard.html index 29b56d2d3..f0efe1dc5 100644 --- a/daiv/accounts/templates/accounts/dashboard.html +++ b/daiv/accounts/templates/accounts/dashboard.html @@ -6,8 +6,13 @@ {% block container_width %}max-w-screen-2xl{% endblock %} {% block topbar_start %} - {# Range-switcher — console-only, static placeholder in 2.1; wired in Epic 3. - Active pill is teal; labels use the mono face + tabular figures. #} + {# Admin-gated console surface toggle, active on "Personal" (the default); absent for non-admins (AC2). #} + {% include "accounts/_manager_lens_toggle.html" with surface="personal" %} + + {% comment %} + Range-switcher — console-only, static placeholder in 2.1; wired in Epic 3. + Active pill is teal; labels use the mono face + tabular figures. + {% endcomment %} {% endblock app_content %} diff --git a/daiv/accounts/templates/accounts/manager_lens.html b/daiv/accounts/templates/accounts/manager_lens.html new file mode 100644 index 000000000..6af9fffa1 --- /dev/null +++ b/daiv/accounts/templates/accounts/manager_lens.html @@ -0,0 +1,16 @@ +{% extends "base_app.html" %} +{% load i18n %} + +{% block title %}{% translate "Manager Lens" %} — DAIV{% endblock %} + +{% block container_width %}max-w-screen-2xl{% endblock %} + +{% block topbar_start %} + {# Admin-gated console surface toggle, active on "Manager Lens". #} + {% include "accounts/_manager_lens_toggle.html" with surface="manager" %} +{% endblock topbar_start %} + +{% block app_content %} +{# #console-main is the HTMX swap target for the top-bar toggle (see the toggle partial). #} +
{% include "accounts/_manager_lens.html" %}
+{% endblock app_content %} diff --git a/daiv/accounts/templates/base_app.html b/daiv/accounts/templates/base_app.html index 8aa235822..c26642f28 100644 --- a/daiv/accounts/templates/base_app.html +++ b/daiv/accounts/templates/base_app.html @@ -7,8 +7,10 @@ class="flex h-dvh bg-ground"> {% include "accounts/_sidebar.html" %} - {# Mobile navigation sheet (< 768px) — reuses the sidebar partial WITHOUT the - icon-rail modifier, so it always shows full labels. #} + {% comment %} + Mobile navigation sheet (< 768px) — reuses the sidebar partial WITHOUT the + icon-rail modifier, so it always shows full labels. + {% endcomment %}
- {# Top-bar left slot for page-specific controls (e.g. the console - range-switcher). Empty on shared pages, so a control only appears - where it makes sense — the dashboard fills it, MCP servers / sandbox - envs / users etc. leave it empty. #} + {% comment %} + Top-bar left slot for page-specific controls (e.g. the console + range-switcher). Empty on shared pages, so a control only appears + where it makes sense — the dashboard fills it, MCP servers / sandbox + envs / users etc. leave it empty. + {% endcomment %} {% block topbar_start %}{% endblock %}
diff --git a/daiv/accounts/urls/dashboard.py b/daiv/accounts/urls/dashboard.py index 9eac277a7..6999a7b1c 100644 --- a/daiv/accounts/urls/dashboard.py +++ b/daiv/accounts/urls/dashboard.py @@ -1,5 +1,8 @@ from django.urls import path -from accounts.views import DashboardView +from accounts.views import DashboardView, ManagerLensView -urlpatterns = [path("", DashboardView.as_view(), name="dashboard")] +urlpatterns = [ + path("", DashboardView.as_view(), name="dashboard"), + path("manager/", ManagerLensView.as_view(), name="manager_lens"), +] diff --git a/daiv/accounts/views.py b/daiv/accounts/views.py index 78525ac00..56cd2b7e8 100644 --- a/daiv/accounts/views.py +++ b/daiv/accounts/views.py @@ -108,6 +108,75 @@ def _format_duration(td: timedelta | None) -> str: return f"{hours}h {minutes}m" +def _resolve_period(request, default: str = DEFAULT_PERIOD) -> tuple[str, date | None]: + """Resolve the stateless ``?period=`` querystring to a ``(period_key, cutoff_date)`` pair. + + Falls back to ``default`` for a missing/invalid value — the personal console uses the module + ``DEFAULT_PERIOD`` ("today"), while the org ``ManagerLensView`` passes ``default="all"`` because + org velocity is cumulative. ``cutoff_date`` is ``None`` for the "all time" period, today's date + for the zero-day "today" period, and ``today - days`` otherwise. Shared by both console surfaces + so they honour the same range semantics with no persisted state. + """ + period = request.GET.get("period", default) + if period not in PERIOD_DAYS: + period = default + days = PERIOD_DAYS[period] + cutoff_date = localdate() - timedelta(days=days) if days is not None else None + if days == 0: + cutoff_date = localdate() + return period, cutoff_date + + +def get_velocity_data(cutoff_date: date | None) -> dict | None: + """Aggregate org-wide code-velocity + DAIV-attribution from ``MergeMetric``. + + Org-wide by design — ``MergeMetric.objects.all()`` (never user-scoped, never derived from + ``RunEnvelope`` per AD-10); an optional ``cutoff_date`` restricts to merges on/after that date. + Returns ``None`` when there are zero matching rows so the caller can render an honest cold-load + state instead of a misleading zero reading. Single source of truth for the Manager Lens. + """ + merges = MergeMetric.objects.all() + if cutoff_date is not None: + merges = merges.filter(merged_at__date__gte=cutoff_date) + + stats = merges.aggregate( + total=Count("id"), + total_added=Sum("lines_added", default=0), + total_removed=Sum("lines_removed", default=0), + daiv_merges=Count("id", filter=Q(daiv_commits__gt=0)), + total_commits_sum=Sum("total_commits", default=0), + daiv_commits_sum=Sum("daiv_commits", default=0), + ) + + if not stats["total"]: + return None + + total_merges = stats["total"] + daiv_merges = stats["daiv_merges"] + human_merges = total_merges - daiv_merges + total_commits = stats["total_commits_sum"] + daiv_commits = stats["daiv_commits_sum"] + human_commits = max(0, total_commits - daiv_commits) + max_lines = max(stats["total_added"], stats["total_removed"], 1) + + return { + "total_merges": total_merges, + "lines_added": stats["total_added"], + "lines_removed": stats["total_removed"], + "net_lines": stats["total_added"] - stats["total_removed"], + "daiv_merges_pct": _format_pct(daiv_merges, total_merges), + "daiv_merges_pct_raw": min(_raw_pct(daiv_merges, total_merges) or 0, 100), + "daiv_merges": daiv_merges, + "human_merges": human_merges, + "daiv_commits_pct": _format_pct(daiv_commits, total_commits), + "daiv_commits_pct_raw": min(_raw_pct(daiv_commits, total_commits) or 0, 100), + "daiv_commits": daiv_commits, + "human_commits": human_commits, + "lines_added_pct": round(stats["total_added"] / max_lines * 100, 1), + "lines_removed_pct": round(stats["total_removed"] / max_lines * 100, 1), + } + + class DashboardView(LoginRequiredMixin, TemplateView): template_name = "accounts/dashboard.html" @@ -122,23 +191,17 @@ def get_template_names(self) -> list[str]: def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - period = self.request.GET.get("period", DEFAULT_PERIOD) - if period not in PERIOD_DAYS: - period = DEFAULT_PERIOD - days = PERIOD_DAYS[period] - cutoff_date = localdate() - timedelta(days=days) if days is not None else None - if days == 0: - cutoff_date = localdate() + period, cutoff_date = _resolve_period(self.request) + # Personal-by-default (AC1): the console default carries NO org/aggregate content for + # anyone — admin OR member. Org velocity / total_users now live ONLY in the Manager Lens + # (``ManagerLensView``); every read here is personal-scoped via ``visible_to`` / ``user=``. user = self.request.user context["activity"] = self._get_activity_data(cutoff_date, user) context["active_api_keys"] = APIKey.objects.filter(user=user, revoked=False).count() context["periods"] = [{"key": key, "label": label} for key, label, _ in PERIOD_CHOICES] context["current_period"] = period - context["velocity"] = self._get_velocity_data(cutoff_date) if user.is_admin else None context["active_schedules"] = ScheduledJob.objects.filter(user=user, is_enabled=True).count() - if user.is_admin: - context["total_users"] = User.objects.count() return context @@ -237,47 +300,41 @@ def _get_activity_data(self, cutoff_date: date | None, user: User) -> dict: "segments": segments, } - def _get_velocity_data(self, cutoff_date: date | None) -> dict | None: - merges = MergeMetric.objects.all() - if cutoff_date is not None: - merges = merges.filter(merged_at__date__gte=cutoff_date) - stats = merges.aggregate( - total=Count("id"), - total_added=Sum("lines_added", default=0), - total_removed=Sum("lines_removed", default=0), - daiv_merges=Count("id", filter=Q(daiv_commits__gt=0)), - total_commits_sum=Sum("total_commits", default=0), - daiv_commits_sum=Sum("daiv_commits", default=0), - ) +class ManagerLensView(AdminRequiredMixin, TemplateView): + """Admin-only org-impact surface — the relocated Code-Velocity / DAIV-attribution content. - if not stats["total"]: - return None + ``AdminRequiredMixin`` raises ``PermissionDenied`` (→ HTTP 403) for a logged-in non-admin, so a + forged direct request is denied server-side, not merely hidden (AC6). Reuses the stateless + ``?period=`` idiom and the shared module-level ``get_velocity_data`` computation (no duplicated + query). Nothing about the current surface is persisted (AC4) — the Lens is simply a distinct + route from the personal console, so a fresh landing is always ``/dashboard/``. + """ - total_merges = stats["total"] - daiv_merges = stats["daiv_merges"] - human_merges = total_merges - daiv_merges - total_commits = stats["total_commits_sum"] - daiv_commits = stats["daiv_commits_sum"] - human_commits = max(0, total_commits - daiv_commits) - max_lines = max(stats["total_added"], stats["total_removed"], 1) + template_name = "accounts/manager_lens.html" - return { - "total_merges": total_merges, - "lines_added": stats["total_added"], - "lines_removed": stats["total_removed"], - "net_lines": stats["total_added"] - stats["total_removed"], - "daiv_merges_pct": _format_pct(daiv_merges, total_merges), - "daiv_merges_pct_raw": min(_raw_pct(daiv_merges, total_merges) or 0, 100), - "daiv_merges": daiv_merges, - "human_merges": human_merges, - "daiv_commits_pct": _format_pct(daiv_commits, total_commits), - "daiv_commits_pct_raw": min(_raw_pct(daiv_commits, total_commits) or 0, 100), - "daiv_commits": daiv_commits, - "human_commits": human_commits, - "lines_added_pct": round(stats["total_added"] / max_lines * 100, 1), - "lines_removed_pct": round(stats["total_removed"] / max_lines * 100, 1), - } + def get_template_names(self) -> list[str]: + # HTMX (the top-bar toggle) swaps in the body fragment; a normal GET / no-JS deep link + # renders the full shell. Mirrors ``SessionListView`` / ``DashboardView``. + if is_htmx(self.request): + return ["accounts/_manager_lens.html"] + return ["accounts/manager_lens.html"] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + _, cutoff_date = _resolve_period(self.request, default="all") + + # Org-scoped by design — this is the ONE place org velocity / total_users render. The Lens + # defaults to the cumulative "all" window (not the personal console's "today"): org velocity + # is cumulative, so a fresh visit shows real historical impact instead of an empty view. + # ``get_velocity_data`` then returns ``None`` only for a genuinely-empty org (zero + # ``MergeMetric`` rows ever) — the honest cold-load skeleton, never a "DAIV did nothing" + # zero (AC5). No range switcher on the Lens yet — that arrives with Epic 3. + context["velocity"] = get_velocity_data(cutoff_date) + context["total_users"] = User.objects.count() + + return context class APIKeyListView(LoginRequiredMixin, ListView): diff --git a/daiv/automation/agent/base.py b/daiv/automation/agent/base.py index 17576300b..171a8219d 100644 --- a/daiv/automation/agent/base.py +++ b/daiv/automation/agent/base.py @@ -11,6 +11,7 @@ from langchain_core.runnables import Runnable from langgraph.graph.state import CompiledStateGraph +from automation.agent.chat_models import ChatOpenRouter from automation.agent.model_catalog.exceptions import MissingApiKeyError from automation.agent.provider_clients import build_sdk_client_kwargs from core.constants import BOT_NAME @@ -34,6 +35,8 @@ "claude-sonnet-5", "claude-opus-4-5", "claude-opus-4-6", + "claude-opus-4-7", + "claude-opus-4-8", "claude-haiku-4-5", "claude-fable-5", "anthropic/claude-sonnet-4.5", @@ -41,6 +44,8 @@ "anthropic/claude-sonnet-5", "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", + "anthropic/claude-opus-4.7", + "anthropic/claude-opus-4.8", "anthropic/claude-haiku-4.5", "anthropic/claude-fable-5", ) @@ -56,7 +61,18 @@ "anthropic/claude-fable-5", ) -OPENAI_THINKING_MODELS = ("gpt-5.2", "gpt-5.3-codex", "openai/gpt-5.2", "openai/gpt-5.3-codex") +OPENAI_THINKING_MODELS = ( + "gpt-5.2", + "gpt-5.3-codex", + "gpt-5.4", + "gpt-5.5", + "gpt-5.6", + "openai/gpt-5.2", + "openai/gpt-5.3-codex", + "openai/gpt-5.4", + "openai/gpt-5.5", + "openai/gpt-5.6", +) ANTHROPIC_STRUCTURED_OUTPUTS_BETA = "structured-outputs-2025-11-13" @@ -300,6 +316,11 @@ def get_model(*, model: str, thinking_level: ThinkingLevel | None = None, **kwar resolved = parse_model_spec(model) model_kwargs = BaseAgent.get_model_kwargs(resolved=resolved, thinking_level=thinking_level, **kwargs) try: + if resolved.row.provider_type == ProviderType.OPENROUTER: + # OpenRouter routes through our ChatOpenAI subclass (reasoning extraction + + # base-URL default). init_chat_model can't return a subclass, so instantiate + # directly. + return ChatOpenRouter(**model_kwargs) return init_chat_model(**model_kwargs) except Exception: _close_insecure_http_clients(model_kwargs) @@ -343,9 +364,11 @@ def get_model_kwargs(*, resolved: ResolvedProvider, thinking_level: ThinkingLeve _apply_openai_reasoning(kw, thinking_level, resolved.model_name) elif row.provider_type == ProviderType.OPENROUTER: - # OpenRouter is OpenAI-compatible over the wire. - kw["model_provider"] = ProviderType.OPENAI.value - kw["openai_api_base"] = sdk_kw["base_url"] or "https://openrouter.ai/api/v1" + # OpenRouter is OpenAI-compatible over the wire, but constructed directly as a + # ChatOpenRouter (not via init_chat_model) — that subclass supplies the base-URL + # default, so mirror the OpenAI branch and only forward an explicit override. + if sdk_kw["base_url"]: + kw["openai_api_base"] = sdk_kw["base_url"] kw["model_kwargs"]["extra_headers"] = {"HTTP-Referer": "https://srtab.github.io/daiv", "X-Title": BOT_NAME} _apply_openrouter_thinking(kw, thinking_level, resolved.model_name) diff --git a/daiv/automation/agent/chat_models.py b/daiv/automation/agent/chat_models.py new file mode 100644 index 000000000..6605e9ed3 --- /dev/null +++ b/daiv/automation/agent/chat_models.py @@ -0,0 +1,73 @@ +"""OpenRouter chat model. + +OpenRouter speaks the OpenAI Chat Completions API, so DAIV reaches it through a +:class:`~langchain_openai.ChatOpenAI` *subclass* rather than a dedicated +third-party package. Staying a ``ChatOpenAI`` subclass is load-bearing: +:class:`~automation.agent.middlewares.prompt_cache.AnthropicPromptCachingMiddleware` +detects OpenRouter-Anthropic models with ``isinstance(model, ChatOpenAI)`` and +injects top-level ``extra_body`` ``cache_control`` — the first-party +``langchain-openrouter`` (a ``BaseChatModel`` whose automatic Anthropic caching +is still an open upstream issue) would silently disable that. + +This class owns the OpenRouter-specific transport bits that would otherwise be +set ad-hoc at construction time: the base-URL default, and — the reason the +subclass exists — extraction of OpenRouter's non-standard streaming reasoning. +Stock ``ChatOpenAI`` targets the official OpenAI spec only and drops provider +extensions like ``reasoning`` / ``reasoning_details`` by design (see its module +docstring), so the reasoning never reaches ``ag_ui_langgraph`` and the chat UI +shows nothing. Surfacing it as ``additional_kwargs["reasoning_content"]`` (a +string) is the shape both ``ag_ui_langgraph.resolve_reasoning_content`` (live +``REASONING_*`` events) and ``chat.turns`` (transcript reload) already render. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from langchain_openai import ChatOpenAI + +from core.models import ProviderType + +if TYPE_CHECKING: + from langchain_core.outputs import ChatGenerationChunk + +OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" + + +class ChatOpenRouter(ChatOpenAI): + """``ChatOpenAI`` pointed at OpenRouter, with reasoning extraction.""" + + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("openai_api_base", OPENROUTER_BASE_URL) + super().__init__(**kwargs) + + @property + def is_anthropic(self) -> bool: + """Whether this routes to an Anthropic model (``anthropic/…``). Drives the + OpenRouter-Anthropic branch of the prompt-caching middleware.""" + return self.model_name.startswith(ProviderType.ANTHROPIC.value) + + def _convert_chunk_to_generation_chunk( + self, chunk: dict, default_chunk_class: type, base_generation_info: dict | None + ) -> ChatGenerationChunk | None: + generation_chunk = super()._convert_chunk_to_generation_chunk(chunk, default_chunk_class, base_generation_info) + if generation_chunk is None: + return None + reasoning = self._reasoning_delta(chunk) + if reasoning: + # String values concatenate when AIMessageChunks are added, so per-chunk + # deltas accumulate into the final message's reasoning_content. + generation_chunk.message.additional_kwargs["reasoning_content"] = reasoning + return generation_chunk + + @staticmethod + def _reasoning_delta(chunk: dict) -> str: + """The OpenRouter ``reasoning`` text on a raw stream chunk, or ``""``. + + Only reached after the parent parsed the same chunk into a non-None generation + chunk, so ``chunk`` is a well-formed dict; the ``or`` guards cover the + empty-choices (usage-only) chunk that the parent still passes through. + """ + delta = (chunk.get("choices") or [{}])[0].get("delta") or {} + reasoning = delta.get("reasoning") + return reasoning if isinstance(reasoning, str) else "" diff --git a/daiv/automation/agent/middlewares/file_system.py b/daiv/automation/agent/middlewares/file_system.py index 07e1ba99d..ad5237ffa 100644 --- a/daiv/automation/agent/middlewares/file_system.py +++ b/daiv/automation/agent/middlewares/file_system.py @@ -5,12 +5,14 @@ import logging import re import stat +import time from pathlib import Path from typing import TYPE_CHECKING, Protocol, cast, runtime_checkable import httpx +import wcmatch.glob as wcglob from deepagents.backends.composite import CompositeBackend -from deepagents.backends.filesystem import FilesystemBackend +from deepagents.backends.filesystem import DEFAULT_GREP_TIMEOUT, FilesystemBackend from deepagents.backends.protocol import ( FILE_NOT_FOUND, BackendProtocol, @@ -292,10 +294,9 @@ async def agrep(self, pattern: str, path: str | None = None, glob: str | None = Validates the pattern with Python ``re`` up front so an invalid regex is a clean, model-fixable error rather than a silent zero-match (the inherited backend greps literally - via ``rg -F``/``re.escape``; ripgrep also exits 2 quietly on a bad regex). Reuses the - parent's ``_python_search`` with the *raw* (unescaped) pattern, which compiles it as a - regex — trading ripgrep's speed for correct semantics on local/disk runs (the deployed - path is the sandbox backend). + via ``rg -F``/``re.escape``; ripgrep also exits 2 quietly on a bad regex). Walks the tree + with a compiled regex — trading ripgrep's speed for correct semantics on local/disk runs + (the deployed path is the sandbox backend). """ try: re.compile(pattern) @@ -303,24 +304,139 @@ async def agrep(self, pattern: str, path: str | None = None, glob: str | None = return GrepResult(error=f"invalid regular expression: {pattern!r} ({exc})") return await asyncio.to_thread(self._regex_grep, pattern, path, glob) + def _grep_error_detail(self, exc: Exception) -> str: + """Agent-safe detail for a grep failure that never embeds the real on-disk path. + + ``OSError.__str__`` appends the offending filename and, in ``virtual_mode``, even + generic exception text can carry the backend's real ``root_dir`` — either would leak + the host layout to the model. Mirrors the sanitisation deepagents applies in + ``FilesystemBackend._python_search`` (a method-local ``_safe_detail`` we cannot reuse), + keeping this shadowing regex walk in parity with the base's literal one. + """ + if isinstance(exc, OSError): + detail = exc.strerror + else: + detail = getattr(exc, "reason", None) + if detail is None and not self.virtual_mode: + detail = str(exc) + return f"{type(exc).__name__}: {detail}" if detail else type(exc).__name__ + def _regex_grep(self, pattern: str, path: str | None, glob: str | None) -> GrepResult: try: base_full = self._resolve_path(path or ".") except ValueError: return GrepResult(matches=[]) except (OSError, RuntimeError) as exc: - return GrepResult(error=f"Error searching path '{path or '.'}': {exc}", matches=[]) + return GrepResult(error=f"Error searching path '{path or '.'}': {self._grep_error_detail(exc)}", matches=[]) try: if not base_full.exists(): return GrepResult(matches=[]) except OSError as exc: - return GrepResult(error=f"Error searching path '{path or '.'}': {exc}", matches=[]) - results = self._python_search(pattern, base_full, glob) - matches = [ - GrepMatch(path=fpath, line=int(line_num), text=line_text) - for fpath, items in results.items() - for (line_num, line_text) in items - ] + return GrepResult(error=f"Error searching path '{path or '.'}': {self._grep_error_detail(exc)}", matches=[]) + + regex = re.compile(pattern) + glob_matcher = wcglob.compile(glob, flags=wcglob.BRACE | wcglob.GLOBSTAR) if glob else None + deadline = time.monotonic() + DEFAULT_GREP_TIMEOUT + root = base_full if base_full.is_dir() else base_full.parent + results: dict[str, list[tuple[int, str]]] = {} + file_errors: list[str] = [] + + def _dump_matches() -> list[GrepMatch]: + return [ + GrepMatch(path=fpath, line=int(line_num), text=line_text) + for fpath, items in results.items() + for (line_num, line_text) in items + ] + + def _timed_out() -> GrepResult: + return GrepResult( + error=( + f"Grep of '{path or '.'}' timed out after {DEFAULT_GREP_TIMEOUT}s " + f"with {len(results)} matching file(s); try a more " + f"specific pattern or a narrower path." + ), + matches=_dump_matches(), + ) + + try: + for fp in root.rglob("*"): + if time.monotonic() > deadline: + return _timed_out() + try: + if not fp.is_file(): + continue + except OSError, RuntimeError: + continue + if glob_matcher is not None and not glob_matcher.match(str(fp.relative_to(root))): + continue + try: + if fp.stat().st_size > self.max_file_size_bytes: + continue + except OSError, RuntimeError: + continue + scanned_lines = 0 + try: + if self.virtual_mode: + try: + virt_path = self._to_virtual_path(fp) + except ValueError: + # Resolved outside the virtual root — expected for stray symlinks; the + # base logs this at DEBUG, so mirror it rather than dropping silently. + logger.debug("skipping grep result outside root: %s", fp) + continue + except OSError, RuntimeError: + # ``resolve()`` failed (permission denied, or a symlink loop -> ELOOP). + # A matched file would be dropped, so log loudly (base parity) instead + # of vanishing without a trace. + logger.warning("could not resolve grep result path: %s", fp, exc_info=True) + continue + else: + virt_path = str(fp) + with fp.open(encoding="utf-8", errors="strict") as handle: + for line_num, raw_line in enumerate(handle, 1): + scanned_lines = line_num + if line_num % 2048 == 0 and time.monotonic() > deadline: + return _timed_out() + if regex.search(raw_line): + results.setdefault(virt_path, []).append((line_num, raw_line.rstrip("\n"))) + except UnicodeDecodeError as exc: + # A file that fails to decode before any line is scanned is treated as binary + # and skipped silently (mirroring ripgrep). Record it only when the decode + # failed partway through (``scanned_lines > 0``), so a truncated per-file read is + # logged (and surfaced if nothing else matched) rather than passing as complete. + if scanned_lines > 0: + file_errors.append(f"- {virt_path}: {self._grep_error_detail(exc)}") + continue + except (OSError, RuntimeError) as exc: + file_errors.append(f"- {virt_path}: {self._grep_error_detail(exc)}") + continue + except (OSError, RuntimeError) as exc: + # The tree walk itself aborted mid-iteration (a directory entry unlinked/renamed during + # the walk, or a symlink loop). Unlike a single unreadable file, the walk is now + # arbitrarily incomplete, so — deliberately, unlike the per-file path below — we *surface* + # (the agent must not trust an aborted walk as a complete search) *and* log for operators + # (base parity: it logs this abort with a traceback). + logger.warning( + "disk grep walk of %r aborted after %d matching file(s)", path or ".", len(results), exc_info=True + ) + return GrepResult( + error=f"Error searching path '{path or '.'}': {self._grep_error_detail(exc)}", matches=_dump_matches() + ) + + matches = _dump_matches() + if file_errors: + # A per-file read failure (permissions, a file unlinked mid-walk, transient I/O) is not + # agent-actionable. When usable matches survive, return them clean and keep the failures + # in the operator logs rather than setting ``GrepResult.error`` — ``DAIVCompositeBackend`` + # ``.agrep`` treats a set ``error`` as fatal and would drop matches from the *other* routed + # backends (and the tool marks the call failed). Only when nothing matched do we surface, + # so an empty-because-unreadable result isn't mistaken for a genuine zero-match. See the + # partial-result-over-bare-error policy; the base's literal ``_python_search`` always + # surfaces here — this is a deliberate, documented divergence. + joined = "\n".join(file_errors) + logger.warning("disk grep could not fully search %d file(s):\n%s", len(file_errors), joined) + if not matches: + return GrepResult(error=f"One or more files could not be fully searched:\n{joined}", matches=matches) return GrepResult(matches=matches) diff --git a/daiv/automation/agent/middlewares/prompt_cache.py b/daiv/automation/agent/middlewares/prompt_cache.py index 3833243ba..9e5217543 100644 --- a/daiv/automation/agent/middlewares/prompt_cache.py +++ b/daiv/automation/agent/middlewares/prompt_cache.py @@ -5,9 +5,8 @@ from langchain_anthropic.middleware.prompt_caching import ( AnthropicPromptCachingMiddleware as AnthropicPromptCachingMiddlewareV0, ) -from langchain_openai.chat_models import ChatOpenAI -from core.models import ProviderType +from automation.agent.chat_models import ChatOpenRouter if TYPE_CHECKING: from collections.abc import Awaitable, Callable @@ -69,4 +68,4 @@ def _is_openrouter_anthropic_model(self, model: BaseChatModel) -> bool: """ Check if the model is an OpenRouter Anthropic model. """ - return isinstance(model, ChatOpenAI) and model.model_name.startswith(ProviderType.ANTHROPIC.value) + return isinstance(model, ChatOpenRouter) and model.is_anthropic diff --git a/daiv/automation/agent/model_catalog/adapters.py b/daiv/automation/agent/model_catalog/adapters.py index 70e4b3375..e92fe365b 100644 --- a/daiv/automation/agent/model_catalog/adapters.py +++ b/daiv/automation/agent/model_catalog/adapters.py @@ -13,6 +13,7 @@ from google import genai as google_genai from google.genai import errors as google_errors +from automation.agent.chat_models import OPENROUTER_BASE_URL from automation.agent.model_catalog.base import ModelCatalogAdapter from automation.agent.model_catalog.exceptions import CatalogFetchError from automation.agent.provider_clients import build_sdk_client_kwargs @@ -176,7 +177,7 @@ async def list_models(self, row: Provider.Cached) -> list[str]: class OpenRouterAdapter(ModelCatalogAdapter): """OpenAI-compatible over the wire — reuse the openai SDK with a different base URL.""" - _DEFAULT_BASE_URL = "https://openrouter.ai/api/v1" + _DEFAULT_BASE_URL = OPENROUTER_BASE_URL # OpenRouter's ``/models`` supports two capability filters we care about: # - ``output_modalities=text`` — drops embeddings, image-generation, TTS, diff --git a/daiv/core/checkpointer.py b/daiv/core/checkpointer.py index 83d0cef76..795cfa348 100644 --- a/daiv/core/checkpointer.py +++ b/daiv/core/checkpointer.py @@ -5,8 +5,10 @@ from django.conf import settings +from langchain_core.messages import AnyMessage # noqa: TC002 from langgraph.checkpoint.redis.aio import AsyncRedisSaver from langgraph.checkpoint.redis.jsonplus_redis import JsonPlusRedisSerializer +from langgraph.graph.message import add_messages from pydantic import BaseModel from codebase.base import MergeRequest @@ -14,6 +16,17 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator + from langchain_core.runnables import RunnableConfig + +# ``_DeltaSnapshot`` wraps a full channel value when ``DeltaChannel`` writes a periodic +# snapshot into ``channel_values`` (see ``aresolve_thread_messages``). It is a beta +# langgraph type; import defensively so a future relocation degrades to the write-replay +# path rather than breaking module import. +try: + from langgraph.checkpoint.serde.types import _DeltaSnapshot +except ImportError: # pragma: no cover - defensive against langgraph beta churn + _DeltaSnapshot = None # ty: ignore[invalid-assignment] + # Domain pydantic models that may live in checkpointed agent state. Listing a model # here lets DAIVRedisSerializer both encode it to RedisJSON and revive it on read. @@ -24,65 +37,39 @@ class DAIVRedisSerializer(JsonPlusRedisSerializer): - """Redis checkpoint serializer that round-trips DAIV domain pydantic models and sets. + """Redis checkpoint serializer that round-trips DAIV domain pydantic models. - Two gaps in the stock ``JsonPlusRedisSerializer`` make checkpointed agent state - unsafe without this override: + The stock ``_default_handler`` encodes plain pydantic models from ``obj.__dict__``; this + class overrides it to emit ``model_dump(mode="json")`` so nested models and types serialise + cleanly (the agent stores a :class:`~codebase.base.MergeRequest` in ``GitState.merge_request``). + LangChain objects (messages) carry ``to_json`` and keep flowing through the parent's safe path + untouched. - * **Sets** -- ``_preprocess_interrupts`` serializes a ``set`` as an ``lc:2`` - envelope keyed by ``kwargs["__set_items__"]``, but the base reviver routes - ``("builtins", "set")`` (a SAFE_MSGPACK_TYPE) through ``_revive_lc2`` → - ``set(**kwargs)`` → ``TypeError``, which ``langgraph-checkpoint>=4.1.1`` turns - into ``None`` -- silently nulling every checkpointed set (e.g. - ``loaded_tool_names``). ``_reviver`` reconstructs the envelope before the base - reviver corrupts it. - - * **Pydantic models** -- the stock ``_default_handler`` encodes plain pydantic - models from ``obj.__dict__``; we override it to emit ``model_dump(mode="json")`` - so nested models and types serialise cleanly. LangChain objects (messages) carry - ``to_json`` and keep flowing through the parent's safe path untouched. + **Sets no longer need an override here.** A checkpointed ``set`` (e.g. ``loaded_tool_names``) + was silently nulled by older ``JsonPlusRedisSerializer`` revivers; ``langgraph-checkpoint-redis`` + 0.5.1 reconstructs it upstream in ``_revive_if_needed``, so the bespoke ``_reviver`` this class + used to carry is gone. ``tests/unit_tests/core/test_checkpointer.py`` guards the round-trip + (mechanism detailed there) so a downgrade or upstream regression fails loudly rather than + nulling production sets. On the happy path decode round-trips: the redis read path (``_revive_if_needed``) - reconstructs the ``lc:2`` envelope for any importable class via - ``_reconstruct_from_constructor``. But that reviver swallows reconstruction failures - (``except Exception: pass``) and returns the raw envelope ``dict`` -- so if a model's - schema drifts across a deploy (a field renamed/required/retyped, or the class relocated), - a checkpointed model silently comes back as a ``dict`` with no log. Consumers must therefore - not assume the revived value is the model: ``GitMiddleware`` guards its ``merge_request`` - read (``_state_merge_request``) and fails loud rather than letting an ``AttributeError`` - surface far downstream. We also register our models on ``allowed_json_modules`` -- but note - the read path taken here (``_reconstruct_from_constructor``) does **not** consult that - allowlist; it is kept only so the *documented* decode gate (``_revive_lc2``) stays correct - should a future upstream route the read path through it, not as a runtime guarantee today. + reconstructs the ``lc:2`` envelope for any importable class by delegating to the base + reviver (``_reviver``). But that reviver can fall back to returning the raw envelope + ``dict`` on a reconstruction failure -- so if a model's schema drifts across a deploy (a + field renamed/required/retyped, or the class relocated), a checkpointed model silently + comes back as a ``dict`` with no log. Consumers must therefore not assume the revived value + is the model: ``GitMiddleware`` guards its ``merge_request`` read (``_state_merge_request``) + and fails loud rather than letting an ``AttributeError`` surface far downstream. We also + register our models on ``allowed_json_modules`` -- but note the read path taken here + (``_revive_if_needed`` → the base reviver) does **not** consult that allowlist; it is kept + only so the *documented* decode gate (``_revive_lc2``) stays correct should a future upstream + route the read path through it, not as a runtime guarantee today. """ def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("allowed_json_modules", CHECKPOINT_JSON_TYPES) super().__init__(**kwargs) - def _reviver(self, value: Any) -> Any: - """Reconstruct adapter-encoded sets before the base reviver corrupts them to ``None``. - - ``JsonPlusRedisSerializer._preprocess_interrupts`` serializes a ``set`` as - ``{"lc": 2, "type": "constructor", "id": ["builtins", "set"], "kwargs": {"__set_items__": [...]}}``; - only the adapter's own ``_reconstruct_from_constructor`` understands ``__set_items__``. But the - read path (``_revive_if_needed``) runs *this* base reviver first, and ``set(**kwargs)`` raises, - so the value comes back ``None`` -- the adapter fallback never fires because it only - triggers when the base returns the *unchanged* dict. Intercept the envelope here. - ``frozenset`` is handled for symmetry though DAIV stores only plain ``set``. - """ - if ( - isinstance(value, dict) - and value.get("lc") == 2 - and value.get("type") == "constructor" - and value.get("id") in (["builtins", "set"], ["builtins", "frozenset"]) - and isinstance(value.get("kwargs"), dict) - and "__set_items__" in value["kwargs"] - ): - items = (self._revive_if_needed(item) for item in value["kwargs"]["__set_items__"]) - return frozenset(items) if value["id"][-1] == "frozenset" else set(items) - return super()._reviver(value) - def _default_handler(self, obj: Any) -> Any: # Intercept plain pydantic models (those without ``to_json``) and encode them # via ``model_dump(mode="json")`` rather than the stock ``__dict__`` path, so @@ -115,3 +102,55 @@ async def open_checkpointer() -> AsyncIterator[AsyncRedisSaver]: ) as cp: cp.serde = DAIVRedisSerializer() yield cp + + +def _unwrap_delta_snapshot(value: Any) -> Any: + """Unwrap a ``DeltaChannel`` snapshot to its stored value; pass anything else through.""" + if _DeltaSnapshot is not None and isinstance(value, _DeltaSnapshot): + return value.value + return value + + +async def aresolve_thread_messages( + cp: AsyncRedisSaver, config: RunnableConfig, channel_values: dict[str, Any] +) -> list[AnyMessage]: + """Return a thread's full ``messages`` list, reconstructing ``DeltaChannel`` state. + + deepagents >= 0.6 declares the agent's ``messages`` key as langgraph's (beta) + ``DeltaChannel``. Its ``checkpoint()`` returns a sentinel on non-snapshot supersteps, + so ``messages`` is **absent** from the checkpoint's ``channel_values`` on essentially + every finished chat (snapshots only land every ~50 message updates). The accumulated + list must instead be replayed from ancestor writes via the + ``aget_delta_channel_history`` contract. ``langgraph-checkpoint-redis`` 0.5.1 does not + reconstruct delta channels inside ``aget_tuple`` (by design — that is what keeps the + checkpoint O(N)), so a raw ``channel_values["messages"]`` read comes back empty and + the transcript renders blank on reload. + + Resolution order: + + * ``messages`` resolves to a list in ``channel_values`` (a plain inline list from + deepagents < 0.6 ``add_messages``, or a ``_DeltaSnapshot`` wrapping one) — return it. + * otherwise (absent on a non-snapshot delta step) — replay ``seed + writes`` from the + delta history. + + The replay folds writes through the public ``add_messages`` reducer. deepagents' own + ``_messages_delta_reducer`` is ``add_messages`` semantics (id-dedup, ``RemoveMessage`` + tombstones, ``REMOVE_ALL_MESSAGES`` reset) minus per-chunk coercion — and only full + messages are ever checkpointed — so the reconstruction is identical without importing + that internal symbol. Returns ``[]`` when nothing is recoverable. + """ + raw = _unwrap_delta_snapshot(channel_values.get("messages")) + if isinstance(raw, list): + return raw + + # ``channel_values`` is passed in (already fetched by the caller) so the inline path + # above serves legacy threads without a walk; only the delta path pays for the history + # replay, which re-reads the latest tuple internally to seed its parent-chain walk. + history = await cp.aget_delta_channel_history(config=config, channels=["messages"]) + entry = history.get("messages") or {} + seed = _unwrap_delta_snapshot(entry.get("seed")) + messages: list[AnyMessage] = list(seed) if isinstance(seed, list) else [] + # PendingWrite is ``(task_id, channel, value)``; fold each channel write oldest-to-newest. + for write in entry.get("writes") or []: + messages = add_messages(messages, write[2]) + return messages diff --git a/daiv/memory/tasks.py b/daiv/memory/tasks.py index 1034818ef..0d3b5eb37 100644 --- a/daiv/memory/tasks.py +++ b/daiv/memory/tasks.py @@ -16,7 +16,7 @@ from automation.agent.base import BaseAgent from codebase.repo_config import RepositoryConfig -from core.checkpointer import open_checkpointer +from core.checkpointer import aresolve_thread_messages, open_checkpointer from core.site_settings import site_settings from memory.models import MemoryObservation, ObservationStatus, RepositoryMemory from memory.prompts import consolidation_human, consolidation_system, extraction_human, extraction_system @@ -206,11 +206,9 @@ async def extract_observations_task(run_id: str) -> None: logger.info("extract_observations_task: memory disabled for repo %s, skipping", run.repo_id) return + thread_config = {"configurable": {"thread_id": str(run.session_id)}} async with open_checkpointer() as checkpointer: - checkpoint_tuple = await checkpointer.aget_tuple({"configurable": {"thread_id": str(run.session_id)}}) - - channel_values = (checkpoint_tuple.checkpoint or {}).get("channel_values", {}) if checkpoint_tuple else {} - if not (messages := channel_values.get("messages", [])): + checkpoint_tuple = await checkpointer.aget_tuple(thread_config) if checkpoint_tuple is None: # Benign: the checkpoint expired from Redis before this task ran. logger.info( @@ -218,16 +216,22 @@ async def extract_observations_task(run_id: str) -> None: run.session_id, run_id, ) - else: - # A present checkpoint with no messages signals a real defect (serialization - # or channel-name drift), not normal TTL expiry — surface it louder. - logger.warning( - "extract_observations_task: checkpoint present but has no messages for thread %s (run=%s); " - "available channels: %s — skipping (serialization or channel-name drift?)", - run.session_id, - run_id, - sorted(channel_values), - ) + return + channel_values = (checkpoint_tuple.checkpoint or {}).get("channel_values", {}) + # ``messages`` is stored in a deepagents ``DeltaChannel`` and is usually absent from + # ``channel_values`` — reconstruct it from the delta write history. + messages = await aresolve_thread_messages(checkpointer, thread_config, channel_values) + + if not messages: + # A present checkpoint with no messages even after DeltaChannel reconstruction signals + # a real defect (serialization or channel-name drift), not normal TTL expiry — louder. + logger.warning( + "extract_observations_task: checkpoint present but has no messages for thread %s (run=%s); " + "available channels: %s — skipping (serialization or channel-name drift?)", + run.session_id, + run_id, + sorted(channel_values), + ) return transcript = serialize_transcript(messages) diff --git a/daiv/sessions/hydration.py b/daiv/sessions/hydration.py index ddcf5cf4e..84960e39a 100644 --- a/daiv/sessions/hydration.py +++ b/daiv/sessions/hydration.py @@ -3,7 +3,7 @@ from typing import Any, NamedTuple from chat.repo_state import mr_to_payload -from core.checkpointer import open_checkpointer +from core.checkpointer import aresolve_thread_messages, open_checkpointer class HydratedThread(NamedTuple): @@ -22,10 +22,13 @@ class HydratedThread(NamedTuple): async def ahydrate_thread(thread_id: str) -> HydratedThread: """Return the hydrated messages, expiry flag, and MR payload for a thread.""" + config = {"configurable": {"thread_id": thread_id}} async with open_checkpointer() as cp: - tup = await cp.aget_tuple({"configurable": {"thread_id": thread_id}}) - if tup is None: - return HydratedThread([], True, None) - channel_values = (tup.checkpoint or {}).get("channel_values", {}) - messages = channel_values.get("messages", []) + tup = await cp.aget_tuple(config) + if tup is None: + return HydratedThread([], True, None) + channel_values = (tup.checkpoint or {}).get("channel_values", {}) + # ``messages`` lives in a deepagents ``DeltaChannel`` and is usually absent from + # ``channel_values``; resolve it via the delta-history contract (see the helper). + messages = await aresolve_thread_messages(cp, config, channel_values) return HydratedThread(messages, False, mr_to_payload(channel_values.get("merge_request"))) diff --git a/pyproject.toml b/pyproject.toml index 1f06f0479..f9644ef5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,12 @@ dependencies = [ "copilotkit==0.1.94", "croniter==6.2.4", "ddgs==9.14.4", - "deepagents==0.5.9", + "deepagents==0.6.12", "django==6.0.7", "django-allauth[socialaccount]==65.18.0", "django-crontask==2.0.0", "django-extensions==4.1.0", - "django-filter==25.2", + "django-filter==26.1", "django-ninja==1.6.2", "django-oauth-toolkit==3.3.0", "django-split-settings==1.3.2", @@ -36,15 +36,15 @@ dependencies = [ "django-tasks-db==0.12.0", "genai-prices==0.0.71", "get-docker-secret==2.0.0", - "gitpython==3.1.50", + "gitpython==3.1.52", "httpx==0.28.1", "ipython==9.15.0", "jinja2==3.1.6", - "langchain[anthropic,community,google-genai,openai]==1.2.18", + "langchain[anthropic,community,google-genai,openai]==1.3.14", "langchain-mcp-adapters==0.3.0", - "langgraph==1.1.10", - "langgraph-checkpoint-redis==0.5.0", - "langsmith[pytest]==0.10.2", + "langgraph==1.2.9", + "langgraph-checkpoint-redis==0.5.1", + "langsmith[pytest]==0.10.5", "langsmith-fetch==0.3.1", "markdown==3.10.2", "markdownify==1.2.3", @@ -60,8 +60,8 @@ dependencies = [ "python-gitlab==8.4.0", "pyyaml==6.0.3", "rank-bm25==0.2.2", - "redis==7.4.0", - "sentry-sdk==2.64.0", + "redis==7.4.1", + "sentry-sdk==2.66.0", "unidiff==0.7.5", "uvicorn[standard]==0.51.0", "whitenoise==6.12.0", @@ -72,12 +72,12 @@ urls.source = "https://github.com/srtab/daiv" [dependency-groups] dev = [ - "coverage==7.15.0", + "coverage==7.15.2", "datasets==5.0.0", "djade==1.9.0", "openevals==0.2.0", - "prek==0.4.8", - "pyproject-fmt==2.25.2", + "prek==0.4.10", + "pyproject-fmt==2.25.3", "pytest==9.1.1", "pytest-asyncio==1.4.0", "pytest-cov==7.1.0", @@ -87,8 +87,8 @@ dev = [ "pytest-mock==3.15.1", "pytest-xdist==3.8.0", "python-dotenv==1.2.2", - "ruff==0.15.21", - "ty==0.0.58", + "ruff==0.15.22", + "ty==0.0.60", "types-pyyaml==6.0.12.20260518", "watchdog==6.0.0", ] diff --git a/tests/unit_tests/accounts/test_dashboard_console.py b/tests/unit_tests/accounts/test_dashboard_console.py index a789e03b1..6bf795f4e 100644 --- a/tests/unit_tests/accounts/test_dashboard_console.py +++ b/tests/unit_tests/accounts/test_dashboard_console.py @@ -6,18 +6,45 @@ job-creation launcher in the console content. """ +from datetime import timedelta from pathlib import Path from django.template.loader import get_template +from django.test import Client from django.urls import reverse +from django.utils import timezone import pytest +from accounts.models import Role +from codebase.models import MergeMetric, PlatformType + # Repo root: tests/unit_tests/accounts/ -> parents[3]. REPO_ROOT = Path(__file__).resolve().parents[3] INPUT_CSS = REPO_ROOT / "daiv" / "static_src" / "css" / "input.css" +def _make_merge_metric(*, iid=1, daiv_commits=1, total_commits=2, lines_added=10, lines_removed=3): + """Create a ``MergeMetric`` row directly (no factory exists). + + Fills the required non-default fields (``merged_at``/``target_branch``/``source_branch``/ + ``platform``); ``merged_at`` is *now* so the row survives the default "today" period filter. + Distinct ``iid`` values keep the ``(repo_id, merge_request_iid, platform)`` uniqueness happy. + """ + return MergeMetric.objects.create( + repo_id="daiv/test", + merge_request_iid=iid, + lines_added=lines_added, + lines_removed=lines_removed, + total_commits=total_commits, + daiv_commits=daiv_commits, + merged_at=timezone.now(), + target_branch="main", + source_branch="feature/x", + platform=PlatformType.GITLAB, + ) + + @pytest.mark.django_db class TestConsoleRegions: def test_three_region_containers_render(self, member_client): @@ -176,3 +203,195 @@ def test_translated_label_renders(self, member_client): # Test settings render en (canonical), so the source string appears verbatim. response = member_client.get(reverse("dashboard")) assert b"Needs-me queue" in response.content + + +# --------------------------------------------------------------------------- +# Story 2.2 — Personal-by-default with the admin Manager Lens +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +class TestPersonalByDefault: + """AC1: the default ``/dashboard/`` carries NO org/aggregate content for anyone.""" + + def test_default_has_no_org_content_for_member(self, member_client): + response = member_client.get(reverse("dashboard")) + assert response.status_code == 200 + assert "velocity" not in response.context + assert "total_users" not in response.context + assert b'data-testid="manager-lens"' not in response.content + assert b"Code Velocity" not in response.content + + def test_default_has_no_org_content_for_admin_even_with_merges(self, admin_client): + # Even with org merges present, the personal default surfaces none of it (admins too). + _make_merge_metric() + response = admin_client.get(reverse("dashboard")) + assert response.status_code == 200 + assert "velocity" not in response.context + assert "total_users" not in response.context + # The relocated velocity/attribution markup must not leak onto the default. + assert b'data-testid="manager-lens"' not in response.content + assert b"Code Velocity" not in response.content + assert b"DAIV attribution" not in response.content + + +@pytest.mark.django_db +class TestManagerLensToggle: + """AC2: toggle present for admins, absent (not shown-then-blocked) for non-admins.""" + + def test_toggle_present_for_admin(self, admin_client): + response = admin_client.get(reverse("dashboard")) + assert b'data-testid="manager-lens-toggle"' in response.content + + def test_toggle_absent_for_member(self, member_client): + response = member_client.get(reverse("dashboard")) + assert b'data-testid="manager-lens-toggle"' not in response.content + + def test_toggle_present_on_lens_page_for_admin(self, admin_client): + response = admin_client.get(reverse("manager_lens")) + assert b'data-testid="manager-lens-toggle"' in response.content + + def test_toggle_wires_both_console_routes(self, admin_client): + response = admin_client.get(reverse("dashboard")) + # The segmented control HTMX-swaps between the two console routes with a plain-link fallback. + assert reverse("manager_lens").encode() in response.content + assert b'hx-target="#console-main"' in response.content + assert b'hx-push-url="true"' in response.content + + +@pytest.mark.django_db +class TestManagerLensContent: + """AC3: the Lens shows the relocated org velocity + DAIV-attribution.""" + + def test_admin_sees_relocated_velocity(self, admin_client): + _make_merge_metric(daiv_commits=1, total_commits=2, lines_added=42, lines_removed=7) + response = admin_client.get(reverse("manager_lens"), {"period": "all"}) + assert response.status_code == 200 + assert response.context["velocity"] is not None + assert response.context["velocity"]["total_merges"] == 1 + assert response.context["total_users"] >= 1 + assert b'data-testid="manager-lens"' in response.content + assert b'data-testid="lens-total-merges"' in response.content + assert b"Code Velocity" in response.content + assert b"DAIV attribution" in response.content + # Data present -> no cold-load skeleton. + assert b'data-testid="manager-lens-skeleton"' not in response.content + + def test_default_shows_cumulative_velocity_not_scoped_to_today(self, admin_client): + # Regression guard: the Lens defaults to the cumulative window, so an org whose only + # merges are older than "today" still shows real velocity — never the cold-load skeleton. + old = _make_merge_metric(iid=99) + MergeMetric.objects.filter(pk=old.pk).update(merged_at=timezone.now() - timedelta(days=40)) + response = admin_client.get(reverse("manager_lens")) # no ?period= -> cumulative default + assert response.status_code == 200 + assert response.context["velocity"] is not None + assert response.context["velocity"]["total_merges"] == 1 + assert b'data-testid="manager-lens-skeleton"' not in response.content + + def test_velocity_uses_shared_module_function(self, admin_client): + # The Lens reads through the extracted module-level ``get_velocity_data`` (AD-10: MergeMetric). + _make_merge_metric(iid=1, daiv_commits=2, total_commits=2) + _make_merge_metric(iid=2, daiv_commits=0, total_commits=3) + response = admin_client.get(reverse("manager_lens"), {"period": "all"}) + velocity = response.context["velocity"] + assert velocity["total_merges"] == 2 + assert velocity["daiv_merges"] == 1 # only the row with daiv_commits > 0 + + +@pytest.mark.django_db +class TestManagerLensColdLoad: + """AC5: zero MergeMetric rows -> cold-load skeleton, never a zero dressed as a fact.""" + + def test_cold_load_shows_skeleton_not_zero(self, admin_client): + assert MergeMetric.objects.count() == 0 + response = admin_client.get(reverse("manager_lens")) + assert response.status_code == 200 + assert response.context["velocity"] is None + assert b'data-testid="manager-lens-skeleton"' in response.content + # The block is NOT hidden, and no zero reading is rendered as a fact. + assert b'data-testid="manager-lens"' in response.content + assert b'data-testid="lens-total-merges"' not in response.content + assert b"merges into default branches" not in response.content + + +@pytest.mark.django_db +class TestManagerLensAccessDenial: + """AC6: a non-admin hitting the route directly is denied SERVER-SIDE (403).""" + + def test_member_gets_403(self, member_client): + response = member_client.get(reverse("manager_lens")) + assert response.status_code == 403 + + def test_member_gets_403_even_over_htmx(self, member_client): + response = member_client.get(reverse("manager_lens"), HTTP_HX_REQUEST="true") + assert response.status_code == 403 + + def test_anonymous_redirected_to_login(self): + response = Client().get(reverse("manager_lens")) + assert response.status_code == 302 + assert "/accounts/login/" in response.url + + +@pytest.mark.django_db +class TestManagerLensNonPersistence: + """AC4: using the toggle persists nothing; the next visit lands personal.""" + + def test_lens_visit_writes_no_state_and_default_stays_personal(self, admin_client, admin_user): + keys_before = set(admin_client.session.keys()) + + first = admin_client.get(reverse("manager_lens")) + assert first.status_code == 200 + + # No app-level state was persisted: only Django's own framework keys (``_csrftoken`` etc., + # all underscore-prefixed) may appear — no surface/lens preference of ANY name is written. + new_keys = set(admin_client.session.keys()) - keys_before + app_keys = {k for k in new_keys if not k.startswith("_")} + assert not app_keys, f"Manager Lens visit unexpectedly wrote app session keys: {app_keys}" + + # Returning to the console lands on the PERSONAL view — no org content flips the default. + second = admin_client.get(reverse("dashboard")) + assert second.status_code == 200 + assert "velocity" not in second.context + assert b'data-testid="manager-lens"' not in second.content + + # No user field was flipped to remember the surface. + admin_user.refresh_from_db() + assert admin_user.role == Role.ADMIN + + +@pytest.mark.django_db +class TestManagerLensHtmxFragment: + """The partial-on-HTMX idiom: HX-Request -> fragment only (no base_app chrome).""" + + def test_htmx_returns_fragment_only(self, admin_client): + _make_merge_metric() + response = admin_client.get(reverse("manager_lens"), {"period": "all"}, HTTP_HX_REQUEST="true") + assert response.status_code == 200 + # Fragment carries the Lens body ... + assert b'data-testid="manager-lens"' in response.content + # ... and none of the shell chrome. + assert b'data-testid="app-sidebar"' not in response.content + assert b'data-testid="app-user-menu"' not in response.content + assert b" 0``): its matches are kept and the truncation is + recorded, unlike a first-chunk binary skip.""" + import logging + + # ~35 KB of valid matching lines (well past TextIOWrapper's ~8 KB read chunk) then bad bytes, + # so decoding only fails after many lines have already been scanned and matched. + (tmp_path / "truncated.py").write_bytes(b"needle\n" * 5000 + b"\xff\xfe still not utf-8\n") + backend = DAIVFilesystemBackend(root_dir=tmp_path, virtual_mode=True) + + with caplog.at_level(logging.WARNING, logger="daiv.tools"): + result = await backend.agrep("needle") + + # Matches from the decodable prefix survive; the partial read is flagged (returned clean + # because matches exist, and logged for operators). + assert result.error is None + assert result.matches and any(m["path"] == "/truncated.py" for m in result.matches) + assert "could not fully search" in caplog.text + assert "UnicodeDecodeError" in caplog.text + + def test_grep_error_detail_sanitizes_and_respects_virtual_mode(self, tmp_path: Path): + """Direct contract for the sanitizer: never leak the on-disk path, and only include generic + exception text (which can carry ``root_dir``) when NOT in virtual mode.""" + virt = DAIVFilesystemBackend(root_dir=tmp_path, virtual_mode=True) + disk = DAIVFilesystemBackend(root_dir=tmp_path, virtual_mode=False) + + # OSError: only strerror, never the filename ``str(exc)`` would append. + oserr = PermissionError(13, "Permission denied", str(tmp_path / "secret")) + assert virt._grep_error_detail(oserr) == "PermissionError: Permission denied" + assert str(tmp_path) not in virt._grep_error_detail(oserr) + + # UnicodeDecodeError: its path-free ``reason``. + ude = UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid start byte") + assert virt._grep_error_detail(ude) == "UnicodeDecodeError: invalid start byte" + + # Generic exception: message suppressed under virtual_mode (it may carry the real root), + # included otherwise. + generic = RuntimeError(f"boom at {tmp_path}") + assert virt._grep_error_detail(generic) == "RuntimeError" + assert disk._grep_error_detail(generic) == f"RuntimeError: boom at {tmp_path}" + class TestDAIVCompositeBackend: """Composite routing must preserve the prefix-stripping invariant for DAIV's two diff --git a/tests/unit_tests/automation/agent/middlewares/test_prompt_cache.py b/tests/unit_tests/automation/agent/middlewares/test_prompt_cache.py index 31cf9a72b..edbe17e30 100644 --- a/tests/unit_tests/automation/agent/middlewares/test_prompt_cache.py +++ b/tests/unit_tests/automation/agent/middlewares/test_prompt_cache.py @@ -2,10 +2,31 @@ from langchain.agents.middleware import ModelRequest, ModelResponse from langchain_core.messages import HumanMessage +from langchain_openai import ChatOpenAI +from automation.agent.chat_models import ChatOpenRouter from automation.agent.middlewares.prompt_cache import AnthropicPromptCachingMiddleware +class TestIsOpenRouterAnthropicModel: + def test_detects_chat_openrouter_anthropic_model(self): + middleware = AnthropicPromptCachingMiddleware() + model = ChatOpenRouter(model="anthropic/claude-haiku-4.5", api_key="x") + assert middleware._is_openrouter_anthropic_model(model) is True + + def test_ignores_chat_openrouter_non_anthropic_model(self): + middleware = AnthropicPromptCachingMiddleware() + model = ChatOpenRouter(model="openai/gpt-5.4", api_key="x") + assert middleware._is_openrouter_anthropic_model(model) is False + + def test_ignores_plain_chat_openai_even_with_anthropic_name(self): + # A vanilla ChatOpenAI can't serve Anthropic caching; only our OpenRouter + # subclass (which forwards extra_body cache_control) should qualify. + middleware = AnthropicPromptCachingMiddleware() + model = ChatOpenAI(model="anthropic/claude-haiku-4.5", api_key="x") + assert middleware._is_openrouter_anthropic_model(model) is False + + class TestAnthropicPromptCachingMiddleware: def test_should_apply_caching_counts_system_prompt_for_openrouter_models(self): middleware = AnthropicPromptCachingMiddleware(min_messages_to_cache=2) diff --git a/tests/unit_tests/automation/agent/test_base.py b/tests/unit_tests/automation/agent/test_base.py index eef662a8a..b7b4760fc 100644 --- a/tests/unit_tests/automation/agent/test_base.py +++ b/tests/unit_tests/automation/agent/test_base.py @@ -6,6 +6,7 @@ from langchain_core.runnables import Runnable from automation.agent.base import BaseAgent, ResolvedProvider, parse_model_spec +from automation.agent.chat_models import OPENROUTER_BASE_URL, ChatOpenRouter from core.models import Provider, ProviderType, ThinkingLevelChoices if TYPE_CHECKING: @@ -273,6 +274,17 @@ def capture_and_raise(**kwargs): assert sync_client.is_closed del orig_init + def test_get_model_openrouter_returns_chat_openrouter(self): + self._enable_seed("openrouter", "sk-or") + model = BaseAgent.get_model(model="openrouter:anthropic/claude-sonnet-4.6") + assert isinstance(model, ChatOpenRouter) + assert model.openai_api_base == OPENROUTER_BASE_URL + + def test_get_model_non_openrouter_is_not_chat_openrouter(self): + self._enable_seed("anthropic", "sk-a") + model = BaseAgent.get_model(model="anthropic:claude-sonnet-4-6") + assert not isinstance(model, ChatOpenRouter) + def test_openrouter_anthropic_thinking(self): self._enable_seed("openrouter", "sk-or") kw = BaseAgent.get_model_kwargs( diff --git a/tests/unit_tests/automation/agent/test_chat_models.py b/tests/unit_tests/automation/agent/test_chat_models.py new file mode 100644 index 000000000..a87e9b490 --- /dev/null +++ b/tests/unit_tests/automation/agent/test_chat_models.py @@ -0,0 +1,77 @@ +"""Tests for the OpenRouter chat model subclass. + +Covers only DAIV's custom behavior (per project convention): reasoning +extraction, the ``is_anthropic`` family flag, and the OpenRouter base-URL +default. The upstream ChatOpenAI streaming machinery is not re-tested. +""" + +from langchain_core.messages import AIMessageChunk +from langchain_openai import ChatOpenAI + +from automation.agent.chat_models import OPENROUTER_BASE_URL, ChatOpenRouter + + +def _chunk(delta: dict) -> dict: + """A raw Chat-Completions stream chunk dict, as the OpenAI SDK hands it to + ``_convert_chunk_to_generation_chunk`` (model_dump of a ChatCompletionChunk).""" + return { + "id": "c", + "model": "anthropic/claude-haiku-4.5", + "choices": [{"index": 0, "delta": delta, "finish_reason": None}], + } + + +class TestChatOpenRouterReasoning: + def test_extracts_streaming_reasoning_into_reasoning_content(self): + model = ChatOpenRouter(model="anthropic/claude-haiku-4.5", api_key="x") + + gen = model._convert_chunk_to_generation_chunk( + _chunk({"content": "", "reasoning": "Let me think about 17*23."}), AIMessageChunk, {} + ) + + assert gen is not None + assert gen.message.additional_kwargs["reasoning_content"] == "Let me think about 17*23." + + def test_content_only_chunk_has_no_reasoning_content(self): + model = ChatOpenRouter(model="anthropic/claude-haiku-4.5", api_key="x") + + gen = model._convert_chunk_to_generation_chunk(_chunk({"content": "391"}), AIMessageChunk, {}) + + assert gen is not None + assert "reasoning_content" not in gen.message.additional_kwargs + + def test_reasoning_content_merges_across_chunks(self): + """AIMessageChunk addition concatenates string additional_kwargs, so the + per-chunk reasoning deltas accumulate into the final message.""" + model = ChatOpenRouter(model="anthropic/claude-haiku-4.5", api_key="x") + + g1 = model._convert_chunk_to_generation_chunk(_chunk({"reasoning": "Break it: "}), AIMessageChunk, {}) + g2 = model._convert_chunk_to_generation_chunk(_chunk({"reasoning": "17*(20+3)."}), AIMessageChunk, {}) + + merged = g1.message + g2.message + assert merged.additional_kwargs["reasoning_content"] == "Break it: 17*(20+3)." + + +class TestChatOpenRouterFamily: + def test_is_anthropic_true_for_anthropic_model(self): + assert ChatOpenRouter(model="anthropic/claude-haiku-4.5", api_key="x").is_anthropic is True + + def test_is_anthropic_false_for_non_anthropic_model(self): + assert ChatOpenRouter(model="openai/gpt-5.4", api_key="x").is_anthropic is False + + +class TestChatOpenRouterDefaults: + def test_defaults_base_url_to_openrouter(self): + model = ChatOpenRouter(model="anthropic/claude-haiku-4.5", api_key="x") + assert model.openai_api_base == OPENROUTER_BASE_URL + + def test_explicit_base_url_is_respected(self): + model = ChatOpenRouter( + model="anthropic/claude-haiku-4.5", api_key="x", openai_api_base="https://proxy.example/v1" + ) + assert model.openai_api_base == "https://proxy.example/v1" + + def test_is_a_chat_openai_subclass(self): + # Load-bearing: AnthropicPromptCachingMiddleware detects OpenRouter-Anthropic + # models via isinstance(model, ChatOpenAI). + assert issubclass(ChatOpenRouter, ChatOpenAI) diff --git a/tests/unit_tests/core/test_checkpointer.py b/tests/unit_tests/core/test_checkpointer.py index 4e5532ff8..9fdaf7be6 100644 --- a/tests/unit_tests/core/test_checkpointer.py +++ b/tests/unit_tests/core/test_checkpointer.py @@ -3,8 +3,10 @@ The agent stores a :class:`~codebase.base.MergeRequest` in checkpointed state (``GitState.merge_request``). ``DAIVRedisSerializer`` encodes plain pydantic models via ``model_dump(mode="json")`` (cleaner nested-model handling than the stock -``__dict__`` path) and -- critically -- reconstructs ``set`` values that the stock -serializer's reviver silently nulls (``loaded_tool_names``). +``__dict__`` path). ``set`` values (``loaded_tool_names``) used to be nulled by the +stock reviver and needed a bespoke override; ``langgraph-checkpoint-redis`` 0.5.1 fixed +that upstream, so the set tests below now guard the *stock* round-trip against a +downgrade or regression. """ from __future__ import annotations @@ -119,22 +121,28 @@ def test_objects_with_to_json_are_not_intercepted(): assert encoded.get("id", [None])[0] != "codebase.base" -def test_stock_redis_serializer_loses_sets_on_round_trip(): - """Regression guard: reproduce the production set→None corruption with the stock serializer. +def test_stock_redis_serializer_round_trips_sets(): + """Regression guard: the stock serializer used to corrupt sets to ``None``. - The adapter encodes a set via ``kwargs["__set_items__"]``, but the base reviver routes - ``builtins.set`` through ``_revive_lc2`` (``set(**kwargs)`` → ``TypeError``), which - ``langgraph-checkpoint>=4.1.1`` swallows by returning ``None`` -- silently nulling the set. + Historically the adapter encoded a set under a ``kwargs["__set_items__"]`` envelope, and + the base reviver routed ``builtins.set`` through ``_revive_lc2`` (``set(**kwargs)`` → + ``TypeError``), which ``langgraph-checkpoint>=4.1.1`` swallowed by returning ``None`` -- + silently nulling the set. ``langgraph-checkpoint-redis`` 0.5.1 fixed this: the stock + serializer now encodes sets via the ``args`` constructor envelope and ``_revive_if_needed`` + reconstructs them (``_reconstruct_set_constructor`` still accepts the legacy + ``__set_items__`` form too), so a round-trip is lossless. The guard is kept (asserting the + fixed behaviour) so a downgrade or upstream regression is caught here rather than silently + corrupting production checkpoints. """ stock = JsonPlusRedisSerializer(allowed_json_modules=[("codebase.base", "MergeRequest")]) restored = stock.loads_typed(stock.dumps_typed({"loaded_tool_names": {"Read", "Edit"}})) - assert restored == {"loaded_tool_names": None} # corrupted + assert restored == {"loaded_tool_names": {"Read", "Edit"}} def test_set_round_trips_through_serde_contract(): - """Through the public serde API the saver calls; our ``_reviver`` reconstructs the set.""" + """Through the public serde API the saver calls; the stock reviver reconstructs the set (0.5.1).""" serde = DAIVRedisSerializer() original = {"loaded_tool_names": {"Read", "Edit", "Grep"}} @@ -206,3 +214,85 @@ def test_dataclass_nested_alongside_set_and_model_round_trips(merge_request): assert restored["loaded_tool_names"] == {"Read", "Edit"} assert isinstance(restored["loaded_tool_names"], set) assert restored["merge_request"] == merge_request + + +# --------------------------------------------------------------------------- +# aresolve_thread_messages: DeltaChannel-aware messages read +# +# deepagents >= 0.6 stores ``messages`` in a langgraph ``DeltaChannel`` whose value is +# usually ABSENT from ``channel_values`` (present only on periodic snapshot steps). The +# helper reconstructs the accumulated list from the delta write history so the transcript +# survives a reload. These tests pin the three resolution branches. +# --------------------------------------------------------------------------- + + +def _history_saver(history): + """A saver stub whose ``aget_delta_channel_history`` returns ``history``.""" + from unittest.mock import AsyncMock, MagicMock + + saver = MagicMock() + saver.aget_delta_channel_history = AsyncMock(return_value=history) + return saver + + +async def test_resolve_messages_returns_inline_list_without_history_walk(): + """deepagents < 0.6 (plain add_messages) keeps the full list inline; return as-is and + never touch the (expensive, beta) delta-history contract.""" + from core.checkpointer import aresolve_thread_messages + + msgs = [HumanMessage(content="hi", id="h1")] + saver = _history_saver({}) + + result = await aresolve_thread_messages(saver, {"configurable": {"thread_id": "t"}}, {"messages": msgs}) + + assert result is msgs + saver.aget_delta_channel_history.assert_not_awaited() + + +async def test_resolve_messages_reconstructs_from_write_history_when_absent(): + """The core bug: ``messages`` absent from ``channel_values`` (DeltaChannel non-snapshot + step) must be rebuilt by folding the ancestor writes, not read as empty.""" + from langchain_core.messages import AIMessage + + from core.checkpointer import aresolve_thread_messages + + writes = [ + ("task-0", "messages", [HumanMessage(content="q", id="h1")]), + ("task-1", "messages", [AIMessage(content="a", id="a1")]), + ] + saver = _history_saver({"messages": {"writes": writes}}) + + # channel_values has other channels but NO messages key -> reconstruction path. + result = await aresolve_thread_messages(saver, {"configurable": {"thread_id": "t"}}, {"session_id": "x"}) + + assert [m.id for m in result] == ["h1", "a1"] + assert [m.content for m in result] == ["q", "a"] + + +async def test_resolve_messages_folds_seed_plus_writes(): + """A snapshot ancestor supplies the ``seed``; later writes fold on top (and id-dedup + replaces an in-place update rather than duplicating).""" + from langchain_core.messages import AIMessage + + from core.checkpointer import aresolve_thread_messages + + seed = [HumanMessage(content="q", id="h1"), AIMessage(content="partial", id="a1")] + writes = [("task-2", "messages", [AIMessage(content="final", id="a1")])] + saver = _history_saver({"messages": {"seed": seed, "writes": writes}}) + + result = await aresolve_thread_messages(saver, {"configurable": {"thread_id": "t"}}, {}) + + assert [m.id for m in result] == ["h1", "a1"] + assert result[1].content == "final" # id-dedup replaced the partial in place + + +async def test_resolve_messages_empty_history_returns_empty_list(): + """No seed and no writes (nothing recoverable) -> empty list, so callers still flag the + genuinely-empty case rather than crashing.""" + from core.checkpointer import aresolve_thread_messages + + saver = _history_saver({"messages": {"writes": []}}) + + result = await aresolve_thread_messages(saver, {"configurable": {"thread_id": "t"}}, {}) + + assert result == [] diff --git a/tests/unit_tests/memory/test_extraction_task.py b/tests/unit_tests/memory/test_extraction_task.py index 1fad71fb2..f263b7a5a 100644 --- a/tests/unit_tests/memory/test_extraction_task.py +++ b/tests/unit_tests/memory/test_extraction_task.py @@ -42,6 +42,23 @@ async def _open(): return _open +def _checkpointer_with_delta(writes): + """A checkpointer whose ``messages`` is a DeltaChannel: absent from ``channel_values``, + recoverable only via ``aget_delta_channel_history``.""" + tup = MagicMock() + tup.checkpoint = {"channel_values": {"session_id": "x"}} # no messages key + + cp = MagicMock() + cp.aget_tuple = AsyncMock(return_value=tup) + cp.aget_delta_channel_history = AsyncMock(return_value={"messages": {"writes": writes}}) + + @asynccontextmanager + async def _open(): + yield cp + + return _open + + def _structured_llm_returning(observations=None, *, error=None): llm = MagicMock() if error is not None: @@ -90,6 +107,30 @@ async def test_extraction_creates_observation_rows(): assert {row.category for row in rows} == {"build_test", "pitfall"} +@pytest.mark.django_db(transaction=True) +async def test_extraction_reconstructs_deltachannel_messages(): + """deepagents' DeltaChannel keeps ``messages`` out of ``channel_values``; extraction + must reconstruct the transcript from the delta write history, not skip as empty.""" + run = await _create_run() + writes = [ + ("t0", "messages", [HumanMessage(content="fix the bug", id="h1")]), + ("t1", "messages", [AIMessage(content="done, ran make test", id="a1")]), + ] + extracted = [ExtractedObservation(category="build_test", content="`make test` sets LANGCHAIN_TRACING_V2=false")] + + with ( + patch("memory.tasks.RepositoryConfig") as cfg, + patch("memory.tasks.open_checkpointer", _checkpointer_with_delta(writes)), + patch("memory.tasks._build_structured_llm", return_value=_structured_llm_returning(extracted)), + ): + cfg.get_config.return_value = _enabled_config() + await extract_observations_task.func(str(run.pk)) + + rows = [obs async for obs in MemoryObservation.objects.filter(repo_id="group/project")] + assert len(rows) == 1 + assert rows[0].category == "build_test" + + @pytest.mark.django_db(transaction=True) async def test_extraction_skips_when_checkpoint_expired(): run = await _create_run() diff --git a/tests/unit_tests/sessions/test_views_detail.py b/tests/unit_tests/sessions/test_views_detail.py index a583483c8..730ed9d2f 100644 --- a/tests/unit_tests/sessions/test_views_detail.py +++ b/tests/unit_tests/sessions/test_views_detail.py @@ -139,6 +139,40 @@ def test_detail_with_live_checkpoint_renders_transcript(member_client, member_us assert turns[0]["segments"] == [{"type": "text", "content": "hello from agent"}] +@pytest.mark.django_db +def test_detail_reconstructs_deltachannel_transcript(member_client, member_user): + """Regression: deepagents' ``DeltaChannel`` leaves ``messages`` out of + ``channel_values`` on non-snapshot steps, so a raw read yields an empty transcript on + reload. The detail view must reconstruct it from the delta write history instead.""" + from langchain_core.messages import AIMessage, HumanMessage + + session = _create_session(user=member_user) + # A live checkpoint whose channel_values has NO messages key (DeltaChannel non-snapshot). + tup = MagicMock(checkpoint={"channel_values": {"session_id": "x"}}) + writes = [ + ("t0", "messages", [HumanMessage(content="count todos", id="h1")]), + ("t1", "messages", [AIMessage(content="there are 18", id="a1")]), + ] + + with ( + patch("sessions.hydration.open_checkpointer") as cp_ctx, + patch("sessions.views.aget_existing_mr_payload", AsyncMock(return_value=None)), + ): + saver = MagicMock() + saver.aget_tuple = AsyncMock(return_value=tup) + saver.aget_delta_channel_history = AsyncMock(return_value={"messages": {"writes": writes}}) + cp_ctx.return_value.__aenter__ = AsyncMock(return_value=saver) + cp_ctx.return_value.__aexit__ = AsyncMock(return_value=None) + resp = member_client.get(reverse("session_detail", kwargs={"thread_id": session.thread_id})) + + assert resp.status_code == 200 + assert resp.context["expired"] is False + turns = resp.context["turns"] + assert [t["role"] for t in turns] == ["user", "assistant"] + assert turns[0]["segments"] == [{"type": "text", "content": "count todos"}] + assert turns[1]["segments"] == [{"type": "text", "content": "there are 18"}] + + @pytest.mark.django_db def test_detail_with_missing_checkpoint_flags_expired(member_client, member_user): session = _create_session(user=member_user) diff --git a/uv.lock b/uv.lock index 1ab65aa82..837d27dad 100644 --- a/uv.lock +++ b/uv.lock @@ -425,41 +425,41 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, - { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, - { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, - { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, - { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, - { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, - { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, - { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, - { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, - { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, - { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, - { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, - { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, - { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, - { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, - { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, ] [[package]] @@ -613,12 +613,12 @@ requires-dist = [ { name = "copilotkit", specifier = "==0.1.94" }, { name = "croniter", specifier = "==6.2.4" }, { name = "ddgs", specifier = "==9.14.4" }, - { name = "deepagents", specifier = "==0.5.9" }, + { name = "deepagents", specifier = "==0.6.12" }, { name = "django", specifier = "==6.0.7" }, { name = "django-allauth", extras = ["socialaccount"], specifier = "==65.18.0" }, { name = "django-crontask", specifier = "==2.0.0" }, { name = "django-extensions", specifier = "==4.1.0" }, - { name = "django-filter", specifier = "==25.2" }, + { name = "django-filter", specifier = "==26.1" }, { name = "django-ninja", specifier = "==1.6.2" }, { name = "django-oauth-toolkit", specifier = "==3.3.0" }, { name = "django-split-settings", specifier = "==1.3.2" }, @@ -626,15 +626,15 @@ requires-dist = [ { name = "django-tasks-db", specifier = "==0.12.0" }, { name = "genai-prices", specifier = "==0.0.71" }, { name = "get-docker-secret", specifier = "==2.0.0" }, - { name = "gitpython", specifier = "==3.1.50" }, + { name = "gitpython", specifier = "==3.1.52" }, { name = "httpx", specifier = "==0.28.1" }, { name = "ipython", specifier = "==9.15.0" }, { name = "jinja2", specifier = "==3.1.6" }, - { name = "langchain", extras = ["anthropic", "community", "google-genai", "openai"], specifier = "==1.2.18" }, + { name = "langchain", extras = ["anthropic", "community", "google-genai", "openai"], specifier = "==1.3.14" }, { name = "langchain-mcp-adapters", specifier = "==0.3.0" }, - { name = "langgraph", specifier = "==1.1.10" }, - { name = "langgraph-checkpoint-redis", specifier = "==0.5.0" }, - { name = "langsmith", extras = ["pytest"], specifier = "==0.10.2" }, + { name = "langgraph", specifier = "==1.2.9" }, + { name = "langgraph-checkpoint-redis", specifier = "==0.5.1" }, + { name = "langsmith", extras = ["pytest"], specifier = "==0.10.5" }, { name = "langsmith-fetch", specifier = "==0.3.1" }, { name = "markdown", specifier = "==3.10.2" }, { name = "markdownify", specifier = "==1.2.3" }, @@ -650,8 +650,8 @@ requires-dist = [ { name = "python-gitlab", specifier = "==8.4.0" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "rank-bm25", specifier = "==0.2.2" }, - { name = "redis", specifier = "==7.4.0" }, - { name = "sentry-sdk", specifier = "==2.64.0" }, + { name = "redis", specifier = "==7.4.1" }, + { name = "sentry-sdk", specifier = "==2.66.0" }, { name = "unidiff", specifier = "==0.7.5" }, { name = "uvicorn", extras = ["standard"], specifier = "==0.51.0" }, { name = "whitenoise", specifier = "==6.12.0" }, @@ -659,12 +659,12 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "coverage", specifier = "==7.15.0" }, + { name = "coverage", specifier = "==7.15.2" }, { name = "datasets", specifier = "==5.0.0" }, { name = "djade", specifier = "==1.9.0" }, { name = "openevals", specifier = "==0.2.0" }, - { name = "prek", specifier = "==0.4.8" }, - { name = "pyproject-fmt", specifier = "==2.25.2" }, + { name = "prek", specifier = "==0.4.10" }, + { name = "pyproject-fmt", specifier = "==2.25.3" }, { name = "pytest", specifier = "==9.1.1" }, { name = "pytest-asyncio", specifier = "==1.4.0" }, { name = "pytest-cov", specifier = "==7.1.0" }, @@ -674,8 +674,8 @@ dev = [ { name = "pytest-mock", specifier = "==3.15.1" }, { name = "pytest-xdist", specifier = "==3.8.0" }, { name = "python-dotenv", specifier = "==1.2.2" }, - { name = "ruff", specifier = "==0.15.21" }, - { name = "ty", specifier = "==0.0.58" }, + { name = "ruff", specifier = "==0.15.22" }, + { name = "ty", specifier = "==0.0.60" }, { name = "types-pyyaml", specifier = "==6.0.12.20260518" }, { name = "watchdog", specifier = "==6.0.0" }, ] @@ -739,7 +739,7 @@ wheels = [ [[package]] name = "deepagents" -version = "0.5.9" +version = "0.6.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, @@ -749,9 +749,9 @@ dependencies = [ { name = "langsmith" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/74/776e606f0508a4d7d4c9d061c797dcde43eed2452fea546ae29047aeaaa6/deepagents-0.5.9.tar.gz", hash = "sha256:74fe0f998641b20bda8adac662a018051c623a3c8e5ed4b6ff9ad53fc493a783", size = 165651, upload-time = "2026-05-10T22:31:17.095Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/db/a6acdc72a9e90c3f07ed10de35c951734a02d4facb693bb59684ad368801/deepagents-0.6.12.tar.gz", hash = "sha256:1f281c0bc5a63132f62e2ee345c1dc593b23188da6e23016401f6879fbe54b5f", size = 211364, upload-time = "2026-06-25T17:26:52.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/f6/9698f98da72dfa8dc8e1d0f0542f2407a40a50cfdccf522fdb5cb43e39e2/deepagents-0.5.9-py3-none-any.whl", hash = "sha256:ce24a41763b2793bd21217411e9fb9f187a9128da6789f5a81654da1de9e4c7c", size = 188118, upload-time = "2026-05-10T22:31:15.974Z" }, + { url = "https://files.pythonhosted.org/packages/98/49/af7219b3c13520fee047bb807cfaefba17f8e4584c551d946773589a4f08/deepagents-0.6.12-py3-none-any.whl", hash = "sha256:28b8fa0119ca0a689e3e18e288c4634e4046062acfc87a1cb34289d3af3a1c88", size = 236120, upload-time = "2026-06-25T17:26:51.736Z" }, ] [[package]] @@ -850,14 +850,14 @@ wheels = [ [[package]] name = "django-filter" -version = "25.2" +version = "26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/563965173d4cbb5fc308087e7b3d11a115b7b67273d093622480b1e31f78/django_filter-26.1.tar.gz", hash = "sha256:66ea04031b068c77c86e1ac26ced7a3f8f13ce797f5795751707e3deefc58054", size = 144299, upload-time = "2026-07-11T09:27:02.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" }, ] [[package]] @@ -1109,14 +1109,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.50" +version = "3.1.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/fd/df0bafa4eb5ea2f51e1adee9f7a94c8e62c5d180e65117045dfca3439c8a/gitpython-3.1.52.tar.gz", hash = "sha256:de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e", size = 223726, upload-time = "2026-07-16T03:15:59.599Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, + { url = "https://files.pythonhosted.org/packages/8d/90/04dff7c1e176bb1c3011ef1647393d368790da710d8dde1cdcfad301f45a/gitpython-3.1.52-py3-none-any.whl", hash = "sha256:79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a", size = 215366, upload-time = "2026-07-16T03:15:58.239Z" }, ] [[package]] @@ -1547,16 +1547,16 @@ wheels = [ [[package]] name = "langchain" -version = "1.2.18" +version = "1.3.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/4e/b651ecac63af474b28519384f7011294493d139937b1a9591581291eef34/langchain-1.2.18.tar.gz", hash = "sha256:7e829dbf117affadfd2067a0e97b4af20222f535f30fb812a28472d842c1074c", size = 582882, upload-time = "2026-05-08T13:59:19.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/68/a6dbad9c22df4087a0f9e79ddd46226c442b30128bfeee538d5889492a73/langchain-1.3.14.tar.gz", hash = "sha256:1b6696c72ba3bbbce54d745e0180742c9f6ece8bbc59ed5a46c3e20b9a435929", size = 645181, upload-time = "2026-07-16T13:28:18.29Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/20/959f6098c79158afe5aedce7de05c3700f10d293890ef9e5dace6c3ad94b/langchain-1.2.18-py3-none-any.whl", hash = "sha256:8432d43a65540845ed6f1a783d38d869c4659a6b9405f9a510169ad40d2f7bae", size = 113643, upload-time = "2026-05-08T13:59:18.461Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ec/0f942e78a621f8e3162ff1ed24284f469aaf51fb4607ee5831c626f2b2bc/langchain-1.3.14-py3-none-any.whl", hash = "sha256:4d10dbe91005952cddd56d0dc77aa108964da6bae90ab20063653957e901f782", size = 139560, upload-time = "2026-07-16T13:28:16.498Z" }, ] [package.optional-dependencies] @@ -1575,16 +1575,16 @@ openai = [ [[package]] name = "langchain-anthropic" -version = "1.4.6" +version = "1.4.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anthropic" }, { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/f5/cd397b94aeed5fa0e8ab9595b9fb578ac99f424d42220defe6626e6a1a7b/langchain_anthropic-1.4.6.tar.gz", hash = "sha256:78942d4458d883b7d362438a095ed501ed84f44d402622404482481fc973b9da", size = 706540, upload-time = "2026-06-12T16:54:15.352Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/22/40ab129b08329ca295b391aa1d48267692b42594757084c6918e22b655ac/langchain_anthropic-1.4.8.tar.gz", hash = "sha256:c76891b2044d56105ff13c106ed12650637b53bd598a4bdf15b4796eefa2a4ec", size = 708524, upload-time = "2026-06-26T21:28:46.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/af/927dbbc5a1f5fea1a69adc2883f034cbd1430004e36f4eacd302d500393a/langchain_anthropic-1.4.6-py3-none-any.whl", hash = "sha256:dbd412a956b6b8b0716d9d8460ef71f834a6731cdbfc59e6160482a4a9fb5200", size = 51797, upload-time = "2026-06-12T16:54:14.159Z" }, + { url = "https://files.pythonhosted.org/packages/b7/14/746235c4da89d9bc6a608c5f489f628e03feb8f697195c146e452c8f23c8/langchain_anthropic-1.4.8-py3-none-any.whl", hash = "sha256:778e9301b6fd517824f76ec1776975ce8add97a1f6a36c50ae3c2f4b03a66f7f", size = 52366, upload-time = "2026-06-26T21:28:45.535Z" }, ] [[package]] @@ -1629,7 +1629,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.4.7" +version = "1.4.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1642,9 +1642,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/2b/fffaff399d20a56d40b9562fa19701e91abd72d8c9d9bc8c2673077b56b6/langchain_core-1.4.7.tar.gz", hash = "sha256:7a825d77de0a3f39adbd9d09612a75e85527e14a52c1601089bcc062972d9f2b", size = 952522, upload-time = "2026-06-12T19:23:57.588Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/b9/e937d0a90b26540bff07e7a7c64349f3b29c2dcc36257cd1cd3fdce17f2a/langchain_core-1.4.9.tar.gz", hash = "sha256:f8078901145bed0466755277500a5a22822a7b628808c4c0a28d4fc88895fcf2", size = 967294, upload-time = "2026-07-08T20:06:54.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3e/dcdffa60078ae7b3a00ebb4cbbf1a204a14c3609983c604886523a7d4418/langchain_core-1.4.7-py3-none-any.whl", hash = "sha256:bcadd51951140ecdcba98311dbd931ba5de02a5ba8a2288dad5069c1eea2a13d", size = 554941, upload-time = "2026-06-12T19:23:55.826Z" }, + { url = "https://files.pythonhosted.org/packages/84/70/ade2fada52772798ef815b6352b59e71b116aa0c32c3aef5be3dc2cbed12/langchain_core-1.4.9-py3-none-any.whl", hash = "sha256:28e3909e2a10cc81504952d795ac0a9e014c0018121ef89d48dd396fa09ec624", size = 558293, upload-time = "2026-07-08T20:06:52.382Z" }, ] [[package]] @@ -1716,7 +1716,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.10" +version = "1.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -1726,9 +1726,9 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/b3/7dec224369c7938eb3227ff69542a0d0f517862a0d27945b8c395f2a781f/langgraph-1.1.10.tar.gz", hash = "sha256:3115beb58203283c98d8752a90c034f3432177d2979a1fe205f76e5f1b744500", size = 560685, upload-time = "2026-04-27T17:19:10.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/4b/0d1130e26b41a99dcc88353bbe7162a1f255c4db746bd94024268e6af27b/langgraph-1.2.9.tar.gz", hash = "sha256:385f87bc1802c35af7e0aa479278ecba8582d103515eb48256cb2ddcd42d0bd4", size = 722869, upload-time = "2026-07-10T01:30:14.985Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/07/057dc1aa7991115fca53f1fa6573a7cc0dd296c05360c672cc67fdb6245b/langgraph-1.1.10-py3-none-any.whl", hash = "sha256:8a4f163f72f4401648d0c11b48ee906947d938ba8cf1f474540fe591534f0d17", size = 173750, upload-time = "2026-04-27T17:19:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/41/16/0b8dc48823f1326f3e0c8012a3c07a40da6f194299e2ec080df236287baf/langgraph-1.2.9-py3-none-any.whl", hash = "sha256:c2d98ad94333937922ba04148641c1da2bfe45b5b8e55d7b6dcb0bb2df809e76", size = 247473, upload-time = "2026-07-10T01:30:13.733Z" }, ] [[package]] @@ -1746,7 +1746,7 @@ wheels = [ [[package]] name = "langgraph-checkpoint-redis" -version = "0.5.0" +version = "0.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langgraph-checkpoint" }, @@ -1754,40 +1754,43 @@ dependencies = [ { name = "redis" }, { name = "redisvl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/11/93cfa9451ff79bdc5ec86b429ef14ff602c3461228c18419b69da96fbfa7/langgraph_checkpoint_redis-0.5.0.tar.gz", hash = "sha256:595ded300fc0a46e81c39cc8bd9ff762f713c1ce6f8126cb13f01d24625c0775", size = 119730, upload-time = "2026-06-25T09:30:02.953Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/81/8a578f048e8ae3ff79cdd223b98158c8b23e3efc640281f3986b03bc8779/langgraph_checkpoint_redis-0.5.1.tar.gz", hash = "sha256:ea3696f7b78ec8380949961cd9b9767c373437ec0c6fcc56dfbab3bde4d78ff4", size = 119869, upload-time = "2026-07-14T17:46:45.176Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/74/106d0b1570aed42a39a7aa537436896e0ad187ff23864203984d645f621f/langgraph_checkpoint_redis-0.5.0-py3-none-any.whl", hash = "sha256:3d39497bc58f8f4ae07081b4f197f136a41c73a7bf73fc96b0de97c862eda126", size = 131508, upload-time = "2026-06-25T09:30:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/4f/71/e968397941d24d013a833e894731a3dadd5022e6bf3650b898efddf10a5b/langgraph_checkpoint_redis-0.5.1-py3-none-any.whl", hash = "sha256:9c9b1184fb98991d49ebed6a7b81ff05f2e994ee7d695bfb91410a9a512c178c", size = 131688, upload-time = "2026-07-14T17:46:43.723Z" }, ] [[package]] name = "langgraph-prebuilt" -version = "1.0.13" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/a4/f8ac75fa7c503103f0cf7680944e28bbaaef74c19a8d163d7346869cc369/langgraph_prebuilt-1.0.13.tar.gz", hash = "sha256:ad219782a80e1718e7e7794de49e0ae307111d45cbcffab9a52725a66a609456", size = 172913, upload-time = "2026-04-30T01:48:15.742Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/ef/5ada0bef4013ef5ae53a0ca1de5736517f1076a54d313f156ca545ec65d5/langgraph_prebuilt-1.0.13-py3-none-any.whl", hash = "sha256:7055e9fad41fbd3593800aed0aea0a6e974b17f33ed51b80d3d3a031212dd7c0", size = 37214, upload-time = "2026-04-30T01:48:14.507Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.3.15" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, { name = "orjson" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/af/cdd4d6f3c05b3c1112ed3f12ef830faf15951b21d22cbc622a4becbbe25c/langgraph_sdk-0.3.15.tar.gz", hash = "sha256:29e805003d2c6e296823dd71992610976fd0428cefaa8b3304fd91f2247037de", size = 201924, upload-time = "2026-05-22T16:54:27.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/a5/0196d9c05749c25bc198e4909d68c998bc3120297e14944921baf2f4c384/langgraph_sdk-0.3.15-py3-none-any.whl", hash = "sha256:3838773acf7456d158165385d49f48f1e856f28b56ccd99ea139a8f27004815d", size = 98166, upload-time = "2026-05-22T16:54:26.013Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, ] [[package]] name = "langsmith" -version = "0.10.2" +version = "0.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1805,9 +1808,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/8e/49a69c6793bf3fc5af62481e7e9b678fce59d1b384323d3ce87959a653e3/langsmith-0.10.2.tar.gz", hash = "sha256:9aa685383fbdec07a0df51dafc333ab0d4b6b995771172a232c3364714eb17a6", size = 4707343, upload-time = "2026-07-10T13:21:54.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/65/3867765976e4d43b98a4ea6a41c0712dda17a600ad998e02976b445874d7/langsmith-0.10.5.tar.gz", hash = "sha256:60053c1d88dc332a002cbac38601cc8b912466e7fc2a86bc9e690fa4d5bc1c78", size = 4720550, upload-time = "2026-07-15T08:28:51.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/17/cc8eaf4e82e4bb22009bdde20085ff91719dce643d355fed94d523300130/langsmith-0.10.2-py3-none-any.whl", hash = "sha256:c2a3929055758ac1831582f0939fafc0973cc08432365bbad335c336338ec37c", size = 652545, upload-time = "2026-07-10T13:21:52.13Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3e/213d9bb122f97d89987bd4c175cc4be9f2fa090e868ac8b5156c3265d8dd/langsmith-0.10.5-py3-none-any.whl", hash = "sha256:116adf2c30dfc1d0daf16919879b90c4093aad6122f44b80cc1f035b874dc9d6", size = 657879, upload-time = "2026-07-15T08:28:48.68Z" }, ] [package.optional-dependencies] @@ -2488,26 +2491,26 @@ wheels = [ [[package]] name = "prek" -version = "0.4.8" +version = "0.4.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/46/e436a6eb9fdb4d3fd08d0ab7fdba19fe03a9e994ec810de57869b853bd8e/prek-0.4.8.tar.gz", hash = "sha256:d15d8bef72ab7b02c7dc01458ac9e05b3131534492b5ce9bb11c4f6f636fa868", size = 494570, upload-time = "2026-07-04T12:05:10.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/54/edc21e275f9fa3540d4d98cf349c2de11621d6729cc401bb7aedf563609e/prek-0.4.10.tar.gz", hash = "sha256:db3122f4e780eb4587635e6a83df881caf2dbb1eb7799d1cca51158216d6f33b", size = 502565, upload-time = "2026-07-16T10:13:00.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/78/b4149c8913ced2e42debb49e261c4788a1ce431e84226921c2e1a7ea8545/prek-0.4.8-py3-none-linux_armv6l.whl", hash = "sha256:1f8f8cdc65836b571824c965daebb81b449f7e4a43894c58621f5708d5a185ed", size = 5668955, upload-time = "2026-07-04T12:04:41.588Z" }, - { url = "https://files.pythonhosted.org/packages/76/5f/7f54a0087b6b2f1751aeb41266d9c15e66fd0055492814798ab818cd0414/prek-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bce1798e96d9e3a6e6abf435da7107e81452f69edb3ca7c6f90a457355ea46e2", size = 6030947, upload-time = "2026-07-04T12:04:43.8Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/f2829fc3902920c36b764a386fa303e71a8219dac25cb3827c575e84199a/prek-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab3a52db17254d701c3cebb7eea58c8230aa7c1959aacfd5b5f25de18edb15d1", size = 5572593, upload-time = "2026-07-04T12:04:45.763Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/c5589955bcd5e3e33b67d8bc3110818cecac82a38fd6bc8b5dfdc5de421c/prek-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b3fcfd620523bbc3f51a21d7cd63449f659b9e2cf3582de12dd5949e23227b8f", size = 5847150, upload-time = "2026-07-04T12:04:47.419Z" }, - { url = "https://files.pythonhosted.org/packages/2d/9d/1f2dc91bdb79d2c4714b27eac9477a51490fba5b4731330dbbebc76bd345/prek-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42e65bc8425e9d7f1691a13ca1da2e07807d1ba76c35740833354b945131689e", size = 5573738, upload-time = "2026-07-04T12:04:49.125Z" }, - { url = "https://files.pythonhosted.org/packages/81/29/69a7b58e16ecbc5f3989bf4b028018d11a82dcdd320b93d6588d72f32aa7/prek-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f578492a8e0c9bc6b4bf6dfbba8716f647d4cd0769bf10ad6cf336e3096fd392", size = 5981054, upload-time = "2026-07-04T12:04:50.842Z" }, - { url = "https://files.pythonhosted.org/packages/63/cc/9b9850a60c22ed18c7755ebd2d72c6eefb37fac58149d09f6adc4691c2cf/prek-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4335f9d5beb123a3884a7fe34f57c9f0828f4fbb7666beab4298833459b104f", size = 6751350, upload-time = "2026-07-04T12:04:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/01/e5/c425aa7272b430630119e6757def3a2007555ba8cbeb2630e0448e7a8b7f/prek-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a8747df9c602e052881d3efb14dd7f7d62a59bd7277ae5171c9e7661d59d84", size = 6243881, upload-time = "2026-07-04T12:04:54.703Z" }, - { url = "https://files.pythonhosted.org/packages/1c/da/accd3ad07fd2891d3c2777eb42435439fdf11982c51d60f087c0b6b6e102/prek-0.4.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4db639db481d5f854eff9b3d2108889e613b8c15868bcf6bdd777c7cee577436", size = 5848846, upload-time = "2026-07-04T12:04:56.402Z" }, - { url = "https://files.pythonhosted.org/packages/15/00/3477704635249f21f5f98ce444cd7690c2aa9dc8d146a045db88ef2cd8c5/prek-0.4.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c3890a6f92316d2cf44eb50584e8d2b23a596dd70487022e61186a71a2ac0900", size = 5713942, upload-time = "2026-07-04T12:04:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e6/3ca4fabaebeadc976d9a92d1d9130674265355ea3b728418bad61583b097/prek-0.4.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fc7e15c24c591a37c6ffce5b25a021b16c299ac2649f183d812b67d665cd6551", size = 5554725, upload-time = "2026-07-04T12:04:59.96Z" }, - { url = "https://files.pythonhosted.org/packages/a5/46/2ab6aaaeff0cedb8955b2e4032071c8712382bdd423bb849718c3720180d/prek-0.4.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:36fe721704ff0c7624c1167639e23a5fe658bfd38c314f487219c9afd1eeb733", size = 5838595, upload-time = "2026-07-04T12:05:01.861Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8b/91398f2b6cd1629d5d8ca8c85b08eca500814a374313b0193f4aaf6ab6c4/prek-0.4.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:162e544abc394a8124f3a4ad68efee116bad09440e679dbd1675177335c2a432", size = 6357222, upload-time = "2026-07-04T12:05:03.845Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/ce5cbfaad36866134a21754640a05ecdba641fcd7ad15aa74cf3443f34f6/prek-0.4.8-py3-none-win32.whl", hash = "sha256:2602e46c8c5da7dfa69f60fcf88c2b57132ac623f49fb08bfb3094298c5f07e3", size = 5354388, upload-time = "2026-07-04T12:05:05.587Z" }, - { url = "https://files.pythonhosted.org/packages/df/03/3bc908bc5f7e430315553e47dfa055f19923a3888f9afe4da19f244b5cbf/prek-0.4.8-py3-none-win_amd64.whl", hash = "sha256:7cb22da60bee41b89c4978c0bea7126a3c0ccc003dae6748cf29b53947815edc", size = 5748221, upload-time = "2026-07-04T12:05:07.559Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/4295e6d5f5028171dfeb115ad38ab76bf3fe0c8df91b70d73c79aa760a94/prek-0.4.8-py3-none-win_arm64.whl", hash = "sha256:da70057f577b15d4bd121bf9dd29ee205fd4b4d75a0cafba062e84d7e8b4378b", size = 5574425, upload-time = "2026-07-04T12:05:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/db/e7/5a63528ba7b95b64f38db3e253aed49ee8e5e8ba16589889d2b7f809edb7/prek-0.4.10-py3-none-linux_armv6l.whl", hash = "sha256:023f302741d79301346c3088ba43a9592aff0ecdbe5ddc3019fa9b1183319c5e", size = 5694609, upload-time = "2026-07-16T10:12:26.352Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ef/ee9e6bf9a5ce242e9e4e66ac4e2e9042a0f6fd9f367cee18ad404456e93d/prek-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:72adc707e16f97564bbae08d22b222ac3bb2491f8fbfb5a0754f80d472c28a71", size = 6044037, upload-time = "2026-07-16T10:12:28.539Z" }, + { url = "https://files.pythonhosted.org/packages/68/7e/da08cc39e5348ccb9234e63a21ee56861f72e8497d6a78f0db1ccae6515d/prek-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:04c9321957e1b32e1fc7cf60bb4f90bba3761f8659d5551ed04f96e25596de49", size = 5535983, upload-time = "2026-07-16T10:12:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/0486a35bb687a9beac7a5810bd1104c6da56d469b30b1eeaeefd03c99da2/prek-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e66ccf6c5e4ebadd05cd98cb338d7f553e4d27aa243cf91279c5a569b3cdccc7", size = 5862085, upload-time = "2026-07-16T10:12:33.042Z" }, + { url = "https://files.pythonhosted.org/packages/52/39/277fe17ae1f121e532e3942456f5a6d01ddacfbc550e481dcb359be7a1b0/prek-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63f9061d75a50ef0ca92c4b596ad352937a845df80758244950e513b27e9e18f", size = 5605697, upload-time = "2026-07-16T10:12:35.498Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/08354af3e000f2656fad086690d834eab6c04631ff41313a219ea6232199/prek-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c2ff7110e4bfaafbbab13c2893a337081aca61ed797f14b6b224d2ea9741eef", size = 6034111, upload-time = "2026-07-16T10:12:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/4702396c8d486132e5ce009ab56a0b37f50cb6866830d371f2617b7bdfdc/prek-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b696a05542e79aa27bcce68d1792e77f4fe6f9c6b012b34d74d62f964f3c72d", size = 6787203, upload-time = "2026-07-16T10:12:40.031Z" }, + { url = "https://files.pythonhosted.org/packages/90/29/b5d5d6fb87ebd64b37471e3e79761de9983f85e14d69c522efe7af6620ce/prek-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:431b44d6054e72815b4b05e1173596dfd02a7f7461211d40a2e3117e414642ad", size = 6261333, upload-time = "2026-07-16T10:12:42.216Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/54ba696d19f7efdc184093353cce713a850aef9c3556e23faeecafa22e94/prek-0.4.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd2b4fd1df790087ba18b4506f680471922a5f13714f19801568434a040dee", size = 5867761, upload-time = "2026-07-16T10:12:44.329Z" }, + { url = "https://files.pythonhosted.org/packages/be/7d/3975098aa2baaabfc10f99f9fcf78045c4f10851beed8e9812b6a2688eab/prek-0.4.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:479e7480b447191aa5c6ed67e80f081d0f5ee4e878b140f4d2cee44165395f1c", size = 5714412, upload-time = "2026-07-16T10:12:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/97/c0/3e0aac190fe95fdef98526343559b61d4d9fd54444c8c9137ba02412afe1/prek-0.4.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0bb7451025cbd2b68e480a13cf665d7a5c87c8b87bf18549a78985c17df817ed", size = 5578145, upload-time = "2026-07-16T10:12:48.261Z" }, + { url = "https://files.pythonhosted.org/packages/d7/44/7b26035534204b8b8a9d5e625479201e616413d287262f557cb32e1f8d77/prek-0.4.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4fb047e5776676805794574b2d7b178cb3ab536793aadf172419fcda56b34a57", size = 5889245, upload-time = "2026-07-16T10:12:50.818Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6c/178a9d768876b4211a1bf63907fe308ae02d173639bcf41cea3c5eed35c1/prek-0.4.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:08318818d19caf79643babb89f872c92fda134a622b4731df1d6ed61e29d2d26", size = 6372849, upload-time = "2026-07-16T10:12:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/4d/84/d5f5ac8193602883f9dd1d675d9d4084e34fbe3ed2ef50a0c336d8a53d8f/prek-0.4.10-py3-none-win32.whl", hash = "sha256:092872714dcde480a662bbdd98b980b248c2d3e10543d4d53a3a58cc9e5b35b0", size = 5413005, upload-time = "2026-07-16T10:12:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/9e648fda10bc02c9b6ba305f93b6a6e4fd37d23d13a269a9d2d6bb44eaa1/prek-0.4.10-py3-none-win_amd64.whl", hash = "sha256:3d323a18d0f8c50e474a8fa29fb93bd2db680116d8afb19b76e72ad4667f58e6", size = 5799075, upload-time = "2026-07-16T10:12:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/22/74/b34d8c80cec8dccc7b922c75b9dca62b18b603b5ed2eea93c9d7c2928d2d/prek-0.4.10-py3-none-win_arm64.whl", hash = "sha256:5e93865ef96756c4a26f37ece04ad514abbc19ae6a23ed1a507b6314e6a0d2fb", size = 5563955, upload-time = "2026-07-16T10:12:59.07Z" }, ] [[package]] @@ -2892,27 +2895,27 @@ wheels = [ [[package]] name = "pyproject-fmt" -version = "2.25.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/a0/25701dec063b07d47da580b490d996099e5c9fc24d61ab380be6d8c4bf3b/pyproject_fmt-2.25.2.tar.gz", hash = "sha256:7a39a5dc9cd37c9006cdd9c0687b7d6cde7f63dafe37154e1f3e373e112a2c9b", size = 292446, upload-time = "2026-07-09T00:25:04.092Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/7d/2555a11d7c2bdce274005bb4fbf93c11cba84cce6406cbbf2efbb78d73e8/pyproject_fmt-2.25.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d99cf008097c932843c327caa76961ecaede9001c60f7f16ad131f1e59008d89", size = 5165736, upload-time = "2026-07-09T00:24:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/c7/6a/bd9cabb2436f65aa36a5fc1fa2a31bbe1ec82bff3eb5bad6cd0b94a883a1/pyproject_fmt-2.25.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1ccfff17c529195197d03764a0f5c4c93e1b916ee8a7f5ece19eadc07020bf9e", size = 4945221, upload-time = "2026-07-09T00:24:15.51Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ab/d4fb8796a5a8be34dd600ea011be9b6105fa84f20f9c6db2f9a42e3b1e61/pyproject_fmt-2.25.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ac53ade14a548b13dea0654795d198ce907abf3ac44b2bdf22bf29defbe08b1a", size = 5093367, upload-time = "2026-07-09T00:24:17.326Z" }, - { url = "https://files.pythonhosted.org/packages/c0/5b/87929c25dba4c05c1640ef99fee01aecf1925758cb898cfbf56e21311092/pyproject_fmt-2.25.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cb09cf45c19760ddcab5d835e8684c559ceb3484bd5f0458a9a08488bf3db6a", size = 5477798, upload-time = "2026-07-09T00:24:19.382Z" }, - { url = "https://files.pythonhosted.org/packages/3f/11/141056e0f65fdf934ae9731932beb133efdaad5fb5a71b2aa19c85cd0282/pyproject_fmt-2.25.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0b16194c62642145611e51f92892d95157d6a14614a66d4925ac336c81171651", size = 5192365, upload-time = "2026-07-09T00:24:21.046Z" }, - { url = "https://files.pythonhosted.org/packages/8b/85/09101c08d9e9f0f15298b9e2ef6d99c2e9988eaf7e1fbc74242df3d63ad4/pyproject_fmt-2.25.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:75f87669c87694e135f93f4c56903168baceeab1b5560d707f2c97fea39211ba", size = 5093143, upload-time = "2026-07-09T00:24:22.864Z" }, - { url = "https://files.pythonhosted.org/packages/51/93/9ba87ccafd1b54b9c124e002cfc952df21d2703870303120bed2b45e35f9/pyproject_fmt-2.25.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b028e6cc33d44af0f7784689727219ed46c826930517d30d0c95dcb84e31f6d", size = 5657939, upload-time = "2026-07-09T00:24:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/27/82/0b8cbd12bb373912751e899d6d5450f984af2c2b5f9a582e3e7493bc725d/pyproject_fmt-2.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:2613ac8b15bdfc3558983f8ecf62a0045776a3661baa4324e399642ad10c773b", size = 5335162, upload-time = "2026-07-09T00:24:26.837Z" }, - { url = "https://files.pythonhosted.org/packages/7d/42/aefc19e45e0b05c26f797c528fa35607032bd3571eec6058498b98939c6d/pyproject_fmt-2.25.2-cp310-abi3-win_arm64.whl", hash = "sha256:9f61832ab40eed172b6bf81eabadcc5e9004096f481fbeaaed0fa93a51d85db5", size = 4870355, upload-time = "2026-07-09T00:24:28.977Z" }, - { url = "https://files.pythonhosted.org/packages/ab/93/4bd9c75ee52d8ebdafc9cbce9a34bd86df22ef17576a6d78a419e6aeaed2/pyproject_fmt-2.25.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:45cd667a072dde2f636240112fe4c874a78b29d6707aa941b66062d5a1300a6f", size = 5161628, upload-time = "2026-07-09T00:24:30.83Z" }, - { url = "https://files.pythonhosted.org/packages/fe/42/3c06d365311beac428d7431fff8abce23bf29ca0a076bc2f2cdbbaca0968/pyproject_fmt-2.25.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ab8962def405fb018f8df9880ff28729078428d4911e16ad2ae0e559c312880b", size = 4936133, upload-time = "2026-07-09T00:24:32.831Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8f/fe78e087ccc07a21de0685e19c73fb80b5e77299ea56196ccd68035a8d85/pyproject_fmt-2.25.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:da1965271f90eda976d32d51ece4eaa81e25485381456215f35d1477f3b21e20", size = 5088527, upload-time = "2026-07-09T00:24:34.644Z" }, - { url = "https://files.pythonhosted.org/packages/b0/38/d59c22bc9122687a8de0c37ddc81a23d7ee09613da4cf298c5a3598c83ff/pyproject_fmt-2.25.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:fdc4b74fec7bfb15df0ab8b9b9a39f5d830f8639c3e39e4a85fe9c848d288166", size = 5472718, upload-time = "2026-07-09T00:24:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/64/97/55fcf261147d13288c19b1b6f27bda70c9067881217307a514dcff23e058/pyproject_fmt-2.25.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9dad3d9e20a861118a6a021019a810d178b4c89f98acc80acd77cd6a55db8f36", size = 5088111, upload-time = "2026-07-09T00:24:38.115Z" }, - { url = "https://files.pythonhosted.org/packages/fe/5e/8ac8622eea8f53a0bc9a56b772ba23bcbe05eebc6cd2e235d267d3f348c9/pyproject_fmt-2.25.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc474bc2d612e99d4d2db45d4124cdf671eb329b4f2708caa34fc374bbb6b5a5", size = 5654271, upload-time = "2026-07-09T00:24:39.961Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a5/3fa4b153a0f1bfa7590d0ecb9b354dbe2c1229b8c03c67a953018ba47d7a/pyproject_fmt-2.25.2-cp314-cp314t-win_amd64.whl", hash = "sha256:4485387138f6c81d018364ca1c00ae03d251f24f3e89b39dc7a3901190a67895", size = 5332045, upload-time = "2026-07-09T00:24:42.093Z" }, - { url = "https://files.pythonhosted.org/packages/05/c7/b52346832d8c083343a13987e574c1fc0feec682d9c371924119209bcd6f/pyproject_fmt-2.25.2-cp314-cp314t-win_arm64.whl", hash = "sha256:448972d6fca1df54f965d9a56732a921080b1379835aeec2cfe4b8ba466db510", size = 4865929, upload-time = "2026-07-09T00:24:43.864Z" }, +version = "2.25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/ef/6e0247361165c52295e9de86f52fe6a2e55a2cf41a29926fc4e105808159/pyproject_fmt-2.25.3.tar.gz", hash = "sha256:f3f8c51afd8eb45f409e273dfa6347520eb76727cbe6b436a573d250c34a5ec8", size = 293215, upload-time = "2026-07-13T18:17:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/18/4e8351743ecc6d195530a13b908aa3790907b5942ecdc7a4b4f2cab96ba7/pyproject_fmt-2.25.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9f7fad5d0161c595fe7bc028e5e3e39fc8cbd3e3cf5267d17d1b95a324ed52b0", size = 5166085, upload-time = "2026-07-13T18:16:28.491Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/7f607e239b1e32f2c1ccd88977bd1d612c515ffbc16a460ee69998baf1ba/pyproject_fmt-2.25.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:cf3ea667ae0e033a073ad3618607e67962c46193056ed9e5b2a6a0cfa862f666", size = 4945909, upload-time = "2026-07-13T18:16:31.136Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5d/78c0811c8baa52435a62e7e0de78afdbcc9aea2b89b3617582a456f05fd2/pyproject_fmt-2.25.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee1694fd54763126e65acede5ccfc9ea20e0fedc24b70dbdfc0ae4c061bd97f4", size = 5093743, upload-time = "2026-07-13T18:16:33.76Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b1/446ef27bc537e916d8092f30885c2f8939fd8fe68a2c72d232089bfd5012/pyproject_fmt-2.25.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8af439dfcd3b9c46315b6e564ccf457673659b0cc0fc6927230f1921f833c661", size = 5479406, upload-time = "2026-07-13T18:16:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5c/15f3f9e93c47bf1b7ca3db3ddf4cac6157ae932e6c4360a71d33f5e6299b/pyproject_fmt-2.25.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:74a12be41355293f3bf1205509f959de498c3a4c5d8cea62e501a2fdacae0364", size = 5192668, upload-time = "2026-07-13T18:16:39.066Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/167854cb39a7d5fb538cdea466f59b1cbdb8f44c6b4e10f777554cd5dbb3/pyproject_fmt-2.25.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:269598a081d4e32bfaa3685e28d0e1ecd71a6a95b995b1835307792169430c5b", size = 5093120, upload-time = "2026-07-13T18:16:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/de/bd/12764fc9fafd80d5ccd59363bfcd7e3aa349ecd4cb2d3dab6a3654c88a5b/pyproject_fmt-2.25.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0534ffdb0e40bb0ce0a05ec5469b0c99460d587a949cfb1316f397321cbaf90d", size = 5658297, upload-time = "2026-07-13T18:16:44.615Z" }, + { url = "https://files.pythonhosted.org/packages/41/a2/bd8051b3c9b339425407b861b7a5f3611ed4bc131bc680418b3d90181fe2/pyproject_fmt-2.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:1bc20402816d78214005827f443f49244d47832be9b085c40474e1fa0cad7597", size = 5335844, upload-time = "2026-07-13T18:16:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/58b76c1434efbc895d594a1b4c0c88a42169d925284665beccc39799eba8/pyproject_fmt-2.25.3-cp310-abi3-win_arm64.whl", hash = "sha256:e90b2288ecf294e5ef2144615eb8b5e4e96cb335902faf438cf070f30a078f50", size = 4869237, upload-time = "2026-07-13T18:16:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/28333d166f50baa917b4fb6dc8fa62ff499d229186213f978bcf05208fdb/pyproject_fmt-2.25.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:0b89701a4a945b90c250068276ac6827fd508e1fa5f0d426bdb73299096da689", size = 5161863, upload-time = "2026-07-13T18:16:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/cccb479a535817c113c109435db0ebd99227ddd2e94659646bf690914a0c/pyproject_fmt-2.25.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7696c4cc85ba69807987429cb9297e6da17b58102ea2881dfa31328ad50b53a", size = 4937692, upload-time = "2026-07-13T18:16:55.077Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c3/8b66fc05f72d490327db2d93fd173a03c85777b4bfafc618824130df5e93/pyproject_fmt-2.25.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:103a409d3f3885b368b0307b91530f90e71e1dcbb51505de5463b530dae4848e", size = 5088947, upload-time = "2026-07-13T18:16:57.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/32/951b4cf1f3da084d087a5f66dcc4f70ca62a5b5316ff9b00eedc8658ad27/pyproject_fmt-2.25.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:eac2a1c3c8eada5ebef77a52ab2963af25cbdb19b4a87dd12791f375b9ed3830", size = 5473785, upload-time = "2026-07-13T18:17:00.55Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/270024754ba55ea310bef4ade83e31af4ba306b95628b1c41178986daf28/pyproject_fmt-2.25.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:57c0227e83fdf610bce2aaa8aa5621b4a538cce0cb99fc693eab21f6975452a3", size = 5087940, upload-time = "2026-07-13T18:17:03.473Z" }, + { url = "https://files.pythonhosted.org/packages/14/85/3afe988ea809c3de22c2e5b04740827f8ea1578fc8348b49759d793a4a20/pyproject_fmt-2.25.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d4b5c1cfb43eba2045b6f49527e014b8a8f374cee50f492fd0cfeed0f28b3d09", size = 5654163, upload-time = "2026-07-13T18:17:06.429Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b7/3f5e6878b9202a96aa6b7fdf949e1eafaf0de31489cb64025223b7bd2b2d/pyproject_fmt-2.25.3-cp314-cp314t-win_amd64.whl", hash = "sha256:a5df88897bdf1febe05d1b31863abd40d450d84c6da67e642e636b2081eb55b1", size = 5332488, upload-time = "2026-07-13T18:17:08.996Z" }, + { url = "https://files.pythonhosted.org/packages/9a/32/91dae8f0438e911220005087f0ea5d89e42faea2fb32c7ecfb476c979245/pyproject_fmt-2.25.3-cp314-cp314t-win_arm64.whl", hash = "sha256:52ba3e6b81782940b1a741a1a1b1a9d73b21e90a49274738553aeb74b92b2d44", size = 4864969, upload-time = "2026-07-13T18:17:11.782Z" }, ] [[package]] @@ -3143,11 +3146,11 @@ wheels = [ [[package]] name = "redis" -version = "7.4.0" +version = "7.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/93/05e7d4a65285066a74f48697f9b9cde5cfce71398033d69ed83c3d98f5c9/redis-7.4.1.tar.gz", hash = "sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e", size = 4945742, upload-time = "2026-06-05T09:10:06.703Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2e/2677f3f93dae0497e7e33b6637302e7f3744efc553f34231183e32584885/redis-7.4.1-py3-none-any.whl", hash = "sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0", size = 410171, upload-time = "2026-06-05T09:10:05.128Z" }, ] [[package]] @@ -3301,40 +3304,40 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, - { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, - { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, - { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, - { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, - { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] [[package]] name = "sentry-sdk" -version = "2.64.0" +version = "2.66.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/31/b7341f156a5f6f36f0b4845d6f1c28a2ae4799171dba7007f3a1e9b234b4/sentry_sdk-2.64.0.tar.gz", hash = "sha256:68be2c29e14ae310f8a39e1a79916b6d85c6cb41dcce789d14ff05fe293e4c55", size = 921020, upload-time = "2026-06-30T08:13:47.682Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/ff/670abe04c5072719b5060ed93851d0d69525d60f8f2c5810f8becd58f9c1/sentry_sdk-2.66.0.tar.gz", hash = "sha256:9727d35aa83c56cd53294676fe65b96296a334c9ce107fa2142bd70f47acb265", size = 935745, upload-time = "2026-07-16T12:42:04.663Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/a8/3fb9a4319efa3b26f5be0e90e6d8918df43fa7c7e977d26390f589501d82/sentry_sdk-2.64.0-py3-none-any.whl", hash = "sha256:715ea91ca860a819e8d8a50a7bde3a80d0df3b4ed7b6660a20fb9a2d084188f1", size = 498901, upload-time = "2026-06-30T08:13:45.566Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bb/49b10783f29067da2eec179320617e94faf63196609de47aeab3c26c3325/sentry_sdk-2.66.0-py3-none-any.whl", hash = "sha256:096136c214c602be2b323524d30755dc5b30ec5a218a206207f33b12c05c6f11", size = 504769, upload-time = "2026-07-16T12:42:02.919Z" }, ] [[package]] @@ -3542,27 +3545,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.58" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/4c/26c90732658903aeb1d289208f7b7b492fa21029e0c4d6c51bdd6f8f5e51/ty-0.0.58.tar.gz", hash = "sha256:8f22484174e65c630660a454bf81b80cae7a3a7e70479f19c170d6cd87949258", size = 6133665, upload-time = "2026-07-10T03:09:30.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e1/5d1aa2a75829459834689f080e4be7a9d8828ce14b939ebed69161a35811/ty-0.0.58-py3-none-linux_armv6l.whl", hash = "sha256:47412850b6fbef61c42f244f6a51aa2f2c9e91f08cfbafd2d1e3730d2419d317", size = 11706915, upload-time = "2026-07-10T03:08:51.028Z" }, - { url = "https://files.pythonhosted.org/packages/96/e1/929eda9cc72a9afe39a03c76f946a503508d37343cc8ff2e64226afda105/ty-0.0.58-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79deb7bb4e5b3a1eee6ab9abc724d6ce3559d4977982707f310a139ee11fc703", size = 11532079, upload-time = "2026-07-10T03:08:53.771Z" }, - { url = "https://files.pythonhosted.org/packages/07/43/ebc58b3fc7d86a7abba2829f1674f7d4ae3a08f9794c1f31b707950c871f/ty-0.0.58-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a28af3187e661708a386d44a4fc32896a5f589fb07b734a11ab2f516e7572b7", size = 11092983, upload-time = "2026-07-10T03:08:56.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/6e/9547dbb8e51e47749cfb721a02b4fc862f9a932fa0f66a34a4d6dc429bb1/ty-0.0.58-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21a6977e34bc362fb378add46e59d5d56331c1c36727e6904767217ca8479718", size = 11490492, upload-time = "2026-07-10T03:08:58.49Z" }, - { url = "https://files.pythonhosted.org/packages/d9/76/9f83b51b5e7795d6c8d76b4bb1bb7cebf4c10d609e846c02ab138e404556/ty-0.0.58-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ac23e6bf6105ceca46632debf1b10b98125aaf60202aeae02f6abfeea242b3d", size = 11503696, upload-time = "2026-07-10T03:09:00.741Z" }, - { url = "https://files.pythonhosted.org/packages/47/9d/9fd48a0696c680f74e50f31cd54e524eeb58c97ea9bb1c3b8e04230ba215/ty-0.0.58-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb6df6a8c6a21894807a49851370ec7fc64aa910296c78ada31db0ef19359112", size = 12158653, upload-time = "2026-07-10T03:09:02.998Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ea/b5de845d2d8edae04d901c7585af7f04a057e18b26311f7fa4ca62b2da30/ty-0.0.58-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9df5847eebc026cde088b44420a03f7c6c169a7db747176bdf0a656eb1144713", size = 12723019, upload-time = "2026-07-10T03:09:05.291Z" }, - { url = "https://files.pythonhosted.org/packages/17/1c/54083b23eeff1e101f50b6df6a2c7f1e14b31abe0577c91bcae9e2c9395d/ty-0.0.58-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53a331a7f1f85872c810676a0f16096ef98c1b95c8c9a573fe7fde64d0a93e7c", size = 12275715, upload-time = "2026-07-10T03:09:07.564Z" }, - { url = "https://files.pythonhosted.org/packages/22/88/16925434b06faa49d36aa7e7508a6821ec6feacb429ca2fd80a3d52716b4/ty-0.0.58-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0b05cd479fdcedc2e6781d376ee1a33569f37ae7f58357004635f615c4374c", size = 12033075, upload-time = "2026-07-10T03:09:10.222Z" }, - { url = "https://files.pythonhosted.org/packages/58/2b/b55708dd483982ae03d14da3667e3f0346cd384e11cca3dd3674a8b598c1/ty-0.0.58-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a0012786077e5becbb6add9fe51eda1d4d36d249afd3ae6cd141d554af15ae3", size = 12367729, upload-time = "2026-07-10T03:09:12.338Z" }, - { url = "https://files.pythonhosted.org/packages/91/a3/99ad66652956408f7e9ac3db6b4a416199d773b6c073cf95b0eea126d340/ty-0.0.58-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4ede96d7f6d149156da254e784b062b817736b68d4c6d660555a3d02d96966fb", size = 11439798, upload-time = "2026-07-10T03:09:14.518Z" }, - { url = "https://files.pythonhosted.org/packages/ee/23/344ceed4fe02ed498711e1f4a47b6e311fb1e9c4fecc19d31894be7a3472/ty-0.0.58-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:47608e58f73901b989402e6f283249cda4c2314282ec368aa74ab61761c62bd5", size = 11512695, upload-time = "2026-07-10T03:09:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/55/cf/3801831812c468f3fd0b3043a80f557a9aa90e6c27375763d7c3121e03f2/ty-0.0.58-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9757de17cc17e4c6bc26e18d4e26dea52ffee53d41a10d9087c6465e6ab12e2b", size = 11812253, upload-time = "2026-07-10T03:09:19.151Z" }, - { url = "https://files.pythonhosted.org/packages/7b/80/bce4f245787b77d1ec9feec7d9161eade5e01a77dbc132e016b24df83b0d/ty-0.0.58-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f3776d54c1d935fcb8f814bd58efba402c86c555f93e1144c59d087e7aa8b906", size = 12123918, upload-time = "2026-07-10T03:09:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/bab3d6268e7e88c792bf7cdde81bd11b31aa587eaba75196502b729747c8/ty-0.0.58-py3-none-win32.whl", hash = "sha256:8f50ec0ac3b42baa4c75895dc367071dc86b00ab440d29fdc72c633286a94815", size = 11230897, upload-time = "2026-07-10T03:09:23.871Z" }, - { url = "https://files.pythonhosted.org/packages/9c/68/d9504c895864aaa84a840dce6ac7f8e681f6938be1882c8d4f60832dbe57/ty-0.0.58-py3-none-win_amd64.whl", hash = "sha256:de8847b3a65475ae4773bddd3126bfcf29f017e88967c4dcc9c75d743a4d3e5c", size = 12299376, upload-time = "2026-07-10T03:09:26.292Z" }, - { url = "https://files.pythonhosted.org/packages/26/04/c9847cb680b5fe8e1f7d7b483edd5cedcc29394496e7b8ed40d96be796ba/ty-0.0.58-py3-none-win_arm64.whl", hash = "sha256:7334bb38789878f60677f2eb9c1de4bfdf4583e2443790989c77da9b10fe0989", size = 11710495, upload-time = "2026-07-10T03:09:28.478Z" }, +version = "0.0.60" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/6e/1ab6f2727622d38ddfb2a7f994209b3087190b76885e2f754dbb6e58e0c9/ty-0.0.60.tar.gz", hash = "sha256:ebd7517d1aa8d8c3793cbf03c263679a42b939eca650df583234f92a5eb5837a", size = 6189323, upload-time = "2026-07-16T10:18:14.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/fc/c82d30b753dfb9d83ac34568478d9487bc42e1e79241a22b57f4b28fabee/ty-0.0.60-py3-none-linux_armv6l.whl", hash = "sha256:0842270c2e10d8416ca9f8aca1357c827efc5212300fbbc709e186e66fc7f9da", size = 11831443, upload-time = "2026-07-16T10:17:34.912Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/09e386d816076d1d90c57baee7f607e1475b625bb9f9279b28302da22f18/ty-0.0.60-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f49d024571cd7e569081638ab0ccc9e6795d642c6b9208addecc5e487364f0a3", size = 11624536, upload-time = "2026-07-16T10:17:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/e527e29fd9a3d41ddc419a43b37927c42a0d8ee4ca8a148ce4e022328f2f/ty-0.0.60-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b006c9a793d735ff808999775bbc0f07f91de50e931799f17322440cd9d055f", size = 11032920, upload-time = "2026-07-16T10:17:39.695Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/0f294bf2521f2836ba1a4682d4301c2038e4dfedee3a70df8f6005694978/ty-0.0.60-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:233033de40a0722420380d40f0eeebe8eb0d3afeeab3a6e7b1388e2caf1a017c", size = 11575334, upload-time = "2026-07-16T10:17:41.644Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8c/682e46e29165d9c94829576687092a8ea4556dd5882b11edd32bc7f4b534/ty-0.0.60-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61bcbd67f653ae2966d0e423879127907c6f8b567d64b6db93cb8f6cca7045ed", size = 11659354, upload-time = "2026-07-16T10:17:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d3/6baf7eb3cf9459416032285a10ffe1309f7e255e3f1974372bc41d44cfd3/ty-0.0.60-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b72e44a0be08ad778693d49b00b03bb784c1465d8265b3b392b355b34257a437", size = 12293710, upload-time = "2026-07-16T10:17:46.032Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/b287d78c93449fc5153891ad040be55e0718711ae7c0f2c54d3b88dd4d00/ty-0.0.60-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13b89f03a76f149350617345ae33bd332d2c6023dc5d560dd10a4f0210fba13a", size = 12837636, upload-time = "2026-07-16T10:17:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f2/3d8c77ca5f836fb6280231030df5a4504476a38d4554330d2be42125888e/ty-0.0.60-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30db8ebf6715da30afac62311ac773bf436ed503ed989cc9d4be4d3bdc3cbd43", size = 12383469, upload-time = "2026-07-16T10:17:50.567Z" }, + { url = "https://files.pythonhosted.org/packages/e8/98/958047501d74f9d0df357df28fea1de2afa18f1e6b2dc4a3ea4975c83e14/ty-0.0.60-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ecd6dc686f90c3bfe22c9b435e4a5325f5066f706bfe95c1616ef425b1f0654", size = 12132334, upload-time = "2026-07-16T10:17:52.718Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/b2853634da28aac01ea0ba8ef7695dc3d6c9aaf9c63dfdcf7c7435bc6c25/ty-0.0.60-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2292124558d839e31afd9829b11dda726ce65096b1feb80d14a1ddc7ccb0f28", size = 12359945, upload-time = "2026-07-16T10:17:55.143Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ae/bfecc71fcf1b833117bfb0efb76e14f5772874775274115ac94f64c5ba7a/ty-0.0.60-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:78812ec03da2c3141030ee1717fb09f0845eb552a77e79680c1cca598861ddaf", size = 11534812, upload-time = "2026-07-16T10:17:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/1236c1ab9cd6b2daba9c558c9dcf04644f7b636f3bab3c15e74c7f80f5e1/ty-0.0.60-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a92c1dbc48f8b414ddcf199c1119054de00f8ee995528fa9d9c2e4ddd13cd0c1", size = 11677591, upload-time = "2026-07-16T10:17:59.747Z" }, + { url = "https://files.pythonhosted.org/packages/49/49/86c9230d9a8bf1755ca4b0165e7cb6bdfa2e495f0a105b24c6a27b5a542c/ty-0.0.60-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2c367d5ffe545004f0603e5599614062230678eeab980acd32850a7c19d60fc9", size = 11901279, upload-time = "2026-07-16T10:18:02.301Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/0c2e9904362d9c63700116feb192a3e10122eef49a417cde84068b2562fd/ty-0.0.60-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a94b1f99aa8f8ca5879341a5351af708b85ce19c7494db320c719a1e129d08e8", size = 12230659, upload-time = "2026-07-16T10:18:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/39c414f6b4bc944b2bac19c181438ee393c8eb34e9e21d97e5a148745f95/ty-0.0.60-py3-none-win32.whl", hash = "sha256:5ea2c85249581fb0060b9ff63b5313330f48930b2e50f6c85a5a322701626cc5", size = 11175286, upload-time = "2026-07-16T10:18:06.523Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/fdf4072d96c71b65fe98c9ca2ea5b3d2d0f6e20873bb2639e8c7827bd5f6/ty-0.0.60-py3-none-win_amd64.whl", hash = "sha256:15d83c3d793cb07840b8888c084cc2c49eb8acc29456a2fcdfbfd509c82526e5", size = 12249240, upload-time = "2026-07-16T10:18:08.701Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a2/83a496d717f712f894cf26690bcd9465ca23cd1c9139cac669d9cca45d14/ty-0.0.60-py3-none-win_arm64.whl", hash = "sha256:32e63228c3d21ddb192385aaf54501f19478be7b764f7572014d5110e53a9085", size = 11620140, upload-time = "2026-07-16T10:18:11.316Z" }, ] [[package]] @@ -3827,29 +3830,11 @@ wheels = [ [[package]] name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]]