mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-13 15:32:24 -06:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e02d1b52c | |||
| 9b29453e9b | |||
| c4ff1caf09 | |||
| 22245145db | |||
| 8eacc4d632 | |||
| 23fed785c4 | |||
| 688c27e68a | |||
| 405baf7cb2 | |||
| 1027c22333 | |||
| 381651049b | |||
| 322b7dabc4 | |||
| d5e86c8493 | |||
| c154ea3966 | |||
| 755ab51802 | |||
| 9df8ab836f | |||
| 8d88e6a7eb | |||
| cce292f793 |
+1
-1
@@ -15,7 +15,7 @@ RUN rm -f /etc/dpkg/dpkg.cfg.d/docker
|
||||
|
||||
# System dependencies: psycopg (libpq5), developer tooling for agent workflows
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
|
||||
libpq5 git curl jq man-db manpages \
|
||||
libpq5 git curl jq man-db manpages procps file \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Non-root user
|
||||
|
||||
@@ -61,7 +61,7 @@ end
|
||||
|
||||
Session -> Session : _init_system_messages()\nevery conversation turn
|
||||
|
||||
Session -> Session : _get_visible_memories(\nlimit=fetch_limit)
|
||||
Session -> Session : _list_visible_memories(\nlimit=fetch_limit)
|
||||
note right
|
||||
**Scope resolution:**
|
||||
1. global scope (always)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "turnstone"
|
||||
version = "0.9.4"
|
||||
version = "0.9.6"
|
||||
description = "Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."
|
||||
readme = "README.md"
|
||||
license = "BUSL-1.1"
|
||||
|
||||
@@ -83,6 +83,8 @@ export interface StatusEvent {
|
||||
effort: string;
|
||||
cache_creation_tokens?: number;
|
||||
cache_read_tokens?: number;
|
||||
tool_calls_this_turn?: number;
|
||||
turn_count?: number;
|
||||
}
|
||||
|
||||
export interface PlanReviewEvent {
|
||||
|
||||
@@ -436,7 +436,7 @@ class TestSkillCatalogDisclosure:
|
||||
"turnstone.core.session.list_skills_by_activation",
|
||||
return_value=search_skills or [],
|
||||
),
|
||||
patch.object(session, "_get_visible_memories", return_value=[]),
|
||||
patch.object(session, "_list_visible_memories", return_value=[]),
|
||||
):
|
||||
session._init_system_messages()
|
||||
|
||||
|
||||
@@ -20,12 +20,15 @@ def _mock_model(
|
||||
*,
|
||||
owned_by: str = "test",
|
||||
meta: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> MagicMock:
|
||||
m = MagicMock()
|
||||
m.id = model_id
|
||||
dumped: dict[str, Any] = {"owned_by": owned_by}
|
||||
if meta is not None:
|
||||
dumped["meta"] = meta
|
||||
if kwargs.get("max_model_len") is not None:
|
||||
dumped["max_model_len"] = kwargs["max_model_len"]
|
||||
m.model_dump.return_value = dumped
|
||||
return m
|
||||
|
||||
@@ -109,6 +112,27 @@ class TestProbeModelEndpoint:
|
||||
result = probe_model_endpoint("openai", "http://localhost:30000/v1", "key")
|
||||
assert result["server_type"] == "sglang"
|
||||
|
||||
@patch("turnstone.core.providers.create_client")
|
||||
def test_vllm_max_model_len(self, mock_cc: MagicMock) -> None:
|
||||
m = _mock_model("/models/nemotron", max_model_len=262144, owned_by="vllm")
|
||||
mock_cc.return_value = _mock_client(m)
|
||||
|
||||
result = probe_model_endpoint("openai", "http://localhost:8000/v1", "key")
|
||||
assert result["context_window"] == 262144
|
||||
assert result["server_type"] == "vllm"
|
||||
|
||||
@patch("turnstone.core.providers.create_client")
|
||||
def test_vllm_max_model_len_preferred_over_meta(self, mock_cc: MagicMock) -> None:
|
||||
m = _mock_model(
|
||||
"/models/test",
|
||||
meta={"n_ctx_train": 8192},
|
||||
max_model_len=131072,
|
||||
)
|
||||
mock_cc.return_value = _mock_client(m)
|
||||
|
||||
result = probe_model_endpoint("openai", "http://localhost:8000/v1", "key")
|
||||
assert result["context_window"] == 131072
|
||||
|
||||
@patch("turnstone.core.providers.create_client")
|
||||
def test_server_type_vllm(self, mock_cc: MagicMock) -> None:
|
||||
m = _mock_model("org/model-name")
|
||||
|
||||
@@ -899,3 +899,125 @@ class TestDetectModelTimeout:
|
||||
|
||||
result = detect_model(client, provider="openai", fatal=False)
|
||||
assert result == (None, None)
|
||||
|
||||
def test_vllm_max_model_len_detected(self) -> None:
|
||||
"""detect_model() reads max_model_len from vLLM model objects."""
|
||||
mock_model = MagicMock()
|
||||
mock_model.id = "/models/nemotron"
|
||||
mock_model.model_dump.return_value = {
|
||||
"owned_by": "vllm",
|
||||
"max_model_len": 262144,
|
||||
}
|
||||
|
||||
fast_client = MagicMock()
|
||||
fast_client.models.list.return_value = MagicMock(data=[mock_model])
|
||||
|
||||
client = MagicMock()
|
||||
client.with_options.return_value = fast_client
|
||||
|
||||
model_id, ctx = detect_model(client, provider="openai")
|
||||
assert model_id == "/models/nemotron"
|
||||
assert ctx == 262144
|
||||
|
||||
|
||||
class TestExtractContextWindow:
|
||||
def test_vllm_max_model_len(self) -> None:
|
||||
from turnstone.core.model_registry import _extract_context_window
|
||||
|
||||
m = MagicMock()
|
||||
m.id = "/models/test"
|
||||
m.model_dump.return_value = {"max_model_len": 131072}
|
||||
assert _extract_context_window(m, "openai") == 131072
|
||||
|
||||
def test_llama_cpp_meta(self) -> None:
|
||||
from turnstone.core.model_registry import _extract_context_window
|
||||
|
||||
m = MagicMock()
|
||||
m.id = "test"
|
||||
m.model_dump.return_value = {"meta": {"n_ctx_train": 8192}}
|
||||
assert _extract_context_window(m, "openai") == 8192
|
||||
|
||||
def test_vllm_preferred_over_meta(self) -> None:
|
||||
from turnstone.core.model_registry import _extract_context_window
|
||||
|
||||
m = MagicMock()
|
||||
m.id = "test"
|
||||
m.model_dump.return_value = {"max_model_len": 262144, "meta": {"n_ctx_train": 4096}}
|
||||
assert _extract_context_window(m, "openai") == 262144
|
||||
|
||||
def test_no_metadata_returns_none(self) -> None:
|
||||
from turnstone.core.model_registry import _extract_context_window
|
||||
|
||||
m = MagicMock()
|
||||
m.id = "test"
|
||||
m.model_dump.return_value = {}
|
||||
assert _extract_context_window(m, "openai") is None
|
||||
|
||||
|
||||
class TestHealthMonitorModelChange:
|
||||
def test_model_change_fires_callback(self) -> None:
|
||||
from turnstone.core.healthcheck import BackendHealthMonitor
|
||||
|
||||
changes: list[tuple[str, int | None]] = []
|
||||
|
||||
def on_change(model_id: str, ctx: int | None) -> None:
|
||||
changes.append((model_id, ctx))
|
||||
|
||||
client = MagicMock()
|
||||
monitor = BackendHealthMonitor(
|
||||
client=client,
|
||||
provider="openai",
|
||||
initial_model="model-a",
|
||||
on_model_changed=on_change,
|
||||
)
|
||||
|
||||
# Simulate probe returning a different model
|
||||
resp = MagicMock()
|
||||
m = MagicMock()
|
||||
m.id = "model-b"
|
||||
m.model_dump.return_value = {"max_model_len": 131072}
|
||||
resp.data = [m]
|
||||
|
||||
monitor._check_model_change(resp)
|
||||
assert len(changes) == 1
|
||||
assert changes[0] == ("model-b", 131072)
|
||||
assert monitor._last_detected_model == "model-b"
|
||||
|
||||
def test_same_model_no_callback(self) -> None:
|
||||
from turnstone.core.healthcheck import BackendHealthMonitor
|
||||
|
||||
changes: list[tuple[str, int | None]] = []
|
||||
|
||||
def on_change(model_id: str, ctx: int | None) -> None:
|
||||
changes.append((model_id, ctx))
|
||||
|
||||
client = MagicMock()
|
||||
monitor = BackendHealthMonitor(
|
||||
client=client,
|
||||
provider="openai",
|
||||
initial_model="model-a",
|
||||
on_model_changed=on_change,
|
||||
)
|
||||
|
||||
resp = MagicMock()
|
||||
m = MagicMock()
|
||||
m.id = "model-a"
|
||||
m.model_dump.return_value = {}
|
||||
resp.data = [m]
|
||||
|
||||
monitor._check_model_change(resp)
|
||||
assert len(changes) == 0
|
||||
|
||||
def test_no_callback_configured(self) -> None:
|
||||
from turnstone.core.healthcheck import BackendHealthMonitor
|
||||
|
||||
client = MagicMock()
|
||||
monitor = BackendHealthMonitor(client=client, initial_model="model-a")
|
||||
|
||||
resp = MagicMock()
|
||||
m = MagicMock()
|
||||
m.id = "model-b"
|
||||
resp.data = [m]
|
||||
|
||||
# Should not raise
|
||||
monitor._check_model_change(resp)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from turnstone.core.memory import (
|
||||
count_structured_memories,
|
||||
delete_structured_memory,
|
||||
get_structured_memory_by_name,
|
||||
list_structured_memories,
|
||||
normalize_key,
|
||||
save_structured_memory,
|
||||
@@ -67,6 +68,29 @@ class TestSearchStructuredMemories:
|
||||
assert any(r["name"] == "db_host" for r in results)
|
||||
|
||||
|
||||
class TestGetStructuredMemoryByName:
|
||||
def test_get_existing(self, tmp_db):
|
||||
save_structured_memory("my_mem", "full content here that is quite long")
|
||||
mem = get_structured_memory_by_name("my_mem", "global", "")
|
||||
assert mem is not None
|
||||
assert mem["content"] == "full content here that is quite long"
|
||||
assert mem["name"] == "my_mem"
|
||||
|
||||
def test_get_nonexistent(self, tmp_db):
|
||||
assert get_structured_memory_by_name("nope", "global", "") is None
|
||||
|
||||
def test_get_wrong_scope(self, tmp_db):
|
||||
save_structured_memory("ws_mem", "data", scope="workstream", scope_id="ws1")
|
||||
assert get_structured_memory_by_name("ws_mem", "global", "") is None
|
||||
assert get_structured_memory_by_name("ws_mem", "workstream", "ws1") is not None
|
||||
|
||||
def test_get_normalizes_key(self, tmp_db):
|
||||
save_structured_memory("My-Key", "value")
|
||||
mem = get_structured_memory_by_name("My-Key", "global", "")
|
||||
assert mem is not None
|
||||
assert mem["name"] == "my_key"
|
||||
|
||||
|
||||
class TestCountStructuredMemories:
|
||||
def test_count_zero(self, tmp_db):
|
||||
assert count_structured_memories() == 0
|
||||
@@ -80,3 +104,109 @@ class TestCountStructuredMemories:
|
||||
class TestNormalizeKey:
|
||||
def test_basic(self):
|
||||
assert normalize_key("My-Key Name") == "my_key_name"
|
||||
|
||||
|
||||
class TestScopeIsolation:
|
||||
"""Verify that list/search without scope only returns visible memories.
|
||||
|
||||
Reproduces the cross-workstream leak: unscoped list/search must not
|
||||
return workstream-scoped memories from other workstreams or
|
||||
user-scoped memories from other users.
|
||||
"""
|
||||
|
||||
def _seed(self):
|
||||
"""Create memories across multiple scopes."""
|
||||
save_structured_memory("global_note", "visible to all", scope="global")
|
||||
save_structured_memory("ws1_note", "belongs to ws1", scope="workstream", scope_id="ws1")
|
||||
save_structured_memory("ws2_note", "belongs to ws2", scope="workstream", scope_id="ws2")
|
||||
save_structured_memory("u1_note", "belongs to user1", scope="user", scope_id="u1")
|
||||
save_structured_memory("u2_note", "belongs to user2", scope="user", scope_id="u2")
|
||||
|
||||
@staticmethod
|
||||
def _list_visible(ws_id: str, user_id: str, mem_type: str = "", limit: int = 50):
|
||||
"""Replicate the scope-filtered list logic from ChatSession."""
|
||||
global_mems = list_structured_memories(mem_type=mem_type, scope="global", limit=limit)
|
||||
ws_mems = list_structured_memories(
|
||||
mem_type=mem_type, scope="workstream", scope_id=ws_id, limit=limit
|
||||
)
|
||||
user_mems = (
|
||||
list_structured_memories(mem_type=mem_type, scope="user", scope_id=user_id, limit=limit)
|
||||
if user_id
|
||||
else []
|
||||
)
|
||||
combined = global_mems + ws_mems + user_mems
|
||||
combined.sort(key=lambda m: m.get("updated", ""), reverse=True)
|
||||
return combined[:limit]
|
||||
|
||||
@staticmethod
|
||||
def _search_visible(query: str, ws_id: str, user_id: str, mem_type: str = "", limit: int = 20):
|
||||
"""Replicate the scope-filtered search logic from ChatSession."""
|
||||
global_mems = search_structured_memories(
|
||||
query, mem_type=mem_type, scope="global", limit=limit
|
||||
)
|
||||
ws_mems = search_structured_memories(
|
||||
query, mem_type=mem_type, scope="workstream", scope_id=ws_id, limit=limit
|
||||
)
|
||||
user_mems = (
|
||||
search_structured_memories(
|
||||
query, mem_type=mem_type, scope="user", scope_id=user_id, limit=limit
|
||||
)
|
||||
if user_id
|
||||
else []
|
||||
)
|
||||
combined = global_mems + ws_mems + user_mems
|
||||
combined.sort(key=lambda m: m.get("updated", ""), reverse=True)
|
||||
return combined[:limit]
|
||||
|
||||
def test_unscoped_list_returns_all_scopes(self, tmp_db):
|
||||
"""Demonstrate the leak: unscoped list returns everything."""
|
||||
self._seed()
|
||||
all_mems = list_structured_memories()
|
||||
assert len(all_mems) == 5 # no scope filter → all memories
|
||||
|
||||
def test_visible_list_excludes_other_workstreams(self, tmp_db):
|
||||
"""Scope-filtered list for ws1/u1 excludes ws2 and u2 memories."""
|
||||
self._seed()
|
||||
visible = self._list_visible("ws1", "u1")
|
||||
names = {m["name"] for m in visible}
|
||||
assert "global_note" in names
|
||||
assert "ws1_note" in names
|
||||
assert "u1_note" in names
|
||||
assert "ws2_note" not in names
|
||||
assert "u2_note" not in names
|
||||
|
||||
def test_visible_list_no_user(self, tmp_db):
|
||||
"""Scope-filtered list with no user_id excludes all user memories."""
|
||||
self._seed()
|
||||
visible = self._list_visible("ws1", "")
|
||||
names = {m["name"] for m in visible}
|
||||
assert "global_note" in names
|
||||
assert "ws1_note" in names
|
||||
assert "u1_note" not in names
|
||||
assert "u2_note" not in names
|
||||
|
||||
def test_visible_search_no_user(self, tmp_db):
|
||||
"""Scope-filtered search with no user_id excludes all user memories."""
|
||||
self._seed()
|
||||
visible = self._search_visible("belongs", "ws1", "")
|
||||
names = {m["name"] for m in visible}
|
||||
assert "ws1_note" in names
|
||||
assert "u1_note" not in names
|
||||
assert "u2_note" not in names
|
||||
|
||||
def test_visible_search_excludes_other_workstreams(self, tmp_db):
|
||||
"""Scope-filtered search for ws1/u1 excludes ws2 and u2 memories."""
|
||||
self._seed()
|
||||
visible = self._search_visible("belongs", "ws1", "u1")
|
||||
names = {m["name"] for m in visible}
|
||||
assert "ws1_note" in names
|
||||
assert "u1_note" in names
|
||||
assert "ws2_note" not in names
|
||||
assert "u2_note" not in names
|
||||
|
||||
def test_explicit_scope_still_works(self, tmp_db):
|
||||
"""Explicit scope filter continues to work as before."""
|
||||
self._seed()
|
||||
ws2_only = list_structured_memories(scope="workstream", scope_id="ws2")
|
||||
assert len(ws2_only) == 1
|
||||
assert ws2_only[0]["name"] == "ws2_note"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""turnstone - Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."""
|
||||
|
||||
__version__ = "0.9.4"
|
||||
__version__ = "0.9.6"
|
||||
|
||||
@@ -183,16 +183,21 @@ class MessageCog:
|
||||
auto_archive_duration=self.ts.config.thread_auto_archive, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
# Create workstream with the initial message.
|
||||
# Create workstream WITHOUT initial_message — subscribe to events
|
||||
# first, then send the message. Sending initial_message through
|
||||
# the bridge races with subscription: Redis pub/sub is fire-and-
|
||||
# forget, so response events published before subscribe completes
|
||||
# are silently dropped.
|
||||
ws_id, _is_new = await self.ts.router.get_or_create_workstream(
|
||||
channel_type="discord",
|
||||
channel_id=str(thread.id),
|
||||
name=thread_name,
|
||||
model=self.ts.config.model,
|
||||
initial_message=content,
|
||||
initial_message="",
|
||||
)
|
||||
|
||||
await self.ts.subscribe_ws(ws_id, thread)
|
||||
await self.ts.router.send_message(ws_id, content)
|
||||
log.info(
|
||||
"discord.workstream_created",
|
||||
ws_id=ws_id,
|
||||
@@ -362,10 +367,11 @@ class MessageCog:
|
||||
channel_id=str(thread.id),
|
||||
name=thread_name,
|
||||
model=self.ts.config.model,
|
||||
initial_message=message,
|
||||
initial_message="",
|
||||
)
|
||||
|
||||
await self.ts.subscribe_ws(ws_id, thread)
|
||||
await self.ts.router.send_message(ws_id, message)
|
||||
|
||||
await interaction.followup.send(
|
||||
f"Workstream started in {thread.mention}",
|
||||
|
||||
@@ -5869,8 +5869,31 @@ def main() -> None:
|
||||
log.info("TLS enabled")
|
||||
except ImportError:
|
||||
log.warning("TLS enabled but lacme not installed — pip install turnstone[tls]")
|
||||
tls_mgr = None
|
||||
except Exception:
|
||||
log.warning("TLS initialization failed", exc_info=True)
|
||||
tls_mgr = None
|
||||
|
||||
# Sync TLS state to ConfigStore so server nodes see the correct value.
|
||||
# Three cases:
|
||||
# 1. TLS succeeded → write true
|
||||
# 2. TLS not configured (DB false/unset) → write false (definitive)
|
||||
# 3. TLS configured (DB true) but init failed → don't overwrite
|
||||
# (transient failure shouldn't permanently disable TLS)
|
||||
try:
|
||||
db_enabled = _cs.get("tls.enabled")
|
||||
if tls_mgr is not None:
|
||||
if not db_enabled:
|
||||
_cs.set("tls.enabled", True, changed_by="console-startup")
|
||||
elif db_enabled:
|
||||
log.warning(
|
||||
"tls.enabled is true in ConfigStore but TLS init failed — "
|
||||
"server nodes will attempt TLS and fall back to plain HTTP"
|
||||
)
|
||||
else:
|
||||
_cs.set("tls.enabled", False, changed_by="console-startup")
|
||||
except Exception:
|
||||
log.debug("Failed to sync TLS state to ConfigStore", exc_info=True)
|
||||
|
||||
app = create_app(
|
||||
collector=collector,
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
Header overrides — wider padding for console layout
|
||||
========================================================================== */
|
||||
#header { padding: 10px 20px; gap: 16px; }
|
||||
#status-bar { font-size: 11px; color: var(--fg-dim); margin-left: auto; }
|
||||
#status-bar.disconnected { color: var(--red); }
|
||||
.header-dim {
|
||||
color: var(--fg-dim);
|
||||
font-weight: 400;
|
||||
|
||||
@@ -5,11 +5,13 @@ from __future__ import annotations
|
||||
import enum
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from turnstone.core.log import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
log = get_logger(__name__)
|
||||
@@ -37,6 +39,10 @@ class BackendHealthMonitor:
|
||||
probe_timeout: float = 5.0,
|
||||
failure_threshold: int = 5,
|
||||
cooldown: float = 60.0,
|
||||
*,
|
||||
provider: str = "openai",
|
||||
initial_model: str = "",
|
||||
on_model_changed: Callable[[str, int | None], None] | None = None,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._probe_interval = probe_interval
|
||||
@@ -44,6 +50,11 @@ class BackendHealthMonitor:
|
||||
self._failure_threshold = failure_threshold
|
||||
self._cooldown = cooldown
|
||||
|
||||
# Model change detection
|
||||
self._provider = provider
|
||||
self._last_detected_model = initial_model
|
||||
self._on_model_changed = on_model_changed
|
||||
|
||||
self._lock = threading.Lock()
|
||||
self._state = CircuitState.CLOSED
|
||||
self._consecutive_failures = 0
|
||||
@@ -197,11 +208,39 @@ class BackendHealthMonitor:
|
||||
def _probe_once(self) -> bool:
|
||||
"""Single probe: call ``client.models.list()``. Returns True on success."""
|
||||
try:
|
||||
self._client.with_options(timeout=self._probe_timeout).models.list()
|
||||
resp = self._client.with_options(timeout=self._probe_timeout).models.list()
|
||||
self._check_model_change(resp)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _check_model_change(self, resp: Any) -> None:
|
||||
"""Compare detected model against last known and fire callback if changed."""
|
||||
if not self._on_model_changed or not resp.data:
|
||||
return
|
||||
try:
|
||||
from turnstone.core.model_registry import (
|
||||
_extract_context_window,
|
||||
_select_best_model,
|
||||
)
|
||||
|
||||
all_ids = [m.id for m in resp.data]
|
||||
selected = _select_best_model(all_ids, self._provider)
|
||||
if selected == self._last_detected_model:
|
||||
return
|
||||
model_obj = next((m for m in resp.data if m.id == selected), None)
|
||||
ctx = _extract_context_window(model_obj, self._provider) if model_obj else None
|
||||
log.info(
|
||||
"Backend model changed: %s -> %s (ctx=%s)",
|
||||
self._last_detected_model,
|
||||
selected,
|
||||
ctx,
|
||||
)
|
||||
self._last_detected_model = selected
|
||||
self._on_model_changed(selected, ctx)
|
||||
except Exception:
|
||||
log.debug("Model change check failed", exc_info=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Metrics
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -331,6 +331,18 @@ def save_structured_memory(
|
||||
return "", None
|
||||
|
||||
|
||||
def get_structured_memory_by_name(
|
||||
name: str, scope: str = "global", scope_id: str = ""
|
||||
) -> dict[str, str] | None:
|
||||
"""Retrieve a single structured memory by name+scope. Returns full content."""
|
||||
name = normalize_key(name)
|
||||
try:
|
||||
return get_storage().get_structured_memory_by_name(name, scope, scope_id)
|
||||
except Exception:
|
||||
log.warning("Failed to get structured memory name=%s", name, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def delete_structured_memory(name: str, scope: str = "global", scope_id: str = "") -> bool:
|
||||
"""Delete a structured memory by name+scope. Returns True if existed."""
|
||||
name = normalize_key(name)
|
||||
|
||||
@@ -366,6 +366,28 @@ def _select_best_model(model_ids: list[str], provider: str) -> str:
|
||||
return model_ids[0]
|
||||
|
||||
|
||||
def _extract_context_window(model_obj: Any, provider: str) -> int | None:
|
||||
"""Extract context window from a model object returned by ``/v1/models``.
|
||||
|
||||
Handles Anthropic (static capability table), vLLM (``max_model_len``),
|
||||
and llama.cpp (``meta.n_ctx_train``). Returns ``None`` when not available.
|
||||
"""
|
||||
if provider == "anthropic":
|
||||
from turnstone.core.providers._anthropic import AnthropicProvider
|
||||
|
||||
return AnthropicProvider().get_capabilities(model_obj.id).context_window
|
||||
model_data = model_obj.model_dump()
|
||||
max_len = model_data.get("max_model_len")
|
||||
if isinstance(max_len, int) and max_len > 0:
|
||||
return max_len
|
||||
meta = model_data.get("meta")
|
||||
if isinstance(meta, dict):
|
||||
n_ctx = meta.get("n_ctx_train")
|
||||
if isinstance(n_ctx, int) and n_ctx > 0:
|
||||
return n_ctx
|
||||
return None
|
||||
|
||||
|
||||
def detect_model(
|
||||
client: Any,
|
||||
log_fn: Any = print,
|
||||
@@ -411,18 +433,7 @@ def detect_model(
|
||||
log_fn(f"Available models: {', '.join(all_ids)}")
|
||||
log_fn(f"Using: {m.id} (override with --model)")
|
||||
|
||||
ctx: int | None = None
|
||||
if provider == "anthropic":
|
||||
from turnstone.core.providers._anthropic import AnthropicProvider
|
||||
|
||||
ctx = AnthropicProvider().get_capabilities(m.id).context_window
|
||||
else:
|
||||
# OpenAI-compatible: extract from backend metadata (llama.cpp, vLLM)
|
||||
meta = m.model_dump().get("meta")
|
||||
if isinstance(meta, dict):
|
||||
n_ctx = meta.get("n_ctx_train")
|
||||
if isinstance(n_ctx, int) and n_ctx > 0:
|
||||
ctx = n_ctx
|
||||
ctx = _extract_context_window(m, provider)
|
||||
return m.id, ctx
|
||||
except SystemExit:
|
||||
raise
|
||||
@@ -514,6 +525,7 @@ def _detect_openai_compat(
|
||||
|
||||
meta: dict[str, Any] | None = None
|
||||
owned_by: str = ""
|
||||
dumped: dict[str, Any] = {}
|
||||
if model_obj is not None:
|
||||
dumped = model_obj.model_dump()
|
||||
raw_meta = dumped.get("meta")
|
||||
@@ -521,12 +533,11 @@ def _detect_openai_compat(
|
||||
meta = raw_meta
|
||||
owned_by = str(dumped.get("owned_by", ""))
|
||||
|
||||
# Context window: prefer backend metadata, fall back to static table
|
||||
# (only for known models — the default 200k would be misleading for local servers)
|
||||
if meta is not None:
|
||||
n_ctx = meta.get("n_ctx_train")
|
||||
if isinstance(n_ctx, int) and n_ctx > 0:
|
||||
result["context_window"] = n_ctx
|
||||
# Context window: prefer backend metadata, fall back to static table.
|
||||
if model_obj is not None:
|
||||
ctx = _extract_context_window(model_obj, "openai")
|
||||
if ctx is not None:
|
||||
result["context_window"] = ctx
|
||||
if result["context_window"] is None:
|
||||
from turnstone.core.providers import lookup_model_capabilities
|
||||
|
||||
|
||||
+219
-34
@@ -40,6 +40,7 @@ from turnstone.core.memory import (
|
||||
delete_structured_memory,
|
||||
delete_workstream,
|
||||
get_skill_by_name,
|
||||
get_structured_memory_by_name,
|
||||
get_workstream_display_name,
|
||||
list_default_skills,
|
||||
list_skills_by_activation,
|
||||
@@ -647,6 +648,43 @@ class ChatSession:
|
||||
"""
|
||||
self._init_system_messages()
|
||||
|
||||
def _refresh_model_from_registry(self) -> None:
|
||||
"""Re-resolve model from registry if the backend changed.
|
||||
|
||||
Called at the top of ``send()`` — two string compares when nothing
|
||||
changed, full re-resolve when the health monitor detected a model swap.
|
||||
"""
|
||||
if not self._registry or not self._model_alias:
|
||||
return
|
||||
try:
|
||||
if not self._registry.has_alias(self._model_alias):
|
||||
return
|
||||
cfg = self._registry.get_config(self._model_alias)
|
||||
if cfg.model == self.model:
|
||||
return
|
||||
client, model_name, new_cfg = self._registry.resolve(self._model_alias)
|
||||
except (ValueError, KeyError):
|
||||
return # alias disappeared during concurrent reload
|
||||
self.client = client
|
||||
self.model = model_name
|
||||
self._provider = self._registry.get_provider(self._model_alias)
|
||||
self._cached_capabilities = None
|
||||
if new_cfg.context_window and new_cfg.context_window != self.context_window:
|
||||
self.context_window = new_cfg.context_window
|
||||
# Recompute auto tool truncation for new context window
|
||||
if not self._manual_tool_truncation:
|
||||
self.tool_truncation = int(new_cfg.context_window * self._chars_per_token * 0.5)
|
||||
# Reset judge so it picks up the new model/provider
|
||||
if self._judge is not None:
|
||||
self._judge = None
|
||||
self._init_system_messages()
|
||||
log.info(
|
||||
"session.model_updated ws=%s model=%s ctx=%d",
|
||||
self._ws_id,
|
||||
model_name,
|
||||
self.context_window,
|
||||
)
|
||||
|
||||
def _rebuild_tool_search(self) -> None:
|
||||
"""Reconstruct ToolSearchManager, preserving expanded tools."""
|
||||
old_expanded = self._tool_search.get_expanded_names() if self._tool_search else []
|
||||
@@ -951,9 +989,24 @@ class ChatSession:
|
||||
]
|
||||
else:
|
||||
dev_parts = [
|
||||
"You are an expert software engineer. You solve problems "
|
||||
"by reading code, making targeted edits, and running commands. "
|
||||
"Always respond with tool calls, not just text.\n\n"
|
||||
"You are a resident engineer on a small, focused infrastructure team. "
|
||||
"Your workspace is an instrumented workbench — a terminal with tools for "
|
||||
"reading, writing, searching, and executing code. You've been here a while. "
|
||||
"You know the codebase. You know the tools. You know their limits.\n\n"
|
||||
"Your team trusts you with real work: investigating bugs, implementing features, "
|
||||
"reviewing security, writing code that ships. You have access to the project's "
|
||||
"files, git history, and a running database. You don't have access to everything "
|
||||
"— some tools require approval, some paths are restricted, and that's by design. "
|
||||
"You work within those boundaries.\n\n"
|
||||
"You think before you act. You read before you edit. You verify before you commit. "
|
||||
"When something breaks, you diagnose before you retry. When you're uncertain, you "
|
||||
"say so. When a request is ambiguous, you make a reasonable call and note what you "
|
||||
"assumed — you don't stall asking for permission on every judgment call.\n\n"
|
||||
"When you disagree with a direction, you push back with reasoning — then defer to "
|
||||
"the team's call.\n\n"
|
||||
"You are not performing a demo. There is no audience. The code you write will run. "
|
||||
"The files you edit are real. The commits you make go to a shared repository. "
|
||||
"Act accordingly.\n\n"
|
||||
"TOOL PATTERNS:\n\n"
|
||||
"Modify existing file → read_file then edit_file:\n"
|
||||
" read_file(path='config.py') → "
|
||||
@@ -1093,7 +1146,7 @@ class ChatSession:
|
||||
if self.instructions:
|
||||
dev_parts.append("")
|
||||
dev_parts.append(self.instructions)
|
||||
visible_mems = self._get_visible_memories(limit=self._mem_cfg.fetch_limit)
|
||||
visible_mems = self._list_visible_memories(limit=self._mem_cfg.fetch_limit)
|
||||
if visible_mems:
|
||||
context = extract_recent_context(self.messages)
|
||||
relevant = score_memories(visible_mems, context, k=self._mem_cfg.relevance_k)
|
||||
@@ -1322,6 +1375,7 @@ class ChatSession:
|
||||
|
||||
def send(self, user_input: str) -> None:
|
||||
"""Send user input and handle the response loop (including tool calls)."""
|
||||
self._refresh_model_from_registry()
|
||||
# Token budget approval gate
|
||||
if self._budget_exhausted:
|
||||
approved, _ = self.ui.approve_tools(
|
||||
@@ -3566,17 +3620,6 @@ class ChatSession:
|
||||
}
|
||||
return None
|
||||
|
||||
def _get_visible_memories(self, limit: int = 50) -> list[dict[str, str]]:
|
||||
"""Return memories visible to this session (scope-filtered)."""
|
||||
global_mems = list_structured_memories(scope="global", limit=limit)
|
||||
ws_mems = list_structured_memories(scope="workstream", scope_id=self._ws_id, limit=limit)
|
||||
user_mems: list[dict[str, str]] = []
|
||||
if self._user_id:
|
||||
user_mems = list_structured_memories(scope="user", scope_id=self._user_id, limit=limit)
|
||||
combined = global_mems + ws_mems + user_mems
|
||||
combined.sort(key=lambda m: m.get("updated", ""), reverse=True)
|
||||
return combined[:limit]
|
||||
|
||||
def _visible_memory_count(self) -> int:
|
||||
"""Count memories visible to this session (cheap — counts only)."""
|
||||
n = count_structured_memories(scope="global")
|
||||
@@ -3585,6 +3628,40 @@ class ChatSession:
|
||||
n += count_structured_memories(scope="user", scope_id=self._user_id)
|
||||
return n
|
||||
|
||||
def _list_visible_memories(self, mem_type: str = "", limit: int = 50) -> list[dict[str, str]]:
|
||||
"""List memories visible to this session with optional type filter."""
|
||||
global_mems = list_structured_memories(mem_type=mem_type, scope="global", limit=limit)
|
||||
ws_mems = list_structured_memories(
|
||||
mem_type=mem_type, scope="workstream", scope_id=self._ws_id, limit=limit
|
||||
)
|
||||
user_mems: list[dict[str, str]] = []
|
||||
if self._user_id:
|
||||
user_mems = list_structured_memories(
|
||||
mem_type=mem_type, scope="user", scope_id=self._user_id, limit=limit
|
||||
)
|
||||
combined = global_mems + ws_mems + user_mems
|
||||
combined.sort(key=lambda m: m.get("updated", ""), reverse=True)
|
||||
return combined[:limit]
|
||||
|
||||
def _search_visible_memories(
|
||||
self, query: str, mem_type: str = "", limit: int = 20
|
||||
) -> list[dict[str, str]]:
|
||||
"""Search memories visible to this session (scope-filtered)."""
|
||||
global_mems = search_structured_memories(
|
||||
query, mem_type=mem_type, scope="global", limit=limit
|
||||
)
|
||||
ws_mems = search_structured_memories(
|
||||
query, mem_type=mem_type, scope="workstream", scope_id=self._ws_id, limit=limit
|
||||
)
|
||||
user_mems: list[dict[str, str]] = []
|
||||
if self._user_id:
|
||||
user_mems = search_structured_memories(
|
||||
query, mem_type=mem_type, scope="user", scope_id=self._user_id, limit=limit
|
||||
)
|
||||
combined = global_mems + ws_mems + user_mems
|
||||
combined.sort(key=lambda m: m.get("updated", ""), reverse=True)
|
||||
return combined[:limit]
|
||||
|
||||
def _check_metacognitive_nudge(self, user_message: str) -> tuple[str, str] | None:
|
||||
"""Check if a metacognitive nudge should be injected.
|
||||
|
||||
@@ -3626,7 +3703,7 @@ class ChatSession:
|
||||
return None
|
||||
|
||||
def _prepare_memory(self, call_id: str, args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Prepare a memory tool action (save/search/delete/list)."""
|
||||
"""Prepare a memory tool action (save/get/search/delete/list)."""
|
||||
action = (args.get("action") or "").strip().lower()
|
||||
|
||||
if action == "save":
|
||||
@@ -3687,6 +3764,51 @@ class ChatSession:
|
||||
"scope_id": scope_id,
|
||||
}
|
||||
|
||||
if action == "get":
|
||||
name = normalize_key((args.get("name") or args.get("key") or "").strip())
|
||||
if not name:
|
||||
return {
|
||||
"call_id": call_id,
|
||||
"func_name": "memory",
|
||||
"header": "\u2717 memory get: missing name",
|
||||
"preview": "",
|
||||
"needs_approval": False,
|
||||
"error": "Error: 'name' is required for get",
|
||||
}
|
||||
explicit_scope = (args.get("scope") or "").strip().lower()
|
||||
valid_scopes = ("global", "workstream", "user")
|
||||
if explicit_scope and explicit_scope not in valid_scopes:
|
||||
return {
|
||||
"call_id": call_id,
|
||||
"func_name": "memory",
|
||||
"header": "\u2717 memory get: invalid scope",
|
||||
"preview": "",
|
||||
"needs_approval": False,
|
||||
"error": f"Error: invalid scope '{explicit_scope}'. Valid: {', '.join(valid_scopes)}",
|
||||
}
|
||||
if explicit_scope:
|
||||
scope_err = self._validate_scope(explicit_scope, call_id)
|
||||
if scope_err:
|
||||
return scope_err
|
||||
scopes_to_try = [(explicit_scope, self._resolve_scope_id(explicit_scope))]
|
||||
else:
|
||||
scopes_to_try = []
|
||||
for s in ("workstream", "user", "global"):
|
||||
sid = self._resolve_scope_id(s)
|
||||
if sid or s == "global":
|
||||
scopes_to_try.append((s, sid))
|
||||
return {
|
||||
"call_id": call_id,
|
||||
"func_name": "memory",
|
||||
"header": f"\u2699 memory get: {name}",
|
||||
"preview": "",
|
||||
"needs_approval": False,
|
||||
"execute": self._exec_memory,
|
||||
"action": "get",
|
||||
"name": name,
|
||||
"scopes_to_try": scopes_to_try,
|
||||
}
|
||||
|
||||
if action == "delete":
|
||||
name = normalize_key((args.get("name") or args.get("key") or "").strip())
|
||||
if not name:
|
||||
@@ -3745,6 +3867,10 @@ class ChatSession:
|
||||
scope = (args.get("scope") or "").strip().lower()
|
||||
if scope and scope not in ("global", "workstream", "user"):
|
||||
scope = ""
|
||||
if scope:
|
||||
scope_err = self._validate_scope(scope, call_id)
|
||||
if scope_err:
|
||||
return scope_err
|
||||
scope_id = self._resolve_scope_id(scope) if scope else ""
|
||||
limit = args.get("limit", 20)
|
||||
if isinstance(limit, str):
|
||||
@@ -3774,6 +3900,10 @@ class ChatSession:
|
||||
scope = (args.get("scope") or "").strip().lower()
|
||||
if scope and scope not in ("global", "workstream", "user"):
|
||||
scope = ""
|
||||
if scope:
|
||||
scope_err = self._validate_scope(scope, call_id)
|
||||
if scope_err:
|
||||
return scope_err
|
||||
scope_id = self._resolve_scope_id(scope) if scope else ""
|
||||
limit = args.get("limit", 20)
|
||||
if isinstance(limit, str):
|
||||
@@ -3801,7 +3931,7 @@ class ChatSession:
|
||||
"header": "\u2717 memory: invalid action",
|
||||
"preview": "",
|
||||
"needs_approval": False,
|
||||
"error": f"Error: action must be save/search/delete/list, got '{action}'",
|
||||
"error": f"Error: action must be save/get/search/delete/list, got '{action}'",
|
||||
}
|
||||
|
||||
def _prepare_recall(self, call_id: str, args: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -5075,6 +5205,29 @@ class ChatSession:
|
||||
self._report_tool_result(call_id, "memory", msg)
|
||||
return call_id, msg
|
||||
|
||||
if action == "get":
|
||||
scopes = item["scopes_to_try"]
|
||||
mem = None
|
||||
found_scope = ""
|
||||
for scope, scope_id in scopes:
|
||||
mem = get_structured_memory_by_name(item["name"], scope, scope_id)
|
||||
if mem:
|
||||
found_scope = scope
|
||||
break
|
||||
if mem:
|
||||
content = mem.get("content", "")
|
||||
desc = mem.get("description", "")
|
||||
mem_type = mem.get("type", "")
|
||||
header = f"[{mem_type}:{found_scope}] {item['name']}"
|
||||
if desc:
|
||||
header += f" — {desc}"
|
||||
msg = f"{header}\n\n{content}"
|
||||
else:
|
||||
tried = ", ".join(s for s, _ in scopes)
|
||||
msg = f"Error: memory '{item['name']}' not found (searched scopes: {tried})"
|
||||
self._report_tool_result(call_id, "memory", msg, is_error=mem is None)
|
||||
return call_id, msg
|
||||
|
||||
if action == "delete":
|
||||
scopes = item["scopes_to_try"]
|
||||
deleted = False
|
||||
@@ -5095,22 +5248,39 @@ class ChatSession:
|
||||
return call_id, msg
|
||||
|
||||
if action == "search":
|
||||
rows = search_structured_memories(
|
||||
item["query"],
|
||||
mem_type=item.get("mem_type", ""),
|
||||
scope=item.get("scope", ""),
|
||||
scope_id=item.get("scope_id", ""),
|
||||
limit=item["limit"],
|
||||
)
|
||||
scope = item.get("scope", "")
|
||||
scope_id = item.get("scope_id", "")
|
||||
# Defense-in-depth: reject scoped queries with empty scope_id
|
||||
if scope in ("user", "workstream") and not scope_id:
|
||||
msg = f"Error: '{scope}' scope requires a valid identity"
|
||||
self._report_tool_result(call_id, "memory", msg, is_error=True)
|
||||
return call_id, msg
|
||||
if scope:
|
||||
rows = search_structured_memories(
|
||||
item["query"],
|
||||
mem_type=item.get("mem_type", ""),
|
||||
scope=scope,
|
||||
scope_id=scope_id,
|
||||
limit=item["limit"],
|
||||
)
|
||||
else:
|
||||
rows = self._search_visible_memories(
|
||||
item["query"],
|
||||
mem_type=item.get("mem_type", ""),
|
||||
limit=item["limit"],
|
||||
)
|
||||
if rows:
|
||||
lines = []
|
||||
for m in rows:
|
||||
desc = f" — {m['description']}" if m.get("description") else ""
|
||||
preview = m["content"][:200]
|
||||
if len(m["content"]) > 200:
|
||||
preview += "..."
|
||||
lines.append(
|
||||
f" [{m['type']}:{m['scope']}] {m['name']}{desc}\n"
|
||||
f" {m['content'][:500]}"
|
||||
f" [{m['type']}:{m['scope']}] {m['name']}{desc}\n {preview}"
|
||||
)
|
||||
msg = f"Memories ({len(rows)} results):\n" + "\n".join(lines)
|
||||
msg += "\n\nUse memory(action='get', name='...') for full content."
|
||||
else:
|
||||
msg = (
|
||||
f"No memories found for '{item['query']}'."
|
||||
@@ -5121,21 +5291,36 @@ class ChatSession:
|
||||
return call_id, msg
|
||||
|
||||
if action == "list":
|
||||
rows = list_structured_memories(
|
||||
mem_type=item.get("mem_type", ""),
|
||||
scope=item.get("scope", ""),
|
||||
scope_id=item.get("scope_id", ""),
|
||||
limit=item["limit"],
|
||||
)
|
||||
scope = item.get("scope", "")
|
||||
scope_id = item.get("scope_id", "")
|
||||
if scope in ("user", "workstream") and not scope_id:
|
||||
msg = f"Error: '{scope}' scope requires a valid identity"
|
||||
self._report_tool_result(call_id, "memory", msg, is_error=True)
|
||||
return call_id, msg
|
||||
if scope:
|
||||
rows = list_structured_memories(
|
||||
mem_type=item.get("mem_type", ""),
|
||||
scope=scope,
|
||||
scope_id=scope_id,
|
||||
limit=item["limit"],
|
||||
)
|
||||
else:
|
||||
rows = self._list_visible_memories(
|
||||
mem_type=item.get("mem_type", ""),
|
||||
limit=item["limit"],
|
||||
)
|
||||
if rows:
|
||||
lines = []
|
||||
for m in rows:
|
||||
desc = f" — {m['description']}" if m.get("description") else ""
|
||||
preview = m["content"][:200]
|
||||
if len(m["content"]) > 200:
|
||||
preview += "..."
|
||||
lines.append(
|
||||
f" [{m['type']}:{m['scope']}] {m['name']}{desc}\n"
|
||||
f" {m['content'][:500]}"
|
||||
f" [{m['type']}:{m['scope']}] {m['name']}{desc}\n {preview}"
|
||||
)
|
||||
msg = f"Memories ({len(rows)}):\n" + "\n".join(lines)
|
||||
msg += "\n\nUse memory(action='get', name='...') for full content."
|
||||
else:
|
||||
msg = "No memories stored."
|
||||
self._report_tool_result(call_id, "memory", msg)
|
||||
|
||||
@@ -72,6 +72,7 @@ from turnstone.core.storage._utils import (
|
||||
from turnstone.core.storage._utils import (
|
||||
row_to_dict as _row_to_dict,
|
||||
)
|
||||
from turnstone.core.storage._utils import sanitize_text
|
||||
from turnstone.core.storage._utils import (
|
||||
scan_skill_content as _scan_skill_content,
|
||||
)
|
||||
@@ -112,6 +113,8 @@ class PostgreSQLBackend:
|
||||
tool_calls: str | None = None,
|
||||
) -> None:
|
||||
now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S")
|
||||
content = sanitize_text(content)
|
||||
provider_data = sanitize_text(provider_data)
|
||||
with self._engine.connect() as conn:
|
||||
conn.execute(
|
||||
sa.insert(conversations),
|
||||
|
||||
@@ -72,6 +72,7 @@ from turnstone.core.storage._utils import (
|
||||
from turnstone.core.storage._utils import (
|
||||
row_to_dict as _row_to_dict,
|
||||
)
|
||||
from turnstone.core.storage._utils import sanitize_text
|
||||
from turnstone.core.storage._utils import (
|
||||
scan_skill_content as _scan_skill_content,
|
||||
)
|
||||
@@ -163,6 +164,8 @@ class SQLiteBackend:
|
||||
tool_calls: str | None = None,
|
||||
) -> None:
|
||||
now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S")
|
||||
content = sanitize_text(content)
|
||||
provider_data = sanitize_text(provider_data)
|
||||
with self._engine.connect() as conn:
|
||||
result = conn.execute(
|
||||
sa.insert(conversations),
|
||||
|
||||
@@ -10,6 +10,22 @@ from turnstone.core.log import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Text sanitization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sanitize_text(value: str | None) -> str | None:
|
||||
"""Strip NUL bytes that PostgreSQL text fields cannot store.
|
||||
|
||||
SQLite tolerates NUL in TEXT but they cause downstream issues (API
|
||||
payloads, web UI rendering), so both backends use this.
|
||||
"""
|
||||
if value and "\x00" in value:
|
||||
return value.replace("\x00", "")
|
||||
return value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Row helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -665,6 +665,8 @@ class Bridge:
|
||||
effort=data.get("effort", ""),
|
||||
cache_creation_tokens=data.get("cache_creation_tokens", 0),
|
||||
cache_read_tokens=data.get("cache_read_tokens", 0),
|
||||
tool_calls_this_turn=data.get("tool_calls_this_turn", 0),
|
||||
turn_count=data.get("turn_count", 0),
|
||||
),
|
||||
)
|
||||
elif etype == "error":
|
||||
|
||||
@@ -252,6 +252,8 @@ class StatusEvent(OutboundEvent):
|
||||
effort: str = ""
|
||||
cache_creation_tokens: int = 0
|
||||
cache_read_tokens: int = 0
|
||||
tool_calls_this_turn: int = 0
|
||||
turn_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -128,6 +128,8 @@ class StatusEvent(ServerEvent):
|
||||
effort: str = ""
|
||||
cache_creation_tokens: int = 0
|
||||
cache_read_tokens: int = 0
|
||||
tool_calls_this_turn: int = 0
|
||||
turn_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+72
-2
@@ -102,6 +102,7 @@ class WebUI:
|
||||
self._ws_tool_calls: dict[str, int] = {}
|
||||
self._ws_tool_calls_reported: int = 0 # last cumulative total sent to usage
|
||||
self._ws_context_ratio: float = 0.0
|
||||
self._ws_turn_tool_calls: int = 0
|
||||
# Activity tracking for dashboard (current tool / thinking / approval)
|
||||
self._ws_current_activity: str = ""
|
||||
self._ws_activity_state: str = "" # "tool" | "approval" | "thinking" | ""
|
||||
@@ -393,6 +394,7 @@ class WebUI:
|
||||
_metrics.record_tool_call(name)
|
||||
with self._ws_lock:
|
||||
self._ws_tool_calls[name] = self._ws_tool_calls.get(name, 0) + 1
|
||||
self._ws_turn_tool_calls += 1
|
||||
self._ws_current_activity = ""
|
||||
self._ws_activity_state = ""
|
||||
self._broadcast_activity()
|
||||
@@ -424,6 +426,8 @@ class WebUI:
|
||||
tool_total = sum(self._ws_tool_calls.values())
|
||||
tool_count = tool_total - self._ws_tool_calls_reported
|
||||
self._ws_tool_calls_reported = tool_total
|
||||
turn_tool_calls = self._ws_turn_tool_calls
|
||||
turn_count = self._ws_messages
|
||||
self._enqueue(
|
||||
{
|
||||
"type": "status",
|
||||
@@ -435,6 +439,8 @@ class WebUI:
|
||||
"effort": effort,
|
||||
"cache_creation_tokens": cache_creation,
|
||||
"cache_read_tokens": cache_read,
|
||||
"tool_calls_this_turn": turn_tool_calls,
|
||||
"turn_count": turn_count,
|
||||
}
|
||||
)
|
||||
# Record usage event for governance dashboard
|
||||
@@ -856,6 +862,32 @@ async def events_sse(request: Request) -> Response:
|
||||
}
|
||||
)
|
||||
}
|
||||
# Replay last status so the per-pane status bar populates on resume
|
||||
if session._last_usage is not None:
|
||||
u = session._last_usage
|
||||
total_tok = u["prompt_tokens"] + u["completion_tokens"]
|
||||
cw = session.context_window
|
||||
pct = total_tok / cw * 100 if cw > 0 else 0
|
||||
with ui._ws_lock:
|
||||
turn_tool_calls = ui._ws_turn_tool_calls
|
||||
turn_count = ui._ws_messages
|
||||
yield {
|
||||
"data": json.dumps(
|
||||
{
|
||||
"type": "status",
|
||||
"prompt_tokens": u["prompt_tokens"],
|
||||
"completion_tokens": u["completion_tokens"],
|
||||
"total_tokens": total_tok,
|
||||
"context_window": cw,
|
||||
"pct": round(pct, 1),
|
||||
"effort": session.reasoning_effort,
|
||||
"cache_creation_tokens": u.get("cache_creation_tokens", 0),
|
||||
"cache_read_tokens": u.get("cache_read_tokens", 0),
|
||||
"tool_calls_this_turn": turn_tool_calls,
|
||||
"turn_count": turn_count,
|
||||
}
|
||||
)
|
||||
}
|
||||
# History replay
|
||||
history = _build_history(session, has_pending_approval=ui._pending_approval is not None)
|
||||
if history:
|
||||
@@ -1240,6 +1272,7 @@ async def send_message(request: Request) -> JSONResponse:
|
||||
_metrics.record_message_sent()
|
||||
with ui._ws_lock:
|
||||
ui._ws_messages += 1
|
||||
ui._ws_turn_tool_calls = 0
|
||||
return JSONResponse({"status": "ok"})
|
||||
|
||||
|
||||
@@ -2444,12 +2477,43 @@ def main() -> None:
|
||||
# Backend health monitor with circuit breaker
|
||||
from turnstone.core.healthcheck import BackendHealthMonitor
|
||||
|
||||
def _handle_model_change(new_model_id: str, new_ctx: int | None) -> None:
|
||||
"""Called from health probe thread when backend model changes."""
|
||||
cli_args = getattr(getattr(app, "state", None), "cli_model_args", None)
|
||||
if not cli_args or cli_args.get("_user_specified_model"):
|
||||
return
|
||||
old_model = cli_args["model"]
|
||||
ctx = new_ctx or cli_args["context_window"]
|
||||
log.info("Backend model changed: %s -> %s (ctx=%s)", old_model, new_model_id, ctx)
|
||||
new_reg = None
|
||||
try:
|
||||
new_reg = load_model_registry(
|
||||
base_url=cli_args["base_url"],
|
||||
api_key=cli_args["api_key"],
|
||||
model=new_model_id,
|
||||
context_window=ctx,
|
||||
provider=cli_args["provider"],
|
||||
storage=get_storage(),
|
||||
)
|
||||
registry.reload(new_reg.models, new_reg.default, new_reg.fallback, new_reg.agent_model)
|
||||
# Update cli_model_args only after successful reload
|
||||
cli_args["model"] = new_model_id
|
||||
cli_args["context_window"] = ctx
|
||||
except Exception:
|
||||
log.warning("Model change reload failed", exc_info=True)
|
||||
finally:
|
||||
if new_reg is not None:
|
||||
new_reg.shutdown()
|
||||
|
||||
health_monitor = BackendHealthMonitor(
|
||||
client=client,
|
||||
probe_interval=config_store.get("health.backend_probe_interval"),
|
||||
probe_timeout=config_store.get("health.backend_probe_timeout"),
|
||||
failure_threshold=config_store.get("health.circuit_breaker_threshold"),
|
||||
cooldown=config_store.get("health.circuit_breaker_cooldown"),
|
||||
provider=provider_name,
|
||||
initial_model=model,
|
||||
on_model_changed=_handle_model_change,
|
||||
)
|
||||
health_monitor.start()
|
||||
|
||||
@@ -2668,6 +2732,7 @@ def main() -> None:
|
||||
"model": model,
|
||||
"context_window": context_window,
|
||||
"provider": provider_name,
|
||||
"_user_specified_model": bool(effective_model),
|
||||
}
|
||||
|
||||
log.info("Server starting on http://%s:%s", args.host, args.port)
|
||||
@@ -2735,8 +2800,13 @@ def main() -> None:
|
||||
log.info("TLS enabled — serving HTTPS")
|
||||
else:
|
||||
log.warning("TLS enabled but no cert available")
|
||||
except Exception:
|
||||
log.warning("TLS initialization failed — serving plain HTTP", exc_info=True)
|
||||
except Exception as exc:
|
||||
log.warning(
|
||||
"TLS initialization failed — serving plain HTTP: %s: %s",
|
||||
type(exc).__name__,
|
||||
exc,
|
||||
)
|
||||
log.debug("TLS init traceback", exc_info=True)
|
||||
|
||||
print("Press Ctrl+C to stop.")
|
||||
|
||||
|
||||
@@ -133,8 +133,6 @@ body {
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
#status-bar { font-size: 11px; color: var(--fg-dim); margin-left: auto; }
|
||||
#status-bar.disconnected { color: var(--red); }
|
||||
|
||||
.header-btn {
|
||||
background: none;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "memory",
|
||||
"description": "Persistent memory across sessions. Actions: 'save' stores a memory, 'search' finds memories by query, 'delete' removes a memory, 'list' shows all memories. Memories have a type (user/project/feedback/reference) and scope (global/workstream/user).",
|
||||
"description": "Persistent memory across sessions. Actions: 'save' stores a memory, 'get' retrieves full content by name, 'search' finds memories by query, 'delete' removes a memory, 'list' shows all memories. Use 'get' to read full content — search/list truncate previews to 200 chars. Memories have a type (user/project/feedback/reference) and scope (global/workstream/user).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["save", "search", "delete", "list"],
|
||||
"enum": ["save", "get", "search", "delete", "list"],
|
||||
"description": "Action to perform."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Memory identifier (required for 'save' and 'delete'). Short snake_case key."
|
||||
"description": "Memory identifier (required for 'save', 'get', and 'delete'). Short snake_case key."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
|
||||
+86
-138
@@ -29,7 +29,7 @@ function Pane(wsId) {
|
||||
this.retryDelay = 1000;
|
||||
this.model = "";
|
||||
this.modelAlias = "";
|
||||
this.statusText = "";
|
||||
this._lastStatusEvt = null;
|
||||
this._cancelTimeout = null;
|
||||
this._forceTimeout = null;
|
||||
this._pendingEditSend = null;
|
||||
@@ -134,6 +134,37 @@ Pane.prototype._createDOM = function () {
|
||||
this.messagesEl.setAttribute("aria-label", "Chat messages");
|
||||
this.el.appendChild(this.messagesEl);
|
||||
|
||||
// Per-workstream status bar (above input)
|
||||
this.statusBarEl = document.createElement("div");
|
||||
this.statusBarEl.className = "ws-status-bar";
|
||||
this.statusBarEl.setAttribute("role", "status");
|
||||
this.statusBarEl.setAttribute("aria-live", "polite");
|
||||
this.statusBarEl.setAttribute("aria-atomic", "true");
|
||||
this.statusBarEl.setAttribute("aria-label", "Workstream status");
|
||||
|
||||
this._sbModel = document.createElement("span");
|
||||
this._sbModel.className = "ws-sb-model";
|
||||
this._sbModel.textContent = "\u2014";
|
||||
this._sbModel.setAttribute("aria-label", "Model");
|
||||
this._sbTokens = document.createElement("span");
|
||||
this._sbTokens.className = "ws-sb-tokens";
|
||||
this._sbTokens.textContent = "0 / \u2014";
|
||||
this._sbTokens.setAttribute("aria-label", "Token usage");
|
||||
this._sbTools = document.createElement("span");
|
||||
this._sbTools.className = "ws-sb-tools";
|
||||
this._sbTools.textContent = "0 tools";
|
||||
this._sbTools.setAttribute("aria-label", "Tool calls this turn");
|
||||
this._sbTurns = document.createElement("span");
|
||||
this._sbTurns.className = "ws-sb-turns";
|
||||
this._sbTurns.textContent = "turn 0";
|
||||
this._sbTurns.setAttribute("aria-label", "Conversation turn");
|
||||
|
||||
this.statusBarEl.appendChild(this._sbModel);
|
||||
this.statusBarEl.appendChild(this._sbTokens);
|
||||
this.statusBarEl.appendChild(this._sbTools);
|
||||
this.statusBarEl.appendChild(this._sbTurns);
|
||||
this.el.appendChild(this.statusBarEl);
|
||||
|
||||
// Input area
|
||||
var inputArea = document.createElement("div");
|
||||
inputArea.className = "pane-input-area";
|
||||
@@ -248,11 +279,8 @@ Pane.prototype.connectSSE = function (wsId) {
|
||||
|
||||
this.evtSource.onopen = function () {
|
||||
self.retryDelay = 1000;
|
||||
if (self.id === focusedPaneId) {
|
||||
var statusBar = document.getElementById("status-bar");
|
||||
statusBar.classList.remove("disconnected");
|
||||
statusBar.textContent = self.statusText || "";
|
||||
}
|
||||
self.statusBarEl.classList.remove("ws-sb-disconnected");
|
||||
if (self._lastStatusEvt) self.updateStatus(self._lastStatusEvt);
|
||||
};
|
||||
|
||||
this.evtSource.onmessage = function (e) {
|
||||
@@ -265,11 +293,8 @@ Pane.prototype.connectSSE = function (wsId) {
|
||||
self.evtSource = null;
|
||||
var loginOverlay = document.getElementById("login-overlay");
|
||||
if (loginOverlay && loginOverlay.style.display !== "none") return;
|
||||
if (self.id === focusedPaneId) {
|
||||
var statusBar = document.getElementById("status-bar");
|
||||
statusBar.textContent = "Reconnecting\u2026";
|
||||
statusBar.classList.add("disconnected");
|
||||
}
|
||||
self.statusBarEl.classList.add("ws-sb-disconnected");
|
||||
self._sbTokens.textContent = "Reconnecting\u2026";
|
||||
// Only the focused pane refreshes the global workstream list to avoid
|
||||
// race conditions when multiple panes disconnect simultaneously.
|
||||
if (self.id === focusedPaneId) {
|
||||
@@ -514,9 +539,8 @@ Pane.prototype.handleEvent = function (evt) {
|
||||
case "connected":
|
||||
this.model = evt.model || "";
|
||||
this.modelAlias = evt.model_alias || evt.model || "";
|
||||
if (this.id === focusedPaneId) {
|
||||
updateHeaderForFocusedPane();
|
||||
}
|
||||
this._sbModel.textContent = this.modelAlias || this.model || "";
|
||||
this._sbModel.title = this.model || "";
|
||||
if (evt.skip_permissions) {
|
||||
var existing = document.querySelector(".skip-permissions-warning");
|
||||
if (!existing) {
|
||||
@@ -1314,19 +1338,32 @@ Pane.prototype.addErrorMessage = function (text) {
|
||||
};
|
||||
|
||||
Pane.prototype.updateStatus = function (evt) {
|
||||
var parts = [
|
||||
this._sbModel.textContent = this.modelAlias || this.model || "";
|
||||
this._sbModel.title = this.model || "";
|
||||
|
||||
var tokenText =
|
||||
evt.total_tokens.toLocaleString() +
|
||||
" / " +
|
||||
evt.context_window.toLocaleString() +
|
||||
" tokens (" +
|
||||
evt.pct +
|
||||
"%)",
|
||||
];
|
||||
if (evt.effort !== "medium") parts.push("reasoning: " + evt.effort);
|
||||
this.statusText = parts.join(" \u00b7 ");
|
||||
if (this.id === focusedPaneId) {
|
||||
document.getElementById("status-bar").textContent = this.statusText;
|
||||
}
|
||||
" / " +
|
||||
evt.context_window.toLocaleString() +
|
||||
" (" +
|
||||
evt.pct +
|
||||
"%)";
|
||||
if (evt.effort && evt.effort !== "medium")
|
||||
tokenText += " \u00b7 " + evt.effort;
|
||||
if (evt.pct >= 95) tokenText = "\u26a0 " + tokenText;
|
||||
else if (evt.pct >= 80) tokenText = "\u25b2 " + tokenText;
|
||||
this._sbTokens.textContent = tokenText;
|
||||
|
||||
var tc = evt.tool_calls_this_turn || 0;
|
||||
this._sbTools.textContent = tc + " tool" + (tc !== 1 ? "s" : "");
|
||||
|
||||
var turns = evt.turn_count || 0;
|
||||
this._sbTurns.textContent = "turn " + turns;
|
||||
|
||||
this.statusBarEl.classList.toggle("ws-sb-warn", evt.pct >= 80);
|
||||
this.statusBarEl.classList.toggle("ws-sb-danger", evt.pct >= 95);
|
||||
|
||||
this._lastStatusEvt = evt;
|
||||
};
|
||||
|
||||
Pane.prototype.isNearBottom = function () {
|
||||
@@ -1437,7 +1474,6 @@ function setFocusedPane(paneId) {
|
||||
panes[paneId].el.classList.add("focused");
|
||||
currentWsId = panes[paneId].wsId;
|
||||
renderTabBar();
|
||||
updateHeaderForFocusedPane();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1447,17 +1483,6 @@ function createPane(wsId) {
|
||||
return p;
|
||||
}
|
||||
|
||||
function updateHeaderForFocusedPane() {
|
||||
var pane = getFocusedPane();
|
||||
var modelName = document.getElementById("model-name");
|
||||
var statusBar = document.getElementById("status-bar");
|
||||
if (pane) {
|
||||
modelName.textContent = pane.modelAlias || pane.model || "";
|
||||
modelName.title = pane.model || "";
|
||||
statusBar.textContent = pane.statusText || "";
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaneHeaders() {
|
||||
var root = document.getElementById("split-root");
|
||||
var leafCount = countLeaves(splitRoot);
|
||||
@@ -2126,101 +2151,34 @@ window.onLogout = function () {
|
||||
};
|
||||
|
||||
// ===========================================================================
|
||||
// 7. Theme + hamburger menu
|
||||
// 7. Theme toggle
|
||||
// ===========================================================================
|
||||
|
||||
function updateThemeMenuItem() {
|
||||
var isLight = document.documentElement.dataset.theme === "light";
|
||||
document.getElementById("theme-menu-icon").textContent = isLight
|
||||
? "\u263E"
|
||||
: "\u2600";
|
||||
document.getElementById("theme-menu-label").textContent = isLight
|
||||
? "Dark mode"
|
||||
: "Light mode";
|
||||
document
|
||||
.getElementById("theme-menu-item")
|
||||
.setAttribute(
|
||||
window.onThemeChange = function (next) {
|
||||
var btn = document.getElementById("theme-toggle");
|
||||
if (btn) {
|
||||
var isLight = next === "light";
|
||||
btn.textContent = isLight ? "\u2600" : "\u263E";
|
||||
btn.title = isLight ? "Switch to dark theme" : "Switch to light theme";
|
||||
btn.setAttribute(
|
||||
"aria-label",
|
||||
isLight
|
||||
? "Switch to dark mode (currently light)"
|
||||
: "Switch to light mode (currently dark)",
|
||||
isLight ? "Switch to dark theme" : "Switch to light theme",
|
||||
);
|
||||
}
|
||||
window.onThemeChange = function () {
|
||||
updateThemeMenuItem();
|
||||
}
|
||||
reRenderAllMermaid();
|
||||
};
|
||||
updateThemeMenuItem();
|
||||
|
||||
function toggleHamburger() {
|
||||
var menu = document.getElementById("hamburger-menu");
|
||||
var btn = document.getElementById("hamburger-btn");
|
||||
var open = menu.classList.toggle("open");
|
||||
btn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
if (open) {
|
||||
updateThemeMenuItem();
|
||||
var first = menu.querySelector(".hmenu-item");
|
||||
if (first) first.focus();
|
||||
(function () {
|
||||
var btn = document.getElementById("theme-toggle");
|
||||
if (btn) {
|
||||
var isLight = document.documentElement.dataset.theme === "light";
|
||||
btn.textContent = isLight ? "\u2600" : "\u263E";
|
||||
btn.title = isLight ? "Switch to dark theme" : "Switch to light theme";
|
||||
btn.setAttribute(
|
||||
"aria-label",
|
||||
isLight ? "Switch to dark theme" : "Switch to light theme",
|
||||
);
|
||||
}
|
||||
}
|
||||
function closeHamburger() {
|
||||
document.getElementById("hamburger-menu").classList.remove("open");
|
||||
document
|
||||
.getElementById("hamburger-btn")
|
||||
.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
function hamburgerDashboard() {
|
||||
closeHamburger();
|
||||
toggleDashboard();
|
||||
}
|
||||
function hamburgerTheme() {
|
||||
toggleTheme();
|
||||
closeHamburger();
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener("click", function (e) {
|
||||
var wrap = document.getElementById("hamburger-wrap");
|
||||
if (wrap && !wrap.contains(e.target)) closeHamburger();
|
||||
});
|
||||
// Keyboard nav within menu
|
||||
document
|
||||
.getElementById("hamburger-menu")
|
||||
.addEventListener("keydown", function (e) {
|
||||
var items = Array.from(this.querySelectorAll(".hmenu-item"));
|
||||
var idx = items.indexOf(document.activeElement);
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
items[(idx + 1) % items.length].focus();
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
items[(idx - 1 + items.length) % items.length].focus();
|
||||
} else if (e.key === "Home") {
|
||||
e.preventDefault();
|
||||
items[0].focus();
|
||||
} else if (e.key === "End") {
|
||||
e.preventDefault();
|
||||
items[items.length - 1].focus();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
closeHamburger();
|
||||
document.getElementById("hamburger-btn").focus();
|
||||
} else if (e.key === "Tab") {
|
||||
closeHamburger();
|
||||
}
|
||||
});
|
||||
// Escape when focus is on the button itself
|
||||
document
|
||||
.getElementById("hamburger-btn")
|
||||
.addEventListener("keydown", function (e) {
|
||||
if (
|
||||
e.key === "Escape" &&
|
||||
document.getElementById("hamburger-menu").classList.contains("open")
|
||||
) {
|
||||
e.preventDefault();
|
||||
closeHamburger();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// ===========================================================================
|
||||
// 8. Tab bar
|
||||
@@ -2381,7 +2339,8 @@ function showNewWsModal() {
|
||||
|
||||
// Populate model dropdown
|
||||
var modelSelect = document.getElementById("new-ws-model");
|
||||
var curModel = document.getElementById("model-name").textContent;
|
||||
var fp = getFocusedPane();
|
||||
var curModel = fp ? fp.modelAlias || fp.model || "" : "";
|
||||
modelSelect.textContent = "";
|
||||
var defaultOpt = document.createElement("option");
|
||||
defaultOpt.value = "";
|
||||
@@ -2615,7 +2574,6 @@ function closeWorkstream(wsId) {
|
||||
|
||||
function showDashboard() {
|
||||
dashboardVisible = true;
|
||||
closeHamburger();
|
||||
document.getElementById("dashboard").classList.add("active");
|
||||
document.getElementById("header").inert = true;
|
||||
document.getElementById("tab-bar").inert = true;
|
||||
@@ -3324,16 +3282,6 @@ document.addEventListener("keydown", function (e) {
|
||||
var nwsOverlay = document.getElementById("new-ws-overlay");
|
||||
if (nwsOverlay && nwsOverlay.style.display !== "none") return;
|
||||
|
||||
// Escape: close hamburger first, then dashboard
|
||||
if (
|
||||
e.key === "Escape" &&
|
||||
document.getElementById("hamburger-menu").classList.contains("open")
|
||||
) {
|
||||
e.preventDefault();
|
||||
closeHamburger();
|
||||
document.getElementById("hamburger-btn").focus();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape" && dashboardVisible) {
|
||||
e.preventDefault();
|
||||
hideDashboard();
|
||||
|
||||
@@ -13,26 +13,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<div id="hamburger-wrap">
|
||||
<button id="hamburger-btn" onclick="toggleHamburger()" aria-label="Menu" aria-haspopup="true" aria-expanded="false" aria-controls="hamburger-menu">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
<div id="hamburger-menu" role="menu">
|
||||
<button class="hmenu-item" role="menuitem" tabindex="-1" onclick="hamburgerDashboard()">
|
||||
<span class="hmenu-icon">⌂</span> Dashboard
|
||||
</button>
|
||||
<div class="hmenu-sep" role="separator"></div>
|
||||
<button class="hmenu-item" role="menuitem" tabindex="-1" id="theme-menu-item" onclick="hamburgerTheme()">
|
||||
<span class="hmenu-icon" id="theme-menu-icon">☾</span> <span id="theme-menu-label">Light mode</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h1>turnstone</h1>
|
||||
<span id="model-name"></span>
|
||||
<span id="mcp-status" role="status" aria-live="polite"></span>
|
||||
<span id="status-bar"></span>
|
||||
<span id="health-indicator" class="health-ok" role="status" aria-live="polite" aria-atomic="true"></span>
|
||||
<button id="logout-btn" class="header-btn" onclick="logout()" style="display:none">logout</button>
|
||||
<button id="theme-toggle" class="header-btn" onclick="toggleTheme()" aria-label="Toggle light/dark theme" title="Switch to light theme">☾</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-bar" role="toolbar" aria-label="Workstreams">
|
||||
@@ -134,7 +118,7 @@ window.TURNSTONE_KB_SHORTCUTS = [
|
||||
]},
|
||||
{ title: "Navigation", keys: [
|
||||
{ desc: "Navigate table rows", badge: '<span class="kb-key">\u2191</span> <span class="kb-key">\u2193</span>' },
|
||||
{ desc: "Close dashboard / menu", badge: '<span class="kb-key">Esc</span>' }
|
||||
{ desc: "Close dashboard", badge: '<span class="kb-key">Esc</span>' }
|
||||
]},
|
||||
{ title: "General", keys: [
|
||||
{ desc: "Show this help", badge: '<span class="kb-key">?</span>' }
|
||||
|
||||
@@ -6,12 +6,6 @@
|
||||
/* ==========================================================================
|
||||
Header — server-specific elements
|
||||
========================================================================== */
|
||||
#model-name {
|
||||
font-size: 11px;
|
||||
color: var(--fg-dim);
|
||||
font-family: var(--font-display);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
#mcp-status {
|
||||
color: var(--magenta);
|
||||
font-size: 11px;
|
||||
@@ -49,68 +43,14 @@
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Hamburger menu
|
||||
Theme toggle
|
||||
========================================================================== */
|
||||
#theme-toggle { color: var(--fg); margin-left: auto; font-size: 14px; line-height: 1; }
|
||||
|
||||
/* ==========================================================================
|
||||
Mobile overrides
|
||||
========================================================================== */
|
||||
#hamburger-wrap { position: relative; }
|
||||
#hamburger-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--fg);
|
||||
border-radius: var(--radius-sm);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
#hamburger-btn:hover { background: var(--bg-highlight); border-color: var(--accent-dim); }
|
||||
#hamburger-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
#hamburger-btn span { display: block; width: 14px; height: 2px; background: var(--fg); border-radius: 1px; }
|
||||
#hamburger-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
min-width: 180px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
z-index: 40;
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-theme="light"] #hamburger-menu { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); }
|
||||
#hamburger-menu.open { display: block; }
|
||||
.hmenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--fg);
|
||||
font: inherit;
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.hmenu-item:hover { background: var(--bg-highlight); }
|
||||
.hmenu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
.hmenu-item .hmenu-icon { width: 16px; text-align: center; font-size: 14px; opacity: 0.8; }
|
||||
.hmenu-sep { height: 1px; background: var(--border-strong); margin: 4px 0; }
|
||||
@media (max-width: 600px) {
|
||||
#hamburger-btn { width: 40px; height: 40px; }
|
||||
.hmenu-item { padding: 12px 14px; }
|
||||
.ws-tab .tab-close { opacity: 1; padding: 4px 6px; font-size: 16px; }
|
||||
#split-btn { display: none; }
|
||||
}
|
||||
@@ -849,6 +789,63 @@ body { position: static; }
|
||||
.pane-stop:focus-visible { outline: 2px solid var(--fg-bright, #e8ecf4); outline-offset: 2px; }
|
||||
[data-theme="light"] .pane-stop { color: #fff; }
|
||||
|
||||
/* ==========================================================================
|
||||
Per-workstream status bar — above input
|
||||
========================================================================== */
|
||||
.ws-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 16px;
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--fg-dim);
|
||||
flex-shrink: 0;
|
||||
min-height: 22px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.01em;
|
||||
overflow: hidden;
|
||||
transition: background 0.3s, border-color 0.3s;
|
||||
}
|
||||
.ws-sb-model {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 500;
|
||||
color: var(--accent);
|
||||
font-size: 10px;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ws-sb-tokens { color: var(--fg-dim); white-space: nowrap; }
|
||||
.ws-sb-tools { color: var(--fg-dim); white-space: nowrap; }
|
||||
.ws-sb-turns { color: var(--fg-dim); white-space: nowrap; margin-left: auto; }
|
||||
|
||||
/* Context warning states */
|
||||
.ws-status-bar.ws-sb-warn .ws-sb-tokens { color: var(--yellow); font-weight: 600; }
|
||||
.ws-status-bar.ws-sb-danger .ws-sb-tokens {
|
||||
color: var(--red);
|
||||
font-weight: 600;
|
||||
text-shadow: 0 0 4px var(--red-glow);
|
||||
}
|
||||
[data-theme="light"] .ws-status-bar.ws-sb-danger .ws-sb-tokens { text-shadow: none; }
|
||||
|
||||
/* Disconnected state */
|
||||
.ws-status-bar.ws-sb-disconnected {
|
||||
border-top: 2px solid var(--red);
|
||||
background: rgba(248, 113, 113, 0.04);
|
||||
}
|
||||
.ws-status-bar.ws-sb-disconnected .ws-sb-tokens { color: var(--red); }
|
||||
.ws-status-bar.ws-sb-disconnected .ws-sb-model,
|
||||
.ws-status-bar.ws-sb-disconnected .ws-sb-tools,
|
||||
.ws-status-bar.ws-sb-disconnected .ws-sb-turns { opacity: 0.4; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ws-status-bar { transition: none; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Inline approval blocks
|
||||
========================================================================== */
|
||||
@@ -1463,11 +1460,11 @@ body { position: static; }
|
||||
.judge-spinner-dot { animation: none; opacity: 1; }
|
||||
.thinking-indicator::after { animation: none; content: '...'; }
|
||||
.ws-tab, .ws-tab .tab-close, #new-tab-btn, #split-btn,
|
||||
.hmenu-item, .dashboard-card,
|
||||
.dashboard-card,
|
||||
.approval-btn, .approval-feedback-input,
|
||||
#plan-buttons button, .pane-input-area button,
|
||||
.dashboard-new-btn, .dashboard-input,
|
||||
#health-indicator, #hamburger-btn,
|
||||
#health-indicator, #theme-toggle,
|
||||
#mcp-status, .msg-assistant tbody tr,
|
||||
.msg-assistant .img-placeholder,
|
||||
#new-ws-cancel, #new-ws-submit,
|
||||
|
||||
@@ -1457,81 +1457,81 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1582,40 +1582,40 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "primp"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/47/b3bc742632ceec08125f44f65d11fed668e977dabe8254e8262322d9be9c/primp-1.2.0.tar.gz", hash = "sha256:e5a7238888880ef8b7cb0a91f9e43b6b126182dd7466f8cd694815dbd573b9d8", size = 163029, upload-time = "2026-03-27T06:48:03.39Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/d1/e4df56552783475e6e580059ab88733e0934e0c9d7b38438501f15c8e06b/primp-1.2.1.tar.gz", hash = "sha256:77da763a7b5ab435e94f667da480ef3aab868d844badd30d1206b79ac5b460a5", size = 165839, upload-time = "2026-03-30T12:19:03.179Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/77/148bde5c61658818dfe9f7abe20dde3aa66c693058a7fbf6b74da6030587/primp-1.2.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bbc5c0f9961087d9e4fccc5f58b5c6c1b620dd36f55df5d61175a34dd4a6f205", size = 4340702, upload-time = "2026-03-27T06:48:16.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/13/da986592d85739328d9a40b68d17e42e13bc91a973c62c2ab69066ce01ad/primp-1.2.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:94283e1874b084d0d0104260f4a2b3b61c7836c6983476fc8a5d5f6b8e41277f", size = 4016031, upload-time = "2026-03-27T06:48:01.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/8f/13520f647e7faeaaab065a787d1674ef6613e56781f97e6642a2999f29fe/primp-1.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e5caea2c774f314e504b5da3ae189ba1f0406712d746e037f6990614c0eab97", size = 4294852, upload-time = "2026-03-27T06:48:07.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/06/2b480ed4c66e00cbf8be7030825d4d85517f60b87d829cd062c782d9999e/primp-1.2.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44b67e2759d4f5944ca075256ec52f19124532af1a176d98e7f4053b4d4daae3", size = 3887640, upload-time = "2026-03-27T06:48:10.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/75/cf8465eef07d9595e47efe840bad9e3b7a8724e2ff1c1d47709a95988788/primp-1.2.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f6ab28c9a50a2a58ed9997f1821a169fb5d5bce612a3e49d7bdcdb021014d6", size = 4145009, upload-time = "2026-03-27T06:48:32.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/a5/c8f0e6c92915da2205169d30967291db33e69f85233d39cbad99b235863c/primp-1.2.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:538c7877208d93b402766e1a95203c196591c9919814b96e830e36494fa7e1e7", size = 4420116, upload-time = "2026-03-27T06:48:05.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/2d/2ed76ce32a1fe2a3884c47f2d395cd51790cdd458a0e0f564a154a1e21e7/primp-1.2.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45cc81c19124b01c87f71606b18840b1e29350fa62bf1d77d30d6f491ef06730", size = 4319884, upload-time = "2026-03-27T06:48:13.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/4e/6d4681be11ea6e4d4be3f7561636dec7ea0ef3eb741f44f7661d3dec61bd/primp-1.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d3889e2df064c504064e7c935eb8b2662a4877edd57d172d4ce9217ca4a17c", size = 4527647, upload-time = "2026-03-27T06:48:36.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ba/43aeeba9db2c8118bd27799cd9caca596374101badd46092b9505565b251/primp-1.2.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ecff32b61fd32f91fefdb055cf5dec7d8c8a1addc48d736a7e39ff5598b48e5", size = 4460168, upload-time = "2026-03-27T06:48:37.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/3f/5e69db3307878bc3dec43e28ae1b9a80ff1e3824936882d4a24a80d2c8d3/primp-1.2.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:a636a6198603080c6454d08565796956393e3510e1a419f04243514e0524d410", size = 4123422, upload-time = "2026-03-27T06:48:26.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/f7/d48d29843a99bad516669d7c696eaa6567ffbc5911e3cb2160c434b00035/primp-1.2.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:fcbff0f8774dee3076b0fdebe2c96a78fc8d9b8720733f40c0801d8952b75829", size = 4268557, upload-time = "2026-03-27T06:48:39.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/fc/89f5aea2bf783d16fd0fad5fa19baaf39db1d927f6b1c0b046df87efe8da/primp-1.2.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3f262e9758cd7534c688679aa595b9ae5f2ad1a0523dc8a2e68e7bdae6970d41", size = 4781482, upload-time = "2026-03-27T06:48:11.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/62/f693f24bee4f20b5d1c6f8b24e93fb307440ece392a4f2af0a777783467e/primp-1.2.0-cp310-abi3-win32.whl", hash = "sha256:e842bb1709b00ed76fa7bb36452cc40a849525292880389d5cb96b3816b81085", size = 3495841, upload-time = "2026-03-27T06:48:22.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/8c/30220457fdf3aef691c4576fd5061845ec9616b350f3f4f4b9244b1a45dd/primp-1.2.0-cp310-abi3-win_amd64.whl", hash = "sha256:534d5f4758c6e6de8cb5468371088b9b63c1c9fb1598dde3a812c3e0041ff764", size = 3874396, upload-time = "2026-03-27T06:48:34.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e6/e85992bc182487e82af364b83c4940fd79bfc6225fdfd1c3704a1ec2e1ba/primp-1.2.0-cp310-abi3-win_arm64.whl", hash = "sha256:4d6b76832cddbaf5015f351ef92297ca2b5ff1c0a9b7d48869950bfcb7fc0c3f", size = 3859529, upload-time = "2026-03-27T06:48:44.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d5/b0cf726d1ce1188df28e1a341e55cf72fe27b9830979cda3ace88f26ab2a/primp-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7b6c91d80eaf8111aaed2614cd6a1828a34f6a4e808304db5e68d45814a94d17", size = 4323365, upload-time = "2026-03-27T06:48:41.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/87/36eea1248c75075fc0043b9267f94a74178796eac8e31f1e8692d7703564/primp-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:85a0752fcab7620f2c0e33ae645798db7ba4338ec29afa2f76214cba3200f113", size = 4014867, upload-time = "2026-03-27T06:48:23.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/73/7c7296bec28db2021b03193d2dae18802178189cd7df8a4083822a9e5bfc/primp-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b45bd9ab3bd9ffd03d9fb0da402f74655a1c91957602327901235ee44eee13b6", size = 4288377, upload-time = "2026-03-27T06:48:08.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/a8/493c73ee22070c15941526b6dbf9155fac3a4585bc6a99f096e88d8a73ca/primp-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:471ee87c9cdacbea4745f4062fee8fb46f90f31258c3fed3439a8679fb76a180", size = 3889957, upload-time = "2026-03-27T06:48:19.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/0b/fd94f740b56c2bfb5553e9f80ffaae028614db06d3dc15f905e230a2cd2b/primp-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad9f2b09a3fa40a4c777dd326c40123e2466619451df54add44197ddaaba6664", size = 4141722, upload-time = "2026-03-27T06:48:42.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e3/8b8f2fa8b30c59fbee62036b98d06085c599761771da8a732439eecd0d78/primp-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c309a1fa2b01abd568ad489e343414add9827416411998a8e30b994e5102bf8", size = 4414221, upload-time = "2026-03-27T06:48:31.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/34/94ebfad7a725599c027258105419965f7ced13195861c78281e93a9c7444/primp-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:457cee253d7b609d98718433e5df05f7afa735d864431d969bdb264a19f0da44", size = 4305538, upload-time = "2026-03-27T06:48:21.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/64/1dfe0a5cd84292c549df9003b8e4a600b87a92cb7958d0e90d1991b725e7/primp-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3fca95d5c32d3a05750fa55a0a9259efd926dbebe64ab9e17be0e764e6c1b6d", size = 4523230, upload-time = "2026-03-27T06:48:48.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a5/e32dcb9fc837f34dec87530a0e2ae3d49d1f77fb6b7bf3e9fe032077072d/primp-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0015a435b95c09daff1997032d6d6fca0898d4dba438f5e4814ae9687a0d7523", size = 4450713, upload-time = "2026-03-27T06:48:25.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/be/8866cfe40ac56f8be705f4df6d142a9721d82e21d3cb4500347fa02bfbbb/primp-1.2.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e77490c117a46e96a96899b3f45969197b86b7916264ce1bff7b32dd024aa122", size = 4116166, upload-time = "2026-03-27T06:48:29.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/15/1cb8b26ae3db007c14108e26f66a83be12b92669b3cf8de47d50fbffff94/primp-1.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5ff36e707ba27bb63963d289eac995639a4839ce3a0810fc6814c82ed34637bc", size = 4265289, upload-time = "2026-03-27T06:48:47.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cd/3e9d9b5f39e588244054ea167a3937a11b65047617ac14cf7f94c32b3f9f/primp-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f10264895525f097f83a1c4834f064bd6f07b2ff877bfcaed08520f849366d6", size = 4776628, upload-time = "2026-03-27T06:48:45.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/1a/390717fc42daf236524bb2c7ec3707daed0a7cbb5675a31459e97eb56416/primp-1.2.0-cp314-cp314t-win32.whl", hash = "sha256:7ba931c8331f44fcea68e451553f653d23238b9eac4733a318051b9ec5f8219b", size = 3495791, upload-time = "2026-03-27T06:48:18.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/bd/304e6314dddd776620ab9713989ea699f6476762e09b0bc17c074440f7c5/primp-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c5db92428d76d3302c2b103e702723c4598cfb9cf8d637deb98c53a99302557d", size = 3867412, upload-time = "2026-03-27T06:48:28.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/23/6776c98be6683b6097e27c46bb5931af83364a759345a64ff69c76106918/primp-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:53a46046a17adbc3d06d7fdc53a681ab1a64e531e0cbf4aacace94d233f353c0", size = 3857698, upload-time = "2026-03-27T06:48:04.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/9b/839567a1ed4235fc8dc9e278d613e9ece2c364063e6a1d725f689d9c0405/primp-1.2.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f1123eb5822eadb3cdadb29b910d45b4626ff4559e0b14ac39dda69fce751756", size = 4355623, upload-time = "2026-03-30T12:19:24.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/27/43922770620219fee11be4cbd33ffa8689b9e37780b2464a72cde540edc9/primp-1.2.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eec51c71754c40fd3590c4fe13c4a6dfcdb776ad84d805901659600d88a25ace", size = 4035804, upload-time = "2026-03-30T12:19:16.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/59/909becbcbfa27b87ab56fb9b730ba64dfcb90de3b6bce58b748f19d5c8ce/primp-1.2.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d2ace1328a153b5a28af98b636cf58271932da62d30cc7ed4a9296fd46abc3a", size = 4308476, upload-time = "2026-03-30T12:19:41.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/7f/006692e21190c83e553ac5da8fb6b5a4a1150fec6cee2e5f2eaba4fc05db/primp-1.2.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1b5569c990d2754d7327972bd0fcca1ad3c5fb422ab275b897ac945e552569", size = 3904546, upload-time = "2026-03-30T12:19:38.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/23/1d6a8eb431eb50a264c07f8a90a6a0c64906f544ab66c17afb426fed67b9/primp-1.2.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:945e38ac8dfb440746f83fae519c6461a105f797cc67803ddce12619c9d7dff6", size = 4154849, upload-time = "2026-03-30T12:19:11.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/e8/587e850c18c686dceb0083ce8cabd96d16bdddf27d5e00521674492cf1c2/primp-1.2.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ca3784de907a8818c4fd69763e84518bbd78ba6d416bbb4b613d64c1f33fb5f", size = 4443881, upload-time = "2026-03-30T12:19:23.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/ba/b774d79ed000e04f2000fa7939728a35f28fa04006c0c275299824ebe58c/primp-1.2.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca4b8b5e6717d3ecffaf707a15e10e73fb344f9b35752057e6ea320560d0ab60", size = 4334549, upload-time = "2026-03-30T12:19:29.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/bb/44254a82f76e9ab75311971c6bbded3adfa5a9b6a48c36a2d07eb9449137/primp-1.2.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00d43a98f0725175a8e9f49515d1c211713e40a95f24bdc5a35a84f7edba3566", size = 4539205, upload-time = "2026-03-30T12:19:06.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/28/cc3ea87172a451d352068f86e76c04d2a8a64a3c2e5178c9bd15d188209f/primp-1.2.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:44d41ac824f13dbb65bede87ad91e5baa27418e744221e03a187e2baa100eab3", size = 4469006, upload-time = "2026-03-30T12:19:35.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/8e/1a6ec24e1a5a3ab6f77dcbe7df1297d85d39d9315d2a76197c0f71ad03b9/primp-1.2.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5990f6da9909291d3884ff2333ff1caf765769a07175e3be473461f490d8a61c", size = 4132529, upload-time = "2026-03-30T12:19:20.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/06/78f5eaa8a200ba563472bcef3582d4bd9078b15e7922711a1a073e7f478a/primp-1.2.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:79b34bd865712bbb03d8713c08ba4ab5a4a8c7ad580c6c730ade957c76b6be53", size = 4282622, upload-time = "2026-03-30T12:19:13.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/c6/c28b716ba8acaac41a8cb0cd31f9b77c357438d9a87e899acd4cd3d0557d/primp-1.2.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c07f526d9e31054d08c5766a3f3add5ca70dfadb87a652ed5b46d8eb92ae0de7", size = 4794078, upload-time = "2026-03-30T12:19:01.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/ea/18dbdf68ffff0d6c0a58b70341cf4c881e556a894074ec971c761ca11881/primp-1.2.1-cp310-abi3-win32.whl", hash = "sha256:e82e6a61f972aab5809f6e680867e522d0b475e20fcc70f67c2b061da9eee746", size = 3512964, upload-time = "2026-03-30T12:19:22.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/51/c55ba2e7051d1f2f3dfe4ce93d0a1b0061fa8d70b1dda664ee9f4b238773/primp-1.2.1-cp310-abi3-win_amd64.whl", hash = "sha256:77eae842a8cefdcbc1ca02d11b8a3c40548b82447f7259c50e34314161ee7464", size = 3887675, upload-time = "2026-03-30T12:19:39.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/c4/02325db4b4db008083170dc880b85247edea651d049dde8ed51d1af5897a/primp-1.2.1-cp310-abi3-win_arm64.whl", hash = "sha256:21debb530039c087ad477e07e945c4000ef6b655732b0dea01007d52feb138ae", size = 3879095, upload-time = "2026-03-30T12:19:27.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/d5/27b896cfd4ea38033e0b65ceed34d64ba41a00f0ef6e5ca58a6350b4a4ef/primp-1.2.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:06837bf3ececc1482fbafb7c8cf306121a86c43ac7e57ec7a4d57add7e8ad126", size = 4344723, upload-time = "2026-03-30T12:19:00.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/44/ef009ac63dc7f4053c4e9a82699fa1d259542a8c334f1a46397cf415499b/primp-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3388ed838b8469592d9ae04ada7bc0d324211a1a16b2f64b5ed08f36e9c54ff6", size = 4028205, upload-time = "2026-03-30T12:19:07.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/11/7f2380127cd174fdc74face38d017a0d4a3feb711b5cd306e34e113911e4/primp-1.2.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c86f524e6380697f16e0eeed74f0a1fb360c4ec6c43e1d2843f0ba0bdd7e238", size = 4300606, upload-time = "2026-03-30T12:19:37.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d4/552fef7bae66cfd922506f532c6f15655b9fbbd9821a43db1b00d4541ee0/primp-1.2.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e272d6f97d5e1696018a6cbfef68ba51c89f4d3f676595ca49ce71544cacd7b6", size = 3899190, upload-time = "2026-03-30T12:19:26.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/9a/66304f7e8408aac88731001bbbef9f480ba2d351e764e8aecaecfd9b88a3/primp-1.2.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92e992e8d0689511b8311951b3ed0a468c98fda949567616f30a6f0fac824c8b", size = 4152615, upload-time = "2026-03-30T12:18:58.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c5/fecebde683382533e7d217da5fb48a262e7af0d288f833e89a96cb0076c0/primp-1.2.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:faa681c76a306db378ef6543ae84c405b604a1315a8ecedac2b52cb4159c4465", size = 4428506, upload-time = "2026-03-30T12:19:12.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/51/6a25ca1ff9700ab62f6d4bd7f9c8d6f413e135308344bacb9302c0b78043/primp-1.2.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3fa5d5f89fd35f1a7bac6d003f2d23857a4216e5b52adefd6f661a7e6d333ed", size = 4321356, upload-time = "2026-03-30T12:19:31.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/14/953a923c6ae93f4f7c7ac9d241ede544e9c3684b637874b539ee812bf380/primp-1.2.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0890ff425d2f95cd624ae6789828c5f3f4fd9b82ef091dfd3441c8489b81c12e", size = 4533517, upload-time = "2026-03-30T12:19:09.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/45/559e6a2166fd287eaef56d0a65efaaeed7a76c1b52c485fe40a9a5bccb2f/primp-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:688b21b73ffcababbad757f2a81d641c01567a59c2e0f3b5cc74b37b872598c2", size = 4464386, upload-time = "2026-03-30T12:19:04.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/cb2d02b527c15a284055bf417db680c666c52bac1ac714a32a79d5888e24/primp-1.2.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ad9ac3e347e193982961c0d333f0a9f57f83224104b239421ef85b0bab3409e3", size = 4130750, upload-time = "2026-03-30T12:19:43.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/f1/9e73a29e63d3440522d841700c29f8e49dfceee6f0b701b0d72f70b22c61/primp-1.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:52c8e8eeccc67302120673cdc32ab7022b3df8ea218d46bda9d81ad90f1d32bb", size = 4287536, upload-time = "2026-03-30T12:19:17.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/8b/0eec2862d8b3b6471cba851de105fda4457ac07f50f1ba62bc2643312054/primp-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c03c6111c372ea82ed552c37a437ecb2f7e2a0e26033620303ab4fe4380155a", size = 4782711, upload-time = "2026-03-30T12:19:14.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/69/6439585738b6b3d79d0d8e83f91abaaf857941f5ef81d1c8e24b8869adfc/primp-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:56e7b1f6d062bf72f3e06a0fc03630a8dcb6349c44397a3513da6668f51d3fc9", size = 3506042, upload-time = "2026-03-30T12:19:32.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f1/a00d35e01512a10ad1c607b52fcdb4a39cfabe2a8c8c7975a84d2da5e5ea/primp-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3e449fa51e7d06889c30247bba7a73f6b2e3958179d9f37e4ffef78fda714a3c", size = 3883151, upload-time = "2026-03-30T12:19:19.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/7f/1c4bd7a311da73832086cff75274bf2620ed0ceb38e92fbcf606cc0567a4/primp-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a20ca7542e912c6cb83dc86f42a34cc4e2618e3a24878101433888259e566433", size = 3875623, upload-time = "2026-03-30T12:19:33.952Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1923,11 +1923,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2393,15 +2393,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.3.3"
|
||||
version = "3.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/2f/9223c24f568bb7a0c03d751e609844dce0968f13b39a3f73fbb3a96cd27a/sse_starlette-3.3.3.tar.gz", hash = "sha256:72a95d7575fd5129bd0ae15275ac6432bb35ac542fdebb82889c24bb9f3f4049", size = 32420, upload-time = "2026-03-17T20:05:55.529Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/e2/b8cff57a67dddf9a464d7e943218e031617fb3ddc133aeeb0602ff5f6c85/sse_starlette-3.3.3-py3-none-any.whl", hash = "sha256:c5abb5082a1cc1c6294d89c5290c46b5f67808cfdb612b7ec27e8ba061c22e8d", size = 14329, upload-time = "2026-03-17T20:05:54.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2506,7 +2506,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "turnstone"
|
||||
version = "0.9.4"
|
||||
version = "0.9.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
|
||||
Reference in New Issue
Block a user