mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
feat(cli): refuse db reset while basic-memory mcp processes run (#776)
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
@@ -48,6 +48,7 @@ dependencies = [
|
||||
"sqlite-vec>=0.1.6",
|
||||
"openai>=1.100.2",
|
||||
"logfire>=4.19.0",
|
||||
"psutil>=5.9.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Database management commands."""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
|
||||
import psutil
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
@@ -21,6 +23,107 @@ from basic_memory.sync.sync_service import get_sync_service
|
||||
console = Console()
|
||||
|
||||
|
||||
def _is_basic_memory_mcp(cmdline: list[str]) -> bool:
|
||||
"""Heuristic: does this argv represent a `basic-memory mcp` server?
|
||||
|
||||
The MCP server can be launched any of:
|
||||
basic-memory mcp
|
||||
bm mcp # entrypoint alias from pyproject.toml
|
||||
python -m basic_memory.cli.main mcp # module form
|
||||
uv run basic-memory mcp / uv run bm mcp # uv wrappers
|
||||
/abs/path/to/{bm,basic-memory}[.exe] mcp
|
||||
|
||||
A reliable match needs both signals:
|
||||
1. "mcp" appears as an exact argv token (not "mcp-foo").
|
||||
2. Some argv token names the basic-memory entrypoint — either by
|
||||
hyphen/underscore form, or as a `bm` script (covers `/usr/local/bin/bm`,
|
||||
`bm.exe`, etc. via Path.stem).
|
||||
"""
|
||||
if "mcp" not in cmdline:
|
||||
return False
|
||||
for arg in cmdline:
|
||||
if "basic-memory" in arg or "basic_memory" in arg:
|
||||
return True
|
||||
# Try both POSIX and Windows path interpretations so a test on
|
||||
# macOS still recognizes `C:\\...\\bm.exe`, and a real Windows
|
||||
# run still recognizes `/usr/local/bin/bm`. Path() alone uses
|
||||
# the host OS, which gives wrong stems for foreign separators.
|
||||
if PurePosixPath(arg).stem == "bm" or PureWindowsPath(arg).stem == "bm":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _find_live_mcp_processes() -> list[tuple[int, str]]:
|
||||
"""Return (pid, joined_cmdline) for live `basic-memory mcp` processes.
|
||||
|
||||
Why this exists (issue #765):
|
||||
On POSIX, `Path.unlink()` removes the directory entry but the inode
|
||||
survives as long as any process holds the file open. A `bm reset`
|
||||
run while Claude Desktop (or another MCP client) is alive will
|
||||
therefore "succeed" — but the still-running MCP keeps reading the
|
||||
old, now-invisible memory.db inode and returns phantom rows. On
|
||||
Windows the OS naturally raises PermissionError on `unlink()`, so
|
||||
the bug is POSIX-specific. We detect proactively to give the same
|
||||
error experience on every platform before doing damage.
|
||||
|
||||
The current process is excluded so this can be called from inside a
|
||||
`bm reset` invocation. NoSuchProcess / AccessDenied are swallowed
|
||||
because process tables race with the scan and we don't want a
|
||||
transient permission error to mask a real zombie.
|
||||
"""
|
||||
me = os.getpid()
|
||||
matches: list[tuple[int, str]] = []
|
||||
for proc in psutil.process_iter(["pid", "cmdline"]):
|
||||
try:
|
||||
pid = proc.info.get("pid")
|
||||
if pid is None or pid == me:
|
||||
continue
|
||||
cmdline = proc.info.get("cmdline") or []
|
||||
if not cmdline:
|
||||
continue
|
||||
if _is_basic_memory_mcp(cmdline):
|
||||
matches.append((pid, " ".join(cmdline)))
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
return matches
|
||||
|
||||
|
||||
def _abort_if_mcp_processes_alive() -> None:
|
||||
"""Refuse `bm reset` while basic-memory MCP processes are still running.
|
||||
|
||||
See _find_live_mcp_processes for the underlying POSIX-vs-Windows
|
||||
rationale. Prints a per-PID list and platform-appropriate cleanup
|
||||
instructions, then exits non-zero so destructive work never starts.
|
||||
"""
|
||||
zombies = _find_live_mcp_processes()
|
||||
if not zombies:
|
||||
return
|
||||
|
||||
console.print(
|
||||
"[red]Refusing to reset:[/red] basic-memory MCP processes are still running."
|
||||
)
|
||||
console.print(
|
||||
"[yellow]On macOS/Linux these would keep reading the deleted memory.db inode "
|
||||
"and return phantom search results (see #765).[/yellow]"
|
||||
)
|
||||
for pid, cmd in zombies:
|
||||
console.print(f" PID {pid}: {cmd}")
|
||||
console.print("\n[bold]How to clean up:[/bold]")
|
||||
console.print(" 1. Quit Claude Desktop and any other MCP clients.")
|
||||
if os.name == "nt":
|
||||
console.print(
|
||||
" 2. Verify nothing remains: "
|
||||
"[green]Get-CimInstance Win32_Process | "
|
||||
"Where-Object {$_.CommandLine -like '*basic-memory*mcp*'}[/green]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
" 2. Verify nothing remains: [green]pgrep -fa 'basic-memory mcp'[/green]"
|
||||
)
|
||||
console.print(" 3. Re-run [green]bm reset[/green].")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EmbeddingProgress:
|
||||
"""Typed CLI progress payload for embedding backfills."""
|
||||
@@ -86,6 +189,16 @@ async def _reindex_projects(app_config):
|
||||
@app.command()
|
||||
def reset(
|
||||
reindex: bool = typer.Option(False, "--reindex", help="Rebuild db index from filesystem"),
|
||||
force: bool = typer.Option(
|
||||
False,
|
||||
"--force",
|
||||
help=(
|
||||
"Skip the pre-flight check that refuses to reset while "
|
||||
"basic-memory MCP processes are running. Use only in "
|
||||
"automated workflows where you've already ensured no MCP "
|
||||
"clients are attached to the database."
|
||||
),
|
||||
),
|
||||
): # pragma: no cover
|
||||
"""Reset database (drop all tables and recreate)."""
|
||||
console.print(
|
||||
@@ -94,6 +207,14 @@ def reset(
|
||||
"Use [green]bm reset --reindex[/green] to automatically rebuild the index afterward."
|
||||
)
|
||||
if typer.confirm("Reset the database index?"):
|
||||
# Pre-flight: refuse to proceed if MCP processes still hold the DB
|
||||
# file open. POSIX would silently let us unlink the inode while
|
||||
# they keep reading it; Windows would error here anyway. See
|
||||
# _find_live_mcp_processes for the full story. --force is the
|
||||
# documented escape hatch for scripted/CI runs.
|
||||
if not force:
|
||||
_abort_if_mcp_processes_alive()
|
||||
|
||||
logger.info("Resetting database...")
|
||||
config_manager = ConfigManager()
|
||||
app_config = config_manager.config
|
||||
|
||||
@@ -26,9 +26,14 @@ def _isolated_env(tmp_path: Path) -> dict[str, str]:
|
||||
|
||||
@skip_on_windows
|
||||
def test_bm_reset_exits_cleanly(tmp_path: Path):
|
||||
"""bm reset should finish and exit cleanly with non-interactive confirmation."""
|
||||
"""bm reset should finish and exit cleanly with non-interactive confirmation.
|
||||
|
||||
Uses --force to skip the live-MCP pre-flight (#765); we're verifying
|
||||
process exit semantics here, not the pre-flight, which has dedicated
|
||||
coverage in test_db_reset_zombie_check.py.
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["uv", "run", "bm", "reset"],
|
||||
["uv", "run", "bm", "reset", "--force"],
|
||||
input="y\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -44,7 +49,7 @@ def test_bm_reset_exits_cleanly(tmp_path: Path):
|
||||
def test_bm_reset_reindex_exits_cleanly(tmp_path: Path):
|
||||
"""bm reset --reindex should finish and exit cleanly with non-interactive confirmation."""
|
||||
result = subprocess.run(
|
||||
["uv", "run", "bm", "reset", "--reindex"],
|
||||
["uv", "run", "bm", "reset", "--reindex", "--force"],
|
||||
input="y\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Regression tests for the live-MCP-process pre-flight in `bm reset` (#765)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import psutil
|
||||
import pytest
|
||||
import typer
|
||||
|
||||
from basic_memory.cli.commands import db as db_cmd
|
||||
|
||||
|
||||
class _FakeProc:
|
||||
"""Minimal stand-in for psutil.Process; only exposes .info."""
|
||||
|
||||
def __init__(self, pid: int, cmdline: list[str] | None):
|
||||
self.info = {"pid": pid, "cmdline": cmdline}
|
||||
|
||||
|
||||
def _patch_iter(monkeypatch: pytest.MonkeyPatch, procs) -> None:
|
||||
"""Replace psutil.process_iter with a fixed iterator.
|
||||
|
||||
Procs is intentionally untyped: tests pass a mix of _FakeProc and
|
||||
error-raising stand-ins to exercise the per-process exception path.
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
psutil,
|
||||
"process_iter",
|
||||
lambda attrs=None: iter(procs),
|
||||
)
|
||||
|
||||
|
||||
class TestFindLiveMcpProcesses:
|
||||
def test_returns_empty_when_no_mcp_processes(self, monkeypatch):
|
||||
_patch_iter(
|
||||
monkeypatch,
|
||||
[
|
||||
_FakeProc(pid=11, cmdline=["python", "-m", "http.server"]),
|
||||
_FakeProc(pid=22, cmdline=["bm", "sync"]),
|
||||
],
|
||||
)
|
||||
assert db_cmd._find_live_mcp_processes() == []
|
||||
|
||||
def test_matches_basic_memory_mcp_invocations(self, monkeypatch):
|
||||
_patch_iter(
|
||||
monkeypatch,
|
||||
[
|
||||
# Direct `basic-memory mcp`.
|
||||
_FakeProc(pid=101, cmdline=["/usr/bin/python", "basic-memory", "mcp"]),
|
||||
# `bm mcp` alias entrypoint — must also match (#765 P1).
|
||||
_FakeProc(pid=202, cmdline=["bm", "mcp"]),
|
||||
# Python module form, underscore name.
|
||||
_FakeProc(
|
||||
pid=303,
|
||||
cmdline=["python", "-m", "basic_memory.cli.main", "mcp"],
|
||||
),
|
||||
# Absolute path to the bm script.
|
||||
_FakeProc(pid=404, cmdline=["/usr/local/bin/bm", "mcp"]),
|
||||
# Windows-style bm.exe.
|
||||
_FakeProc(pid=505, cmdline=["C:\\Users\\me\\.venv\\Scripts\\bm.exe", "mcp"]),
|
||||
# Should NOT match — `mcp` is a substring of another arg, not a token.
|
||||
_FakeProc(pid=606, cmdline=["python", "basic-memory", "mcp-helper"]),
|
||||
# Should NOT match — has `mcp` but no basic-memory/bm signature.
|
||||
_FakeProc(pid=707, cmdline=["python", "/some/other/server.py", "mcp"]),
|
||||
],
|
||||
)
|
||||
result = db_cmd._find_live_mcp_processes()
|
||||
pids = sorted(pid for pid, _ in result)
|
||||
assert pids == [101, 202, 303, 404, 505]
|
||||
|
||||
def test_skips_current_process(self, monkeypatch):
|
||||
me = os.getpid()
|
||||
_patch_iter(
|
||||
monkeypatch,
|
||||
[
|
||||
_FakeProc(pid=me, cmdline=["python", "basic-memory", "mcp"]),
|
||||
],
|
||||
)
|
||||
# Self-match is suppressed so the helper can be called from inside
|
||||
# `bm reset` without flagging the running process.
|
||||
assert db_cmd._find_live_mcp_processes() == []
|
||||
|
||||
def test_skips_processes_with_no_cmdline(self, monkeypatch):
|
||||
_patch_iter(
|
||||
monkeypatch,
|
||||
[
|
||||
_FakeProc(pid=1, cmdline=None), # kernel-style process
|
||||
_FakeProc(pid=2, cmdline=[]),
|
||||
],
|
||||
)
|
||||
assert db_cmd._find_live_mcp_processes() == []
|
||||
|
||||
def test_swallows_per_process_errors(self, monkeypatch):
|
||||
"""A NoSuchProcess race during iteration must not abort the scan."""
|
||||
|
||||
class _Raising:
|
||||
@property
|
||||
def info(self):
|
||||
raise psutil.NoSuchProcess(pid=999)
|
||||
|
||||
_patch_iter(
|
||||
monkeypatch,
|
||||
[
|
||||
_Raising(),
|
||||
_FakeProc(pid=42, cmdline=["python", "basic-memory", "mcp"]),
|
||||
],
|
||||
)
|
||||
result = db_cmd._find_live_mcp_processes()
|
||||
assert [pid for pid, _ in result] == [42]
|
||||
|
||||
|
||||
class TestAbortIfMcpProcessesAlive:
|
||||
def test_no_op_when_no_processes(self, monkeypatch):
|
||||
monkeypatch.setattr(db_cmd, "_find_live_mcp_processes", lambda: [])
|
||||
# Must not raise — destructive work should proceed.
|
||||
db_cmd._abort_if_mcp_processes_alive()
|
||||
|
||||
def test_exits_with_pids_when_processes_alive(self, monkeypatch, capsys):
|
||||
monkeypatch.setattr(
|
||||
db_cmd,
|
||||
"_find_live_mcp_processes",
|
||||
lambda: [(123, "python basic-memory mcp"), (456, "uv run bm mcp wrapper")],
|
||||
)
|
||||
with pytest.raises(typer.Exit) as exc_info:
|
||||
db_cmd._abort_if_mcp_processes_alive()
|
||||
assert exc_info.value.exit_code == 1
|
||||
|
||||
captured = capsys.readouterr()
|
||||
# PIDs surface so the user can target the cleanup themselves.
|
||||
assert "123" in captured.out
|
||||
assert "456" in captured.out
|
||||
assert "MCP processes" in captured.out
|
||||
|
||||
def test_prints_platform_specific_cleanup_hint_posix(self, monkeypatch, capsys):
|
||||
monkeypatch.setattr(os, "name", "posix")
|
||||
monkeypatch.setattr(
|
||||
db_cmd,
|
||||
"_find_live_mcp_processes",
|
||||
lambda: [(7, "python basic-memory mcp")],
|
||||
)
|
||||
with pytest.raises(typer.Exit):
|
||||
db_cmd._abort_if_mcp_processes_alive()
|
||||
out = capsys.readouterr().out
|
||||
assert "pgrep -fa 'basic-memory mcp'" in out
|
||||
|
||||
def test_prints_platform_specific_cleanup_hint_windows(self, monkeypatch, capsys):
|
||||
monkeypatch.setattr(os, "name", "nt")
|
||||
monkeypatch.setattr(
|
||||
db_cmd,
|
||||
"_find_live_mcp_processes",
|
||||
lambda: [(7, "python basic-memory mcp")],
|
||||
)
|
||||
with pytest.raises(typer.Exit):
|
||||
db_cmd._abort_if_mcp_processes_alive()
|
||||
out = capsys.readouterr().out
|
||||
assert "Get-CimInstance" in out
|
||||
@@ -177,6 +177,7 @@ dependencies = [
|
||||
{ name = "nest-asyncio" },
|
||||
{ name = "openai" },
|
||||
{ name = "pillow" },
|
||||
{ name = "psutil" },
|
||||
{ name = "psycopg" },
|
||||
{ name = "pybars3" },
|
||||
{ name = "pydantic", extra = ["email", "timezone"] },
|
||||
@@ -241,6 +242,7 @@ requires-dist = [
|
||||
{ name = "nest-asyncio", specifier = ">=1.6.0" },
|
||||
{ name = "openai", specifier = ">=1.100.2" },
|
||||
{ name = "pillow", specifier = ">=11.1.0" },
|
||||
{ name = "psutil", specifier = ">=5.9.0" },
|
||||
{ name = "psycopg", specifier = "==3.3.1" },
|
||||
{ name = "pybars3", specifier = ">=0.9.7" },
|
||||
{ name = "pydantic", extras = ["email", "timezone"], specifier = ">=2.12.0" },
|
||||
@@ -2136,6 +2138,34 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.3.1"
|
||||
|
||||
Reference in New Issue
Block a user