meshagent-cli 0.35.4__tar.gz → 0.35.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/PKG-INFO +15 -14
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/api_keys.py +35 -6
- meshagent_cli-0.35.6/meshagent/cli/api_keys_test.py +79 -0
- meshagent_cli-0.35.6/meshagent/cli/async_typer.py +380 -0
- meshagent_cli-0.35.6/meshagent/cli/async_typer_test.py +128 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/chatbot.py +83 -16
- meshagent_cli-0.35.6/meshagent/cli/cli.py +293 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/cli_secrets.py +32 -6
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/codex.py +19 -4
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/containers.py +24 -58
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/containers_test.py +65 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/multi.py +85 -44
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/oci_archive.py +66 -16
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/oci_archive_test.py +32 -5
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/process_test.py +199 -1
- meshagent_cli-0.35.6/meshagent/cli/room.py +78 -0
- meshagent_cli-0.35.6/meshagent/cli/root_commands.py +122 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sync.py +7 -2
- meshagent_cli-0.35.6/meshagent/cli/version.py +1 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/PKG-INFO +15 -14
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/SOURCES.txt +3 -0
- meshagent_cli-0.35.6/meshagent_cli.egg-info/requires.txt +30 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/pyproject.toml +14 -13
- meshagent_cli-0.35.4/meshagent/cli/async_typer.py +0 -142
- meshagent_cli-0.35.4/meshagent/cli/cli.py +0 -281
- meshagent_cli-0.35.4/meshagent/cli/room.py +0 -48
- meshagent_cli-0.35.4/meshagent/cli/version.py +0 -1
- meshagent_cli-0.35.4/meshagent_cli.egg-info/requires.txt +0 -29
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/README.md +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/__init__.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/agent.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/auth.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/auth_async.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/call.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/cli_mcp.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/cli_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/common_options.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/database.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/developer.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/developer_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/helper.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/helper_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/helpers.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/host.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/image.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/image_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/mailbot.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/mailboxes.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/meeting_transcriber.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/memory.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/memory_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/messaging.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/oauth2.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/participant_token.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/port.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/port_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/process.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/projects.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/queue.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/queue_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/room_services.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/rooms.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/routes.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/scheduled_tasks.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/scheduled_tasks_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/services.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/services_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sessions.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sessions_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/storage.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/storage_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/sync_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/task_runner.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/__init__.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/setup.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/setup_splash_frames.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/tui/setup_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/voicebot.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/webhook.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/webserver.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/webserver_test.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent/cli/worker.py +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/dependency_links.txt +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/entry_points.txt +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/meshagent_cli.egg-info/top_level.txt +0 -0
- {meshagent_cli-0.35.4 → meshagent_cli-0.35.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-cli
|
|
3
|
-
Version: 0.35.
|
|
3
|
+
Version: 0.35.6
|
|
4
4
|
Summary: CLI for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -14,26 +14,27 @@ Requires-Dist: opentelemetry-distro~=0.59b0
|
|
|
14
14
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.33
|
|
15
15
|
Requires-Dist: art~=6.5
|
|
16
16
|
Requires-Dist: pydantic-yaml~=1.5
|
|
17
|
-
Requires-Dist: pathspec
|
|
17
|
+
Requires-Dist: pathspec<2,>=1.0.3
|
|
18
|
+
Requires-Dist: zstandard~=0.25.0
|
|
18
19
|
Requires-Dist: rich~=14.3.0
|
|
19
20
|
Requires-Dist: textual<2.0,>=0.50
|
|
20
21
|
Requires-Dist: prompt-toolkit~=3.0.52
|
|
21
22
|
Provides-Extra: all
|
|
22
|
-
Requires-Dist: meshagent-agents[all]~=0.35.
|
|
23
|
-
Requires-Dist: meshagent-api[all]~=0.35.
|
|
24
|
-
Requires-Dist: meshagent-computers~=0.35.
|
|
25
|
-
Requires-Dist: meshagent-openai~=0.35.
|
|
26
|
-
Requires-Dist: meshagent-anthropic~=0.35.
|
|
27
|
-
Requires-Dist: meshagent-codex~=0.35.
|
|
28
|
-
Requires-Dist: meshagent-mcp~=0.35.
|
|
29
|
-
Requires-Dist: meshagent-tools~=0.35.
|
|
23
|
+
Requires-Dist: meshagent-agents[all]~=0.35.6; extra == "all"
|
|
24
|
+
Requires-Dist: meshagent-api[all]~=0.35.6; extra == "all"
|
|
25
|
+
Requires-Dist: meshagent-computers~=0.35.6; extra == "all"
|
|
26
|
+
Requires-Dist: meshagent-openai~=0.35.6; extra == "all"
|
|
27
|
+
Requires-Dist: meshagent-anthropic~=0.35.6; extra == "all"
|
|
28
|
+
Requires-Dist: meshagent-codex~=0.35.6; extra == "all"
|
|
29
|
+
Requires-Dist: meshagent-mcp~=0.35.6; extra == "all"
|
|
30
|
+
Requires-Dist: meshagent-tools~=0.35.6; extra == "all"
|
|
30
31
|
Requires-Dist: supabase-auth~=2.28.0; extra == "all"
|
|
31
32
|
Requires-Dist: prompt-toolkit~=3.0.52; extra == "all"
|
|
32
33
|
Provides-Extra: mcp-service
|
|
33
|
-
Requires-Dist: meshagent-agents[all]~=0.35.
|
|
34
|
-
Requires-Dist: meshagent-api~=0.35.
|
|
35
|
-
Requires-Dist: meshagent-mcp~=0.35.
|
|
36
|
-
Requires-Dist: meshagent-tools~=0.35.
|
|
34
|
+
Requires-Dist: meshagent-agents[all]~=0.35.6; extra == "mcp-service"
|
|
35
|
+
Requires-Dist: meshagent-api~=0.35.6; extra == "mcp-service"
|
|
36
|
+
Requires-Dist: meshagent-mcp~=0.35.6; extra == "mcp-service"
|
|
37
|
+
Requires-Dist: meshagent-tools~=0.35.6; extra == "mcp-service"
|
|
37
38
|
Requires-Dist: supabase-auth~=2.28.0; extra == "mcp-service"
|
|
38
39
|
|
|
39
40
|
# [Meshagent](https://www.meshagent.com)
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import shlex
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
2
6
|
from rich import print
|
|
3
7
|
|
|
4
|
-
from meshagent.cli.common_options import ProjectIdOption
|
|
5
8
|
from meshagent.cli import async_typer
|
|
9
|
+
from meshagent.cli.common_options import OutputFormatOption, ProjectIdOption
|
|
6
10
|
from meshagent.cli.helper import (
|
|
11
|
+
get_active_api_key,
|
|
7
12
|
get_client,
|
|
8
13
|
print_json_table,
|
|
9
14
|
resolve_project_id,
|
|
10
15
|
set_active_api_key,
|
|
11
16
|
)
|
|
12
|
-
from meshagent.cli.common_options import OutputFormatOption
|
|
13
|
-
from typing import Annotated
|
|
14
|
-
import typer
|
|
15
17
|
|
|
16
18
|
app = async_typer.AsyncTyper(help="Manage or activate api-keys for your project")
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
async def _require_active_api_key(*, project_id: str | None) -> str:
|
|
22
|
+
resolved_project_id = await resolve_project_id(project_id=project_id)
|
|
23
|
+
key = await get_active_api_key(project_id=resolved_project_id)
|
|
24
|
+
if key is None:
|
|
25
|
+
print(
|
|
26
|
+
f"[red]No activated API key found for project {resolved_project_id}. "
|
|
27
|
+
"Use meshagent api-key activate or meshagent api-key create "
|
|
28
|
+
"--activate to store one locally.[/red]"
|
|
29
|
+
)
|
|
30
|
+
raise typer.Exit(code=1)
|
|
31
|
+
return key
|
|
32
|
+
|
|
33
|
+
|
|
19
34
|
@app.async_command("list", help="List API keys for a project.")
|
|
20
35
|
async def list(
|
|
21
36
|
*,
|
|
@@ -82,6 +97,21 @@ async def create(
|
|
|
82
97
|
)
|
|
83
98
|
|
|
84
99
|
|
|
100
|
+
@app.async_command("show", help="Show the activated API key for a project.")
|
|
101
|
+
async def show(*, project_id: ProjectIdOption):
|
|
102
|
+
key = await _require_active_api_key(project_id=project_id)
|
|
103
|
+
typer.echo(key)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.async_command(
|
|
107
|
+
"env",
|
|
108
|
+
help="Print the activated API key as a shell export snippet.",
|
|
109
|
+
)
|
|
110
|
+
async def env(*, project_id: ProjectIdOption):
|
|
111
|
+
key = await _require_active_api_key(project_id=project_id)
|
|
112
|
+
typer.echo(f"export MESHAGENT_API_KEY={shlex.quote(key)}")
|
|
113
|
+
|
|
114
|
+
|
|
85
115
|
@app.async_command(
|
|
86
116
|
"activate",
|
|
87
117
|
help="Set the default API key for a project in local CLI settings.",
|
|
@@ -92,8 +122,7 @@ async def activate(
|
|
|
92
122
|
key: str,
|
|
93
123
|
):
|
|
94
124
|
project_id = await resolve_project_id(project_id=project_id)
|
|
95
|
-
|
|
96
|
-
await set_active_api_key(project_id=project_id, key=key)
|
|
125
|
+
await set_active_api_key(project_id=project_id, key=key)
|
|
97
126
|
|
|
98
127
|
|
|
99
128
|
@app.async_command("delete", help="Delete an API key from a project.")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
from meshagent.cli import api_keys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_show_prints_activated_api_key(
|
|
9
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
10
|
+
) -> None:
|
|
11
|
+
printed: list[str] = []
|
|
12
|
+
|
|
13
|
+
async def fake_resolve_project_id(*, project_id: str | None) -> str:
|
|
14
|
+
assert project_id == "project-1"
|
|
15
|
+
return "resolved-project"
|
|
16
|
+
|
|
17
|
+
async def fake_get_active_api_key(*, project_id: str) -> str | None:
|
|
18
|
+
assert project_id == "resolved-project"
|
|
19
|
+
return "ma-key-1"
|
|
20
|
+
|
|
21
|
+
monkeypatch.setattr(api_keys, "resolve_project_id", fake_resolve_project_id)
|
|
22
|
+
monkeypatch.setattr(api_keys, "get_active_api_key", fake_get_active_api_key)
|
|
23
|
+
monkeypatch.setattr(api_keys.typer, "echo", printed.append)
|
|
24
|
+
|
|
25
|
+
await api_keys.show(project_id="project-1")
|
|
26
|
+
|
|
27
|
+
assert printed == ["ma-key-1"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_env_prints_shell_export_for_activated_api_key(
|
|
32
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
33
|
+
) -> None:
|
|
34
|
+
printed: list[str] = []
|
|
35
|
+
|
|
36
|
+
async def fake_resolve_project_id(*, project_id: str | None) -> str:
|
|
37
|
+
assert project_id == "project-1"
|
|
38
|
+
return "resolved-project"
|
|
39
|
+
|
|
40
|
+
async def fake_get_active_api_key(*, project_id: str) -> str | None:
|
|
41
|
+
assert project_id == "resolved-project"
|
|
42
|
+
return "ma-key-1"
|
|
43
|
+
|
|
44
|
+
monkeypatch.setattr(api_keys, "resolve_project_id", fake_resolve_project_id)
|
|
45
|
+
monkeypatch.setattr(api_keys, "get_active_api_key", fake_get_active_api_key)
|
|
46
|
+
monkeypatch.setattr(api_keys.typer, "echo", printed.append)
|
|
47
|
+
|
|
48
|
+
await api_keys.env(project_id="project-1")
|
|
49
|
+
|
|
50
|
+
assert printed == ["export MESHAGENT_API_KEY=ma-key-1"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_show_exits_when_no_activated_api_key(
|
|
55
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
56
|
+
) -> None:
|
|
57
|
+
printed: list[str] = []
|
|
58
|
+
|
|
59
|
+
async def fake_resolve_project_id(*, project_id: str | None) -> str:
|
|
60
|
+
assert project_id == "project-1"
|
|
61
|
+
return "resolved-project"
|
|
62
|
+
|
|
63
|
+
async def fake_get_active_api_key(*, project_id: str) -> str | None:
|
|
64
|
+
assert project_id == "resolved-project"
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
monkeypatch.setattr(api_keys, "resolve_project_id", fake_resolve_project_id)
|
|
68
|
+
monkeypatch.setattr(api_keys, "get_active_api_key", fake_get_active_api_key)
|
|
69
|
+
monkeypatch.setattr(api_keys, "print", printed.append)
|
|
70
|
+
|
|
71
|
+
with pytest.raises(typer.Exit) as exc_info:
|
|
72
|
+
await api_keys.show(project_id="project-1")
|
|
73
|
+
|
|
74
|
+
assert exc_info.value.exit_code == 1
|
|
75
|
+
assert printed == [
|
|
76
|
+
"[red]No activated API key found for project resolved-project. "
|
|
77
|
+
"Use meshagent api-key activate or meshagent api-key create "
|
|
78
|
+
"--activate to store one locally.[/red]"
|
|
79
|
+
]
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib
|
|
5
|
+
import inspect
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from functools import partial, wraps
|
|
10
|
+
from typing import Any, Callable, Sequence, TypeVar
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import typer
|
|
14
|
+
from typer import Typer
|
|
15
|
+
from typer.main import DeveloperExceptionConfig
|
|
16
|
+
from typer.main import _typer_developer_exception_attr_name
|
|
17
|
+
from typer.main import except_hook
|
|
18
|
+
from typer.main import get_command as get_typer_command
|
|
19
|
+
from typer.main import get_group as get_typer_group
|
|
20
|
+
from typer.main import get_install_completion_arguments
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class LazyCommandRegistration:
|
|
27
|
+
name: str
|
|
28
|
+
module: str
|
|
29
|
+
attribute: str = "app"
|
|
30
|
+
help: str | None = None
|
|
31
|
+
short_help: str | None = None
|
|
32
|
+
hidden: bool = False
|
|
33
|
+
deprecated: bool = False
|
|
34
|
+
command_path: tuple[str, ...] = ()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LazyLoadedCommand(click.Command):
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
registration: LazyCommandRegistration,
|
|
42
|
+
) -> None:
|
|
43
|
+
super().__init__(
|
|
44
|
+
name=registration.name,
|
|
45
|
+
help=registration.help,
|
|
46
|
+
short_help=registration.short_help,
|
|
47
|
+
hidden=registration.hidden,
|
|
48
|
+
deprecated=registration.deprecated,
|
|
49
|
+
)
|
|
50
|
+
self._registration = registration
|
|
51
|
+
self._loaded_command: click.Command | None = None
|
|
52
|
+
|
|
53
|
+
def _load_command(self) -> click.Command:
|
|
54
|
+
if self._loaded_command is not None:
|
|
55
|
+
return self._loaded_command
|
|
56
|
+
|
|
57
|
+
module = importlib.import_module(self._registration.module)
|
|
58
|
+
try:
|
|
59
|
+
target = module.__dict__[self._registration.attribute]
|
|
60
|
+
except KeyError as exc:
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
f"{self._registration.module} has no attribute {self._registration.attribute}"
|
|
63
|
+
) from exc
|
|
64
|
+
|
|
65
|
+
command = _coerce_to_click_command(target)
|
|
66
|
+
for segment in self._registration.command_path:
|
|
67
|
+
if not isinstance(command, click.Group):
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
f"{self._registration.module}.{self._registration.attribute} does not expose subcommand path "
|
|
70
|
+
f"{' '.join(self._registration.command_path)}"
|
|
71
|
+
)
|
|
72
|
+
resolved = command.get_command(click.Context(command), segment)
|
|
73
|
+
if resolved is None:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
f"{self._registration.module}.{self._registration.attribute} has no subcommand {segment}"
|
|
76
|
+
)
|
|
77
|
+
command = resolved
|
|
78
|
+
|
|
79
|
+
self._loaded_command = command
|
|
80
|
+
return command
|
|
81
|
+
|
|
82
|
+
def make_context(
|
|
83
|
+
self,
|
|
84
|
+
info_name: str | None,
|
|
85
|
+
args: list[str],
|
|
86
|
+
parent: click.Context | None = None,
|
|
87
|
+
**extra: Any,
|
|
88
|
+
) -> click.Context:
|
|
89
|
+
return self._load_command().make_context(
|
|
90
|
+
info_name,
|
|
91
|
+
args,
|
|
92
|
+
parent=parent,
|
|
93
|
+
**extra,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def shell_complete(
|
|
97
|
+
self, ctx: click.Context, incomplete: str
|
|
98
|
+
) -> list[click.shell_completion.CompletionItem]:
|
|
99
|
+
return self._load_command().shell_complete(ctx, incomplete)
|
|
100
|
+
|
|
101
|
+
def get_help(self, ctx: click.Context) -> str:
|
|
102
|
+
return self._load_command().get_help(ctx)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _coerce_to_click_command(target: Any) -> click.Command:
|
|
106
|
+
if isinstance(target, click.Command):
|
|
107
|
+
return target
|
|
108
|
+
if isinstance(target, Typer):
|
|
109
|
+
return get_command(target)
|
|
110
|
+
raise TypeError(f"Unsupported lazy command target: {target!r}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _materialize_command(command: click.Command) -> click.Command:
|
|
114
|
+
if isinstance(command, LazyLoadedCommand):
|
|
115
|
+
return _materialize_command(command._load_command())
|
|
116
|
+
|
|
117
|
+
if isinstance(command, click.Group):
|
|
118
|
+
command.commands = {
|
|
119
|
+
name: _materialize_command(subcommand)
|
|
120
|
+
for name, subcommand in command.commands.items()
|
|
121
|
+
}
|
|
122
|
+
return command
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_command(
|
|
126
|
+
typer_instance: Typer | click.Command,
|
|
127
|
+
*,
|
|
128
|
+
materialize_lazy: bool = False,
|
|
129
|
+
) -> click.Command:
|
|
130
|
+
if isinstance(typer_instance, click.Command):
|
|
131
|
+
click_command = typer_instance
|
|
132
|
+
elif isinstance(typer_instance, LazyTyper):
|
|
133
|
+
if len(typer_instance.registered_lazy_commands) > 0:
|
|
134
|
+
click_command = get_typer_group(typer_instance)
|
|
135
|
+
for registration in typer_instance.registered_lazy_commands:
|
|
136
|
+
click_command.commands[registration.name] = LazyLoadedCommand(
|
|
137
|
+
registration=registration,
|
|
138
|
+
)
|
|
139
|
+
if typer_instance._add_completion:
|
|
140
|
+
click_install_param, click_show_param = (
|
|
141
|
+
get_install_completion_arguments()
|
|
142
|
+
)
|
|
143
|
+
click_command.params.append(click_install_param)
|
|
144
|
+
click_command.params.append(click_show_param)
|
|
145
|
+
else:
|
|
146
|
+
click_command = get_typer_command(typer_instance)
|
|
147
|
+
else:
|
|
148
|
+
click_command = get_typer_command(typer_instance)
|
|
149
|
+
|
|
150
|
+
if materialize_lazy:
|
|
151
|
+
return _materialize_command(click_command)
|
|
152
|
+
return click_command
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def collect_lazy_command_modules(root: "LazyTyper") -> list[str]:
|
|
156
|
+
modules: set[str] = set()
|
|
157
|
+
pending: list[LazyTyper] = [root]
|
|
158
|
+
seen_apps: set[int] = set()
|
|
159
|
+
seen_targets: set[tuple[str, str]] = set()
|
|
160
|
+
|
|
161
|
+
while pending:
|
|
162
|
+
app = pending.pop()
|
|
163
|
+
app_id = id(app)
|
|
164
|
+
if app_id in seen_apps:
|
|
165
|
+
continue
|
|
166
|
+
seen_apps.add(app_id)
|
|
167
|
+
|
|
168
|
+
for registration in app.registered_lazy_commands:
|
|
169
|
+
modules.add(registration.module)
|
|
170
|
+
|
|
171
|
+
target_key = (registration.module, registration.attribute)
|
|
172
|
+
if target_key in seen_targets:
|
|
173
|
+
continue
|
|
174
|
+
seen_targets.add(target_key)
|
|
175
|
+
|
|
176
|
+
module = importlib.import_module(registration.module)
|
|
177
|
+
try:
|
|
178
|
+
target = module.__dict__[registration.attribute]
|
|
179
|
+
except KeyError as exc:
|
|
180
|
+
raise RuntimeError(
|
|
181
|
+
f"{registration.module} has no attribute {registration.attribute}"
|
|
182
|
+
) from exc
|
|
183
|
+
|
|
184
|
+
if isinstance(target, LazyTyper):
|
|
185
|
+
pending.append(target)
|
|
186
|
+
|
|
187
|
+
return sorted(modules)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def collect_lazy_command_modules_from_entrypoint(
|
|
191
|
+
module_name: str,
|
|
192
|
+
*,
|
|
193
|
+
attribute: str = "app",
|
|
194
|
+
) -> list[str]:
|
|
195
|
+
module = importlib.import_module(module_name)
|
|
196
|
+
try:
|
|
197
|
+
target = module.__dict__[attribute]
|
|
198
|
+
except KeyError as exc:
|
|
199
|
+
raise RuntimeError(f"{module_name} has no attribute {attribute}") from exc
|
|
200
|
+
|
|
201
|
+
if not isinstance(target, LazyTyper):
|
|
202
|
+
raise TypeError(
|
|
203
|
+
f"{module_name}.{attribute} must be a LazyTyper, got {type(target)!r}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return collect_lazy_command_modules(target)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _run_coroutine_sync(
|
|
210
|
+
coro: "asyncio.Future[T] | asyncio.coroutines.Coroutine[Any, Any, T]",
|
|
211
|
+
) -> T:
|
|
212
|
+
"""
|
|
213
|
+
Run an awaitable from sync code.
|
|
214
|
+
|
|
215
|
+
- If we're not currently in an event loop, use asyncio.run().
|
|
216
|
+
- If we ARE in a running loop (e.g. inside an agent / notebook / ASGI app),
|
|
217
|
+
run asyncio.run() in a separate thread and block for the result.
|
|
218
|
+
|
|
219
|
+
This avoids: RuntimeError: asyncio.run() cannot be called from a running event loop
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
asyncio.get_running_loop()
|
|
223
|
+
in_running_loop = True
|
|
224
|
+
except RuntimeError:
|
|
225
|
+
in_running_loop = False
|
|
226
|
+
|
|
227
|
+
if not in_running_loop:
|
|
228
|
+
return asyncio.run(coro) # type: ignore[arg-type]
|
|
229
|
+
|
|
230
|
+
result: dict[str, Any] = {}
|
|
231
|
+
done = threading.Event()
|
|
232
|
+
|
|
233
|
+
def _worker() -> None:
|
|
234
|
+
try:
|
|
235
|
+
result["value"] = asyncio.run(coro) # type: ignore[arg-type]
|
|
236
|
+
except BaseException as e:
|
|
237
|
+
result["error"] = e
|
|
238
|
+
finally:
|
|
239
|
+
done.set()
|
|
240
|
+
|
|
241
|
+
t = threading.Thread(target=_worker, daemon=True)
|
|
242
|
+
t.start()
|
|
243
|
+
done.wait()
|
|
244
|
+
|
|
245
|
+
if "error" in result:
|
|
246
|
+
raise result["error"]
|
|
247
|
+
return result["value"] # type: ignore[return-value]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _missing_parameter_name(error: click.MissingParameter) -> str:
|
|
251
|
+
param = error.param
|
|
252
|
+
if isinstance(param, click.Option):
|
|
253
|
+
for option_name in param.opts:
|
|
254
|
+
if option_name.startswith("--"):
|
|
255
|
+
return option_name
|
|
256
|
+
if param.opts:
|
|
257
|
+
return param.opts[0]
|
|
258
|
+
if param.secondary_opts:
|
|
259
|
+
return param.secondary_opts[0]
|
|
260
|
+
if isinstance(param, click.Argument) and param.name is not None:
|
|
261
|
+
return param.name
|
|
262
|
+
if param is not None and param.name is not None:
|
|
263
|
+
return param.name
|
|
264
|
+
|
|
265
|
+
param_hint = error.param_hint
|
|
266
|
+
if isinstance(param_hint, str):
|
|
267
|
+
return param_hint
|
|
268
|
+
if param_hint:
|
|
269
|
+
return param_hint[0]
|
|
270
|
+
return "unknown"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class AsyncTyper(Typer):
|
|
274
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
275
|
+
if "no_args_is_help" not in kwargs:
|
|
276
|
+
kwargs["no_args_is_help"] = True
|
|
277
|
+
super().__init__(*args, **kwargs)
|
|
278
|
+
|
|
279
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
280
|
+
explicit_standalone_mode = "standalone_mode" in kwargs
|
|
281
|
+
if not explicit_standalone_mode:
|
|
282
|
+
kwargs["standalone_mode"] = False
|
|
283
|
+
|
|
284
|
+
if sys.excepthook != except_hook:
|
|
285
|
+
sys.excepthook = except_hook
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
return get_command(self)(*args, **kwargs)
|
|
289
|
+
except click.MissingParameter as e:
|
|
290
|
+
if explicit_standalone_mode:
|
|
291
|
+
raise
|
|
292
|
+
missing_name = _missing_parameter_name(e)
|
|
293
|
+
click.secho(
|
|
294
|
+
f" Required parameter is missing: {missing_name}",
|
|
295
|
+
fg="red",
|
|
296
|
+
err=True,
|
|
297
|
+
)
|
|
298
|
+
if e.ctx is not None:
|
|
299
|
+
typer.echo(e.ctx.get_help(), err=True)
|
|
300
|
+
raise SystemExit(e.exit_code)
|
|
301
|
+
except click.ClickException as e:
|
|
302
|
+
if explicit_standalone_mode:
|
|
303
|
+
raise
|
|
304
|
+
e.show()
|
|
305
|
+
raise SystemExit(e.exit_code)
|
|
306
|
+
except click.Abort:
|
|
307
|
+
if explicit_standalone_mode:
|
|
308
|
+
raise
|
|
309
|
+
raise SystemExit(1)
|
|
310
|
+
except click.exceptions.Exit as e:
|
|
311
|
+
if explicit_standalone_mode:
|
|
312
|
+
raise
|
|
313
|
+
raise SystemExit(e.exit_code)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
setattr(
|
|
316
|
+
e,
|
|
317
|
+
_typer_developer_exception_attr_name,
|
|
318
|
+
DeveloperExceptionConfig(
|
|
319
|
+
pretty_exceptions_enable=self.pretty_exceptions_enable,
|
|
320
|
+
pretty_exceptions_show_locals=self.pretty_exceptions_show_locals,
|
|
321
|
+
pretty_exceptions_short=self.pretty_exceptions_short,
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
raise
|
|
325
|
+
|
|
326
|
+
@staticmethod
|
|
327
|
+
def maybe_run_async(decorator: Callable[..., Any], func: Callable[..., Any]) -> Any:
|
|
328
|
+
if inspect.iscoroutinefunction(func):
|
|
329
|
+
|
|
330
|
+
@wraps(func)
|
|
331
|
+
def runner(*args: Any, **kwargs: Any) -> Any:
|
|
332
|
+
return _run_coroutine_sync(func(*args, **kwargs))
|
|
333
|
+
|
|
334
|
+
decorator(runner)
|
|
335
|
+
else:
|
|
336
|
+
decorator(func)
|
|
337
|
+
return func
|
|
338
|
+
|
|
339
|
+
def callback(self, *args: Any, **kwargs: Any) -> Any:
|
|
340
|
+
decorator = super().callback(*args, **kwargs)
|
|
341
|
+
return partial(self.maybe_run_async, decorator)
|
|
342
|
+
|
|
343
|
+
def command(self, *args: Any, **kwargs: Any) -> Any:
|
|
344
|
+
decorator = super().command(*args, **kwargs)
|
|
345
|
+
return partial(self.maybe_run_async, decorator)
|
|
346
|
+
|
|
347
|
+
# keep your existing name if you prefer
|
|
348
|
+
def async_command(self, *args: Any, **kwargs: Any) -> Any:
|
|
349
|
+
return self.command(*args, **kwargs)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class LazyTyper(AsyncTyper):
|
|
353
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
354
|
+
super().__init__(*args, **kwargs)
|
|
355
|
+
self.registered_lazy_commands: list[LazyCommandRegistration] = []
|
|
356
|
+
|
|
357
|
+
def add_lazy_command(
|
|
358
|
+
self,
|
|
359
|
+
*,
|
|
360
|
+
name: str,
|
|
361
|
+
module: str,
|
|
362
|
+
attribute: str = "app",
|
|
363
|
+
help: str | None = None,
|
|
364
|
+
short_help: str | None = None,
|
|
365
|
+
hidden: bool = False,
|
|
366
|
+
deprecated: bool = False,
|
|
367
|
+
command_path: Sequence[str] = (),
|
|
368
|
+
) -> None:
|
|
369
|
+
self.registered_lazy_commands.append(
|
|
370
|
+
LazyCommandRegistration(
|
|
371
|
+
name=name,
|
|
372
|
+
module=module,
|
|
373
|
+
attribute=attribute,
|
|
374
|
+
help=help,
|
|
375
|
+
short_help=short_help,
|
|
376
|
+
hidden=hidden,
|
|
377
|
+
deprecated=deprecated,
|
|
378
|
+
command_path=tuple(command_path),
|
|
379
|
+
)
|
|
380
|
+
)
|