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.
Files changed (42) hide show
  1. bocai_cli/__init__.py +3 -0
  2. bocai_cli/api/__init__.py +29 -0
  3. bocai_cli/api/auth_client.py +57 -0
  4. bocai_cli/api/card_client.py +222 -0
  5. bocai_cli/api/center_client.py +55 -0
  6. bocai_cli/api/checkin_client.py +29 -0
  7. bocai_cli/api/course_client.py +85 -0
  8. bocai_cli/api/facade.py +40 -0
  9. bocai_cli/api/guanyuan_client.py +50 -0
  10. bocai_cli/api/member_client.py +97 -0
  11. bocai_cli/api/month_dashboard_client.py +130 -0
  12. bocai_cli/api/qywx_client.py +28 -0
  13. bocai_cli/api/report_client.py +41 -0
  14. bocai_cli/api/reservation_client.py +29 -0
  15. bocai_cli/api/transport.py +166 -0
  16. bocai_cli/app.py +12 -0
  17. bocai_cli/client.py +69 -0
  18. bocai_cli/command_utils.py +28 -0
  19. bocai_cli/commands/__init__.py +8 -0
  20. bocai_cli/commands/auth.py +211 -0
  21. bocai_cli/commands/card.py +653 -0
  22. bocai_cli/commands/center.py +138 -0
  23. bocai_cli/commands/checkin.py +71 -0
  24. bocai_cli/commands/course.py +260 -0
  25. bocai_cli/commands/customreport.py +77 -0
  26. bocai_cli/commands/guanyuan.py +94 -0
  27. bocai_cli/commands/member.py +288 -0
  28. bocai_cli/commands/month_dashboard.py +235 -0
  29. bocai_cli/commands/qywx.py +112 -0
  30. bocai_cli/commands/registry.py +70 -0
  31. bocai_cli/commands/report.py +93 -0
  32. bocai_cli/commands/reservation.py +71 -0
  33. bocai_cli/config.py +121 -0
  34. bocai_cli/date_range_validation.py +65 -0
  35. bocai_cli/errors.py +22 -0
  36. bocai_cli/main.py +8 -0
  37. bocai_cli/output.py +28 -0
  38. bocai_cli/product.py +10 -0
  39. bocai_cli_core-0.2.0.dist-info/METADATA +138 -0
  40. bocai_cli_core-0.2.0.dist-info/RECORD +42 -0
  41. bocai_cli_core-0.2.0.dist-info/WHEEL +4 -0
  42. 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
+ )