uro-cli 0.5.0__tar.gz → 0.7.0__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.
- {uro_cli-0.5.0 → uro_cli-0.7.0}/PKG-INFO +3 -3
- {uro_cli-0.5.0 → uro_cli-0.7.0}/pyproject.toml +3 -3
- uro_cli-0.7.0/tests/test_cli_scaffold.py +182 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/uro_cli/main.py +892 -118
- uro_cli-0.5.0/tests/test_cli_scaffold.py +0 -89
- {uro_cli-0.5.0 → uro_cli-0.7.0}/.gitignore +0 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/LICENSE +0 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/README.md +0 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/tests/test_registry_router.py +0 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/tests/test_wiring.py +0 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/uro_cli/__init__.py +0 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/uro_cli/py.typed +0 -0
- {uro_cli-0.5.0 → uro_cli-0.7.0}/uro_cli/wiring.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: uro-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Uro Engine reference client — play, dry-run, and dev tools against the engine
|
|
5
5
|
Project-URL: Homepage, https://github.com/cupskeee/uro
|
|
6
6
|
Project-URL: Repository, https://github.com/cupskeee/uro
|
|
@@ -12,8 +12,8 @@ Requires-Python: >=3.12
|
|
|
12
12
|
Requires-Dist: python-dotenv>=1.0
|
|
13
13
|
Requires-Dist: rich>=13.9
|
|
14
14
|
Requires-Dist: typer>=0.27.2
|
|
15
|
-
Requires-Dist: uro-core[llm,postgres]==0.
|
|
16
|
-
Requires-Dist: uro-server==0.
|
|
15
|
+
Requires-Dist: uro-core[llm,postgres]==0.7.0
|
|
16
|
+
Requires-Dist: uro-server==0.7.0
|
|
17
17
|
Requires-Dist: websockets>=16.1
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uro-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Uro Engine reference client — play, dry-run, and dev tools against the engine"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
@@ -13,8 +13,8 @@ requires-python = ">=3.12"
|
|
|
13
13
|
# pair a new uro-cli with an old uro-core; `just release` bumps all three together, and the
|
|
14
14
|
# `{workspace = true}` sources make a drifted pin fail `uv sync` locally.
|
|
15
15
|
dependencies = [
|
|
16
|
-
"uro-core[postgres,llm]==0.
|
|
17
|
-
"uro-server==0.
|
|
16
|
+
"uro-core[postgres,llm]==0.7.0",
|
|
17
|
+
"uro-server==0.7.0",
|
|
18
18
|
"typer>=0.27.2",
|
|
19
19
|
"rich>=13.9",
|
|
20
20
|
"websockets>=16.1",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from typer.testing import CliRunner
|
|
2
|
+
from uro_cli.main import app
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_cli_version_runs() -> None:
|
|
6
|
+
result = CliRunner().invoke(app, ["version"])
|
|
7
|
+
assert result.exit_code == 0
|
|
8
|
+
assert "uro-core" in result.output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_provider_codex_login(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
12
|
+
"""The whole `uro provider codex-login` flow, with codex_auth + the store mocked."""
|
|
13
|
+
from uro_cli import main
|
|
14
|
+
from uro_core.providers import codex_auth
|
|
15
|
+
|
|
16
|
+
async def dev(**kw: object) -> dict[str, object]:
|
|
17
|
+
return {
|
|
18
|
+
"device_auth_id": "d",
|
|
19
|
+
"user_code": "J7DE-8NXJS",
|
|
20
|
+
"verification_uri": "https://auth.openai.com/codex/device",
|
|
21
|
+
"interval": 0, # instant poll — no real waiting in the test
|
|
22
|
+
"expires_in": 10,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async def poll(dev_id: str, code: str, **kw: object) -> dict[str, str]:
|
|
26
|
+
return {"authorization_code": "AC", "code_verifier": "V"}
|
|
27
|
+
|
|
28
|
+
async def exch(code: str, verifier: str, **kw: object) -> dict[str, str]:
|
|
29
|
+
return {"access_token": "AT", "refresh_token": "RT"}
|
|
30
|
+
|
|
31
|
+
async def disc(token: str, **kw: object) -> list[dict[str, str]]:
|
|
32
|
+
return [{"id": "gpt-5-codex", "modality": "chat"}]
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(codex_auth, "request_device_code", dev)
|
|
35
|
+
monkeypatch.setattr(codex_auth, "poll_device_auth", poll)
|
|
36
|
+
monkeypatch.setattr(codex_auth, "exchange_code", exch)
|
|
37
|
+
monkeypatch.setattr(codex_auth, "discover_codex_models", disc)
|
|
38
|
+
|
|
39
|
+
class _Store:
|
|
40
|
+
async def add_credential(self, **kw: object) -> str:
|
|
41
|
+
assert kw["auth_mode"] == "oauth_device" and kw["provider"] == "codex"
|
|
42
|
+
return "cred-1"
|
|
43
|
+
|
|
44
|
+
async def add_connection(self, **kw: object) -> str:
|
|
45
|
+
return "conn-1"
|
|
46
|
+
|
|
47
|
+
async def set_connection_models(self, cid: str, models: object) -> bool:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
async def close(self) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
async def _noop(store: object) -> None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
monkeypatch.setattr(main, "build_store", lambda: _Store())
|
|
57
|
+
monkeypatch.setattr(main, "connect_store", _noop)
|
|
58
|
+
|
|
59
|
+
result = CliRunner().invoke(app, ["provider", "codex-login", "--no-browser"])
|
|
60
|
+
assert result.exit_code == 0, result.output
|
|
61
|
+
assert "J7DE-8NXJS" in result.output # the displayed device code
|
|
62
|
+
assert "connected: conn-1" in result.output
|
|
63
|
+
assert "gpt-5-codex" in result.output # discovered model listed
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_provider_bind_rejects_a_chat_model_for_the_embedder(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
67
|
+
"""CLI parity with the server's embedder-modality guard (review): a chat model (codex is always
|
|
68
|
+
chat) can't back the embedder role."""
|
|
69
|
+
from uro_cli import main
|
|
70
|
+
from uro_core.ports.model_registry import ModelConnection
|
|
71
|
+
|
|
72
|
+
class _Store:
|
|
73
|
+
async def get_connection(self, cid: str) -> ModelConnection:
|
|
74
|
+
return ModelConnection(id=cid, name="cx", provider="codex", auth_id="a1")
|
|
75
|
+
|
|
76
|
+
async def set_role_binding(self, *a: object, **kw: object) -> None:
|
|
77
|
+
raise AssertionError("should not bind — the guard must reject first")
|
|
78
|
+
|
|
79
|
+
async def close(self) -> None:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
async def _noop(store: object) -> None:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
monkeypatch.setattr(main, "build_store", lambda: _Store())
|
|
86
|
+
monkeypatch.setattr(main, "connect_store", _noop)
|
|
87
|
+
result = CliRunner().invoke(app, ["provider", "bind", "embedder", "conn-1", "gpt-5-codex"])
|
|
88
|
+
assert result.exit_code == 1
|
|
89
|
+
assert "embedding model" in result.output
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _credential_store(monkeypatch, linked: list[str]): # type: ignore[no-untyped-def]
|
|
93
|
+
"""A fake registry holding credential `cred-1`, used by the connections in `linked`."""
|
|
94
|
+
from uro_cli import main
|
|
95
|
+
from uro_core.ports.model_registry import ModelConnection
|
|
96
|
+
|
|
97
|
+
deleted: list[str] = []
|
|
98
|
+
|
|
99
|
+
class _Cred:
|
|
100
|
+
id = "cred-1"
|
|
101
|
+
|
|
102
|
+
class _Store:
|
|
103
|
+
async def list_credentials(self) -> list[_Cred]:
|
|
104
|
+
return [_Cred()]
|
|
105
|
+
|
|
106
|
+
async def list_connections(self) -> list[ModelConnection]:
|
|
107
|
+
return [
|
|
108
|
+
ModelConnection(id=c, name=c, provider="codex", auth_id="cred-1") for c in linked
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
async def delete_credential(self, credential_id: str) -> bool:
|
|
112
|
+
deleted.append(credential_id)
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
async def close(self) -> None:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
async def _noop(store: object) -> None:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
monkeypatch.setattr(main, "build_store", lambda: _Store())
|
|
122
|
+
monkeypatch.setattr(main, "connect_store", _noop)
|
|
123
|
+
return deleted
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_provider_credential_rm_deletes_an_unused_credential(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
127
|
+
deleted = _credential_store(monkeypatch, linked=[])
|
|
128
|
+
result = CliRunner().invoke(app, ["provider", "credential", "rm", "cred-1"])
|
|
129
|
+
assert result.exit_code == 0, result.output
|
|
130
|
+
assert deleted == ["cred-1"] and "deleted" in result.output
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_provider_credential_rm_refuses_one_in_use_without_force(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
134
|
+
"""Deleting a linked credential unlinks its connections (docs/20) — live roles would lose auth,
|
|
135
|
+
so the CLI makes that an explicit --force."""
|
|
136
|
+
deleted = _credential_store(monkeypatch, linked=["conn-1"])
|
|
137
|
+
refused = CliRunner().invoke(app, ["provider", "credential", "rm", "cred-1"])
|
|
138
|
+
assert refused.exit_code == 1 and "conn-1" in refused.output and deleted == []
|
|
139
|
+
forced = CliRunner().invoke(app, ["provider", "credential", "rm", "cred-1", "--force"])
|
|
140
|
+
assert forced.exit_code == 0, forced.output
|
|
141
|
+
assert deleted == ["cred-1"] and "unlinked (now unauthenticated): conn-1" in forced.output
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_provider_credential_rm_reports_an_unknown_id(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
145
|
+
deleted = _credential_store(monkeypatch, linked=[])
|
|
146
|
+
result = CliRunner().invoke(app, ["provider", "credential", "rm", "nope"])
|
|
147
|
+
assert result.exit_code == 1 and "no such credential: nope" in result.output and deleted == []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_token_revoke_by_participant_and_list(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
151
|
+
from uro_cli import main
|
|
152
|
+
|
|
153
|
+
calls: list[tuple[str, object]] = []
|
|
154
|
+
|
|
155
|
+
def fake(server: str, path: str, token: str, body: object) -> dict[str, object]:
|
|
156
|
+
calls.append((path, body))
|
|
157
|
+
if path.endswith("/revoke"):
|
|
158
|
+
return {"revoked": True, "count": 2}
|
|
159
|
+
return {
|
|
160
|
+
"tokens": [
|
|
161
|
+
{
|
|
162
|
+
"token_id": "abc123def456",
|
|
163
|
+
"participant_id": "bob",
|
|
164
|
+
"revoked": False,
|
|
165
|
+
"created_at": "2026-10-30T00:00:00Z",
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
monkeypatch.setattr(main, "_server_post", fake)
|
|
171
|
+
out = CliRunner().invoke(
|
|
172
|
+
app, ["token", "revoke", "c1", "--participant", "bob", "--token", "op"]
|
|
173
|
+
)
|
|
174
|
+
assert out.exit_code == 0 and "revoked 2 token(s) for bob" in out.output
|
|
175
|
+
assert calls[-1] == ("/campaigns/c1/tokens/revoke", {"participant": "bob"})
|
|
176
|
+
out = CliRunner().invoke(app, ["token", "list", "c1", "--token", "op"])
|
|
177
|
+
assert out.exit_code == 0 and "abc123def456 bob" in out.output and "live" in out.output
|
|
178
|
+
assert calls[-1] == ("/campaigns/c1/tokens", None) # a GET
|
|
179
|
+
both = CliRunner().invoke(
|
|
180
|
+
app, ["token", "revoke", "c1", "--participant", "bob", "--target", "t", "--token", "op"]
|
|
181
|
+
)
|
|
182
|
+
assert both.exit_code == 1
|