Merge pull request #7 from Donaldttt/fix/lean-end-user-install

fix: reduce end-user installation footprint
This commit is contained in:
Donaldttt
2026-08-15 11:00:45 +08:00
committed by GitHub
10 changed files with 98 additions and 79 deletions
+6 -4
View File
@@ -49,13 +49,13 @@ git -C <install-dir> describe --tags --exact-match HEAD
### 2. 环境检查与确定性准备 ### 2. 环境检查与确定性准备
需要:Git、`uv`、Python 3.12+、NocoBase `nb` CLI 2.1+。先运行只读检查: 需要:Git、`uv`、Python 3.12+、NocoBase `nb` CLI 2.1+。Git 只用于取得和校验发布版本;`uv` 用于创建锁定的项目环境以及后续诊断/更新。若机器尚未安装 `nb`,安装官方 CLI 还会条件性需要 Node.js 与 npm。先运行只读检查:
```bash ```bash
uv run --no-project --python 3.12 python <install-dir>/scripts/agent_install.py --project-root <install-dir> inspect --json uv run --no-project --python 3.12 python <install-dir>/scripts/agent_install.py --project-root <install-dir> inspect --json
``` ```
若 `uv` 或 `nb` 不存在,先取得用户同意再安装。NocoBase CLI 当前官方包入口是 `npm install -g @nocobase/cli`;安装后必须重新运行检查,不要通过修改 PATH 输出伪造成功。 若 `uv`、`nb` 或安装 `nb` 所需的 Node.js/npm 不存在,先向用户说明来源、安装位置和影响范围并取得同意。NocoBase CLI 当前官方包入口是 `npm install -g @nocobase/cli`;安装后必须重新运行检查,不要通过修改 PATH 输出伪造成功。
然后运行幂等准备步骤: 然后运行幂等准备步骤:
@@ -63,7 +63,9 @@ uv run --no-project --python 3.12 python <install-dir>/scripts/agent_install.py
uv run --no-project --python 3.12 python <install-dir>/scripts/agent_install.py --project-root <install-dir> prepare --json uv run --no-project --python 3.12 python <install-dir>/scripts/agent_install.py --project-root <install-dir> prepare --json
``` ```
该脚本只会校验项目文件、在缺失时从示例创建 `config.json`,并执行锁定依赖同步。已有 `config.json` 会被保留,依赖必须来自 `uv.lock`。 该脚本只会校验项目文件、在缺失时从示例创建 `config.json`,并把 `uv.lock` 中的**运行时依赖**同步到项目自己的 `.venv`。它使用 `--no-dev`,不会给终端用户安装 `mypy`、`ruff`、`pip-audit` 或其传递依赖。已有 `config.json` 会被保留。
安装会在用户电脑留下以下内容:固定 Release 的仓库副本及浅层 `.git`、项目 `.venv`、不含 Key 的 `config.json`、由 `nb` CLI 管理的个人登录环境,以及合并后的 MCP 客户端配置和原配置备份。`uv` 可能保留下载缓存,缓存不是 MCP 运行所必需;安装器不会自动删除用户的共享缓存。默认 stdio 流程不会创建 systemd 服务、监听端口或 `.runtime` Token。
### 3. 连接用户自己的 NocoBase 账号 ### 3. 连接用户自己的 NocoBase 账号
@@ -104,7 +106,7 @@ Key 过期或撤销后,Agent 重新启动 `connect-key`,用户在新的隐
uv run --no-project --python 3.12 python <install-dir>/scripts/agent_install.py --project-root <install-dir> client-config uv run --no-project --python 3.12 python <install-dir>/scripts/agent_install.py --project-root <install-dir> client-config
``` ```
输出采用常见的 `mcpServers` JSON 形状,包含绝对的项目、`uv` 和 `nb` 路径。Agent 应查明自己客户端当前版本支持的 MCP 配置格式,将其中的 `command`、`args`、`env` 等价映射过去。 输出采用常见的 `mcpServers` JSON 形状,包含项目 `.venv` 中 Python、项目配置和 `nb` 的绝对路径。MCP 客户端直接启动已准备好的虚拟环境,不在每次调用时经过 `uv` 或同步依赖。Agent 应查明自己客户端当前版本支持的 MCP 配置格式,将其中的 `command`、`args`、`env` 等价映射过去。
写入前: 写入前:
+2
View File
@@ -6,6 +6,8 @@ Each user runs this MCP on their own computer and signs in to the remote NocoBas
For an agent-guided installation that works across MCP clients, give the installing Agent the immutable Release version of [`INSTALL.md`](INSTALL.md). The guide combines adaptive prompts with an idempotent helper script; it defaults to local stdio and read-only access. Do not distribute a default-branch installation link as a release. For an agent-guided installation that works across MCP clients, give the installing Agent the immutable Release version of [`INSTALL.md`](INSTALL.md). The guide combines adaptive prompts with an idempotent helper script; it defaults to local stdio and read-only access. Do not distribute a default-branch installation link as a release.
The end-user installer creates a project-local `.venv` from the locked runtime dependency set only; it does not install the development tools from the `dev` group. Generated MCP client configuration launches that environment's Python directly, so normal MCP startup does not resync packages or require `uv` to remain in the client command.
The default stdio server is read-only. Add `--enable-writes` for fixed-schema project create/update tools. Optional local HTTP mode also provides authenticated attachment uploads. Neither mode exposes arbitrary collections, fields, commands, or delete operations. The default stdio server is read-only. Add `--enable-writes` for fixed-schema project create/update tools. Optional local HTTP mode also provides authenticated attachment uploads. Neither mode exposes arbitrary collections, fields, commands, or delete operations.
## Configure ## Configure
+16 -12
View File
@@ -13,8 +13,10 @@ MCP 不保存公共业务账号,也不需要部署一个多人共用的远程
## 环境要求 ## 环境要求
- Python 3.12 或更高版本 - Python 3.12 或更高版本
- Git(仅安装、版本校验和更新)
- `uv` - `uv`
- NocoBase `nb` CLI 2.1 或更高版本 - NocoBase `nb` CLI 2.1 或更高版本
- Node.js/npm(仅在尚未安装 `nb` 时用于安装官方 CLI)
- 可通过浏览器访问目标 NocoBase - 可通过浏览器访问目标 NocoBase
- 管理员已为用户配置所需的 NocoBase 角色与数据权限 - 管理员已为用户配置所需的 NocoBase 角色与数据权限
@@ -31,7 +33,7 @@ MCP 不保存公共业务账号,也不需要部署一个多人共用的远程
```bash ```bash
git clone https://github.com/Donaldttt/nocobase-mcp.git git clone https://github.com/Donaldttt/nocobase-mcp.git
cd nocobase-mcp cd nocobase-mcp
uv sync --locked uv sync --locked --no-dev
cp config.example.json config.json cp config.example.json config.json
``` ```
@@ -47,7 +49,7 @@ Agent 先引导用户在 NocoBase 个人设置中创建可撤销、有合理有
先进行健康检查: 先进行健康检查:
```bash ```bash
uv run nocobase-crm-mcp --config config.json .venv/bin/python -m nocobase_crm_mcp.server --config config.json
``` ```
然后让 MCP 客户端调用 `crm_health`。如果返回 `nb_auth_failed`,由 Agent 重新启动 `connect-key`,用户在新的本机隐藏提示中输入新 Key;不要让用户手动尝试 `nb` 命令。 然后让 MCP 客户端调用 `crm_health`。如果返回 `nb_auth_failed`,由 Agent 重新启动 `connect-key`,用户在新的本机隐藏提示中输入新 Key;不要让用户手动尝试 `nb` 命令。
@@ -60,21 +62,23 @@ uv run nocobase-crm-mcp --config config.json
{ {
"mcpServers": { "mcpServers": {
"nocobase-crm": { "nocobase-crm": {
"command": "uv", "command": "/absolute/path/to/nocobase-mcp/.venv/bin/python",
"args": [ "args": [
"--directory", "-m",
"/absolute/path/to/nocobase-mcp", "nocobase_crm_mcp.server",
"run",
"--no-sync",
"nocobase-crm-mcp",
"--config", "--config",
"/absolute/path/to/nocobase-mcp/config.json" "/absolute/path/to/nocobase-mcp/config.json"
] ],
"env": {
"NOCOBASE_MCP_NB_BIN": "/absolute/path/to/nb"
}
} }
} }
} }
``` ```
不要手写猜测虚拟环境路径;优先使用 `scripts/agent_install.py client-config` 的输出。上例是 POSIX 路径,Windows 输出会自动使用 `.venv\\Scripts\\python.exe`。
默认只读。如需项目创建和更新,在参数末尾加入 `--enable-writes`。这些写工具只允许预先定义的项目字段,不提供删除或任意资源访问能力。 默认只读。如需项目创建和更新,在参数末尾加入 `--enable-writes`。这些写工具只允许预先定义的项目字段,不提供删除或任意资源访问能力。
附件上传需要可选的本机 HTTP 模式。HTTP 服务默认只监听 `127.0.0.1:10087`;上传路由本身也默认关闭,不得暴露到局域网或公网: 附件上传需要可选的本机 HTTP 模式。HTTP 服务默认只监听 `127.0.0.1:10087`;上传路由本身也默认关闭,不得暴露到局域网或公网:
@@ -120,8 +124,8 @@ Linux 用户也可以执行 `bash scripts/mcp_service.sh start` 安装用户级
```bash ```bash
git rev-parse HEAD git rev-parse HEAD
git pull --ff-only git pull --ff-only
uv sync --locked uv sync --locked --no-dev
uv run python -m unittest discover -s tests .venv/bin/python -m unittest discover -s tests
``` ```
使用 systemd 本机 HTTP 服务时,再执行: 使用 systemd 本机 HTTP 服务时,再执行:
@@ -136,7 +140,7 @@ journalctl --user -u nocobase-crm-mcp.service -n 200 --no-pager
```bash ```bash
git switch --detach <previous-release-tag-or-commit> git switch --detach <previous-release-tag-or-commit>
uv sync --locked uv sync --locked --no-dev
bash scripts/mcp_service.sh start bash scripts/mcp_service.sh start
``` ```
+17
View File
@@ -15,8 +15,25 @@
"defaultTransport": "stdio", "defaultTransport": "stdio",
"python": ">=3.12", "python": ">=3.12",
"packageManager": "uv", "packageManager": "uv",
"environment": ".venv",
"launcher": "project-venv-python",
"dependencyProfile": "runtime-only-no-dev",
"nocobaseCli": ">=2.1" "nocobaseCli": ">=2.1"
}, },
"installation": {
"requiredTools": ["git", "uv", "python", "nb"],
"conditionalTools": {
"nodeAndNpm": "Required only when the NocoBase nb CLI is not already installed."
},
"persistentArtifacts": [
"release checkout",
"project .venv",
"config.json",
"nb CLI user environment",
"MCP client configuration and backup"
],
"defaultBackgroundService": false
},
"authentication": { "authentication": {
"type": "personal-api-key", "type": "personal-api-key",
"cliAuthType": "token", "cliAuthType": "token",
+1 -1
View File
@@ -90,7 +90,7 @@ def create_server(
allowed_origins: list[str] | None = None, allowed_origins: list[str] | None = None,
) -> Any: ) -> Any:
if FastMCP is None: if FastMCP is None:
raise RuntimeError("Missing MCP SDK. Install dependencies with: uv sync") raise RuntimeError("Missing MCP SDK. Install runtime dependencies with: uv sync --locked --no-dev")
service = crm_service or _make_service(config_path) service = crm_service or _make_service(config_path)
if not readonly and project_service is None: if not readonly and project_service is None:
raise ValueError("project_service is required when readonly is false") raise ValueError("project_service is required when readonly is false")
+1 -1
View File
@@ -6,7 +6,7 @@ requires-python = ">=3.12"
dependencies = [ dependencies = [
"cryptography>=50,<51", "cryptography>=50,<51",
"httpx>=0.27,<1", "httpx>=0.27,<1",
"mcp[cli]>=1.27,<2", "mcp>=1.27,<2",
"python-multipart>=0.0.9,<1", "python-multipart>=0.0.9,<1",
] ]
+2 -5
View File
@@ -13,7 +13,7 @@ from .common import (
shutil, shutil,
subprocess, subprocess,
) )
from .project import inspect_project from .project import _venv_python_path, inspect_project
def doctor_project( def doctor_project(
@@ -47,10 +47,7 @@ def doctor_project(
commands = ( commands = (
[uv_bin, "lock", "--check"], [uv_bin, "lock", "--check"],
[ [
uv_bin, str(_venv_python_path(root)),
"run",
"--no-sync",
"python",
"-c", "-c",
"import mcp; import nocobase_crm_mcp", "import mcp; import nocobase_crm_mcp",
], ],
+25 -12
View File
@@ -101,9 +101,19 @@ def prepare_project(
shutil.copy2(example_path, config_path) shutil.copy2(example_path, config_path)
created_config = True created_config = True
command = [uv_bin, "sync", "--locked", "--python", f"{MIN_PYTHON[0]}.{MIN_PYTHON[1]}"] venv_path = root / ".venv"
command = [uv_bin, "sync", "--locked", "--no-dev", "--python", f"{MIN_PYTHON[0]}.{MIN_PYTHON[1]}"]
command_environment = os.environ.copy()
command_environment["UV_PROJECT_ENVIRONMENT"] = str(venv_path)
try: try:
result = runner(command, cwd=str(root), capture_output=True, text=True, check=False) result = runner(
command,
cwd=str(root),
env=command_environment,
capture_output=True,
text=True,
check=False,
)
except OSError as error: except OSError as error:
raise InstallerError(f"Could not execute uv sync: {error}") from error raise InstallerError(f"Could not execute uv sync: {error}") from error
if result.returncode != 0: if result.returncode != 0:
@@ -116,7 +126,8 @@ def prepare_project(
"projectRoot": str(root), "projectRoot": str(root),
"configPath": str(config_path), "configPath": str(config_path),
"configCreated": created_config, "configCreated": created_config,
"dependencies": "locked", "dependencies": "locked-runtime-only",
"environmentPath": str(venv_path),
} }
@@ -169,20 +180,16 @@ def render_client_config(
*, *,
enable_writes: bool = False, enable_writes: bool = False,
which: Which = shutil.which, which: Which = shutil.which,
os_name: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
root = _project_root(project_root) root = _project_root(project_root)
uv_bin = which("uv") or "uv"
nb_bin = which("nb") or "nb" nb_bin = which("nb") or "nb"
if os.path.isabs(uv_bin):
uv_bin = os.path.abspath(uv_bin)
if os.path.isabs(nb_bin): if os.path.isabs(nb_bin):
nb_bin = os.path.abspath(nb_bin) nb_bin = os.path.abspath(nb_bin)
python_bin = _venv_python_path(root, os_name=os_name)
args = [ args = [
"--directory", "-m",
str(root), "nocobase_crm_mcp.server",
"run",
"--no-sync",
"nocobase-crm-mcp",
"--config", "--config",
str(root / "config.json"), str(root / "config.json"),
] ]
@@ -191,9 +198,15 @@ def render_client_config(
return { return {
"mcpServers": { "mcpServers": {
SERVER_NAME: { SERVER_NAME: {
"command": uv_bin, "command": str(python_bin),
"args": args, "args": args,
"env": {"NOCOBASE_MCP_NB_BIN": nb_bin}, "env": {"NOCOBASE_MCP_NB_BIN": nb_bin},
} }
} }
} }
def _venv_python_path(root: Path, *, os_name: str | None = None) -> Path:
if (os_name or os.name) == "nt":
return root / ".venv" / "Scripts" / "python.exe"
return root / ".venv" / "bin" / "python"
+26 -3
View File
@@ -73,7 +73,10 @@ class AgentInstallTests(unittest.TestCase):
self.assertTrue(first["configCreated"]) self.assertTrue(first["configCreated"])
self.assertFalse(second["configCreated"]) self.assertFalse(second["configCreated"])
self.assertEqual(preserved["env"], "custom-env") self.assertEqual(preserved["env"], "custom-env")
self.assertEqual(calls[0][0], ["/opt/bin/uv", "sync", "--locked", "--python", "3.12"]) self.assertEqual(calls[0][0], ["/opt/bin/uv", "sync", "--locked", "--no-dev", "--python", "3.12"])
self.assertEqual(calls[0][1]["env"]["UV_PROJECT_ENVIRONMENT"], str(root / ".venv"))
self.assertEqual(first["dependencies"], "locked-runtime-only")
self.assertEqual(first["environmentPath"], str(root / ".venv"))
self.assertEqual(len(calls), 2) self.assertEqual(len(calls), 2)
def test_set_env_backs_up_config_before_changing_it(self) -> None: def test_set_env_backs_up_config_before_changing_it(self) -> None:
@@ -227,12 +230,21 @@ class AgentInstallTests(unittest.TestCase):
writable = agent_install.render_client_config(root, enable_writes=True, which=fake_which) writable = agent_install.render_client_config(root, enable_writes=True, which=fake_which)
server = config["mcpServers"]["nocobase-crm"] server = config["mcpServers"]["nocobase-crm"]
self.assertEqual(server["command"], "/opt/bin/uv") self.assertEqual(server["command"], str(root.resolve() / ".venv" / "bin" / "python"))
self.assertEqual(server["env"]["NOCOBASE_MCP_NB_BIN"], "/opt/bin/nb") self.assertEqual(server["env"]["NOCOBASE_MCP_NB_BIN"], "/opt/bin/nb")
self.assertIn(str(root.resolve()), server["args"]) self.assertIn(str(root.resolve() / "config.json"), server["args"])
self.assertEqual(server["args"][:2], ["-m", "nocobase_crm_mcp.server"])
self.assertNotIn("--enable-writes", server["args"]) self.assertNotIn("--enable-writes", server["args"])
self.assertIn("--enable-writes", writable["mcpServers"]["nocobase-crm"]["args"]) self.assertIn("--enable-writes", writable["mcpServers"]["nocobase-crm"]["args"])
def test_client_config_uses_windows_project_environment_python(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
config = agent_install.render_client_config(root, which=fake_which, os_name="nt")
server = config["mcpServers"]["nocobase-crm"]
self.assertEqual(server["command"], str(root.resolve() / ".venv" / "Scripts" / "python.exe"))
def test_doctor_checks_local_install_without_reading_nb_secrets(self) -> None: def test_doctor_checks_local_install_without_reading_nb_secrets(self) -> None:
commands = [] commands = []
@@ -254,6 +266,14 @@ class AgentInstallTests(unittest.TestCase):
self.assertTrue(result["ok"]) self.assertTrue(result["ok"])
self.assertEqual(commands[0], ["/opt/bin/uv", "lock", "--check"]) self.assertEqual(commands[0], ["/opt/bin/uv", "lock", "--check"])
self.assertEqual(
commands[1],
[
str(root.resolve() / ".venv" / "bin" / "python"),
"-c",
"import mcp; import nocobase_crm_mcp",
],
)
self.assertNotIn("--show-secrets", json.dumps(result)) self.assertNotIn("--show-secrets", json.dumps(result))
self.assertTrue(any("crm_health" in step for step in result["remainingVerification"])) self.assertTrue(any("crm_health" in step for step in result["remainingVerification"]))
@@ -269,6 +289,9 @@ class AgentInstallTests(unittest.TestCase):
self.assertEqual(manifest["authentication"]["secretEntry"], "user-controlled-private-tty") self.assertEqual(manifest["authentication"]["secretEntry"], "user-controlled-private-tty")
self.assertFalse(manifest["authentication"]["allowPassword"]) self.assertFalse(manifest["authentication"]["allowPassword"])
self.assertFalse(manifest["authentication"]["allowCommandLineSecret"]) self.assertFalse(manifest["authentication"]["allowCommandLineSecret"])
self.assertEqual(manifest["runtime"]["launcher"], "project-venv-python")
self.assertEqual(manifest["runtime"]["dependencyProfile"], "runtime-only-no-dev")
self.assertFalse(manifest["installation"]["defaultBackgroundService"])
self.assertTrue(manifest["securityDefaults"]["readOnly"]) self.assertTrue(manifest["securityDefaults"]["readOnly"])
self.assertFalse(manifest["securityDefaults"]["uploads"]) self.assertFalse(manifest["securityDefaults"]["uploads"])
Generated
+2 -41
View File
@@ -10,15 +10,6 @@ resolution-markers = [
"python_full_version < '3.14' and sys_platform != 'win32'", "python_full_version < '3.14' and sys_platform != 'win32'",
] ]
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
version = "0.7.0" version = "0.7.0"
@@ -639,12 +630,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
] ]
[package.optional-dependencies]
cli = [
{ name = "python-dotenv" },
{ name = "typer" },
]
[[package]] [[package]]
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"
@@ -765,7 +750,7 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "cryptography" }, { name = "cryptography" },
{ name = "httpx" }, { name = "httpx" },
{ name = "mcp", extra = ["cli"] }, { name = "mcp" },
{ name = "python-multipart" }, { name = "python-multipart" },
] ]
@@ -780,7 +765,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "cryptography", specifier = ">=50,<51" }, { name = "cryptography", specifier = ">=50,<51" },
{ name = "httpx", specifier = ">=0.27,<1" }, { name = "httpx", specifier = ">=0.27,<1" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.27,<2" }, { name = "mcp", specifier = ">=1.27,<2" },
{ name = "python-multipart", specifier = ">=0.0.9,<1" }, { name = "python-multipart", specifier = ">=0.0.9,<1" },
] ]
@@ -1239,15 +1224,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" },
] ]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]] [[package]]
name = "sortedcontainers" name = "sortedcontainers"
version = "2.4.0" version = "2.4.0"
@@ -1337,21 +1313,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
] ]
[[package]]
name = "typer"
version = "0.26.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.16.0" version = "4.16.0"