autotd-buaa 0.1.1__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.
@@ -0,0 +1,8 @@
1
+ include README.md
2
+ include pyproject.toml
3
+ recursive-include src/auto_td/templates *.json
4
+ recursive-include tests *.py
5
+ exclude config.json
6
+ exclude user_config.jsonl
7
+ exclude td.py
8
+ prune images
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: autotd-buaa
3
+ Version: 0.1.1
4
+ Summary: BUAA TD automation command line utility
5
+ Author: autoTD maintainers
6
+ Keywords: buaa,td,cli
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Environment :: Console
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+
17
+ # AutoTD
18
+
19
+ `autoTD` 是一个可安装的命令行工具包。安装后提供 `autotd` 命令,用于初始化本地配置目录、管理用户和图片、运行 TD 打卡流程、查询服务器返回的本学期锻炼次数,以及以前台进程方式进行定时轮询。
20
+
21
+ > `autotd user count` 会发送真实 `checkdata` 请求来解析服务器消息中的次数,可能产生服务器侧刷卡/打卡记录。
22
+
23
+ ## 安装与打包
24
+
25
+ 本项目采用标准 `pyproject.toml` 包结构:
26
+
27
+ ```bash
28
+ python3 -m pip install -e .
29
+ autotd --help
30
+ ```
31
+
32
+ 构建本地 wheel/sdist:
33
+
34
+ ```bash
35
+ python3 -m build
36
+ ```
37
+
38
+ 内部测试发布可使用 TestPyPI:
39
+
40
+ ```bash
41
+ python3 -m twine upload --repository testpypi dist/*
42
+ ```
43
+
44
+ ## 初始化
45
+
46
+ 所有运行数据存储在 `~/.autoTD` 下。测试或临时运行时可用 `AUTOTD_HOME` 覆盖:
47
+
48
+ ```bash
49
+ autotd init
50
+ autotd init --from ./autoTD
51
+ ```
52
+
53
+ 目录结构:
54
+
55
+ ```text
56
+ ~/.autoTD/
57
+ ├── config.json
58
+ ├── users.json
59
+ ├── settings.json
60
+ ├── state.json
61
+ ├── images/
62
+ └── logs/
63
+ └── YYYY-MM-DD-daytime-log.txt
64
+ ```
65
+
66
+ ## 用户管理
67
+
68
+ ```bash
69
+ autotd user add 22375080 --entrance 2 --exit 6 --entrance-image image3.jpg --exit-image image2.jpg
70
+ autotd user add 22375080 --quick 沙河
71
+ autotd user add 22375080 --quick 学院路
72
+ autotd user list
73
+ autotd user show 22375080
74
+ autotd user update 22375080 --card-id CARDID --entrance 3 --exit 7 --entrance-image image1.jpg --exit-image image2.jpg
75
+ autotd user delete 22375080
76
+ autotd user count 22375080
77
+ ```
78
+
79
+ `card_id` 为空时会自动使用学号的 16 进制大写形式。
80
+
81
+ 使用 `--quick` 时,只需要指定校区:
82
+
83
+ - `--quick 沙河`: 随机选择沙河入口机 `8/9/10`、沙河出口机 `11/12/13`,并从 `~/.autoTD/images/` 随机选择入口/出口图片。
84
+ - `--quick 学院路`: 随机选择学院路/本部入口机 `2/3/4`、学院路/本部出口机 `5/6/7`,并从 `~/.autoTD/images/` 随机选择入口/出口图片。
85
+ - 其他字段使用默认值:`rounds=3`、`wait_time_min=180`、`wait_time_max=240`、`card_id` 自动生成。
86
+
87
+ ## 图片管理
88
+
89
+ ```bash
90
+ autotd image add ./image3.jpg
91
+ autotd image add ./photo.jpg --name entrance.jpg
92
+ autotd image add ./photo.jpg --name entrance.jpg --overwrite
93
+ autotd image list
94
+ ```
95
+
96
+ 图片会复制到 `~/.autoTD/images/`。用户配置中引用的是图片文件名。
97
+
98
+ ## 运行
99
+
100
+ 立即执行一次所有用户:
101
+
102
+ ```bash
103
+ autotd run --once
104
+ ```
105
+
106
+ 如果定时开关关闭,`autotd run` 也会立即执行一次。执行时某个用户失败会记录日志并继续后续用户。
107
+
108
+ ## 定时运行
109
+
110
+ 定时运行是前台常驻进程,不创建系统服务。
111
+
112
+ ```bash
113
+ autotd schedule show
114
+ autotd schedule enable
115
+ autotd schedule disable
116
+ autotd schedule set --poll-seconds 60 --windows "07:30-10:00,11:30-14:00,15:30-20:00"
117
+ autotd run
118
+ ```
119
+
120
+ 默认时间段为:
121
+
122
+ - 07:30 - 10:00
123
+ - 11:30 - 14:00
124
+ - 15:30 - 20:00
125
+
126
+ 定时模式按北京时间判断日期。一天只会做一次定时批量尝试;无论成功或失败,当天都不会自动重试。
127
+
128
+ ## 测试
129
+
130
+ ```bash
131
+ python3 -m unittest discover -s tests -v
132
+ ```
@@ -0,0 +1,116 @@
1
+ # AutoTD
2
+
3
+ `autoTD` 是一个可安装的命令行工具包。安装后提供 `autotd` 命令,用于初始化本地配置目录、管理用户和图片、运行 TD 打卡流程、查询服务器返回的本学期锻炼次数,以及以前台进程方式进行定时轮询。
4
+
5
+ > `autotd user count` 会发送真实 `checkdata` 请求来解析服务器消息中的次数,可能产生服务器侧刷卡/打卡记录。
6
+
7
+ ## 安装与打包
8
+
9
+ 本项目采用标准 `pyproject.toml` 包结构:
10
+
11
+ ```bash
12
+ python3 -m pip install -e .
13
+ autotd --help
14
+ ```
15
+
16
+ 构建本地 wheel/sdist:
17
+
18
+ ```bash
19
+ python3 -m build
20
+ ```
21
+
22
+ 内部测试发布可使用 TestPyPI:
23
+
24
+ ```bash
25
+ python3 -m twine upload --repository testpypi dist/*
26
+ ```
27
+
28
+ ## 初始化
29
+
30
+ 所有运行数据存储在 `~/.autoTD` 下。测试或临时运行时可用 `AUTOTD_HOME` 覆盖:
31
+
32
+ ```bash
33
+ autotd init
34
+ autotd init --from ./autoTD
35
+ ```
36
+
37
+ 目录结构:
38
+
39
+ ```text
40
+ ~/.autoTD/
41
+ ├── config.json
42
+ ├── users.json
43
+ ├── settings.json
44
+ ├── state.json
45
+ ├── images/
46
+ └── logs/
47
+ └── YYYY-MM-DD-daytime-log.txt
48
+ ```
49
+
50
+ ## 用户管理
51
+
52
+ ```bash
53
+ autotd user add 22375080 --entrance 2 --exit 6 --entrance-image image3.jpg --exit-image image2.jpg
54
+ autotd user add 22375080 --quick 沙河
55
+ autotd user add 22375080 --quick 学院路
56
+ autotd user list
57
+ autotd user show 22375080
58
+ autotd user update 22375080 --card-id CARDID --entrance 3 --exit 7 --entrance-image image1.jpg --exit-image image2.jpg
59
+ autotd user delete 22375080
60
+ autotd user count 22375080
61
+ ```
62
+
63
+ `card_id` 为空时会自动使用学号的 16 进制大写形式。
64
+
65
+ 使用 `--quick` 时,只需要指定校区:
66
+
67
+ - `--quick 沙河`: 随机选择沙河入口机 `8/9/10`、沙河出口机 `11/12/13`,并从 `~/.autoTD/images/` 随机选择入口/出口图片。
68
+ - `--quick 学院路`: 随机选择学院路/本部入口机 `2/3/4`、学院路/本部出口机 `5/6/7`,并从 `~/.autoTD/images/` 随机选择入口/出口图片。
69
+ - 其他字段使用默认值:`rounds=3`、`wait_time_min=180`、`wait_time_max=240`、`card_id` 自动生成。
70
+
71
+ ## 图片管理
72
+
73
+ ```bash
74
+ autotd image add ./image3.jpg
75
+ autotd image add ./photo.jpg --name entrance.jpg
76
+ autotd image add ./photo.jpg --name entrance.jpg --overwrite
77
+ autotd image list
78
+ ```
79
+
80
+ 图片会复制到 `~/.autoTD/images/`。用户配置中引用的是图片文件名。
81
+
82
+ ## 运行
83
+
84
+ 立即执行一次所有用户:
85
+
86
+ ```bash
87
+ autotd run --once
88
+ ```
89
+
90
+ 如果定时开关关闭,`autotd run` 也会立即执行一次。执行时某个用户失败会记录日志并继续后续用户。
91
+
92
+ ## 定时运行
93
+
94
+ 定时运行是前台常驻进程,不创建系统服务。
95
+
96
+ ```bash
97
+ autotd schedule show
98
+ autotd schedule enable
99
+ autotd schedule disable
100
+ autotd schedule set --poll-seconds 60 --windows "07:30-10:00,11:30-14:00,15:30-20:00"
101
+ autotd run
102
+ ```
103
+
104
+ 默认时间段为:
105
+
106
+ - 07:30 - 10:00
107
+ - 11:30 - 14:00
108
+ - 15:30 - 20:00
109
+
110
+ 定时模式按北京时间判断日期。一天只会做一次定时批量尝试;无论成功或失败,当天都不会自动重试。
111
+
112
+ ## 测试
113
+
114
+ ```bash
115
+ python3 -m unittest discover -s tests -v
116
+ ```
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "autotd-buaa"
7
+ version = "0.1.1"
8
+ description = "BUAA TD automation command line utility"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ { name = "autoTD maintainers" }
13
+ ]
14
+ keywords = ["buaa", "td", "cli"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12"
23
+ ]
24
+
25
+ [project.scripts]
26
+ autotd = "auto_td.cli:main"
27
+
28
+ [tool.setuptools]
29
+ package-dir = {"" = "src"}
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.setuptools.package-data]
35
+ auto_td = ["templates/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """autoTD command line package."""
2
+
3
+ __version__ = "0.1.1"
4
+
5
+ # Expose submodule for test-time patching and simple interactive imports.
6
+ from . import quick as quick # noqa: E402,F401
@@ -0,0 +1,293 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+
6
+ from .client import TDClient
7
+ from .logging_utils import setup_logging
8
+ from .models import User
9
+ from .quick import build_quick_user
10
+ from .runner import run_all_users
11
+ from .scheduler import run_forever
12
+ from .storage import AppStorage
13
+
14
+
15
+ def main(argv: Optional[List[str]] = None) -> int:
16
+ parser = build_parser()
17
+ args = parser.parse_args(argv)
18
+ try:
19
+ return int(args.func(args) or 0)
20
+ except Exception as exc:
21
+ print(f"error: {exc}")
22
+ return 1
23
+
24
+
25
+ def build_parser() -> argparse.ArgumentParser:
26
+ parser = argparse.ArgumentParser(prog="autotd")
27
+ subparsers = parser.add_subparsers(dest="command", required=True)
28
+
29
+ init_parser = subparsers.add_parser("init", help="初始化 ~/.autoTD")
30
+ init_parser.add_argument("--from", dest="source", help="从旧 autoTD 项目导入配置")
31
+ init_parser.set_defaults(func=cmd_init)
32
+
33
+ user_parser = subparsers.add_parser("user", help="用户管理")
34
+ user_sub = user_parser.add_subparsers(dest="user_command", required=True)
35
+ user_add = user_sub.add_parser("add", help="新增用户")
36
+ _add_user_identity_args(user_add)
37
+ user_add.set_defaults(func=cmd_user_add)
38
+
39
+ user_sub.add_parser("list", help="列出用户").set_defaults(func=cmd_user_list)
40
+ user_show = user_sub.add_parser("show", help="查看用户")
41
+ user_show.add_argument("student_id")
42
+ user_show.set_defaults(func=cmd_user_show)
43
+
44
+ user_update = user_sub.add_parser("update", help="修改用户")
45
+ user_update.add_argument("student_id")
46
+ user_update.add_argument("--card-id")
47
+ user_update.add_argument("--entrance", type=int)
48
+ user_update.add_argument("--exit", type=int)
49
+ user_update.add_argument("--entrance-image")
50
+ user_update.add_argument("--exit-image")
51
+ user_update.add_argument("--rounds", type=int)
52
+ user_update.add_argument("--wait-time-min", type=int)
53
+ user_update.add_argument("--wait-time-max", type=int)
54
+ user_update.set_defaults(func=cmd_user_update)
55
+
56
+ user_delete = user_sub.add_parser("delete", help="删除用户")
57
+ user_delete.add_argument("student_id")
58
+ user_delete.set_defaults(func=cmd_user_delete)
59
+
60
+ user_count = user_sub.add_parser(
61
+ "count",
62
+ help="发送真实 checkdata 请求并解析服务器返回的本学期锻炼次数;可能产生服务器侧刷卡记录",
63
+ )
64
+ user_count.add_argument("student_id")
65
+ user_count.set_defaults(func=cmd_user_count)
66
+
67
+ image_parser = subparsers.add_parser("image", help="图片管理")
68
+ image_sub = image_parser.add_subparsers(dest="image_command", required=True)
69
+ image_add = image_sub.add_parser("add", help="新增图片")
70
+ image_add.add_argument("path")
71
+ image_add.add_argument("--name")
72
+ image_add.add_argument("--overwrite", action="store_true")
73
+ image_add.set_defaults(func=cmd_image_add)
74
+ image_sub.add_parser("list", help="列出图片").set_defaults(func=cmd_image_list)
75
+
76
+ run_parser = subparsers.add_parser("run", help="运行打卡")
77
+ run_parser.add_argument("--once", action="store_true", help="强制立即运行一次")
78
+ run_parser.set_defaults(func=cmd_run)
79
+
80
+ schedule_parser = subparsers.add_parser("schedule", help="定时配置")
81
+ schedule_sub = schedule_parser.add_subparsers(dest="schedule_command", required=True)
82
+ schedule_sub.add_parser("show", help="查看定时配置").set_defaults(func=cmd_schedule_show)
83
+ schedule_sub.add_parser("enable", help="启用定时").set_defaults(func=cmd_schedule_enable)
84
+ schedule_sub.add_parser("disable", help="关闭定时").set_defaults(func=cmd_schedule_disable)
85
+ schedule_set = schedule_sub.add_parser("set", help="设置定时参数")
86
+ schedule_set.add_argument("--poll-seconds", type=int)
87
+ schedule_set.add_argument("--windows")
88
+ schedule_set.set_defaults(func=cmd_schedule_set)
89
+ return parser
90
+
91
+
92
+ def cmd_init(args) -> int:
93
+ storage = AppStorage()
94
+ storage.initialize(Path(args.source) if args.source else None)
95
+ print(f"initialized {storage.home}")
96
+ return 0
97
+
98
+
99
+ def cmd_user_add(args) -> int:
100
+ storage = _storage()
101
+ if args.quick:
102
+ user = build_quick_user(storage, args.student_id, args.quick, card_id=args.card_id or "")
103
+ else:
104
+ _require_manual_user_args(args)
105
+ user = User.from_dict(_user_args_to_dict(args))
106
+ storage.save_user(user)
107
+ print(f"added {user.student_id}")
108
+ return 0
109
+
110
+
111
+ def cmd_user_list(_args) -> int:
112
+ storage = _storage()
113
+ users = storage.list_users()
114
+ if not users:
115
+ print("no users")
116
+ return 0
117
+ for user in users:
118
+ print(f"{user.student_id}\tcard={user.card_id}\tentrance={user.entrance_machine_id}\texit={user.exit_machine_id}")
119
+ return 0
120
+
121
+
122
+ def cmd_user_show(args) -> int:
123
+ storage = _storage()
124
+ user = storage.get_user(args.student_id)
125
+ if user is None:
126
+ raise KeyError(f"用户不存在: {args.student_id}")
127
+ print(json.dumps(user, ensure_ascii=False, indent=2))
128
+ return 0
129
+
130
+
131
+ def cmd_user_update(args) -> int:
132
+ storage = _storage()
133
+ current = storage.get_user(args.student_id)
134
+ if current is None:
135
+ raise KeyError(f"用户不存在: {args.student_id}")
136
+ updates = {
137
+ "card_id": args.card_id,
138
+ "entrance_machine_id": args.entrance,
139
+ "exit_machine_id": args.exit,
140
+ "entrance_image": args.entrance_image,
141
+ "exit_image": args.exit_image,
142
+ "rounds": args.rounds,
143
+ "wait_time_min": args.wait_time_min,
144
+ "wait_time_max": args.wait_time_max,
145
+ }
146
+ for key, value in updates.items():
147
+ if value is not None:
148
+ current[key] = value
149
+ user = User.from_dict(current)
150
+ storage.save_user(user)
151
+ print(f"updated {user.student_id}")
152
+ return 0
153
+
154
+
155
+ def cmd_user_delete(args) -> int:
156
+ storage = _storage()
157
+ if not storage.delete_user(args.student_id):
158
+ raise KeyError(f"用户不存在: {args.student_id}")
159
+ print(f"deleted {args.student_id}")
160
+ return 0
161
+
162
+
163
+ def cmd_user_count(args) -> int:
164
+ storage = _storage()
165
+ user = storage.get_user_model(args.student_id)
166
+ client = TDClient(storage.load_config())
167
+ count = client.query_count(user, machine_id=user.entrance_machine_id)
168
+ storage.set_last_count(user.student_id, count)
169
+ print(f"{user.student_id}: 本学期锻炼次数 {count}")
170
+ return 0
171
+
172
+
173
+ def cmd_image_add(args) -> int:
174
+ storage = _storage()
175
+ name = storage.add_image(Path(args.path), name=args.name, overwrite=args.overwrite)
176
+ print(f"added image {name}")
177
+ return 0
178
+
179
+
180
+ def cmd_image_list(_args) -> int:
181
+ storage = _storage()
182
+ for name in storage.list_images():
183
+ print(name)
184
+ return 0
185
+
186
+
187
+ def cmd_run(args) -> int:
188
+ storage = _storage()
189
+ logger = setup_logging(storage)
190
+
191
+ def run_once():
192
+ result = run_all_users(storage, logger=logger)
193
+ print_summary(result)
194
+ return result
195
+
196
+ if args.once or not storage.load_settings().get("schedule", {}).get("enabled", False):
197
+ run_once()
198
+ else:
199
+ run_forever(storage, run_once)
200
+ return 0
201
+
202
+
203
+ def cmd_schedule_show(_args) -> int:
204
+ storage = _storage()
205
+ print(json.dumps(storage.load_settings().get("schedule", {}), ensure_ascii=False, indent=2))
206
+ return 0
207
+
208
+
209
+ def cmd_schedule_enable(_args) -> int:
210
+ storage = _storage()
211
+ storage.update_settings({"schedule": {"enabled": True}})
212
+ print("schedule enabled")
213
+ return 0
214
+
215
+
216
+ def cmd_schedule_disable(_args) -> int:
217
+ storage = _storage()
218
+ storage.update_settings({"schedule": {"enabled": False}})
219
+ print("schedule disabled")
220
+ return 0
221
+
222
+
223
+ def cmd_schedule_set(args) -> int:
224
+ storage = _storage()
225
+ patch = {"schedule": {}}
226
+ if args.poll_seconds is not None:
227
+ if args.poll_seconds <= 0:
228
+ raise ValueError("poll-seconds 必须大于 0")
229
+ patch["schedule"]["poll_seconds"] = args.poll_seconds
230
+ if args.windows:
231
+ patch["schedule"]["windows"] = [item.strip() for item in args.windows.split(",") if item.strip()]
232
+ storage.update_settings(patch)
233
+ print("schedule updated")
234
+ return 0
235
+
236
+
237
+ def print_summary(result) -> None:
238
+ print(f"total={result.total} success={result.success_count} failure={result.failure_count}")
239
+ for item in result.results:
240
+ status = "success" if item.success else "failure"
241
+ print(f"{item.index}. {item.student_id}: {status} {item.message}")
242
+
243
+
244
+ def _storage() -> AppStorage:
245
+ storage = AppStorage()
246
+ if not storage.home.exists():
247
+ raise FileNotFoundError(f"配置目录不存在,请先运行 autotd init: {storage.home}")
248
+ return storage
249
+
250
+
251
+ def _add_user_identity_args(parser: argparse.ArgumentParser) -> None:
252
+ parser.add_argument("student_id")
253
+ parser.add_argument("--card-id")
254
+ parser.add_argument("--quick", help="快速配置校区:沙河 或 学院路")
255
+ parser.add_argument("--entrance", type=int)
256
+ parser.add_argument("--exit", type=int)
257
+ parser.add_argument("--entrance-image")
258
+ parser.add_argument("--exit-image")
259
+ parser.add_argument("--rounds", type=int, default=3)
260
+ parser.add_argument("--wait-time-min", type=int, default=180)
261
+ parser.add_argument("--wait-time-max", type=int, default=240)
262
+
263
+
264
+ def _user_args_to_dict(args) -> dict:
265
+ return {
266
+ "student_id": args.student_id,
267
+ "card_id": args.card_id or "",
268
+ "entrance_machine_id": args.entrance,
269
+ "exit_machine_id": args.exit,
270
+ "entrance_image": args.entrance_image,
271
+ "exit_image": args.exit_image,
272
+ "rounds": args.rounds,
273
+ "wait_time_min": args.wait_time_min,
274
+ "wait_time_max": args.wait_time_max,
275
+ }
276
+
277
+
278
+ def _require_manual_user_args(args) -> None:
279
+ missing = []
280
+ for attr, flag in (
281
+ ("entrance", "--entrance"),
282
+ ("exit", "--exit"),
283
+ ("entrance_image", "--entrance-image"),
284
+ ("exit_image", "--exit-image"),
285
+ ):
286
+ if getattr(args, attr) is None:
287
+ missing.append(flag)
288
+ if missing:
289
+ raise ValueError("非 quick 添加用户时必须提供: " + ", ".join(missing))
290
+
291
+
292
+ if __name__ == "__main__":
293
+ raise SystemExit(main())