mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-09 12:17:35 +08:00
Compare commits
1 Commits
bb/windows
...
fix/lazy-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52ea424ec1 |
32
cli.py
32
cli.py
@@ -934,6 +934,20 @@ def _run_state_db_auto_maintenance(session_db) -> None:
|
||||
try:
|
||||
from hermes_cli.config import load_config as _load_full_config
|
||||
from hermes_constants import get_hermes_home as _get_hermes_home
|
||||
_hermes_home_maint = _get_hermes_home()
|
||||
|
||||
# One-time prune of empty TUI ghost sessions.
|
||||
try:
|
||||
if not session_db.get_meta("ghost_session_prune_v1"):
|
||||
pruned = session_db.prune_empty_ghost_sessions(
|
||||
sessions_dir=_hermes_home_maint / "sessions"
|
||||
)
|
||||
session_db.set_meta("ghost_session_prune_v1", "1")
|
||||
if pruned:
|
||||
logger.info("Pruned %d empty TUI ghost sessions", pruned)
|
||||
except Exception as _prune_exc:
|
||||
logger.debug("Ghost session prune skipped: %s", _prune_exc)
|
||||
|
||||
cfg = (_load_full_config().get("sessions") or {})
|
||||
if not cfg.get("auto_prune", False):
|
||||
return
|
||||
@@ -941,7 +955,7 @@ def _run_state_db_auto_maintenance(session_db) -> None:
|
||||
retention_days=int(cfg.get("retention_days", 90)),
|
||||
min_interval_hours=int(cfg.get("min_interval_hours", 24)),
|
||||
vacuum=bool(cfg.get("vacuum_after_prune", True)),
|
||||
sessions_dir=_get_hermes_home() / "sessions",
|
||||
sessions_dir=_hermes_home_maint / "sessions",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug("state.db auto-maintenance skipped: %s", exc)
|
||||
@@ -3618,14 +3632,18 @@ class HermesCLI:
|
||||
tuple(runtime.get("args") or ()),
|
||||
)
|
||||
|
||||
if self._pending_title and self._session_db:
|
||||
# Force-create DB row on /title intent, then apply title.
|
||||
if self._pending_title and self._session_db and self.agent:
|
||||
try:
|
||||
self._session_db.set_session_title(self.session_id, self._pending_title)
|
||||
_cprint(f" Session title applied: {self._pending_title}")
|
||||
self._pending_title = None
|
||||
self.agent._ensure_db_session()
|
||||
if self.agent._session_db_created:
|
||||
self._session_db.set_session_title(self.session_id, self._pending_title)
|
||||
_cprint(f" Session title applied: {self._pending_title}")
|
||||
self._pending_title = None
|
||||
# else: row creation failed transiently — keep _pending_title for retry
|
||||
except (ValueError, Exception) as e:
|
||||
_cprint(f" Could not apply pending title: {e}")
|
||||
self._pending_title = None
|
||||
# Keep _pending_title so it can be retried after row creation succeeds
|
||||
return True
|
||||
except Exception as e:
|
||||
ChatConsole().print(f"[bold red]Failed to initialize agent: {e}[/]")
|
||||
@@ -4953,6 +4971,7 @@ class HermesCLI:
|
||||
|
||||
if self._session_db:
|
||||
try:
|
||||
self.agent._session_db_created = False
|
||||
self._session_db.create_session(
|
||||
session_id=self.session_id,
|
||||
source=os.environ.get("HERMES_SESSION_SOURCE", "cli"),
|
||||
@@ -4962,6 +4981,7 @@ class HermesCLI:
|
||||
"reasoning_config": self.reasoning_config,
|
||||
},
|
||||
)
|
||||
self.agent._session_db_created = True
|
||||
except Exception:
|
||||
pass
|
||||
# Notify memory providers that session_id rotated to a fresh
|
||||
|
||||
@@ -800,6 +800,8 @@ def _print_tui_exit_summary(session_id: Optional[str], active_session_file: Opti
|
||||
|
||||
title = db.get_session_title(target)
|
||||
message_count = int(session.get("message_count") or 0)
|
||||
if message_count == 0:
|
||||
return # No real conversation — don't show resume info
|
||||
input_tokens = int(session.get("input_tokens") or 0)
|
||||
output_tokens = int(session.get("output_tokens") or 0)
|
||||
cache_read_tokens = int(session.get("cache_read_tokens") or 0)
|
||||
|
||||
@@ -514,7 +514,7 @@ class SessionDB:
|
||||
# Session lifecycle
|
||||
# =========================================================================
|
||||
|
||||
def create_session(
|
||||
def _insert_session_row(
|
||||
self,
|
||||
session_id: str,
|
||||
source: str,
|
||||
@@ -523,8 +523,8 @@ class SessionDB:
|
||||
system_prompt: str = None,
|
||||
user_id: str = None,
|
||||
parent_session_id: str = None,
|
||||
) -> str:
|
||||
"""Create a new session record. Returns the session_id."""
|
||||
) -> None:
|
||||
"""Shared INSERT OR IGNORE for session rows."""
|
||||
def _do(conn):
|
||||
conn.execute(
|
||||
"""INSERT OR IGNORE INTO sessions (id, source, user_id, model, model_config,
|
||||
@@ -542,8 +542,11 @@ class SessionDB:
|
||||
),
|
||||
)
|
||||
self._execute_write(_do)
|
||||
return session_id
|
||||
|
||||
def create_session(self, session_id: str, source: str, **kwargs) -> str:
|
||||
"""Create a new session record. Returns the session_id."""
|
||||
self._insert_session_row(session_id, source, **kwargs)
|
||||
return session_id
|
||||
def end_session(self, session_id: str, end_reason: str) -> None:
|
||||
"""Mark a session as ended.
|
||||
|
||||
@@ -679,21 +682,41 @@ class SessionDB:
|
||||
session_id: str,
|
||||
source: str = "unknown",
|
||||
model: str = None,
|
||||
) -> None:
|
||||
"""Ensure a session row exists, creating it with minimal metadata if absent.
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""Ensure a session row exists (INSERT OR IGNORE). Accepts optional kwargs."""
|
||||
self._insert_session_row(session_id, source, model=model, **kwargs)
|
||||
return session_id
|
||||
|
||||
def prune_empty_ghost_sessions(self, sessions_dir: "Optional[Path]" = None) -> int:
|
||||
"""Remove closed empty TUI ghost sessions (no messages, no title, >24hr old)."""
|
||||
cutoff = time.time() - 86400 # Only sessions older than 24 hours
|
||||
|
||||
Used by _flush_messages_to_session_db to recover from a failed
|
||||
create_session() call (e.g. transient SQLite lock at agent startup).
|
||||
INSERT OR IGNORE is safe to call even when the row already exists.
|
||||
"""
|
||||
def _do(conn):
|
||||
conn.execute(
|
||||
"""INSERT OR IGNORE INTO sessions
|
||||
(id, source, model, started_at)
|
||||
VALUES (?, ?, ?, ?)""",
|
||||
(session_id, source, model, time.time()),
|
||||
)
|
||||
self._execute_write(_do)
|
||||
rows = conn.execute("""
|
||||
SELECT id FROM sessions
|
||||
WHERE source = 'tui'
|
||||
AND title IS NULL
|
||||
AND ended_at IS NOT NULL
|
||||
AND started_at < ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM messages WHERE messages.session_id = sessions.id
|
||||
)
|
||||
""", (cutoff,)).fetchall()
|
||||
ids = [r[0] if isinstance(r, (tuple, list)) else r["id"] for r in rows]
|
||||
if ids:
|
||||
placeholders = ",".join("?" * len(ids))
|
||||
conn.execute(
|
||||
f"DELETE FROM sessions WHERE id IN ({placeholders})", ids
|
||||
)
|
||||
return ids
|
||||
|
||||
removed_ids = self._execute_write(_do) or []
|
||||
# Clean up any on-disk session files (belt-and-suspenders)
|
||||
if sessions_dir and removed_ids:
|
||||
for sid in removed_ids:
|
||||
self._remove_session_files(sessions_dir, sid)
|
||||
return len(removed_ids)
|
||||
|
||||
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get a session by ID."""
|
||||
@@ -2222,4 +2245,3 @@ class SessionDB:
|
||||
result["error"] = str(exc)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
68
run_agent.py
68
run_agent.py
@@ -1632,30 +1632,12 @@ class AIAgent:
|
||||
self._session_db = session_db
|
||||
self._parent_session_id = parent_session_id
|
||||
self._last_flushed_db_idx = 0 # tracks DB-write cursor to prevent duplicate writes
|
||||
if self._session_db:
|
||||
try:
|
||||
self._session_db.create_session(
|
||||
session_id=self.session_id,
|
||||
source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
|
||||
model=self.model,
|
||||
model_config={
|
||||
"max_iterations": self.max_iterations,
|
||||
"reasoning_config": reasoning_config,
|
||||
"max_tokens": max_tokens,
|
||||
},
|
||||
user_id=None,
|
||||
parent_session_id=self._parent_session_id,
|
||||
)
|
||||
except Exception as e:
|
||||
# Transient SQLite lock contention (e.g. CLI and gateway writing
|
||||
# concurrently) must NOT permanently disable session_search for
|
||||
# this agent. Keep _session_db alive — subsequent message
|
||||
# flushes and session_search calls will still work once the
|
||||
# lock clears. The session row may be missing from the index
|
||||
# for this run, but that is recoverable (flushes upsert rows).
|
||||
logger.warning(
|
||||
"Session DB create_session failed (session_search still available): %s", e
|
||||
)
|
||||
self._session_db_created = False # DB row deferred to run_conversation()
|
||||
self._session_init_model_config = {
|
||||
"max_iterations": self.max_iterations,
|
||||
"reasoning_config": reasoning_config,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
|
||||
# In-memory todo list for task planning (one per agent/session)
|
||||
from tools.todo_tool import TodoStore
|
||||
@@ -2170,6 +2152,28 @@ class AIAgent:
|
||||
"is_anthropic_oauth": self._is_anthropic_oauth,
|
||||
})
|
||||
|
||||
def _ensure_db_session(self) -> None:
|
||||
"""Create session DB row on first use. Disables _session_db on failure."""
|
||||
if self._session_db_created or not self._session_db:
|
||||
return
|
||||
try:
|
||||
self._session_db.create_session(
|
||||
session_id=self.session_id,
|
||||
source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
|
||||
model=self.model,
|
||||
model_config=self._session_init_model_config,
|
||||
system_prompt=self._cached_system_prompt,
|
||||
user_id=None,
|
||||
parent_session_id=self._parent_session_id,
|
||||
)
|
||||
self._session_db_created = True
|
||||
except Exception as e:
|
||||
# Transient failure (e.g. SQLite lock). Keep _session_db alive —
|
||||
# _session_db_created stays False so next run_conversation() retries.
|
||||
logger.warning(
|
||||
"Session DB creation failed (will retry next turn): %s", e
|
||||
)
|
||||
|
||||
def reset_session_state(self):
|
||||
"""Reset all session-scoped token counters to 0 for a fresh session.
|
||||
|
||||
@@ -3719,14 +3723,9 @@ class AIAgent:
|
||||
return
|
||||
self._apply_persist_user_message_override(messages)
|
||||
try:
|
||||
# If create_session() failed at startup (e.g. transient lock), the
|
||||
# session row may not exist yet. ensure_session() uses INSERT OR
|
||||
# IGNORE so it is a no-op when the row is already there.
|
||||
self._session_db.ensure_session(
|
||||
self.session_id,
|
||||
source=self.platform or "cli",
|
||||
model=self.model,
|
||||
)
|
||||
# Retry row creation if the earlier attempt failed transiently.
|
||||
if not self._session_db_created:
|
||||
self._ensure_db_session()
|
||||
start_idx = len(conversation_history) if conversation_history else 0
|
||||
flush_from = max(start_idx, self._last_flushed_db_idx)
|
||||
for msg in messages[flush_from:]:
|
||||
@@ -9056,12 +9055,15 @@ class AIAgent:
|
||||
self.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
||||
# Update session_log_file to point to the new session's JSON file
|
||||
self.session_log_file = self.logs_dir / f"session_{self.session_id}.json"
|
||||
self._session_db_created = False
|
||||
self._session_db.create_session(
|
||||
session_id=self.session_id,
|
||||
source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
|
||||
model=self.model,
|
||||
model_config=self._session_init_model_config,
|
||||
parent_session_id=old_session_id,
|
||||
)
|
||||
self._session_db_created = True
|
||||
# Auto-number the title for the continuation session
|
||||
if old_title:
|
||||
try:
|
||||
@@ -10351,6 +10353,8 @@ class AIAgent:
|
||||
# Installed once, transparent when streams are healthy, prevents crash on write.
|
||||
_install_safe_stdio()
|
||||
|
||||
self._ensure_db_session()
|
||||
|
||||
# Tag all log records on this thread with the session ID so
|
||||
# ``hermes logs --session <id>`` can filter a single conversation.
|
||||
from hermes_logging import set_session_context
|
||||
|
||||
@@ -38,6 +38,8 @@ class TestFlushDeduplication:
|
||||
skip_context_files=True,
|
||||
skip_memory=True,
|
||||
)
|
||||
# Simulate lazy session creation (normally done by run_conversation)
|
||||
agent._ensure_db_session()
|
||||
return agent
|
||||
|
||||
def test_flush_writes_only_new_messages(self):
|
||||
|
||||
@@ -2479,6 +2479,39 @@ class TestAutoMaintenance:
|
||||
assert (sessions_dir / "active.jsonl").exists()
|
||||
|
||||
|
||||
class TestEmptyGhostSessionPrune:
|
||||
def _make_old_tui_session(self, db, session_id, ended=False):
|
||||
db.create_session(session_id=session_id, source="tui")
|
||||
old_started_at = time.time() - 90000
|
||||
db._execute_write(
|
||||
lambda conn: conn.execute(
|
||||
"UPDATE sessions SET started_at = ? WHERE id = ?",
|
||||
(old_started_at, session_id),
|
||||
)
|
||||
)
|
||||
if ended:
|
||||
db.end_session(session_id, "tui_close")
|
||||
|
||||
def test_prunes_only_closed_empty_tui_sessions(self, db):
|
||||
self._make_old_tui_session(db, "closed-empty", ended=True)
|
||||
self._make_old_tui_session(db, "live-empty", ended=False)
|
||||
|
||||
assert db.prune_empty_ghost_sessions() == 1
|
||||
assert db.get_session("closed-empty") is None
|
||||
assert db.get_session("live-empty") is not None
|
||||
|
||||
def test_prune_empty_ghost_sessions_keeps_nonempty_closed_session(self, db):
|
||||
self._make_old_tui_session(db, "closed-with-message", ended=True)
|
||||
db.append_message(
|
||||
session_id="closed-with-message",
|
||||
role="user",
|
||||
content="hello",
|
||||
)
|
||||
|
||||
assert db.prune_empty_ghost_sessions() == 0
|
||||
assert db.get_session("closed-with-message") is not None
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# FTS5 indexing of tool_calls / tool_name (#16751)
|
||||
# =========================================================================
|
||||
@@ -2672,4 +2705,3 @@ class TestFTS5ToolCallMigration:
|
||||
assert version == 11
|
||||
finally:
|
||||
session_db.close()
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ def _notify_session_boundary(event_type: str, session_id: str | None) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _finalize_session(session: dict | None) -> None:
|
||||
def _finalize_session(session: dict | None, end_reason: str = "tui_close") -> None:
|
||||
"""Best-effort finalize hook + memory commit for a session."""
|
||||
if not session or session.get("_finalized"):
|
||||
return
|
||||
@@ -299,13 +299,24 @@ def _finalize_session(session: dict | None) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
session_id = getattr(agent, "session_id", None) or session.get("session_key")
|
||||
session_key = session.get("session_key")
|
||||
session_id = getattr(agent, "session_id", None) or session_key
|
||||
_notify_session_boundary("on_session_finalize", session_id)
|
||||
|
||||
# Mark session ended in DB so it doesn't linger as a ghost row in /resume.
|
||||
# Adapted from #18283 (luyao618) and #18299 (Bartok9).
|
||||
if session_key:
|
||||
try:
|
||||
db = _get_db()
|
||||
if db is not None:
|
||||
db.end_session(session_key, end_reason)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _shutdown_sessions() -> None:
|
||||
for session in list(_sessions.values()):
|
||||
_finalize_session(session)
|
||||
_finalize_session(session, end_reason="tui_shutdown")
|
||||
try:
|
||||
worker = session.get("slash_worker")
|
||||
if worker:
|
||||
@@ -539,32 +550,8 @@ def _start_agent_build(sid: str, session: dict) -> None:
|
||||
finally:
|
||||
_clear_session_context(tokens)
|
||||
|
||||
db = _get_db()
|
||||
if db is not None:
|
||||
db.create_session(key, source="tui", model=_resolve_model())
|
||||
pending_title = (current.get("pending_title") or "").strip()
|
||||
if pending_title:
|
||||
try:
|
||||
title_applied = db.set_session_title(key, pending_title)
|
||||
if title_applied:
|
||||
current["pending_title"] = None
|
||||
else:
|
||||
existing_row = db.get_session(key)
|
||||
existing_title = ((existing_row or {}).get("title") or "").strip()
|
||||
if existing_title == pending_title:
|
||||
current["pending_title"] = None
|
||||
else:
|
||||
logger.info(
|
||||
"Pending title still queued for session %s (wanted=%r, current=%r)",
|
||||
sid,
|
||||
pending_title,
|
||||
existing_title,
|
||||
)
|
||||
except ValueError as e:
|
||||
current["pending_title"] = None
|
||||
logger.info("Dropping pending title for session %s: %s", sid, e)
|
||||
except Exception:
|
||||
logger.warning("Failed to apply pending title for session %s", sid, exc_info=True)
|
||||
# Session DB row deferred to first run_conversation() call.
|
||||
# pending_title applied post-first-message (see cli.exec handler).
|
||||
current["agent"] = agent
|
||||
|
||||
try:
|
||||
@@ -2996,6 +2983,17 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None:
|
||||
payload["rendered"] = rendered
|
||||
_emit("message.complete", sid, payload)
|
||||
|
||||
# Apply pending_title now that the DB row exists.
|
||||
_pending = session.get("pending_title")
|
||||
if _pending and status == "complete":
|
||||
_pdb = _get_db()
|
||||
if _pdb:
|
||||
try:
|
||||
if _pdb.set_session_title(session.get("session_key") or sid, _pending):
|
||||
session["pending_title"] = None
|
||||
except Exception:
|
||||
pass # Best effort — auto-title will handle it below
|
||||
|
||||
if (
|
||||
status == "complete"
|
||||
and isinstance(raw, str)
|
||||
|
||||
198
uv.lock
generated
198
uv.lock
generated
@@ -9,7 +9,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-04-17T16:49:45.944715922Z"
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
exclude-newer-span = "P7D"
|
||||
|
||||
[[package]]
|
||||
@@ -156,6 +156,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", size = 9981, upload-time = "2024-11-06T10:44:52.917Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp-socks"
|
||||
version = "0.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "python-socks" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/cc/e5bbd54f76bd56291522251e47267b645dac76327b2657ade9545e30522c/aiohttp_socks-0.11.0.tar.gz", hash = "sha256:0afe51638527c79077e4bd6e57052c87c4824233d6e20bb061c53766421b10f0", size = 11196, upload-time = "2025-12-09T13:35:52.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/7d/4b633d709b8901d59444d2e512b93e72fe62d2b492a040097c3f7ba017bb/aiohttp_socks-0.11.0-py3-none-any.whl", hash = "sha256:9aacce57c931b8fbf8f6d333cf3cafe4c35b971b35430309e167a35a8aab9ec1", size = 10556, upload-time = "2025-12-09T13:35:50.18Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.4.0"
|
||||
@@ -1759,6 +1772,77 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
version = "2.30.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-auth" },
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "proto-plus" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.194.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core" },
|
||||
{ name = "google-auth" },
|
||||
{ name = "google-auth-httplib2" },
|
||||
{ name = "httplib2" },
|
||||
{ name = "uritemplate" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/ab/e83af0eb043e4ccc49571ca7a6a49984e9d00f4e9e6e6f1238d60bc84dce/google_api_python_client-2.194.0.tar.gz", hash = "sha256:db92647bd1a90f40b79c9618461553c2b20b6a43ce7395fa6de07132dc14f023", size = 14443469, upload-time = "2026-04-08T23:07:35.757Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/34/5a624e49f179aa5b0cb87b2ce8093960299030ff40423bfbde09360eb908/google_api_python_client-2.194.0-py3-none-any.whl", hash = "sha256:61eaaac3b8fc8fdf11c08af87abc3d1342d1b37319cc1b57405f86ef7697e717", size = 15016514, upload-time = "2026-04-08T23:07:33.093Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth"
|
||||
version = "2.49.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "pyasn1-modules" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth-httplib2"
|
||||
version = "0.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-auth" },
|
||||
{ name = "httplib2" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/99/107612bef8d24b298bb5a7c8466f908ecda791d43f9466f5c3978f5b24c1/google_auth_httplib2-0.3.1.tar.gz", hash = "sha256:0af542e815784cb64159b4469aa5d71dd41069ba93effa006e1916b1dcd88e55", size = 11152, upload-time = "2026-03-30T22:50:26.766Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/e9/93afb14d23a949acaa3f4e7cc51a0024671174e116e35f42850764b99634/google_auth_httplib2-0.3.1-py3-none-any.whl", hash = "sha256:682356a90ef4ba3d06548c37e9112eea6fc00395a11b0303a644c1a86abc275c", size = 9534, upload-time = "2026-03-30T22:49:03.384Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth-oauthlib"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-auth" },
|
||||
{ name = "requests-oauthlib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/82/62482931dcbe5266a2680d0da17096f2aab983ecb320277d9556700ce00e/google_auth_oauthlib-1.3.1.tar.gz", hash = "sha256:14c22c7b3dd3d06dbe44264144409039465effdd1eef94f7ce3710e486cc4bfa", size = 21663, upload-time = "2026-03-30T22:49:56.408Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e0/cb454a95f460903e39f101e950038ec24a072ca69d0a294a6df625cc1627/google_auth_oauthlib-1.3.1-py3-none-any.whl", hash = "sha256:1a139ef23f1318756805b0e95f655c238bffd29655329a2978218248da4ee7f8", size = 19247, upload-time = "2026-03-30T20:02:23.894Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.73.0"
|
||||
@@ -1870,10 +1954,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hermes-agent"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "croniter" },
|
||||
{ name = "edge-tts" },
|
||||
{ name = "exa-py" },
|
||||
{ name = "fal-client" },
|
||||
@@ -1900,11 +1985,11 @@ acp = [
|
||||
all = [
|
||||
{ name = "agent-client-protocol" },
|
||||
{ name = "aiohttp" },
|
||||
{ name = "aiohttp-socks", marker = "sys_platform == 'linux'" },
|
||||
{ name = "aiosqlite", marker = "sys_platform == 'linux'" },
|
||||
{ name = "alibabacloud-dingtalk" },
|
||||
{ name = "asyncpg", marker = "sys_platform == 'linux'" },
|
||||
{ name = "boto3" },
|
||||
{ name = "croniter" },
|
||||
{ name = "daytona" },
|
||||
{ name = "debugpy" },
|
||||
{ name = "dingtalk-stream" },
|
||||
@@ -1912,6 +1997,9 @@ all = [
|
||||
{ name = "elevenlabs" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "faster-whisper" },
|
||||
{ name = "google-api-python-client" },
|
||||
{ name = "google-auth-httplib2" },
|
||||
{ name = "google-auth-oauthlib" },
|
||||
{ name = "honcho-ai" },
|
||||
{ name = "lark-oapi" },
|
||||
{ name = "markdown", marker = "sys_platform == 'linux'" },
|
||||
@@ -1942,9 +2030,6 @@ bedrock = [
|
||||
cli = [
|
||||
{ name = "simple-term-menu" },
|
||||
]
|
||||
cron = [
|
||||
{ name = "croniter" },
|
||||
]
|
||||
daytona = [
|
||||
{ name = "daytona" },
|
||||
]
|
||||
@@ -1966,6 +2051,11 @@ feishu = [
|
||||
{ name = "lark-oapi" },
|
||||
{ name = "qrcode" },
|
||||
]
|
||||
google = [
|
||||
{ name = "google-api-python-client" },
|
||||
{ name = "google-auth-httplib2" },
|
||||
{ name = "google-auth-oauthlib" },
|
||||
]
|
||||
homeassistant = [
|
||||
{ name = "aiohttp" },
|
||||
]
|
||||
@@ -1973,6 +2063,7 @@ honcho = [
|
||||
{ name = "honcho-ai" },
|
||||
]
|
||||
matrix = [
|
||||
{ name = "aiohttp-socks" },
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "markdown" },
|
||||
@@ -2015,7 +2106,6 @@ sms = [
|
||||
]
|
||||
termux = [
|
||||
{ name = "agent-client-protocol" },
|
||||
{ name = "croniter" },
|
||||
{ name = "honcho-ai" },
|
||||
{ name = "mcp" },
|
||||
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
|
||||
@@ -2048,13 +2138,14 @@ requires-dist = [
|
||||
{ name = "aiohttp", marker = "extra == 'homeassistant'", specifier = ">=3.9.0,<4" },
|
||||
{ name = "aiohttp", marker = "extra == 'messaging'", specifier = ">=3.13.3,<4" },
|
||||
{ name = "aiohttp", marker = "extra == 'sms'", specifier = ">=3.9.0,<4" },
|
||||
{ name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = ">=0.10,<1" },
|
||||
{ name = "aiosqlite", marker = "extra == 'matrix'", specifier = ">=0.20" },
|
||||
{ name = "alibabacloud-dingtalk", marker = "extra == 'dingtalk'", specifier = ">=2.0.0" },
|
||||
{ name = "anthropic", specifier = ">=0.39.0,<1" },
|
||||
{ name = "asyncpg", marker = "extra == 'matrix'", specifier = ">=0.29" },
|
||||
{ name = "atroposlib", marker = "extra == 'rl'", git = "https://github.com/NousResearch/atropos.git?rev=c20c85256e5a45ad31edf8b7276e9c5ee1995a30" },
|
||||
{ name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.35.0,<2" },
|
||||
{ name = "croniter", marker = "extra == 'cron'", specifier = ">=6.0.0,<7" },
|
||||
{ name = "croniter", specifier = ">=6.0.0,<7" },
|
||||
{ name = "daytona", marker = "extra == 'daytona'", specifier = ">=0.148.0,<1" },
|
||||
{ name = "debugpy", marker = "extra == 'dev'", specifier = ">=1.8.0,<2" },
|
||||
{ name = "dingtalk-stream", marker = "extra == 'dingtalk'", specifier = ">=0.20,<1" },
|
||||
@@ -2068,6 +2159,9 @@ requires-dist = [
|
||||
{ name = "faster-whisper", marker = "extra == 'voice'", specifier = ">=1.0.0,<2" },
|
||||
{ name = "fire", specifier = ">=0.7.1,<1" },
|
||||
{ name = "firecrawl-py", specifier = ">=4.16.0,<5" },
|
||||
{ name = "google-api-python-client", marker = "extra == 'google'", specifier = ">=2.100,<3" },
|
||||
{ name = "google-auth-httplib2", marker = "extra == 'google'", specifier = ">=0.2,<1" },
|
||||
{ name = "google-auth-oauthlib", marker = "extra == 'google'", specifier = ">=1.0,<2" },
|
||||
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'termux'" },
|
||||
{ name = "hermes-agent", extras = ["bedrock"], marker = "extra == 'all'" },
|
||||
@@ -2079,6 +2173,7 @@ requires-dist = [
|
||||
{ name = "hermes-agent", extras = ["dev"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["dingtalk"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["feishu"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["google"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["homeassistant"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["honcho"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["honcho"], marker = "extra == 'termux'" },
|
||||
@@ -2142,7 +2237,7 @@ requires-dist = [
|
||||
{ name = "wandb", marker = "extra == 'rl'", specifier = ">=0.15.0,<1" },
|
||||
{ name = "yc-bench", marker = "python_full_version >= '3.12' and extra == 'yc-bench'", git = "https://github.com/collinear-ai/yc-bench.git?rev=bfb0c88062450f46341bd9a5298903fc2e952a5c" },
|
||||
]
|
||||
provides-extras = ["modal", "daytona", "vercel", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "acp", "mistral", "bedrock", "termux", "dingtalk", "feishu", "web", "rl", "yc-bench", "all"]
|
||||
provides-extras = ["modal", "daytona", "vercel", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "acp", "mistral", "bedrock", "termux", "dingtalk", "feishu", "google", "web", "rl", "yc-bench", "all"]
|
||||
|
||||
[[package]]
|
||||
name = "hf-transfer"
|
||||
@@ -2244,6 +2339,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httplib2"
|
||||
version = "0.31.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyparsing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.7.1"
|
||||
@@ -3283,6 +3390,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oauthlib"
|
||||
version = "3.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "obstore"
|
||||
version = "0.8.2"
|
||||
@@ -3861,6 +3977,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proto-plus"
|
||||
version = "1.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.5"
|
||||
@@ -3935,6 +4063,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1-modules"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyasn1" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
@@ -4275,6 +4424,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/93/f6729f10149305262194774d6c8b438c0b084740cf239f48ab97b4df02fa/python_olm-3.2.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a5e68a2f4b5a2bfa5fdb5dbfa22396a551730df6c4a572235acaa96e997d3f", size = 297000, upload-time = "2023-11-28T19:25:31.045Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-socks"
|
||||
version = "2.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/0b/cd77011c1bc01b76404f7aba07fca18aca02a19c7626e329b40201217624/python_socks-2.8.1.tar.gz", hash = "sha256:698daa9616d46dddaffe65b87db222f2902177a2d2b2c0b9a9361df607ab3687", size = 38909, upload-time = "2026-02-16T05:24:00.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/fe/9a58cb6eec633ff6afae150ca53c16f8cc8b65862ccb3d088051efdfceb7/python_socks-2.8.1-py3-none-any.whl", hash = "sha256:28232739c4988064e725cdbcd15be194743dd23f1c910f784163365b9d7be035", size = 55087, upload-time = "2026-02-16T05:23:59.147Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-telegram-bot"
|
||||
version = "22.6"
|
||||
@@ -4535,6 +4693,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests-oauthlib"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "oauthlib" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests-toolbelt"
|
||||
version = "1.0.0"
|
||||
@@ -5274,6 +5445,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/a7/563b2d8fb7edc07320bf69ac6a7eedcd7a1a9d663a6bb90a4d9bd2eda5f7/unpaddedbase64-2.1.0-py3-none-any.whl", hash = "sha256:485eff129c30175d2cd6f0cd8d2310dff51e666f7f36175f738d75dfdbd0b1c6", size = 6083, upload-time = "2021-03-09T11:35:46.7Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uritemplate"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
|
||||
Reference in New Issue
Block a user