refactor: update terminal api

This commit is contained in:
arabcoders 2026-04-29 22:25:39 +03:00
parent 084f5082ad
commit 11bd4204aa
14 changed files with 1268 additions and 436 deletions

42
API.md
View file

@ -98,6 +98,7 @@ This document describes the available endpoints and their usage. All endpoints r
- [GET /api/system/configuration](#get-apisystemconfiguration) - [GET /api/system/configuration](#get-apisystemconfiguration)
- [GET /api/system/limits](#get-apisystemlimits) - [GET /api/system/limits](#get-apisystemlimits)
- [POST /api/system/terminal](#post-apisystemterminal) - [POST /api/system/terminal](#post-apisystemterminal)
- [GET /api/system/terminal](#get-apisystemterminal)
- [GET /api/system/terminal/active](#get-apisystemterminalactive) - [GET /api/system/terminal/active](#get-apisystemterminalactive)
- [GET /api/system/terminal/{session\_id}](#get-apisystemterminalsession_id) - [GET /api/system/terminal/{session\_id}](#get-apisystemterminalsession_id)
- [DELETE /api/system/terminal/{session\_id}](#delete-apisystemterminalsession_id) - [DELETE /api/system/terminal/{session\_id}](#delete-apisystemterminalsession_id)
@ -2579,6 +2580,36 @@ or an error:
--- ---
### GET /api/system/terminal
**Purpose**: List recent terminal sessions.
**Response**:
```json
{
"items": [
{
"session_id": "3a8c5f7e2d3b4a8f9c0d1e2f3a4b5c6d",
"command": "--help",
"status": "completed",
"created_at": 1713000000.0,
"started_at": 1713000000.0,
"finished_at": 1713000004.0,
"expires_at": 1713086404.0,
"available_until": 1713086404.0,
"exit_code": 0,
"last_sequence": 15
}
]
}
```
**Notes**:
- Expired sessions are removed automatically later.
- `403 Forbidden` if console is disabled.
---
### GET /api/system/terminal/active ### GET /api/system/terminal/active
**Purpose**: Return the currently active terminal session metadata, or `null` if no session is active. **Purpose**: Return the currently active terminal session metadata, or `null` if no session is active.
@ -2606,7 +2637,7 @@ or
--- ---
### GET /api/system/terminal/{session_id} ### GET /api/system/terminal/{session_id}
**Purpose**: Return metadata for a specific terminal session while it is active or still within the replay/drain window. **Purpose**: Return metadata for a specific terminal session.
**Response**: **Response**:
```json ```json
@ -2617,7 +2648,7 @@ or
"created_at": 1713000000.0, "created_at": 1713000000.0,
"started_at": 1713000000.0, "started_at": 1713000000.0,
"finished_at": 1713000004.0, "finished_at": 1713000004.0,
"expires_at": 1713000034.0, "expires_at": 1713086404.0,
"exit_code": 0, "exit_code": 0,
"last_sequence": 15 "last_sequence": 15
} }
@ -2641,8 +2672,7 @@ or
**Notes**: **Notes**:
- This only applies to the currently active session. - This only applies to the currently active session.
- The client should stay attached to the stream to receive the final `close` event and refreshed terminal status. - Cancelled sessions are marked as `interrupted`.
- Cancelled sessions finalize as `interrupted` and remain replayable until the drain window expires.
- `403 Forbidden` if console is disabled. - `403 Forbidden` if console is disabled.
- `404 Not Found` if the session does not exist or has already expired. - `404 Not Found` if the session does not exist or has already expired.
@ -2674,10 +2704,6 @@ If both `since` and `Last-Event-ID` are present, the larger value is used.
{ "exitcode": 0 } { "exitcode": 0 }
``` ```
**Notes**:
- Replay/restore works while the session is still running or until the finished session expires.
- Finished sessions are removed lazily after the transcript drain window elapses.
- `403 Forbidden` if console is disabled. - `403 Forbidden` if console is disabled.
- `400 Bad Request` if the replay cursor is invalid. - `400 Bad Request` if the replay cursor is invalid.
- `404 Not Found` if the session does not exist or has already expired. - `404 Not Found` if the session does not exist or has already expired.

View file

@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any
from aiohttp import web from aiohttp import web
from app.library.config import Config from app.library.config import Config
from app.library.Scheduler import Scheduler
from app.library.Services import Services from app.library.Services import Services
from app.library.Singleton import Singleton from app.library.Singleton import Singleton
@ -31,8 +32,10 @@ ACTIVE_FILE_NAME = "active.json"
METADATA_FILE_NAME = "metadata.json" METADATA_FILE_NAME = "metadata.json"
TRANSCRIPT_FILE_NAME = "transcript.jsonl" TRANSCRIPT_FILE_NAME = "transcript.jsonl"
DEFAULT_DRAIN_TTL = 30.0 DEFAULT_DRAIN_TTL = 30.0
COMPLETED_SESSION_RETENTION = 86400.0
DEFAULT_KEEPALIVE_INTERVAL = 15.0 DEFAULT_KEEPALIVE_INTERVAL = 15.0
DEFAULT_SHUTDOWN_TIMEOUT = 5.0 DEFAULT_SHUTDOWN_TIMEOUT = 5.0
CLEANUP_SCHEDULE = "5 */1 * * *"
class TerminalSessionConflictError(RuntimeError): class TerminalSessionConflictError(RuntimeError):
@ -55,8 +58,10 @@ class TerminalSessionManager(metaclass=Singleton):
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
self._active: ActiveTerminalSession | None = None self._active: ActiveTerminalSession | None = None
self._drain_ttl: float = DEFAULT_DRAIN_TTL self._drain_ttl: float = DEFAULT_DRAIN_TTL
self._completed_retention: float = COMPLETED_SESSION_RETENTION
self._keepalive_interval: float = DEFAULT_KEEPALIVE_INTERVAL self._keepalive_interval: float = DEFAULT_KEEPALIVE_INTERVAL
self._shutdown_timeout: float = DEFAULT_SHUTDOWN_TIMEOUT self._shutdown_timeout: float = DEFAULT_SHUTDOWN_TIMEOUT
self._cleanup_job_id: str | None = None
@staticmethod @staticmethod
def get_instance() -> TerminalSessionManager: def get_instance() -> TerminalSessionManager:
@ -67,11 +72,20 @@ class TerminalSessionManager(metaclass=Singleton):
Services.get_instance().add("terminal_manager", self) Services.get_instance().add("terminal_manager", self)
app.on_startup.append(self.on_startup) app.on_startup.append(self.on_startup)
app.on_shutdown.append(self.on_shutdown) app.on_shutdown.append(self.on_shutdown)
self._cleanup_job_id = Scheduler.get_instance().add(
timer=CLEANUP_SCHEDULE,
func=self.cleanup,
id=f"{__class__.__name__}.{__class__.cleanup.__name__}",
)
async def on_startup(self, _: web.Application) -> None: async def on_startup(self, _: web.Application) -> None:
await self.initialize() await self.initialize()
async def on_shutdown(self, _: web.Application) -> None: async def on_shutdown(self, _: web.Application) -> None:
if self._cleanup_job_id is not None:
Scheduler.get_instance().remove(self._cleanup_job_id)
self._cleanup_job_id = None
session_id: str | None = None session_id: str | None = None
task: asyncio.Task[None] | None = None task: asyncio.Task[None] | None = None
@ -121,12 +135,17 @@ class TerminalSessionManager(metaclass=Singleton):
async def cleanup(self) -> None: async def cleanup(self) -> None:
async with self._lock: async with self._lock:
self._cleanup_expired_sessions(time.time()) LOG.debug("Running scheduled terminal session cleanup.")
removed = self._cleanup_expired_sessions(time.time())
LOG.debug("Scheduled terminal session cleanup finished. Removed %d expired sessions.", removed)
async def list_sessions(self) -> list[dict[str, Any]]:
async with self._lock:
return self._list_sessions_locked()
async def create_session(self, command: str) -> dict[str, Any]: async def create_session(self, command: str) -> dict[str, Any]:
async with self._lock: async with self._lock:
now = time.time() now = time.time()
self._cleanup_expired_sessions(now)
active_session = self._get_active_session_locked() active_session = self._get_active_session_locked()
if active_session is not None: if active_session is not None:
@ -160,22 +179,20 @@ class TerminalSessionManager(metaclass=Singleton):
async def get_active_session(self) -> dict[str, Any] | None: async def get_active_session(self) -> dict[str, Any] | None:
async with self._lock: async with self._lock:
self._cleanup_expired_sessions(time.time())
metadata = self._get_active_session_locked() metadata = self._get_active_session_locked()
return None if metadata is None else dict(metadata) return None if metadata is None else dict(metadata)
async def get_session(self, session_id: str) -> dict[str, Any] | None: async def get_session(self, session_id: str) -> dict[str, Any] | None:
async with self._lock: async with self._lock:
self._cleanup_expired_sessions(time.time())
metadata = self._load_metadata(session_id) metadata = self._load_metadata(session_id)
return None if metadata is None else dict(metadata) if metadata is None or self._is_expired_metadata(metadata, time.time()):
return None
return dict(metadata)
async def cancel_session(self, session_id: str) -> dict[str, Any]: async def cancel_session(self, session_id: str) -> dict[str, Any]:
async with self._lock: async with self._lock:
self._cleanup_expired_sessions(time.time())
metadata = self._load_metadata(session_id) metadata = self._load_metadata(session_id)
if metadata is None: if metadata is None or self._is_expired_metadata(metadata, time.time()):
msg = f"Unknown terminal session '{session_id}'." msg = f"Unknown terminal session '{session_id}'."
raise FileNotFoundError(msg) raise FileNotFoundError(msg)
@ -218,6 +235,9 @@ class TerminalSessionManager(metaclass=Singleton):
async with self._lock: async with self._lock:
metadata = self._load_metadata(session_id) metadata = self._load_metadata(session_id)
if metadata is not None: if metadata is not None:
if self._is_expired_metadata(metadata, time.time()):
return response
replay_until = int(metadata.get("last_sequence", last_sent)) replay_until = int(metadata.get("last_sequence", last_sent))
runtime = self._active runtime = self._active
@ -443,7 +463,7 @@ class TerminalSessionManager(metaclass=Singleton):
metadata["status"] = status metadata["status"] = status
metadata["exit_code"] = exit_code metadata["exit_code"] = exit_code
metadata["finished_at"] = now metadata["finished_at"] = now
metadata["expires_at"] = now + self._drain_ttl metadata["expires_at"] = now + self._completed_retention
self._write_json(self._metadata_path(session_id), metadata) self._write_json(self._metadata_path(session_id), metadata)
if self._load_active_marker() == session_id: if self._load_active_marker() == session_id:
@ -468,6 +488,31 @@ class TerminalSessionManager(metaclass=Singleton):
return metadata return metadata
def _list_sessions_locked(self) -> list[dict[str, Any]]:
self._ensure_root()
items: list[dict[str, Any]] = []
for path in self.root_path.iterdir():
if path.name == ACTIVE_FILE_NAME or path.is_dir() is False:
continue
metadata = self._load_metadata(path.name)
if metadata is None:
continue
if self._is_expired_metadata(metadata, time.time()):
continue
item = dict(metadata)
item["available_until"] = self._available_until(metadata)
items.append(item)
items.sort(
key=lambda item: float(item.get("finished_at") or item.get("started_at") or item.get("created_at") or 0),
reverse=True,
)
return items
def _recover_orphaned_active_session(self, now: float) -> None: def _recover_orphaned_active_session(self, now: float) -> None:
session_id = self._load_active_marker() session_id = self._load_active_marker()
if session_id is None: if session_id is None:
@ -480,14 +525,15 @@ class TerminalSessionManager(metaclass=Singleton):
metadata["status"] = "interrupted" metadata["status"] = "interrupted"
metadata["finished_at"] = now metadata["finished_at"] = now
metadata["expires_at"] = now + self._drain_ttl metadata["expires_at"] = now + self._completed_retention
metadata["exit_code"] = -1 if metadata.get("exit_code") is None else metadata["exit_code"] metadata["exit_code"] = -1 if metadata.get("exit_code") is None else metadata["exit_code"]
self._write_json(self._metadata_path(session_id), metadata) self._write_json(self._metadata_path(session_id), metadata)
self._clear_active_marker() self._clear_active_marker()
def _cleanup_expired_sessions(self, now: float) -> None: def _cleanup_expired_sessions(self, now: float) -> int:
self._ensure_root() self._ensure_root()
active_session_id = self._load_active_marker() active_session_id = self._load_active_marker()
removed = 0
for path in self.root_path.iterdir(): for path in self.root_path.iterdir():
if path.name == ACTIVE_FILE_NAME or path.is_dir() is False: if path.name == ACTIVE_FILE_NAME or path.is_dir() is False:
@ -496,10 +542,10 @@ class TerminalSessionManager(metaclass=Singleton):
metadata = self._load_metadata(path.name) metadata = self._load_metadata(path.name)
if metadata is None: if metadata is None:
shutil.rmtree(path, ignore_errors=True) shutil.rmtree(path, ignore_errors=True)
removed += 1
continue continue
expires_at = metadata.get("expires_at") if not self._is_expired_metadata(metadata, now):
if expires_at is None or float(expires_at) > now:
continue continue
if active_session_id == path.name: if active_session_id == path.name:
@ -508,6 +554,10 @@ class TerminalSessionManager(metaclass=Singleton):
shutil.rmtree(path, ignore_errors=True) shutil.rmtree(path, ignore_errors=True)
removed += 1
return removed
def _parse_since(self, request: Request) -> int: def _parse_since(self, request: Request) -> int:
values: list[int] = [] values: list[int] = []
candidates = [request.query.get("since"), request.headers.get("Last-Event-ID")] candidates = [request.query.get("since"), request.headers.get("Last-Event-ID")]
@ -537,7 +587,7 @@ class TerminalSessionManager(metaclass=Singleton):
now = time.time() now = time.time()
metadata["status"] = "interrupted" metadata["status"] = "interrupted"
metadata["finished_at"] = now metadata["finished_at"] = now
metadata["expires_at"] = now + self._drain_ttl metadata["expires_at"] = now + self._completed_retention
metadata["exit_code"] = -1 if metadata.get("exit_code") is None else metadata["exit_code"] metadata["exit_code"] = -1 if metadata.get("exit_code") is None else metadata["exit_code"]
self._write_json(self._metadata_path(session_id), metadata) self._write_json(self._metadata_path(session_id), metadata)
@ -618,6 +668,20 @@ class TerminalSessionManager(metaclass=Singleton):
return events return events
def _available_until(self, metadata: dict[str, Any]) -> float | None:
expires_at = metadata.get("expires_at")
if expires_at is None:
return None
return float(expires_at)
def _is_expired_metadata(self, metadata: dict[str, Any], now: float) -> bool:
expires_at = self._available_until(metadata)
if expires_at is None:
return False
return expires_at <= now
def _build_env(self) -> dict[str, str]: def _build_env(self) -> dict[str, str]:
env_vars = os.environ.copy() env_vars = os.environ.copy()
env_vars.update( env_vars.update(

View file

@ -274,6 +274,20 @@ async def create_terminal_session(
return web.json_response(data=metadata, status=web.HTTPOk.status_code, dumps=encoder.encode) return web.json_response(data=metadata, status=web.HTTPOk.status_code, dumps=encoder.encode)
@route("GET", "api/system/terminal", "system.terminal.list")
async def list_terminal_sessions(
config: Config, encoder: Encoder, terminal_manager: TerminalSessionManager
) -> Response:
if not config.console_enabled:
return web.json_response(
{"error": "Console feature is disabled."},
status=web.HTTPForbidden.status_code,
)
items = await terminal_manager.list_sessions()
return web.json_response(data={"items": items}, status=web.HTTPOk.status_code, dumps=encoder.encode)
@route("GET", "api/system/terminal/active", "system.terminal.active") @route("GET", "api/system/terminal/active", "system.terminal.active")
async def get_active_terminal_session( async def get_active_terminal_session(
config: Config, encoder: Encoder, terminal_manager: TerminalSessionManager config: Config, encoder: Encoder, terminal_manager: TerminalSessionManager

View file

@ -11,7 +11,7 @@ from uuid import uuid4
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterator from collections.abc import Iterator
_TEST_RUN_ROOT = Path(gettempdir()) / "ytptube-tests" / uuid4().hex _TEST_RUN_ROOT = Path(gettempdir()) / "tests-ytptube" / uuid4().hex
_TEST_SYSTEM_TEMP_ROOT = _TEST_RUN_ROOT / "tmp" _TEST_SYSTEM_TEMP_ROOT = _TEST_RUN_ROOT / "tmp"

View file

@ -1,10 +1,13 @@
import asyncio import asyncio
import json import json
from pathlib import Path from pathlib import Path
import time
from typing import Any, cast from typing import Any, cast
import pytest import pytest
from aiohttp import web
from app.library.Scheduler import Scheduler
from app.library.Services import Services from app.library.Services import Services
from app.library.TerminalSessionManager import TerminalSessionManager from app.library.TerminalSessionManager import TerminalSessionManager
from app.library.config import Config from app.library.config import Config
@ -13,6 +16,7 @@ from app.routes.api.system import (
cancel_terminal_session, cancel_terminal_session,
create_terminal_session, create_terminal_session,
get_active_terminal_session, get_active_terminal_session,
list_terminal_sessions,
get_terminal_session, get_terminal_session,
stream_terminal_session, stream_terminal_session,
) )
@ -133,6 +137,7 @@ class _TerminableProc:
@pytest.fixture @pytest.fixture
def terminal_setup(tmp_path: Path) -> tuple[Config, TerminalSessionManager, Encoder]: def terminal_setup(tmp_path: Path) -> tuple[Config, TerminalSessionManager, Encoder]:
Scheduler._reset_singleton()
Services._reset_singleton() Services._reset_singleton()
Config._reset_singleton() Config._reset_singleton()
TerminalSessionManager._reset_singleton() TerminalSessionManager._reset_singleton()
@ -150,6 +155,16 @@ def terminal_setup(tmp_path: Path) -> tuple[Config, TerminalSessionManager, Enco
class TestTerminalSessionRoutes: class TestTerminalSessionRoutes:
def test_attach_registers_cleanup_job(self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder]) -> None:
_config, manager, _encoder = terminal_setup
app = web.Application()
manager.attach(app)
scheduler = Scheduler.get_instance()
assert scheduler.has(f"{TerminalSessionManager.__name__}.{TerminalSessionManager.cleanup.__name__}")
assert manager._cleanup_job_id == f"{TerminalSessionManager.__name__}.{TerminalSessionManager.cleanup.__name__}"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_start_returns_session_metadata_and_active_conflict( async def test_start_returns_session_metadata_and_active_conflict(
self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch
@ -245,7 +260,7 @@ class TestTerminalSessionRoutes:
self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
config, manager, encoder = terminal_setup config, manager, encoder = terminal_setup
manager._drain_ttl = 0.05 manager._completed_retention = 0.05
await manager.initialize() await manager.initialize()
async def fake_create_subprocess_exec(*_args, **_kwargs): async def fake_create_subprocess_exec(*_args, **_kwargs):
@ -271,8 +286,130 @@ class TestTerminalSessionRoutes:
expired = await manager.get_session(session_id) expired = await manager.get_session(session_id)
assert expired is None assert expired is None
assert (manager.root_path / session_id).exists()
await manager.cleanup()
assert not (manager.root_path / session_id).exists() assert not (manager.root_path / session_id).exists()
@pytest.mark.asyncio
async def test_list_sessions_returns_recent_completed_session_until_retention_expires(
self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch
) -> None:
config, manager, encoder = terminal_setup
manager._completed_retention = 0.2
manager._drain_ttl = 0.01
await manager.initialize()
async def fake_create_subprocess_exec(*_args, **_kwargs):
return _CompletedProc([b"done\n"])
monkeypatch.setattr(
"app.library.TerminalSessionManager.asyncio.create_subprocess_exec", fake_create_subprocess_exec
)
monkeypatch.setattr(manager, "_open_pty", lambda: None)
start_request = _FakeRequest(payload={"command": "--help"}, can_read_body=True)
start_response = await create_terminal_session(start_request, config, encoder, manager)
session_id = json.loads(start_response.body.decode("utf-8"))["session_id"]
assert manager._active is not None
await manager._active.task
await asyncio.sleep(0.03)
listed_response = await list_terminal_sessions(config, encoder, manager)
listed_payload = json.loads(listed_response.body.decode("utf-8"))
assert 200 == listed_response.status
assert 1 == len(listed_payload["items"])
assert session_id == listed_payload["items"][0]["session_id"]
assert "completed" == listed_payload["items"][0]["status"]
assert listed_payload["items"][0]["available_until"] is not None
persisted = await manager.get_session(session_id)
assert persisted is not None
await asyncio.sleep(0.19)
expired = await manager.get_session(session_id)
assert expired is None
assert (manager.root_path / session_id).exists()
await manager.cleanup()
assert not (manager.root_path / session_id).exists()
@pytest.mark.asyncio
async def test_list_sessions_orders_newest_first_and_skips_expired_items(
self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder]
) -> None:
config, manager, encoder = terminal_setup
await manager.initialize()
first_id = "first"
second_id = "second"
expired_id = "expired"
for session_id in [first_id, second_id, expired_id]:
session_dir = manager.root_path / session_id
session_dir.mkdir(parents=True, exist_ok=True)
manager._transcript_path(session_id).touch(exist_ok=True)
manager._write_json(
manager._metadata_path(first_id),
{
"session_id": first_id,
"command": "--first",
"status": "completed",
"created_at": 10.0,
"started_at": 11.0,
"finished_at": 20.0,
"expires_at": time.time() + 60,
"exit_code": 0,
"last_sequence": 2,
},
)
manager._write_json(
manager._metadata_path(second_id),
{
"session_id": second_id,
"command": "--second",
"status": "running",
"created_at": 30.0,
"started_at": 31.0,
"finished_at": None,
"expires_at": None,
"exit_code": None,
"last_sequence": 4,
},
)
manager._write_json(
manager._metadata_path(expired_id),
{
"session_id": expired_id,
"command": "--expired",
"status": "completed",
"created_at": 1.0,
"started_at": 2.0,
"finished_at": 3.0,
"expires_at": time.time() - 1,
"exit_code": 1,
"last_sequence": 1,
},
)
listed_response = await list_terminal_sessions(config, encoder, manager)
listed_payload = json.loads(listed_response.body.decode("utf-8"))
assert 200 == listed_response.status
assert [second_id, first_id] == [item["session_id"] for item in listed_payload["items"]]
assert (manager.root_path / expired_id).exists()
await manager.cleanup()
assert not (manager.root_path / expired_id).exists()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_shutdown_interrupts_active_session_and_clears_active_marker( async def test_shutdown_interrupts_active_session_and_clears_active_marker(
self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch self, terminal_setup: tuple[Config, TerminalSessionManager, Encoder], monkeypatch: pytest.MonkeyPatch

View file

@ -3,6 +3,7 @@ import type { EventSourceMessage } from '@microsoft/fetch-event-source';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import type { TerminalSessionItem, TerminalSessionsResponse } from '~/types';
import { parse_api_error, parse_api_response, request, uri } from '~/utils'; import { parse_api_error, parse_api_response, request, uri } from '~/utils';
type ConsoleSessionStatus = type ConsoleSessionStatus =
@ -48,10 +49,25 @@ type CancelConsoleSessionResult = {
message?: string; message?: string;
}; };
type RecentSessionItem = {
sessionId: string;
command: string;
status: 'starting' | 'running' | 'completed';
createdAt: number | null;
startedAt: number | null;
finishedAt: number | null;
expiresAt: number | null;
availableUntil: number | null;
exitCode: number | null;
lastSequence: number;
};
const STORAGE_KEY = 'console_session_state'; const STORAGE_KEY = 'console_session_state';
const HIDDEN_RECENT_SESSIONS_KEY = 'console_hidden_recent_sessions';
const MAX_TRANSCRIPT_CHUNKS = 1500; const MAX_TRANSCRIPT_CHUNKS = 1500;
const MAX_TRANSCRIPT_CHARS = 120000; const MAX_TRANSCRIPT_CHARS = 120000;
const RECONNECT_DELAY_MS = 1500; const RECONNECT_DELAY_MS = 1500;
const RECENT_SESSIONS_MAX = 30;
const DEFAULT_STATE: ConsoleSessionState = { const DEFAULT_STATE: ConsoleSessionState = {
sessionId: null, sessionId: null,
@ -160,6 +176,8 @@ const normalizePersistedState = (
}; };
const sessionState = useStorage<ConsoleSessionState>(STORAGE_KEY, { ...DEFAULT_STATE }); const sessionState = useStorage<ConsoleSessionState>(STORAGE_KEY, { ...DEFAULT_STATE });
const hiddenRecentSessions = useStorage<Array<string>>(HIDDEN_RECENT_SESSIONS_KEY, []);
const recentSessions = ref<Array<RecentSessionItem>>([]);
const streamController = ref<AbortController | null>(null); const streamController = ref<AbortController | null>(null);
const reconnectTimer = ref<ReturnType<typeof setTimeout> | null>(null); const reconnectTimer = ref<ReturnType<typeof setTimeout> | null>(null);
const connectionNonce = ref(0); const connectionNonce = ref(0);
@ -176,6 +194,68 @@ const updateState = (patch: Partial<ConsoleSessionState>): void => {
); );
}; };
const normalizeHiddenRecentSessions = (): Array<string> => {
return hiddenRecentSessions.value
.filter((value): value is string => typeof value === 'string')
.map((value) => value.trim())
.filter((value) => Boolean(value));
};
const setHiddenRecentSessions = (items: Array<string>): void => {
hiddenRecentSessions.value = Array.from(new Set(items));
};
const isRecentSessionHidden = (sessionId: string): boolean => {
return normalizeHiddenRecentSessions().includes(sessionId);
};
const sortRecentSessions = (items: Array<RecentSessionItem>): Array<RecentSessionItem> => {
return items.slice().sort((left, right) => {
const leftTime = left.finishedAt ?? left.startedAt ?? left.createdAt ?? 0;
const rightTime = right.finishedAt ?? right.startedAt ?? right.createdAt ?? 0;
return rightTime - leftTime;
});
};
const trimRecentSessions = (items: Array<RecentSessionItem>): Array<RecentSessionItem> => {
return items.slice(0, RECENT_SESSIONS_MAX);
};
const setRecentSessions = (items: Array<RecentSessionItem>): void => {
recentSessions.value = trimRecentSessions(
sortRecentSessions(items.filter((item) => !isRecentSessionHidden(item.sessionId))),
);
};
const upsertRecentSession = (item: RecentSessionItem): void => {
if (isRecentSessionHidden(item.sessionId)) {
return;
}
setRecentSessions([
item,
...recentSessions.value.filter((entry) => entry.sessionId !== item.sessionId),
]);
};
const hideRecentSession = (sessionId: string): void => {
const normalized = sessionId.trim();
if (!normalized) {
return;
}
setHiddenRecentSessions([...normalizeHiddenRecentSessions(), normalized]);
setRecentSessions(recentSessions.value.filter((item) => item.sessionId !== normalized));
};
const clearRecentSessions = (): void => {
setHiddenRecentSessions([
...normalizeHiddenRecentSessions(),
...recentSessions.value.map((item) => item.sessionId),
]);
recentSessions.value = [];
};
const appendTranscript = (chunk: string): void => { const appendTranscript = (chunk: string): void => {
if (!chunk) { if (!chunk) {
return; return;
@ -215,6 +295,7 @@ const clearTranscript = (): void => {
const markExpired = (): void => { const markExpired = (): void => {
stopStream(); stopStream();
updateState({ status: 'expired', error: '' }); updateState({ status: 'expired', error: '' });
syncCurrentSessionToRecentSessions();
}; };
const finishSession = ( const finishSession = (
@ -227,6 +308,7 @@ const finishSession = (
exitCode, exitCode,
error: '', error: '',
}); });
syncCurrentSessionToRecentSessions();
}; };
const scheduleReconnect = (): void => { const scheduleReconnect = (): void => {
@ -289,6 +371,97 @@ const parseResponseError = async (response: Response): Promise<string> => {
return response.statusText || 'Request failed.'; return response.statusText || 'Request failed.';
}; };
const normalizeRecentSessionStatus = (
status: string | null | undefined,
): RecentSessionItem['status'] => {
switch (normalizeStatus(status, 'finished')) {
case 'starting':
return 'starting';
case 'running':
case 'reconnecting':
return 'running';
default:
return 'completed';
}
};
const recentSessionFromPayload = (payload: TerminalSessionItem): RecentSessionItem | null => {
if (typeof payload.session_id !== 'string' || !payload.session_id.trim()) {
return null;
}
if (typeof payload.command !== 'string') {
return null;
}
return {
sessionId: payload.session_id,
command: payload.command,
status: normalizeRecentSessionStatus(payload.status),
createdAt: typeof payload.created_at === 'number' ? payload.created_at : null,
startedAt: typeof payload.started_at === 'number' ? payload.started_at : null,
finishedAt: typeof payload.finished_at === 'number' ? payload.finished_at : null,
expiresAt: typeof payload.expires_at === 'number' ? payload.expires_at : null,
availableUntil: typeof payload.available_until === 'number' ? payload.available_until : null,
exitCode: typeof payload.exit_code === 'number' ? payload.exit_code : null,
lastSequence: typeof payload.last_sequence === 'number' ? payload.last_sequence : 0,
};
};
const syncCurrentSessionToRecentSessions = (): void => {
if (!sessionState.value.sessionId || !sessionState.value.command.trim()) {
return;
}
const existing =
recentSessions.value.find((item) => item.sessionId === sessionState.value.sessionId) ?? null;
const isFinished = ['finished', 'interrupted', 'error', 'expired'].includes(
sessionState.value.status,
);
const lastSequence =
sessionState.value.lastEventId && /^\d+$/.test(sessionState.value.lastEventId)
? Number(sessionState.value.lastEventId)
: (existing?.lastSequence ?? 0);
upsertRecentSession({
sessionId: sessionState.value.sessionId,
command: sessionState.value.command,
status: normalizeRecentSessionStatus(sessionState.value.status),
createdAt: existing?.createdAt ?? null,
startedAt: existing?.startedAt ?? null,
finishedAt: isFinished
? (existing?.finishedAt ?? Date.now() / 1000)
: (existing?.finishedAt ?? null),
expiresAt: existing?.expiresAt ?? null,
availableUntil: existing?.availableUntil ?? null,
exitCode: sessionState.value.exitCode ?? existing?.exitCode ?? null,
lastSequence,
});
};
const fetchRecentSessions = async (): Promise<void> => {
try {
const response = await request('/api/system/terminal');
if (!response.ok) {
return;
}
const payload = await parse_api_response<TerminalSessionsResponse>(response.json());
const items = Array.isArray(payload?.items)
? payload.items
.map((item) => recentSessionFromPayload(item))
.filter((item): item is RecentSessionItem => item !== null)
: [];
setRecentSessions(items);
syncCurrentSessionToRecentSessions();
} catch {
return;
}
};
const refreshSessionMetadata = async (): Promise<void> => { const refreshSessionMetadata = async (): Promise<void> => {
if (!sessionState.value.sessionId) { if (!sessionState.value.sessionId) {
return; return;
@ -303,10 +476,14 @@ const refreshSessionMetadata = async (): Promise<void> => {
} }
updateState(normalizeResponse(payload)); updateState(normalizeResponse(payload));
syncCurrentSessionToRecentSessions();
} catch (error) { } catch (error) {
if (error instanceof ConsoleSessionExpiredError) { if (error instanceof ConsoleSessionExpiredError) {
markExpired(); markExpired();
return;
} }
return;
} }
}; };
@ -400,6 +577,7 @@ const connectStream = async (): Promise<void> => {
onopen: async (response) => { onopen: async (response) => {
if (response.ok) { if (response.ok) {
updateState({ status: 'running', error: '' }); updateState({ status: 'running', error: '' });
syncCurrentSessionToRecentSessions();
return; return;
} }
@ -434,6 +612,7 @@ const connectStream = async (): Promise<void> => {
if (event.event === 'status') { if (event.event === 'status') {
updateState(normalizeResponse(payload as ConsoleSessionResponse)); updateState(normalizeResponse(payload as ConsoleSessionResponse));
syncCurrentSessionToRecentSessions();
return; return;
} }
@ -501,6 +680,7 @@ const connectStream = async (): Promise<void> => {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
appendTranscript(`Error: ${message}\n`); appendTranscript(`Error: ${message}\n`);
updateState({ status: 'error', error: message }); updateState({ status: 'error', error: message });
syncCurrentSessionToRecentSessions();
} finally { } finally {
if (streamController.value === controller) { if (streamController.value === controller) {
streamController.value = null; streamController.value = null;
@ -517,12 +697,15 @@ const restoreSession = async (): Promise<void> => {
if (payload) { if (payload) {
updateState(normalizeResponse(payload)); updateState(normalizeResponse(payload));
syncCurrentSessionToRecentSessions();
} }
if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) { if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) {
await connectStream(); await connectStream();
} }
await fetchRecentSessions();
return; return;
} }
@ -531,28 +714,36 @@ const restoreSession = async (): Promise<void> => {
sessionState.value.command || sessionState.value.command ||
sessionState.value.transcript.length > 0 sessionState.value.transcript.length > 0
) { ) {
await fetchRecentSessions();
return; return;
} }
const active = await readSessionResponse('/api/system/terminal/active', { allowMissing: true }); const active = await readSessionResponse('/api/system/terminal/active', { allowMissing: true });
if (!active) { if (!active) {
await fetchRecentSessions();
return; return;
} }
updateState(normalizeResponse(active)); updateState(normalizeResponse(active));
syncCurrentSessionToRecentSessions();
if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) { if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) {
await connectStream(); await connectStream();
} }
await fetchRecentSessions();
} catch (error) { } catch (error) {
if (error instanceof ConsoleSessionExpiredError) { if (error instanceof ConsoleSessionExpiredError) {
markExpired(); markExpired();
await fetchRecentSessions();
return; return;
} }
if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) { if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) {
await connectStream(); await connectStream();
} }
await fetchRecentSessions();
} }
}; };
@ -594,16 +785,20 @@ const startSession = async ({
]), ]),
error: '', error: '',
}); });
syncCurrentSessionToRecentSessions();
if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) { if (sessionState.value.sessionId && isActiveSessionStatus(sessionState.value.status)) {
await connectStream(); await connectStream();
} }
await fetchRecentSessions();
return Boolean(sessionState.value.sessionId); return Boolean(sessionState.value.sessionId);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
appendTranscript(`Error: ${message}\n`); appendTranscript(`Error: ${message}\n`);
updateState({ status: 'error', error: message }); updateState({ status: 'error', error: message });
syncCurrentSessionToRecentSessions();
return false; return false;
} }
}; };
@ -623,6 +818,7 @@ const cancelSession = async (): Promise<CancelConsoleSessionResult> => {
if (response.status === 404) { if (response.status === 404) {
await refreshSessionMetadata(); await refreshSessionMetadata();
await fetchRecentSessions();
return { status: 'missing', message: 'Terminal session not found.' }; return { status: 'missing', message: 'Terminal session not found.' };
} }
@ -631,6 +827,7 @@ const cancelSession = async (): Promise<CancelConsoleSessionResult> => {
} }
updateState({ error: '' }); updateState({ error: '' });
await fetchRecentSessions();
return { status: 'cancelled' }; return { status: 'cancelled' };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
@ -642,6 +839,32 @@ const cancelSession = async (): Promise<CancelConsoleSessionResult> => {
const resetState = (): void => { const resetState = (): void => {
stopStream(); stopStream();
sessionState.value = { ...DEFAULT_STATE }; sessionState.value = { ...DEFAULT_STATE };
recentSessions.value = [];
hiddenRecentSessions.value = [];
};
const replaySession = async (item: RecentSessionItem): Promise<boolean> => {
stopStream();
updateState({
sessionId: item.sessionId,
command: item.command,
status: item.status === 'running' ? 'reconnecting' : 'finished',
lastEventId: null,
exitCode: item.exitCode,
transcript: [],
error: '',
});
syncCurrentSessionToRecentSessions();
await connectStream();
await fetchRecentSessions();
if (sessionState.value.status === 'expired') {
hideRecentSession(item.sessionId);
return false;
}
return sessionState.value.status !== 'error';
}; };
const useConsoleSession = () => { const useConsoleSession = () => {
@ -649,7 +872,12 @@ const useConsoleSession = () => {
state: sessionState, state: sessionState,
bufferedTranscript: computed(() => sessionState.value.transcript.slice()), bufferedTranscript: computed(() => sessionState.value.transcript.slice()),
isLoading: computed(() => isActiveSessionStatus(sessionState.value.status)), isLoading: computed(() => isActiveSessionStatus(sessionState.value.status)),
recentSessions: computed(() => recentSessions.value.slice()),
clearTranscript, clearTranscript,
clearRecentSessions,
fetchRecentSessions,
hideRecentSession,
replaySession,
restoreSession, restoreSession,
startSession, startSession,
cancelSession, cancelSession,

View file

@ -177,27 +177,27 @@
class="flex items-center gap-2 text-sm font-semibold text-highlighted" class="flex items-center gap-2 text-sm font-semibold text-highlighted"
> >
<UIcon name="i-lucide-history" class="size-4 text-toned" /> <UIcon name="i-lucide-history" class="size-4 text-toned" />
<span>Command history</span> <span>Recent runs</span>
</div> </div>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-trash" icon="i-lucide-eye-off"
:disabled="historyEntries.length < 1" :disabled="recentSessionEntries.length < 1"
@click="() => void clearHistory()" @click="() => void hideRecentSessions()"
> >
Clear history Hide recent runs
</UButton> </UButton>
</div> </div>
<UAlert <UAlert
v-if="historyEntries.length < 1" v-if="recentSessionEntries.length < 1"
color="info" color="info"
variant="soft" variant="soft"
icon="i-lucide-clock-3" icon="i-lucide-clock-3"
title="Command history is empty" title="No recent console sessions"
/> />
<div <div
@ -205,34 +205,119 @@
class="max-h-96 w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default" class="max-h-96 w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
> >
<div class="w-full max-w-full overflow-auto overscroll-contain"> <div class="w-full max-w-full overflow-auto overscroll-contain">
<table class="min-w-155 w-full text-sm"> <table class="w-full text-sm">
<tbody class="divide-y divide-default"> <tbody class="divide-y divide-default">
<tr <tr
v-for="(cmd, index) in historyEntries" v-for="item in recentSessionEntries"
:key="`${index}-${cmd}`" :key="item.sessionId"
class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0" class="transition-colors hover:bg-elevated/70 [&>td]:border-r [&>td]:border-default/60 [&>td:last-child]:border-r-0"
> >
<td class="px-3 py-3 align-middle"> <td class="px-3 py-3 align-middle">
<button <div class="space-y-2">
type="button" <button
class="block w-full text-left font-mono text-xs text-default hover:text-highlighted" type="button"
@click="() => void loadCommand(cmd)" class="block w-full text-left font-mono text-xs text-default hover:text-highlighted"
> @click="() => void replayRecentSession(item)"
{{ cmd.replace(/\n/g, ' ') }} >
</button> {{ item.command }}
</button>
<div
class="flex flex-wrap items-center gap-2 text-[11px] text-toned"
>
<UBadge
:color="recentSessionStatusColor(item)"
variant="soft"
size="sm"
>
<span
v-if="recentSessionStatusSpinning(item)"
class="inline-flex items-center gap-1.5"
>
<UIcon
:name="recentSessionStatusIcon(item)"
class="size-3.5 animate-spin"
/>
<span>{{ recentSessionStatusLabel(item) }}</span>
</span>
<span v-else class="inline-flex items-center gap-1.5">
<UIcon
:name="recentSessionStatusIcon(item)"
class="size-3.5"
/>
<span>{{ recentSessionStatusLabel(item) }}</span>
</span>
</UBadge>
<UTooltip
v-if="item.finishedAt || item.startedAt || item.createdAt"
:text="
moment
.unix(
item.finishedAt ||
item.startedAt ||
item.createdAt ||
0,
)
.format('YYYY-M-DD H:mm Z')
"
>
<span
class="inline-flex items-center gap-1.5 cursor-help"
>
<UIcon name="i-lucide-clock-3" class="size-3.5" />
<time
:datetime="
moment
.unix(
item.finishedAt ||
item.startedAt ||
item.createdAt ||
0,
)
.toISOString()
"
>
{{
moment
.unix(
item.finishedAt ||
item.startedAt ||
item.createdAt ||
0,
)
.fromNow()
}}
</time>
</span>
</UTooltip>
</div>
</div>
</td> </td>
<td <td
class="w-12 px-3 py-3 text-center align-middle whitespace-nowrap" class="w-28 px-3 py-3 text-center align-middle whitespace-nowrap"
> >
<UButton <div class="flex items-center justify-end gap-1">
color="neutral" <UButton
variant="ghost" color="neutral"
size="xs" variant="ghost"
icon="i-lucide-x" size="xs"
square icon="i-lucide-terminal"
@click="removeFromHistory(index)" @click="() => void loadCommand(item.command)"
/> >
Fill
</UButton>
<UButton
color="neutral"
variant="ghost"
size="xs"
icon="i-lucide-x"
square
@click="hideRecentSession(item.sessionId)"
/>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -315,6 +400,7 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import moment from 'moment';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { FitAddon } from '@xterm/addon-fit'; import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css'; import '@xterm/xterm/css/xterm.css';
@ -327,7 +413,6 @@ import type { AutoCompleteOptions } from '~/types/autocomplete';
import { disableOpacity, enableOpacity } from '~/utils'; import { disableOpacity, enableOpacity } from '~/utils';
import { requirePageShell } from '~/utils/topLevelNavigation'; import { requirePageShell } from '~/utils/topLevelNavigation';
const MAX_HISTORY_ITEMS = 50;
const ACTIVE_SESSION_STATUSES = ['starting', 'running', 'reconnecting']; const ACTIVE_SESSION_STATUSES = ['starting', 'running', 'reconnecting'];
let flushFrame: number | null = null; let flushFrame: number | null = null;
@ -351,7 +436,6 @@ const terminal_window = useTemplateRef<HTMLDivElement>('terminal_window');
const commandInput = ref<InstanceType<typeof InputAutocomplete> | null>(null); const commandInput = ref<InstanceType<typeof InputAutocomplete> | null>(null);
const commandTextarea = ref<InstanceType<typeof TextareaAutocomplete> | null>(null); const commandTextarea = ref<InstanceType<typeof TextareaAutocomplete> | null>(null);
const storedCommand = useStorage<string>('console_command', ''); const storedCommand = useStorage<string>('console_command', '');
const commandHistory = useStorage<string[]>('console_command_history', []);
const pageCardUi = { const pageCardUi = {
root: 'flex min-h-0 flex-1 w-full bg-transparent', root: 'flex min-h-0 flex-1 w-full bg-transparent',
@ -371,6 +455,7 @@ const ytDlpOptions = computed<AutoCompleteOptions>(() =>
); );
const bufferedTranscript = consoleSession.bufferedTranscript; const bufferedTranscript = consoleSession.bufferedTranscript;
const recentSessions = consoleSession.recentSessions;
const sessionStatus = computed(() => consoleSession.state.value.status); const sessionStatus = computed(() => consoleSession.state.value.status);
const sessionError = computed(() => consoleSession.state.value.error); const sessionError = computed(() => consoleSession.state.value.error);
const sessionExitCode = computed(() => consoleSession.state.value.exitCode); const sessionExitCode = computed(() => consoleSession.state.value.exitCode);
@ -380,7 +465,7 @@ const runnableCommand = computed(() => displayCommand.value.replace(/\n/g, ' ').
const hasValidCommand = computed(() => Boolean(runnableCommand.value)); const hasValidCommand = computed(() => Boolean(runnableCommand.value));
const isLoading = computed(() => consoleSession.isLoading.value); const isLoading = computed(() => consoleSession.isLoading.value);
const isMultiLineInput = computed(() => Boolean(command.value && command.value.includes('\n'))); const isMultiLineInput = computed(() => Boolean(command.value && command.value.includes('\n')));
const historyEntries = computed(() => commandHistory.value); const recentSessionEntries = computed(() => recentSessions.value);
const canManualReconnect = computed(() => sessionStatus.value === 'reconnecting'); const canManualReconnect = computed(() => sessionStatus.value === 'reconnecting');
const showCancelButton = computed(() => const showCancelButton = computed(() =>
['starting', 'running', 'reconnecting'].includes(sessionStatus.value), ['starting', 'running', 'reconnecting'].includes(sessionStatus.value),
@ -481,6 +566,63 @@ const sessionStatusIcon = computed(() => {
} }
}); });
const sessionStatusSpinning = computed(() => ACTIVE_SESSION_STATUSES.includes(sessionStatus.value)); const sessionStatusSpinning = computed(() => ACTIVE_SESSION_STATUSES.includes(sessionStatus.value));
const isRecentSessionFailed = (item: (typeof recentSessions.value)[number]): boolean => {
return item.status === 'completed' && item.exitCode !== null && item.exitCode !== 0;
};
const recentSessionStatusLabel = (item: (typeof recentSessions.value)[number]): string => {
if (isRecentSessionFailed(item)) {
return 'Failed';
}
switch (item.status) {
case 'starting':
return 'Starting';
case 'running':
return 'Running';
default:
return 'Completed';
}
};
const recentSessionStatusColor = (
item: (typeof recentSessions.value)[number],
): 'success' | 'error' | 'info' | 'neutral' => {
if (isRecentSessionFailed(item)) {
return 'error';
}
switch (item.status) {
case 'starting':
case 'running':
return 'info';
default:
return item.exitCode === 0 ? 'success' : 'neutral';
}
};
const recentSessionStatusIcon = (item: (typeof recentSessions.value)[number]): string => {
if (isRecentSessionFailed(item)) {
return 'i-lucide-triangle-alert';
}
switch (item.status) {
case 'starting':
case 'running':
return 'i-lucide-loader-circle';
default:
return item.exitCode === 0 ? 'i-lucide-circle-check' : 'i-lucide-circle-dot';
}
};
const recentSessionStatusSpinning = (item: (typeof recentSessions.value)[number]): boolean => {
return ['starting', 'running'].includes(item.status);
};
watch(command, (value) => { watch(command, (value) => {
storedCommand.value = value; storedCommand.value = value;
}); });
@ -765,13 +907,6 @@ const handlePaste = async (event: ClipboardEvent): Promise<void> => {
} }
}; };
const addToHistory = (cmd: string): void => {
commandHistory.value = [cmd, ...commandHistory.value.filter((item) => item !== cmd)].slice(
0,
MAX_HISTORY_ITEMS,
);
};
const runCommand = async (): Promise<void> => { const runCommand = async (): Promise<void> => {
if (!canStartCommand.value) { if (!canStartCommand.value) {
return; return;
@ -795,8 +930,6 @@ const runCommand = async (): Promise<void> => {
await ensureTerminal(); await ensureTerminal();
addToHistory(displayCommand.value);
const started = await consoleSession.startSession({ const started = await consoleSession.startSession({
command: runnableCommand.value, command: runnableCommand.value,
displayCommand: displayCommand.value, displayCommand: displayCommand.value,
@ -871,14 +1004,19 @@ const loadCommand = async (cmd: string): Promise<void> => {
await focusInput(); await focusInput();
}; };
const clearHistory = async (): Promise<void> => { const hideRecentSession = (sessionId: string): void => {
if (historyEntries.value.length < 1) { consoleSession.hideRecentSession(sessionId);
};
const hideRecentSessions = async (): Promise<void> => {
if (recentSessionEntries.value.length < 1) {
return; return;
} }
const { status } = await dialog.confirmDialog({ const { status } = await dialog.confirmDialog({
title: 'Confirm Action', title: 'Confirm Action',
message: 'Clear commands history?', message: 'Hide the current recent runs from this browser?',
confirmText: 'Hide runs',
confirmColor: 'error', confirmColor: 'error',
}); });
@ -886,11 +1024,20 @@ const clearHistory = async (): Promise<void> => {
return; return;
} }
commandHistory.value = []; consoleSession.clearRecentSessions();
await focusInput();
}; };
const removeFromHistory = (index: number): void => { const replayRecentSession = async (item: (typeof recentSessions.value)[number]): Promise<void> => {
commandHistory.value = commandHistory.value.filter((_, i) => i !== index); try {
await consoleSession.replaySession(item);
restoreBufferedTerminalOutput();
} catch {
consoleSession.hideRecentSession(item.sessionId);
} finally {
await nextTick();
await focusInput();
}
}; };
onMounted(async () => { onMounted(async () => {
@ -907,6 +1054,7 @@ onMounted(async () => {
await ensureTerminal(); await ensureTerminal();
await consoleSession.restoreSession(); await consoleSession.restoreSession();
await consoleSession.fetchRecentSessions();
restoreBufferedTerminalOutput(); restoreBufferedTerminalOutput();
if (storedCommand.value.trim()) { if (storedCommand.value.trim()) {

View file

@ -203,7 +203,7 @@
</div> </div>
</div> </div>
<p class="description">Getting the interface ready. Please wait a moment...</p> <p class="description">Getting the interface ready.</p>
<div class="loader" aria-hidden="true"></div> <div class="loader" aria-hidden="true"></div>
<p class="hint">Please wait...</p> <p class="hint">Please wait...</p>

View file

@ -16,4 +16,29 @@ type version_check = {
}; };
}; };
export { ImportedItem, version_check }; type TerminalSessionStatus = 'starting' | 'running' | 'completed' | 'interrupted' | 'failed';
type TerminalSessionItem = {
session_id: string;
command: string;
status: TerminalSessionStatus;
created_at: number | null;
started_at: number | null;
finished_at: number | null;
expires_at: number | null;
available_until: number | null;
exit_code: number | null;
last_sequence: number;
};
type TerminalSessionsResponse = {
items: Array<TerminalSessionItem>;
};
export {
ImportedItem,
TerminalSessionItem,
TerminalSessionStatus,
TerminalSessionsResponse,
version_check,
};

File diff suppressed because it is too large Load diff

View file

@ -90,6 +90,9 @@ export default defineNuxtConfig({
'marked-base-url', 'marked-base-url',
'marked-alert', 'marked-alert',
'marked-gfm-heading-id', 'marked-gfm-heading-id',
'hls.js',
'@vue/devtools-core',
'@vue/devtools-kit',
], ],
}, },
server: { server: {

View file

@ -21,11 +21,11 @@
}, },
"web-types": "./web-types.json", "web-types": "./web-types.json",
"dependencies": { "dependencies": {
"@iconify-json/lucide": "^1.2.102", "@iconify-json/lucide": "^1.2.103",
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@nuxt/eslint": "^1.15.2", "@nuxt/eslint": "^1.15.2",
"@nuxt/eslint-config": "^1.15.2", "@nuxt/eslint-config": "^1.15.2",
"@nuxt/ui": "^4.6.1", "@nuxt/ui": "^4.7.0",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.2.1",
"@vueuse/nuxt": "^14.2.1", "@vueuse/nuxt": "^14.2.1",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
@ -33,30 +33,30 @@
"cron-parser": "^5.5.0", "cron-parser": "^5.5.0",
"cronstrue": "^3.14.0", "cronstrue": "^3.14.0",
"hls.js": "^1.6.16", "hls.js": "^1.6.16",
"marked": "^18.0.0", "marked": "^18.0.2",
"marked-alert": "^2.1.2", "marked-alert": "^2.1.2",
"marked-base-url": "^1.1.9", "marked-base-url": "^1.1.9",
"marked-gfm-heading-id": "^4.1.4", "marked-gfm-heading-id": "^4.1.4",
"moment": "^2.30.1", "moment": "^2.30.1",
"nuxt": "^4.4.2", "nuxt": "^4.4.2",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.4",
"vue": "^3.5.32", "vue": "^3.5.33",
"vue-router": "^5.0.4" "vue-router": "^5.0.6"
}, },
"trustedDependencies": [ "trustedDependencies": [
"esbuild", "esbuild",
"@parcel/watcher" "@parcel/watcher"
], ],
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.12", "@types/bun": "^1.3.13",
"@types/node": "25.6.0", "@types/node": "25.6.0",
"@types/jsdom": "^28.0.1", "@types/jsdom": "^28.0.1",
"@typescript-eslint/parser": "^8.58.2", "@typescript-eslint/parser": "^8.59.0",
"eslint": "^10.2.0", "eslint": "^10.2.1",
"jsdom": "^29.0.2", "jsdom": "^29.0.2",
"oxfmt": "^0.45.0", "oxfmt": "^0.46.0",
"typescript": "^6.0.2", "typescript": "^6.0.3",
"vue-eslint-parser": "^10.4.0", "vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.6" "vue-tsc": "^3.2.7"
} }
} }

View file

@ -410,15 +410,10 @@ describe('useConsoleSession', () => {
expect(session.state.value.status).toBe('running') expect(session.state.value.status).toBe('running')
expect(session.state.value.error).toBe('') expect(session.state.value.error).toBe('')
const requestCall = requestSpy.mock.calls.at(-1) const deleteCall = requestSpy.mock.calls.find(
expect(requestCall).toBeDefined() (call) => call[0] === '/api/system/terminal/sess-stop' && call[1]?.method === 'DELETE',
if (!requestCall) { )
throw new Error('Expected request to be called for session cancellation.') expect(deleteCall).toBeDefined()
}
const [path, options] = requestCall as [string, { method: string }]
expect(path).toBe('/api/system/terminal/sess-stop')
expect(options.method).toBe('DELETE')
requestSpy.mockRestore() requestSpy.mockRestore()
}) })
@ -502,4 +497,128 @@ describe('useConsoleSession', () => {
expect(session.state.value.status).toBe('running') expect(session.state.value.status).toBe('running')
expect(session.state.value.lastEventId).toBe('15') expect(session.state.value.lastEventId).toBe('15')
}) })
it('fetches recent sessions and filters locally hidden entries', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: {
items: [
{
session_id: 'sess-newer',
command: '--version',
status: 'completed',
created_at: 10,
started_at: 11,
finished_at: 20,
expires_at: 100,
available_until: 100,
exit_code: 0,
last_sequence: 2,
},
{
session_id: 'sess-hidden',
command: '--help',
status: 'running',
created_at: 1,
started_at: 2,
finished_at: null,
expires_at: null,
available_until: null,
exit_code: null,
last_sequence: 1,
},
],
},
}),
)
const session = useConsoleSession()
session.hideRecentSession('sess-hidden')
await session.fetchRecentSessions()
expect(session.recentSessions.value).toEqual([
{
sessionId: 'sess-newer',
command: '--version',
status: 'completed',
createdAt: 10,
startedAt: 11,
finishedAt: 20,
expiresAt: 100,
availableUntil: 100,
exitCode: 0,
lastSequence: 2,
},
])
requestSpy.mockRestore()
})
it('replays a recent session by reconnecting to its stream', async () => {
const requestSpy = spyOn(utils, 'request')
requestSpy.mockResolvedValueOnce(
createMockResponse({
ok: true,
status: 200,
jsonData: {
items: [
{
session_id: 'sess-replay',
command: '--help',
status: 'completed',
created_at: 10,
started_at: 11,
finished_at: 12,
expires_at: 100,
available_until: 100,
exit_code: 0,
last_sequence: 2,
},
],
},
}),
)
fetchEventSourceMock.mockImplementationOnce(async (_url, options) => {
await options.onopen(new Response('', { status: 200 }))
options.onmessage({
event: 'output',
id: '1',
data: JSON.stringify({ line: 'restored output' }),
})
options.onmessage({
event: 'close',
id: '2',
data: JSON.stringify({ exitcode: 0 }),
})
})
const session = useConsoleSession()
const replayed = await session.replaySession({
sessionId: 'sess-replay',
command: '--help',
status: 'completed',
createdAt: 10,
startedAt: 11,
finishedAt: 12,
expiresAt: 100,
availableUntil: 100,
exitCode: 0,
lastSequence: 2,
})
await Promise.resolve()
await Promise.resolve()
expect(replayed).toBe(true)
expect(session.state.value.sessionId).toBe('sess-replay')
expect(session.state.value.transcript).toEqual(['restored output\n'])
const streamCall = fetchEventSourceMock.mock.calls.at(-1)
expect(streamCall?.[0]).toContain('/base-path/api/system/terminal/sess-replay/stream')
requestSpy.mockRestore()
})
}) })

286
uv.lock
View file

@ -146,7 +146,7 @@ wheels = [
[[package]] [[package]]
name = "apprise" name = "apprise"
version = "1.9.9" version = "1.10.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@ -157,9 +157,9 @@ dependencies = [
{ name = "requests-oauthlib" }, { name = "requests-oauthlib" },
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/20/f4/be5c7e39b83a2285ab62ae7c19bb10704836f59c0a5b4c471730f54c9f98/apprise-1.9.9.tar.gz", hash = "sha256:fd622c0df16bdc79ed385539735573488cafe2405d25747e87eebd6b09b26012", size = 2032822 } sdist = { url = "https://files.pythonhosted.org/packages/2f/74/9c16829d3e7e45ce7daf1b704687fa4fde7ea00d72eafe8de18c72bf5995/apprise-1.10.0.tar.gz", hash = "sha256:b768f32d99e45ed5f4c3eef1f67903e803c97f97ba61a531a5d0a45d40df90a8", size = 2188611 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/2f/54d068d7e011a8b4e0aae3e93b09a30b33bcf780829fe70c6e8876aeb0e0/apprise-1.9.9-py3-none-any.whl", hash = "sha256:55ceb8827a1c783d683881c9f77fa42eb43b3fc91b854419c452d557101c7068", size = 1519940 }, { url = "https://files.pythonhosted.org/packages/69/f9/177a73589d34e676d10bc4c6a8328710e28af5907234e9f25bb149a04eec/apprise-1.10.0-py3-none-any.whl", hash = "sha256:e685303d3568bb7a057d6ddeafd27ee12fff183ca36483ad4bacc0b9b4efa82c", size = 1632292 },
] ]
[[package]] [[package]]
@ -240,11 +240,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.2.25" version = "2026.4.22"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 } sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707 },
] ]
[[package]] [[package]]
@ -351,14 +351,14 @@ wheels = [
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.2" version = "8.3.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856 } sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379 }, { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502 },
] ]
[[package]] [[package]]
@ -538,39 +538,39 @@ wheels = [
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.4.0" version = "3.5.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913 } sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856 }, { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066 },
{ url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208 }, { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414 },
{ url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269 }, { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349 },
{ url = "https://files.pythonhosted.org/packages/e0/93/c8c508d68ba93232784bbc1b5474d92371f2897dfc6bc281b419f2e0d492/greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f", size = 628455 }, { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234 },
{ url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549 }, { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927 },
{ url = "https://files.pythonhosted.org/packages/7f/46/cfaaa0ade435a60550fd83d07dfd5c41f873a01da17ede5c4cade0b9bab8/greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55", size = 426238 }, { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243 },
{ url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310 }, { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893 },
{ url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435 }, { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060 },
{ url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760 }, { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740 },
{ url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498 }, { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525 },
{ url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291 }, { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564 },
{ url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749 }, { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166 },
{ url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084 }, { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792 },
{ url = "https://files.pythonhosted.org/packages/d2/4a/74078d3936712cff6d3c91a930016f476ce4198d84e224fe6d81d3e02880/greenlet-3.4.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06c2d3b89e0c62ba50bd7adf491b14f39da9e7e701647cb7b9ff4c99bee04b19", size = 673405 }, { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086 },
{ url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621 }, { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933 },
{ url = "https://files.pythonhosted.org/packages/79/3e/df8a83ab894751bc31e1106fdfaa80ca9753222f106b04de93faaa55feb7/greenlet-3.4.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:070b8bac2ff3b4d9e0ff36a0d19e42103331d9737e8504747cd1e659f76297bd", size = 471670 }, { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799 },
{ url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259 }, { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401 },
{ url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916 }, { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038 },
{ url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821 }, { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835 },
{ url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932 }, { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862 },
{ url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681 }, { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614 },
{ url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899 }, { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723 },
{ url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284 }, { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529 },
{ url = "https://files.pythonhosted.org/packages/ef/92/466b0d9afd44b8af623139a3599d651c7564fa4152f25f117e1ee5949ffb/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4cd56a9eb7a6444edbc19062f7b6fbc8f287c663b946e3171d899693b1c19fa", size = 665872 }, { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015 },
{ url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861 }, { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364 },
{ url = "https://files.pythonhosted.org/packages/0d/14/3395a7ef3e260de0325152ddfe19dffb3e49fe10873b94654352b53ad48e/greenlet-3.4.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:1f85f204c4d54134ae850d401fa435c89cd667d5ce9dc567571776b45941af72", size = 489237 }, { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275 },
{ url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182 }, { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204 },
{ url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050 }, { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480 },
{ url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554 }, { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324 },
] ]
[[package]] [[package]]
@ -684,11 +684,11 @@ wheels = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.13"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629 },
] ]
[[package]] [[package]]
@ -892,11 +892,11 @@ wheels = [
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.1" version = "26.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519 } sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831 }, { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195 },
] ]
[[package]] [[package]]
@ -926,11 +926,11 @@ wheels = [
[[package]] [[package]]
name = "pip" name = "pip"
version = "26.0.1" version = "26.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747 } sdist = { url = "https://files.pythonhosted.org/packages/73/7e/d2b04004e1068ad4fdfa2f227b839b5d03e602e47cdbbf49de71137c9546/pip-26.1.tar.gz", hash = "sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3", size = 1840316 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723 }, { url = "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", hash = "sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", size = 1812804 },
] ]
[[package]] [[package]]
@ -944,21 +944,21 @@ wheels = [
[[package]] [[package]]
name = "playwright" name = "playwright"
version = "1.58.0" version = "1.59.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "greenlet" }, { name = "greenlet" },
{ name = "pyee" }, { name = "pyee" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098 }, { url = "https://files.pythonhosted.org/packages/5b/48/abab23f40643b4de8f2665816f0a1bf0994eeecda39d6d62f0f292b2ad01/playwright-1.59.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bfc6940100b57423175c819ce2422ec5880d55fa2769987f62ab7a1f5fe6783e", size = 43156922 },
{ url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625 }, { url = "https://files.pythonhosted.org/packages/08/71/5e4d98b2ce3641b4343623c6450ff33b9de1c979d12a957505e392338b07/playwright-1.59.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:af068143a0c045ec11608b67d6c42e58db7e9cf65a742dd21fddedc1a9802c47", size = 41947177 },
{ url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098 }, { url = "https://files.pythonhosted.org/packages/80/91/fd219aa78ca03d37e93aaedaed4e224131e3090a9264f9bb773c8271d67e/playwright-1.59.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:4a4a2d4842b0e4120de3fa48636e4b69085a05b81d8a35ad4353f530ade72ed6", size = 43156922 },
{ url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268 }, { url = "https://files.pythonhosted.org/packages/73/0c/1e513d37c5be07d12829ebce93dbfe7baee230084cb66966c423432799c4/playwright-1.59.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c5792aad9e22b91a09264b9edbc18553cf05ea5a39404d65dc19a012c6b2e51d", size = 47151793 },
{ url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214 }, { url = "https://files.pythonhosted.org/packages/a3/2d/15f72288cb65d690134e18fefb9483cc4976f7579b580648c45e494481a7/playwright-1.59.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c881a19377d2b900af855fb525b5f22a27bf3cfbecba6d1edb36766d56cb100", size = 46877615 },
{ url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998 }, { url = "https://files.pythonhosted.org/packages/72/a1/717ac5bc99f387c0f60def91271ea4262125c0815d764a5d1776a272275c/playwright-1.59.0-py3-none-win32.whl", hash = "sha256:6989c476be2b9cd3e24a18cc9dcf202e266fb3d91e3e5395cd668c54ea54b119", size = 37713698 },
{ url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005 }, { url = "https://files.pythonhosted.org/packages/0f/a5/4e630ee05d8b46b840f943268e86d6063703e8dadb2d3eb405c7b9b2e48c/playwright-1.59.0-py3-none-win_amd64.whl", hash = "sha256:d5a5cc064b82ca92996080025710844e417f44df8fda9001102c28f44174171c", size = 37713704 },
{ url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919 }, { url = "https://files.pythonhosted.org/packages/eb/0c/3ece41761ba13c8321009aefcaec7a016eb42799c42eef5e03ace7f2de5b/playwright-1.59.0-py3-none-win_arm64.whl", hash = "sha256:93581ad515728cadc8af39b288a5633ba6d36e7d72048e79d890ce01ea2156f9", size = 33956745 },
] ]
[[package]] [[package]]
@ -1110,7 +1110,7 @@ wheels = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.13.2" version = "2.13.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-types" }, { name = "annotated-types" },
@ -1118,65 +1118,65 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/09/e5/06d23afac9973109d1e3c8ad38e1547a12e860610e327c05ee686827dc37/pydantic-2.13.2.tar.gz", hash = "sha256:b418196607e61081c3226dcd4f0672f2a194828abb9109e9cfb84026564df2d1", size = 843836 } sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/ca/b45c378e6e8d0b90577288b533e04e95b7afd61bb1d51b6c263176435489/pydantic-2.13.2-py3-none-any.whl", hash = "sha256:a525087f4c03d7e7456a3de89b64cd693d2229933bb1068b9af6befd5563694e", size = 471947 }, { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981 },
] ]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.46.2" version = "2.46.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/43/bb/4742f05b739b2478459bb16fa8470549518c802e06ddcf3f106c5081315e/pydantic_core-2.46.2.tar.gz", hash = "sha256:37bb079f9ee3f1a519392b73fda2a96379b31f2013c6b467fe693e7f2987f596", size = 471269 } sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/2b/662e48254479a2d3450ba24b1e25061108b64339794232f503990c519144/pydantic_core-2.46.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d26e9eea3715008a09a74585fe9becd0c67fbb145dc4df9756d597d7230a652c", size = 2101762 }, { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109 },
{ url = "https://files.pythonhosted.org/packages/73/ab/bafd7c7503757ccc8ec4d1911e106fe474c629443648c51a88f08b0fe91a/pydantic_core-2.46.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48b36e3235140510dc7861f0cd58b714b1cdd3d48f75e10ce52e69866b746f10", size = 1951814 }, { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820 },
{ url = "https://files.pythonhosted.org/packages/92/cc/7549c2d57ba2e9a42caa5861a2d398dbe31c02c6aca783253ace59ce84f8/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b1f99dc451f1a3981f236151465bcf995bbe712d0727c9f7b236fe228a8133", size = 1977329 }, { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785 },
{ url = "https://files.pythonhosted.org/packages/18/50/7ed4a8a0d478a4dca8f0134a5efa7193f03cc8520dd4c9509339fb2e5002/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8641c8d535c2d95b45c2e19b646ecd23ebba35d461e0ae48a3498277006250ab", size = 2051832 }, { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761 },
{ url = "https://files.pythonhosted.org/packages/dc/16/bb35b193741c0298ddc5f5e4234269efdc0c65e2bcd198aa0de9b68845e4/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20fb194788a0a50993e87013e693494ba183a2af5b44e99cf060bbae10912b11", size = 2233127 }, { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989 },
{ url = "https://files.pythonhosted.org/packages/91/a5/98f4b637149185addea19e1785ea20c373cca31b202f589111d8209d9873/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9262d11d0cd11ee3303a95156939402bed6cedfe5ed0e331b95a283a4da6eb8b", size = 2297418 }, { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975 },
{ url = "https://files.pythonhosted.org/packages/36/90/93a5d21990b152da7b7507b7fddb0b935f6a0984d57ac3ec45a6e17777a2/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac204542736aa295fa25f713b7fad6fc50b46ab7764d16087575c85f085174f3", size = 2093735 }, { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325 },
{ url = "https://files.pythonhosted.org/packages/14/22/b8b1ffdddf08b4e84380bcb67f41dbbf4c171377c1d36fc6290794bb2094/pydantic_core-2.46.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9a7c43a0584742dface3ca0daf6f719d46c1ac2f87cf080050f9ae052c75e1b2", size = 2127570 }, { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368 },
{ url = "https://files.pythonhosted.org/packages/c6/26/e60d72b4e2d0ce1fa811044a974412ac1c567fe067d97b3e6b290530786e/pydantic_core-2.46.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd05e1edb6a90ad446fa268ab09e59202766b837597b714b2492db11ee87fab9", size = 2183524 }, { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908 },
{ url = "https://files.pythonhosted.org/packages/35/32/36bec7584a1eefb17dec4dfa1c946d3fe4440f466c5705b8adfda69c9a9f/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:91155b110788b5501abc7ea954f1d08606219e4e28e3c73a94124307c06efb80", size = 2185408 }, { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422 },
{ url = "https://files.pythonhosted.org/packages/fc/d6/1a5689d873620efd67d6b163db0c444c056adb0849b5bc33e2b9f09665a6/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e4e2c72a529fa03ff228be1d2b76944013f428220b764e03cc50ada67e17a42c", size = 2335171 }, { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709 },
{ url = "https://files.pythonhosted.org/packages/3e/8e/675104802abe8ef502b072050ee5f2e915251aa1a3af87e1015ce31ec42d/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:56291ec1a11c3499890c99a8fd9053b47e60fe837a77ec72c0671b1b8b3dce24", size = 2362743 }, { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428 },
{ url = "https://files.pythonhosted.org/packages/8d/bc/86c5dde4fa6e24467680eef5047da3c1a19be0a527d0d8e14aa76b39307c/pydantic_core-2.46.2-cp313-cp313-win32.whl", hash = "sha256:b50f9c5f826ddca1246f055148df939f5f3f2d0d96db73de28e2233f22210d4c", size = 1958074 }, { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601 },
{ url = "https://files.pythonhosted.org/packages/2a/97/2537e8c1282b2c4eb062580c0d7a4339e10b072b803d1ee0b7f1f0a5c22c/pydantic_core-2.46.2-cp313-cp313-win_amd64.whl", hash = "sha256:251a57788823230ca8cbc99e6245d1a2ed6e180ec4864f251c94182c580c7f2e", size = 2071741 }, { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517 },
{ url = "https://files.pythonhosted.org/packages/da/aa/2ee75798706f9dbc4e76dbe59e41a396c5c311e3d6223b9cf6a5fa7780be/pydantic_core-2.46.2-cp313-cp313-win_arm64.whl", hash = "sha256:315d32d1a71494d6b4e1e14a9fa7a4329597b4c4340088ad7e1a9dafbeed92a9", size = 2025955 }, { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802 },
{ url = "https://files.pythonhosted.org/packages/d0/96/a50ccb6b539ae780f73cea74905468777680e30c6c3bdf714b9d4c116ea0/pydantic_core-2.46.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4f59b45f3ef8650c0c736a57f59031d47ed9df4c0a64e83796849d7d14863a2d", size = 2097111 }, { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614 },
{ url = "https://files.pythonhosted.org/packages/34/5f/fdead7b3afa822ab6e5a18ee0ecffd54937de1877c01ed13a342e0fb3f07/pydantic_core-2.46.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a075a29ebef752784a91532a1a85be6b234ccffec0a9d7978a92696387c3da6", size = 1951904 }, { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896 },
{ url = "https://files.pythonhosted.org/packages/95/e0/1c5d547e550cdab1bec737492aa08865337af6fe7fc9b96f7f45f17d9519/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d12d786e30c04a9d307c5d7080bf720d9bac7f1668191d8e37633a9562749e2", size = 1978667 }, { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314 },
{ url = "https://files.pythonhosted.org/packages/0e/cb/665ce629e218c8228302cb94beff4f6531082a2c87d3ecc3d5e63a26f392/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d5e6d6343b0b5dcacb3503b5de90022968da8ed0ab9ab39d3eda71c20cbf84e", size = 2046721 }, { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133 },
{ url = "https://files.pythonhosted.org/packages/77/e9/6cb2cf60f54c1472bbdfce19d957553b43dbba79d1d7b2930a195c594785/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:233eebac0999b6b9ba76eb56f3ec8fce13164aa16b6d2225a36a79e0f95b5973", size = 2228483 }, { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726 },
{ url = "https://files.pythonhosted.org/packages/0d/2a/93e018dd5571f781ebaeda8c0cf65398489d5bee9b1f484df0b6149b43b9/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cc0eee720dd2f14f3b7c349469402b99ad81a174ab49d3533974529e9d93992", size = 2294663 }, { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214 },
{ url = "https://files.pythonhosted.org/packages/5e/4f/49e57ca55c770c93d9bb046666a54949b42e3c9099a0c5fe94557873fe30/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83ee76bf2c9910513dbc19e7d82367131fa7508dedd6186a462393071cc11059", size = 2098742 }, { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927 },
{ url = "https://files.pythonhosted.org/packages/c6/b0/6e46b5cd3332af665f794b8cdeea206618a8630bd9e7bcc36864518fce81/pydantic_core-2.46.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:d61db38eb4ee5192f0c261b7f2d38e420b554df8912245e3546aee5c45e2fd78", size = 2125922 }, { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789 },
{ url = "https://files.pythonhosted.org/packages/06/d1/40850c81585be443a2abfdf7f795f8fae831baf8e2f9b2133c8246ac671c/pydantic_core-2.46.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f09a713d17bcd55da8ab02ebd9110c5246a49c44182af213b5212800af8bc83", size = 2183000 }, { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815 },
{ url = "https://files.pythonhosted.org/packages/04/af/8493d7dfa03ebb7866909e577c6aa65ea0de7377b86023cc51d0c8e11db3/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:30cacc5fb696e64b8ef6fd31d9549d394dd7d52760db072eecb98e37e3af1677", size = 2180335 }, { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608 },
{ url = "https://files.pythonhosted.org/packages/72/5b/1f6a344c4ffdf284da41c6067b82d5ebcbd11ce1b515ae4b662d4adb6f61/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:7ccfb105fcfe91a22bbb5563ad3dc124bc1aa75bfd2e53a780ab05f78cdf6108", size = 2330002 }, { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968 },
{ url = "https://files.pythonhosted.org/packages/25/ff/9a694126c12d6d2f48a0cafa6f8eef88ef0d8825600e18d03ff2e896c3b2/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:13ffef637dc8370c249e5b26bd18e9a80a4fca3d809618c44e18ec834a7ca7a8", size = 2359920 }, { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842 },
{ url = "https://files.pythonhosted.org/packages/51/c8/3a35c763d68a9cb2675eb10ef242cf66c5d4701b28ae12e688d67d2c180e/pydantic_core-2.46.2-cp314-cp314-win32.whl", hash = "sha256:1b0ab6d756ca2704a938e6c31b53f290c2f9c10d3914235410302a149de1a83e", size = 1953701 }, { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661 },
{ url = "https://files.pythonhosted.org/packages/1a/6a/f2726a780365f7dfd89d62036f984f7acb99978c60c5e1fa7c0cb898ed11/pydantic_core-2.46.2-cp314-cp314-win_amd64.whl", hash = "sha256:99ebade8c9ada4df975372d8dd25883daa0e379a05f1cd0c99aa0c04368d01a6", size = 2071867 }, { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686 },
{ url = "https://files.pythonhosted.org/packages/e1/79/76baacb9feba3d7c399b245ca1a29c74ea0db04ea693811374827eec2290/pydantic_core-2.46.2-cp314-cp314-win_arm64.whl", hash = "sha256:de87422197cf7f83db91d89c86a21660d749b3cd76cd8a45d115b8e675670f02", size = 2017252 }, { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907 },
{ url = "https://files.pythonhosted.org/packages/f1/3b/77c26938f817668d9ad9bab1a905cb23f11d9a3d4bf724d429b3e55a8eaf/pydantic_core-2.46.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:236f22b4a206b5b61db955396b7cf9e2e1ff77f372efe9570128ccfcd6a525eb", size = 2094545 }, { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047 },
{ url = "https://files.pythonhosted.org/packages/fe/de/42c13f590e3c260966aa49bcdb1674774f975467c49abd51191e502bea28/pydantic_core-2.46.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c2012f64d2cd7cca50f49f22445aa5a88691ac2b4498ee0a9a977f8ca4f7289f", size = 1933953 }, { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329 },
{ url = "https://files.pythonhosted.org/packages/4e/84/ebe3ebb3e2d8db656937cfa6f97f544cb7132f2307a4a7dfdcd0ea102a12/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d07d6c63106d3a9c9a333e2636f9c82c703b1a9e3b079299e58747964e4fdb72", size = 1974435 }, { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847 },
{ url = "https://files.pythonhosted.org/packages/b9/15/0bf51ca6709477cd4ef86148b6d7844f3308f029eac361dd0383f1e17b1a/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c326a2b4b85e959d9a1fc3a11f32f84611b6ec07c053e1828a860edf8d068208", size = 2031113 }, { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742 },
{ url = "https://files.pythonhosted.org/packages/02/ae/b7b5af9b79db036d9e61a44c481c17a213dc8fc4b8b71fe6875a72fc778b/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac8a65e798f2462552c00d2e013d532c94d646729dda98458beaf51f9ec7b120", size = 2236325 }, { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235 },
{ url = "https://files.pythonhosted.org/packages/a6/ae/ecef7477b5a03d4a499708f7e75d2836452ebb70b776c2d64612b334f57a/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a3c2bc1cc8164bedbc160b7bb1e8cc1e8b9c27f69ae4f9ae2b976cdae02b2dd", size = 2278135 }, { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633 },
{ url = "https://files.pythonhosted.org/packages/db/e4/2f9d82faa47af6c39fc3f120145fd915971e1e0cb6b55b494fad9fdf8275/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69aa5e10b7e8b1bb4a6888650fd12fcbf11d396ca11d4a44de1450875702830", size = 2109071 }, { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679 },
{ url = "https://files.pythonhosted.org/packages/f1/9c/677cf10873fbd0b116575ab7b97c90482b21564f8a8040beb18edef7a577/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4e6df5c3301e65fb42bc5338bf9a1027a02b0a31dc7f54c33775229af474daf0", size = 2106028 }, { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342 },
{ url = "https://files.pythonhosted.org/packages/d6/53/6a06183544daba51c059123a2064a99039df25f115a06bdb26f2ea177038/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c2f6e32548ac8d559b47944effcf8ae4d81c161f6b6c885edc53bc08b8f192d", size = 2164816 }, { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208 },
{ url = "https://files.pythonhosted.org/packages/57/6f/10fcdd9e3eca66fc828eef0f6f5850f2dd3bca2c59e6e041fb8bc3da39be/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:b089a81c58e6ea0485562bbbbbca4f65c0549521606d5ef27fba217aac9b665a", size = 2166130 }, { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237 },
{ url = "https://files.pythonhosted.org/packages/29/83/92d3fd0e0156cad2e3cb5c26de73794af78ac9fa0c22ab666e566dd67061/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:7f700a6d6f64112ae9193709b84303bbab84424ad4b47d0253301aabce9dfc70", size = 2316605 }, { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540 },
{ url = "https://files.pythonhosted.org/packages/97/f1/facffdb970981068219582e499b8d0871ed163ffcc6b347de5c412669e4c/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:67db6814beaa5fefe91101ec7eb9efda613795767be96f7cf58b1ca8c9ca9972", size = 2358385 }, { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556 },
{ url = "https://files.pythonhosted.org/packages/8b/a1/b8160b2f22b2199467bc68581a4ed380643c16b348a27d6165c6c242d694/pydantic_core-2.46.2-cp314-cp314t-win32.whl", hash = "sha256:32fbc7447be8e3be99bf7869f7066308f16be55b61f9882c2cefc7931f5c7664", size = 1942373 }, { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756 },
{ url = "https://files.pythonhosted.org/packages/0d/90/db89acabe5b150e11d1b59fe3d947dda2ef6abbfef5c82f056ff63802f5d/pydantic_core-2.46.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b317a2b97019c0b95ce99f4f901ae383f40132da6706cdf1731066a73394c25c", size = 2052078 }, { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305 },
{ url = "https://files.pythonhosted.org/packages/97/32/e19b83ceb07a3f1bb21798407790bbc9a31740158fd132b94139cb84e16c/pydantic_core-2.46.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7dcb9d40930dfad7ab6b20bcc6ca9d2b030b0f347a0cd9909b54bd53ead521b1", size = 2016941 }, { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310 },
] ]
[[package]] [[package]]
@ -1202,7 +1202,7 @@ wheels = [
[[package]] [[package]]
name = "pyinstaller" name = "pyinstaller"
version = "6.19.0" version = "6.20.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "altgraph" }, { name = "altgraph" },
@ -1213,19 +1213,19 @@ dependencies = [
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" }, { name = "setuptools" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c8/63/fd62472b6371d89dc138d40c36d87a50dc2de18a035803bbdc376b4ffac4/pyinstaller-6.19.0.tar.gz", hash = "sha256:ec73aeb8bd9b7f2f1240d328a4542e90b3c6e6fbc106014778431c616592a865", size = 4036072 } sdist = { url = "https://files.pythonhosted.org/packages/46/60/d03d52e6690d4e9caf333dcd14550cde634ce6c118b3bc8fa3112c3186fd/pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a", size = 4048728 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/eb/23374721fecfa72677e79800921cb6aceefa6ba48574dc404f3f6c6c3be7/pyinstaller-6.19.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4190e76b74f0c4b5c5f11ac360928cd2e36ec8e3194d437bf6b8648c7bc0c134", size = 1040563 }, { url = "https://files.pythonhosted.org/packages/d0/e4/e228d6d1bbb7fd62dc660a8fb202a583b023d3a3624ca95d1a9290ee4d6a/pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e", size = 1047642 },
{ url = "https://files.pythonhosted.org/packages/cd/7e/dfd724b0b533f5aaec0ee5df406fe2319987ed6964480a706f85478b12ea/pyinstaller-6.19.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8bd68abd812d8a6ba33b9f1810e91fee0f325969733721b78151f0065319ca11", size = 735477 }, { url = "https://files.pythonhosted.org/packages/ce/bd/afb631bcb3f9040efebd4f6d067f0828b51710818f69fb41a2d4b7787f52/pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712", size = 742494 },
{ url = "https://files.pythonhosted.org/packages/88/c9/ee3a4101c31f26344e66896c73c1fd6ed8282bf871473365b7f8674af406/pyinstaller-6.19.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1ec54ef967996ca61dacba676227e2b23219878ccce5ee9d6f3aada7b8ed8abf", size = 747143 }, { url = "https://files.pythonhosted.org/packages/76/08/0729a5bac14754150e5d83b39d87d842eb42b0bffcaa03dbad6252e23a39/pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941", size = 754191 },
{ url = "https://files.pythonhosted.org/packages/da/0a/fc77e9f861be8cf300ac37155f59cc92aff99b29f2ddd78546f563a5b5a6/pyinstaller-6.19.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4ab2bb52e58448e14ddf9450601bdedd66800465043501c1d8f1cab87b60b122", size = 744849 }, { url = "https://files.pythonhosted.org/packages/e6/82/bc0ee4c7b97db1958eb651e0da9fb1e672e5ae53ca8867fd97701de52906/pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e", size = 751902 },
{ url = "https://files.pythonhosted.org/packages/6d/e3/6872e020ee758afe0b821663858492c10745608b07150e5e2c824a5b3e1c/pyinstaller-6.19.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:da6d5c6391ccefe73554b9fa29b86001c8e378e0f20c2a4004f836ba537eff63", size = 741590 }, { url = "https://files.pythonhosted.org/packages/3d/e7/770002d6aaa54173881cb2c49bb195ba67b97bf39bac1cdf320f28401629/pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59", size = 748634 },
{ url = "https://files.pythonhosted.org/packages/53/60/b8db5f1a4b0fb228175f2ea0aa33f949adcc097fbe981cc524f9faf85777/pyinstaller-6.19.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a0fc5f6b3c55aa54353f0c74ffa59b1115433c1850c6f655d62b461a2ed6cbbe", size = 741448 }, { url = "https://files.pythonhosted.org/packages/fe/db/68ba1fccb71278b2124fb90b37b7c8c0bc4c1173fba45b94466df3d9cb7f/pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b", size = 748490 },
{ url = "https://files.pythonhosted.org/packages/6f/4d/63b0600f2694e9141b83129fbc1c488ec84d5a0770b1448ec154dcd0fee9/pyinstaller-6.19.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:e649ba6bd1b0b89b210ad92adb5fbdc8a42dd2c5ca4f72ef3a0bfec83a424b83", size = 740613 }, { url = "https://files.pythonhosted.org/packages/03/0f/ac77ffa996a56be3d5c8f85734a007f8347240691657f9704e7de2527fa3/pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7", size = 747650 },
{ url = "https://files.pythonhosted.org/packages/01/d4/e812ad36178093a0e9fd4b8127577748dd85b0cb71de912229dca21fd741/pyinstaller-6.19.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:481a909c8e60c8692fc60fcb1344d984b44b943f8bc9682f2fcdae305ad297e6", size = 740350 }, { url = "https://files.pythonhosted.org/packages/e0/56/1ee91c3a2bc10ca1f36da10a6fd55ff7efc4dec367171eb25992a827874f/pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9", size = 747413 },
{ url = "https://files.pythonhosted.org/packages/52/03/b2c2ee41fb8e10fd2a45d21f5ec2ef25852cfb978dbf762972eed59e3d63/pyinstaller-6.19.0-py3-none-win32.whl", hash = "sha256:3c5c251054fe4cfaa04c34a363dcfbf811545438cb7198304cd444756bc2edd2", size = 1324317 }, { url = "https://files.pythonhosted.org/packages/d7/55/ae264339996953c4cdf9d89d916a0a8fa26a83cf917a742fff8b9d5f3fe8/pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c", size = 1331584 },
{ url = "https://files.pythonhosted.org/packages/9c/d3/6d5e62b8270e2b53a6065e281b3a7785079b00e9019c8019952828dd1669/pyinstaller-6.19.0-py3-none-win_amd64.whl", hash = "sha256:b5bb6536c6560330d364d91522250f254b107cf69129d9cbcd0e6727c570be33", size = 1384894 }, { url = "https://files.pythonhosted.org/packages/76/8c/300f57578882cce259bfb5ae56fda3b69caa3fe9df40a176c719920ea6e2/pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a", size = 1391851 },
{ url = "https://files.pythonhosted.org/packages/81/65/458cd523308a101a22fd2742893405030cc24994cc74b1b767cecf137160/pyinstaller-6.19.0-py3-none-win_arm64.whl", hash = "sha256:c2d5a539b0bfe6159d5522c8c70e1c0e487f22c2badae0f97d45246223b798ea", size = 1325374 }, { url = "https://files.pythonhosted.org/packages/8a/ea/b2f8e1642aecda78c0b75c7321f708e49e10bb3c00dd4f148c40761a1527/pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2", size = 1332259 },
] ]
[[package]] [[package]]
@ -1638,27 +1638,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.11" version = "0.15.12"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264 } sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943 }, { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713 },
{ url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592 }, { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267 },
{ url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501 }, { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182 },
{ url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693 }, { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012 },
{ url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177 }, { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479 },
{ url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886 }, { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040 },
{ url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183 }, { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377 },
{ url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575 }, { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784 },
{ url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537 }, { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088 },
{ url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813 }, { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770 },
{ url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136 }, { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355 },
{ url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701 }, { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758 },
{ url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887 }, { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498 },
{ url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316 }, { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765 },
{ url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535 }, { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277 },
{ url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692 }, { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758 },
{ url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614 }, { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821 },
] ]
[[package]] [[package]]
@ -1816,11 +1816,11 @@ wheels = [
[[package]] [[package]]
name = "tzdata" name = "tzdata"
version = "2026.1" version = "2026.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639 } sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952 }, { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321 },
] ]
[[package]] [[package]]