cheso-cli 0.2.0__py3-none-any.whl
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.
- cheso_cli/__init__.py +12 -0
- cheso_cli/__main__.py +5 -0
- cheso_cli/auth.py +128 -0
- cheso_cli/cli.py +864 -0
- cheso_cli/client.py +534 -0
- cheso_cli/config.py +108 -0
- cheso_cli/credentials.py +235 -0
- cheso_cli/errors.py +56 -0
- cheso_cli/legacy_commands.py +93 -0
- cheso_cli/oauth.py +797 -0
- cheso_cli/skill/SKILL.md +83 -0
- cheso_cli/skill_install.py +119 -0
- cheso_cli/tasks.py +834 -0
- cheso_cli/transfer.py +294 -0
- cheso_cli-0.2.0.dist-info/METADATA +164 -0
- cheso_cli-0.2.0.dist-info/RECORD +18 -0
- cheso_cli-0.2.0.dist-info/WHEEL +4 -0
- cheso_cli-0.2.0.dist-info/entry_points.txt +4 -0
cheso_cli/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Cheso 命令行工具。"""
|
|
2
|
+
|
|
3
|
+
from cheso_cli.config import VERSION as __version__
|
|
4
|
+
|
|
5
|
+
__all__ = ["__version__", "main"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> None:
|
|
9
|
+
"""命令行脚本入口(``cheso`` / ``cheso-cli``)。"""
|
|
10
|
+
from cheso_cli.cli import main as _main
|
|
11
|
+
|
|
12
|
+
_main(argv)
|
cheso_cli/__main__.py
ADDED
cheso_cli/auth.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""决定命令使用哪个令牌,并在需要时刷新已保存的 OAuth 访问令牌。
|
|
2
|
+
|
|
3
|
+
优先级:``--token`` > ``CHESO_API_TOKEN`` > ``cheso login`` 保存的凭据。
|
|
4
|
+
|
|
5
|
+
刷新令牌每次使用后都会轮换;服务器再次收到已使用的刷新令牌时,会撤销整个授权。
|
|
6
|
+
Agent 经常同时运行多个 ``cheso`` 命令,因此刷新操作始终持有凭据文件锁,并在拿到锁后重新读取文件。
|
|
7
|
+
如果其他进程已经完成刷新,就使用其新访问令牌,避免用已经失效的刷新令牌再次刷新。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable, Mapping
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from cheso_cli.client import StaticToken, TokenSource
|
|
19
|
+
from cheso_cli.config import DEFAULT_SITE, ENV_TOKEN, SITES, Settings
|
|
20
|
+
from cheso_cli.credentials import CredentialStore, SiteCredentials
|
|
21
|
+
from cheso_cli.errors import LoginRequired
|
|
22
|
+
from cheso_cli.oauth import ERROR_INVALID_CLIENT, ERROR_INVALID_GRANT, OAuthClient, OAuthProtocolError
|
|
23
|
+
|
|
24
|
+
# 访问令牌将在以下秒数内过期时提前刷新。
|
|
25
|
+
REFRESH_SKEW_SECONDS = 60.0
|
|
26
|
+
|
|
27
|
+
TOKEN_SOURCE_FLAG = "--token"
|
|
28
|
+
TOKEN_SOURCE_ENV = ENV_TOKEN
|
|
29
|
+
TOKEN_SOURCE_LOGIN = "login"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def login_hint(settings: Settings) -> str:
|
|
33
|
+
"""当前站点的登录命令,例如 ``cheso login --site cn``。"""
|
|
34
|
+
for name, url in SITES.items():
|
|
35
|
+
if settings.base_url == url:
|
|
36
|
+
return "cheso login" if name == DEFAULT_SITE else f"cheso login --site {name}"
|
|
37
|
+
return f"cheso login --base-url {settings.base_url}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OAuthSession(TokenSource):
|
|
41
|
+
"""从凭据文件读取访问令牌,需要时在文件锁内刷新。"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
settings: Settings,
|
|
46
|
+
store: CredentialStore,
|
|
47
|
+
*,
|
|
48
|
+
transport: httpx.BaseTransport | None = None,
|
|
49
|
+
now: Callable[[], float] = time.time,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.settings = settings
|
|
52
|
+
self.store = store
|
|
53
|
+
self._transport = transport
|
|
54
|
+
self._now = now
|
|
55
|
+
self.refresh_count = 0
|
|
56
|
+
|
|
57
|
+
def _fresh(self, creds: SiteCredentials) -> bool:
|
|
58
|
+
return bool(creds.access_token) and creds.expires_at - self._now() > REFRESH_SKEW_SECONDS
|
|
59
|
+
|
|
60
|
+
def token(self) -> str:
|
|
61
|
+
creds = self.store.get(self.settings.base_url)
|
|
62
|
+
if not creds.logged_in:
|
|
63
|
+
raise self._login_required("Not logged in")
|
|
64
|
+
if self._fresh(creds):
|
|
65
|
+
return creds.access_token
|
|
66
|
+
return self._refresh(rejected=None)
|
|
67
|
+
|
|
68
|
+
def refresh_after_unauthorized(self, rejected: str) -> str | None:
|
|
69
|
+
return self._refresh(rejected=rejected)
|
|
70
|
+
|
|
71
|
+
def _refresh(self, *, rejected: str | None) -> str:
|
|
72
|
+
"""在锁内刷新。``rejected`` 是服务器刚以 401 拒绝的访问令牌,没有时为空。"""
|
|
73
|
+
base = self.settings.base_url
|
|
74
|
+
with self.store.locked():
|
|
75
|
+
creds = self.store.get(base)
|
|
76
|
+
if not creds.logged_in:
|
|
77
|
+
raise self._login_required("Not logged in")
|
|
78
|
+
# 等待锁期间,其他进程可能已经刷新了令牌。
|
|
79
|
+
if self._fresh(creds) and creds.access_token != rejected:
|
|
80
|
+
return creds.access_token
|
|
81
|
+
if not creds.refresh_token or not creds.client_id:
|
|
82
|
+
creds.clear_tokens()
|
|
83
|
+
self.store.put(base, creds)
|
|
84
|
+
raise self._login_required("The login has expired")
|
|
85
|
+
with OAuthClient(self.settings, transport=self._transport, now=self._now) as oauth:
|
|
86
|
+
endpoint = creds.token_endpoint or oauth.metadata().token_endpoint
|
|
87
|
+
try:
|
|
88
|
+
tokens = oauth.refresh(endpoint, client_id=creds.client_id, refresh_token=creds.refresh_token)
|
|
89
|
+
except OAuthProtocolError as e:
|
|
90
|
+
if e.error not in (ERROR_INVALID_GRANT, ERROR_INVALID_CLIENT):
|
|
91
|
+
raise
|
|
92
|
+
creds.clear_tokens()
|
|
93
|
+
if e.error == ERROR_INVALID_CLIENT:
|
|
94
|
+
creds.client_id = ""
|
|
95
|
+
self.store.put(base, creds)
|
|
96
|
+
raise self._login_required("The login has expired or was revoked") from e
|
|
97
|
+
self.refresh_count += 1
|
|
98
|
+
creds.access_token = tokens.access_token
|
|
99
|
+
creds.refresh_token = tokens.refresh_token or creds.refresh_token
|
|
100
|
+
creds.expires_at = tokens.expires_at
|
|
101
|
+
creds.token_endpoint = endpoint
|
|
102
|
+
self.store.put(base, creds)
|
|
103
|
+
return creds.access_token
|
|
104
|
+
|
|
105
|
+
def _login_required(self, what: str) -> LoginRequired:
|
|
106
|
+
return LoginRequired(f"{what} for {self.settings.base_url}. Run `{login_hint(self.settings)}`.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_token_source(
|
|
110
|
+
settings: Settings,
|
|
111
|
+
store: CredentialStore,
|
|
112
|
+
*,
|
|
113
|
+
token_flag: str | None,
|
|
114
|
+
env: Mapping[str, str] | None = None,
|
|
115
|
+
transport: httpx.BaseTransport | None = None,
|
|
116
|
+
now: Callable[[], float] = time.time,
|
|
117
|
+
) -> tuple[TokenSource, str]:
|
|
118
|
+
"""返回令牌来源及其名称(``--token``、``CHESO_API_TOKEN`` 或 ``login``)。"""
|
|
119
|
+
env = os.environ if env is None else env
|
|
120
|
+
if token_flag:
|
|
121
|
+
return StaticToken(token_flag), TOKEN_SOURCE_FLAG
|
|
122
|
+
env_token = env.get(ENV_TOKEN, "").strip()
|
|
123
|
+
if env_token:
|
|
124
|
+
return StaticToken(env_token), TOKEN_SOURCE_ENV
|
|
125
|
+
creds = store.get(settings.base_url)
|
|
126
|
+
if not creds.logged_in:
|
|
127
|
+
raise LoginRequired(f"Not logged in to {settings.base_url}. Run `{login_hint(settings)}` (or set {ENV_TOKEN}).")
|
|
128
|
+
return OAuthSession(settings, store, transport=transport, now=now), TOKEN_SOURCE_LOGIN
|