bocai-cli-core 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.
- bocai_cli/__init__.py +3 -0
- bocai_cli/api/__init__.py +29 -0
- bocai_cli/api/auth_client.py +57 -0
- bocai_cli/api/card_client.py +222 -0
- bocai_cli/api/center_client.py +55 -0
- bocai_cli/api/checkin_client.py +29 -0
- bocai_cli/api/course_client.py +85 -0
- bocai_cli/api/facade.py +40 -0
- bocai_cli/api/guanyuan_client.py +50 -0
- bocai_cli/api/member_client.py +97 -0
- bocai_cli/api/month_dashboard_client.py +130 -0
- bocai_cli/api/qywx_client.py +28 -0
- bocai_cli/api/report_client.py +41 -0
- bocai_cli/api/reservation_client.py +29 -0
- bocai_cli/api/transport.py +166 -0
- bocai_cli/app.py +12 -0
- bocai_cli/client.py +69 -0
- bocai_cli/command_utils.py +28 -0
- bocai_cli/commands/__init__.py +8 -0
- bocai_cli/commands/auth.py +211 -0
- bocai_cli/commands/card.py +653 -0
- bocai_cli/commands/center.py +138 -0
- bocai_cli/commands/checkin.py +71 -0
- bocai_cli/commands/course.py +260 -0
- bocai_cli/commands/customreport.py +77 -0
- bocai_cli/commands/guanyuan.py +94 -0
- bocai_cli/commands/member.py +288 -0
- bocai_cli/commands/month_dashboard.py +235 -0
- bocai_cli/commands/qywx.py +112 -0
- bocai_cli/commands/registry.py +70 -0
- bocai_cli/commands/report.py +93 -0
- bocai_cli/commands/reservation.py +71 -0
- bocai_cli/config.py +121 -0
- bocai_cli/date_range_validation.py +65 -0
- bocai_cli/errors.py +22 -0
- bocai_cli/main.py +8 -0
- bocai_cli/output.py +28 -0
- bocai_cli/product.py +10 -0
- bocai_cli_core-0.2.0.dist-info/METADATA +138 -0
- bocai_cli_core-0.2.0.dist-info/RECORD +42 -0
- bocai_cli_core-0.2.0.dist-info/WHEEL +4 -0
- bocai_cli_core-0.2.0.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from bocai_cli.api import ApiFacade
|
|
8
|
+
from bocai_cli.command_utils import command_handler
|
|
9
|
+
from bocai_cli.config import load_config, save_config
|
|
10
|
+
from bocai_cli.errors import ConfigValidationError
|
|
11
|
+
from bocai_cli.output import print_json
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="鉴权相关命令。", invoke_without_command=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.callback()
|
|
17
|
+
def auth_root(ctx: typer.Context) -> None:
|
|
18
|
+
if ctx.invoked_subcommand is None:
|
|
19
|
+
typer.echo(ctx.get_help())
|
|
20
|
+
raise typer.Exit(code=0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_api() -> ApiFacade:
|
|
24
|
+
config = load_config()
|
|
25
|
+
return ApiFacade(
|
|
26
|
+
base_url=config.base_url,
|
|
27
|
+
token=config.token,
|
|
28
|
+
timeout_seconds=config.timeout_seconds,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _now_utc() -> datetime:
|
|
33
|
+
return datetime.now(timezone.utc)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _save_token(config, new_token: str):
|
|
37
|
+
previous_token = config.token
|
|
38
|
+
if previous_token and previous_token != new_token:
|
|
39
|
+
config.token_backup = previous_token
|
|
40
|
+
|
|
41
|
+
config.token = new_token
|
|
42
|
+
config.token_updated_at = _now_utc().isoformat()
|
|
43
|
+
return save_config(config)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _extract_token_field(payload: object, *fields: str) -> str | None:
|
|
47
|
+
if isinstance(payload, dict):
|
|
48
|
+
for field in fields:
|
|
49
|
+
value = payload.get(field)
|
|
50
|
+
if isinstance(value, str) and value.strip():
|
|
51
|
+
return value.strip()
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extract_access_token(payload: object) -> str | None:
|
|
56
|
+
return _extract_token_field(payload, "accessToken", "access_token")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _save_token_payload(config, payload: object):
|
|
60
|
+
new_token = _extract_access_token(payload)
|
|
61
|
+
if new_token is None:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
refresh_token = _extract_token_field(payload, "refreshToken", "refresh_token")
|
|
65
|
+
expires_at = _extract_token_field(payload, "expiresAt", "expires_at")
|
|
66
|
+
if refresh_token is not None:
|
|
67
|
+
config.refresh_token = refresh_token
|
|
68
|
+
if expires_at is not None:
|
|
69
|
+
config.expires_at = expires_at
|
|
70
|
+
return _save_token(config, new_token)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_expires_at(raw_expires_at: str | None) -> datetime | None:
|
|
74
|
+
if raw_expires_at is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
value = raw_expires_at.strip()
|
|
78
|
+
if not value:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
if value.endswith("Z"):
|
|
82
|
+
value = f"{value[:-1]}+00:00"
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
expires_at = datetime.fromisoformat(value)
|
|
86
|
+
except ValueError:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
if expires_at.tzinfo is None:
|
|
90
|
+
return expires_at.replace(tzinfo=timezone.utc)
|
|
91
|
+
return expires_at.astimezone(timezone.utc)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _should_refresh_token(raw_expires_at: str | None) -> bool:
|
|
95
|
+
expires_at = _parse_expires_at(raw_expires_at)
|
|
96
|
+
if expires_at is None:
|
|
97
|
+
return True
|
|
98
|
+
return expires_at <= _now_utc() + timedelta(days=7)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
#@app.command("login", help="登录并获取accessToken")
|
|
102
|
+
#@command_handler
|
|
103
|
+
def login() -> None:
|
|
104
|
+
config = load_config()
|
|
105
|
+
required_fields = ["base_url", "app_id", "secret"]
|
|
106
|
+
missing = [field for field in required_fields if not getattr(config, field)]
|
|
107
|
+
if missing:
|
|
108
|
+
raise ConfigValidationError(
|
|
109
|
+
"配置缺少获取 token 所需字段。",
|
|
110
|
+
details={"missing": missing},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
api = _build_api()
|
|
114
|
+
new_token = api.auth.fetch_access_token(
|
|
115
|
+
token_url=config.token_url or "/api/auth/token",
|
|
116
|
+
app_id=config.app_id or "",
|
|
117
|
+
secret=config.secret or "",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
config_path = _save_token(config, new_token)
|
|
121
|
+
|
|
122
|
+
print_json(
|
|
123
|
+
{
|
|
124
|
+
"status": "ok",
|
|
125
|
+
"message": "登录成功,token 已更新。",
|
|
126
|
+
"token_updated": True,
|
|
127
|
+
"config_path": str(config_path),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.command("start", help="开始授权绑定流程")
|
|
133
|
+
@command_handler
|
|
134
|
+
def start() -> None:
|
|
135
|
+
api = _build_api()
|
|
136
|
+
data = api.auth.start()
|
|
137
|
+
print_json({"status": "ok", "data": data})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("check-status", help="检查授权绑定状态")
|
|
141
|
+
@command_handler
|
|
142
|
+
def check_status(
|
|
143
|
+
auth_code: str = typer.Option(
|
|
144
|
+
...,
|
|
145
|
+
"--auth-code",
|
|
146
|
+
help="授权码。",
|
|
147
|
+
),
|
|
148
|
+
) -> None:
|
|
149
|
+
api = _build_api()
|
|
150
|
+
data = api.auth.check_status(auth_code=auth_code)
|
|
151
|
+
config = load_config()
|
|
152
|
+
config_path = _save_token_payload(config, data)
|
|
153
|
+
if config_path is None:
|
|
154
|
+
print_json({"status": "ok", "data": data, "token_updated": False})
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
print_json(
|
|
158
|
+
{
|
|
159
|
+
"status": "ok",
|
|
160
|
+
"data": data,
|
|
161
|
+
"token_updated": True,
|
|
162
|
+
"config_path": str(config_path),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("refresh-token", help="刷新授权 token")
|
|
168
|
+
@command_handler
|
|
169
|
+
def refresh_token() -> None:
|
|
170
|
+
config = load_config()
|
|
171
|
+
|
|
172
|
+
if not _should_refresh_token(config.expires_at):
|
|
173
|
+
print_json(
|
|
174
|
+
{
|
|
175
|
+
"status": "ok",
|
|
176
|
+
"token_updated": False,
|
|
177
|
+
"refresh_skipped": True,
|
|
178
|
+
"reason": "token_not_expiring_soon",
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
missing = [
|
|
184
|
+
field
|
|
185
|
+
for field in ("token", "refresh_token")
|
|
186
|
+
if not getattr(config, field)
|
|
187
|
+
]
|
|
188
|
+
if missing:
|
|
189
|
+
raise ConfigValidationError(
|
|
190
|
+
"配置缺少刷新 token 所需字段。",
|
|
191
|
+
details={"missing": missing},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
api = _build_api()
|
|
195
|
+
data = api.auth.refresh_user_token(
|
|
196
|
+
access_token=config.token or "",
|
|
197
|
+
refresh_token=config.refresh_token or "",
|
|
198
|
+
)
|
|
199
|
+
config_path = _save_token_payload(config, data)
|
|
200
|
+
if config_path is None:
|
|
201
|
+
print_json({"status": "ok", "data": data, "token_updated": False})
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
print_json(
|
|
205
|
+
{
|
|
206
|
+
"status": "ok",
|
|
207
|
+
"data": data,
|
|
208
|
+
"token_updated": True,
|
|
209
|
+
"config_path": str(config_path),
|
|
210
|
+
}
|
|
211
|
+
)
|