cetnext 1.1.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.
@@ -0,0 +1,272 @@
1
+ """
2
+ CETNext
3
+ ==================================
4
+ 作者: Argon
5
+ GitHub: https://github.com/mifongjvav/CETNext
6
+
7
+ 欢迎使用 CETNext!
8
+ 这是一个Python包,你可以使用 import CETNext 来导入它
9
+
10
+ 开发者不对您使用本项目造成的风险负责,请自行考虑是否使用,谢谢!
11
+ """
12
+ # 版权所有 (C) 2026 Argon
13
+ # 根据 Apache 2.0 许可证发布
14
+ #
15
+ # 修改声明:
16
+ # 本文件基于原作者 WangZixu(2025)的作品修改而来。
17
+ # 主要修改内容:
18
+ # 将原有的 argparse CLI 完全重写为 click 库实现
19
+ # 添加 @click.group() 主命令组及所有子命令装饰器
20
+ # 使用 click.option 和 click.pass_context 替代 argparse.ArgumentParser
21
+ # 将全局参数 --token-file 通过上下文对象传递给子命令
22
+ # 修改 __main__.py 入口调用方式
23
+ # 加入新功能
24
+ # 修改日期:2026-06-06
25
+
26
+ from importlib.metadata import version, PackageNotFoundError
27
+
28
+ try:
29
+ __version__ = version("CETNext")
30
+ except PackageNotFoundError:
31
+ __version__ = "1.1.0"
32
+ __author__ = "Argon"
33
+ __description__ = "为编程猫社区的“老师”们提供更便捷的API调用方案,且用且珍惜"
34
+
35
+ # 线程配置
36
+ max_workers = 8
37
+
38
+ # 举报配置
39
+ report_readtoken_line = 20
40
+
41
+ # 学生名字列表
42
+ student_names = [
43
+ "xvbnmklq",
44
+ "asdfghjk",
45
+ "qwertyui",
46
+ "zxcvbnml",
47
+ "poiuytre",
48
+ "lkjhgfds",
49
+ "mnbvcxza",
50
+ "plokmijn",
51
+ "uhbygvtd",
52
+ "crfvtgby",
53
+ "edcrfvtg",
54
+ "qazwsxed",
55
+ "rfvtgbyh",
56
+ "nujmikol",
57
+ "zxasqwde",
58
+ "plmnkoij",
59
+ "bvcdxsza",
60
+ "qwermnbp",
61
+ "asxcvgfr",
62
+ "lpoikmju",
63
+ "yhnujmik",
64
+ "tgbzdxew",
65
+ "rfvgyhuj",
66
+ "edcwsxqa",
67
+ "zaqxswcd",
68
+ "vfrcdews",
69
+ "bgtnhyuj",
70
+ "mkiopluj",
71
+ "nhybtgvr",
72
+ "cdexswza",
73
+ "qwerfdsa",
74
+ "zxcvfdsa",
75
+ "poiuytrw",
76
+ "lkjhgfda",
77
+ "mnbvcxzs",
78
+ "asdfqwer",
79
+ "zxcvqwer",
80
+ "poiulkjh",
81
+ "mnbvcxas",
82
+ "qwertzui",
83
+ "yxcvbnmq",
84
+ "plokmnji",
85
+ "uhbgyvft",
86
+ "crfvtgyn",
87
+ "edcrfvbg",
88
+ "qazwsxrf",
89
+ "rfvtgbyu",
90
+ "nujmiklp",
91
+ "zxasqwed",
92
+ "plmnkoji",
93
+ "bvcdxsaz",
94
+ "qwermnbo",
95
+ "asxcvfgd",
96
+ "lpoikmjn",
97
+ "yhnujmki",
98
+ "tgbzdxec",
99
+ "rfvgyhuk",
100
+ "edcwsxqz",
101
+ "zaqxswce",
102
+ "vfrcdewa",
103
+ "bgtnhyum",
104
+ "mkioplun",
105
+ "nhybtgvf",
106
+ "cdexswzb",
107
+ "qwerfdsz",
108
+ "zxcvfdsz",
109
+ "poiuytrq",
110
+ "lkjhgfdz",
111
+ "mnbvcxzc",
112
+ "asdfqwez",
113
+ "zxcvqwez",
114
+ "poiulkjm",
115
+ "mnbvcxaq",
116
+ "qwertzuy",
117
+ "yxcvbnmr",
118
+ "plokmnjh",
119
+ "uhbgyvfr",
120
+ "crfvtgyb",
121
+ "edcrfvbn",
122
+ "qazwsxre",
123
+ "rfvtgbyi",
124
+ "nujmiklj",
125
+ "zxasqweg",
126
+ "plmnkojh",
127
+ "bvcdxsay",
128
+ "qwermnbu",
129
+ "asxcvfgh",
130
+ "lpoikmjh",
131
+ "yhnujmko",
132
+ "tgbzdxer",
133
+ "rfvgyhun",
134
+ "edcwsxqv",
135
+ "zaqxswec",
136
+ "vfrcdewq",
137
+ "bgtnhyup",
138
+ "mkiopluh",
139
+ "nhybtgvc",
140
+ "cdexswzg",
141
+ "qwerfdsx",
142
+ "zxcvfdsx",
143
+ ]
144
+
145
+ # 请在创建新功能后更新此处!
146
+
147
+ _LAZY_IMPORTS = {
148
+ # API
149
+ "PostAPI": (".api", "PostAPI"),
150
+ "PostWithoutTokenAPI": (".api", "PostWithoutTokenAPI"),
151
+ "PostEduAPI": (".api", "PostEduAPI"),
152
+ "GetAPI": (".api", "GetAPI"),
153
+ "GetWithoutTokenAPI": (".api", "GetWithoutTokenAPI"),
154
+ "PutAPI": (".api", "PutAPI"),
155
+ "DeleteAPI": (".api", "DeleteAPI"),
156
+ # 用户
157
+ "GetUserToken": (".user", "GetUserToken"),
158
+ "CheckToken": (".user", "CheckToken"),
159
+ "SignatureUser": (".user", "SignatureUser"),
160
+ "FollowUser": (".user", "FollowUser"),
161
+ # 作品
162
+ "GetUserWork": (".work", "GetUserWork"),
163
+ "GetStudioWork": (
164
+ ".work",
165
+ "GetStudioWork",
166
+ ),
167
+ "LikeWork": (".work", "LikeWork"),
168
+ "CollectionWork": (".work", "CollectionWork"),
169
+ "ReportWork": (".work", "ReportWork"),
170
+ "SendReviewToWork": (".work", "SendReviewToWork"),
171
+ "TopReview": (".work", "TopReview"),
172
+ "UnTopReview": (".work", "UnTopReview"),
173
+ "ViewWork": (".work", "ViewWork"),
174
+ "ForkWork": (".work", "ForkWork"),
175
+ # CodemaoEDU
176
+ "CreateClassOnEdu": (".edu", "CreateClassOnEdu"),
177
+ "CreateStudentOnEdu": (".edu", "CreateStudentOnEdu"),
178
+ "MergeStudentXls": (".edu", "MergeStudentXls"),
179
+ "LoginUseEdu": (".edu", "LoginUseEdu"),
180
+ }
181
+
182
+ # 请在创建新功能后更新此处!导入控制
183
+ __all__ = [
184
+ # 配置
185
+ "__version__",
186
+ "__author__",
187
+ "__description__",
188
+ "max_workers",
189
+ "student_names",
190
+ "report_readtoken_line",
191
+ # API
192
+ "PostAPI",
193
+ "PostWithoutTokenAPI",
194
+ "PostEduAPI",
195
+ "GetAPI",
196
+ "GetWithoutTokenAPI",
197
+ "PutAPI",
198
+ "DeleteAPI",
199
+ # 用户
200
+ "GetUserToken",
201
+ "CheckToken",
202
+ "SignatureUser",
203
+ "FollowUser",
204
+ # 作品
205
+ "GetUserWork",
206
+ "GetStudioWork",
207
+ "LikeWork",
208
+ "LikeReview",
209
+ "CollectionWork",
210
+ "ReportWork",
211
+ "SendReviewToWork",
212
+ "TopReview",
213
+ "UnTopReview",
214
+ "ViewWork",
215
+ "ForkWork",
216
+ # CodemaoEDU
217
+ "CreateClassOnEdu",
218
+ "CreateStudentOnEdu",
219
+ "MergeStudentXls",
220
+ "LoginUseEdu",
221
+ ]
222
+
223
+
224
+ def __getattr__(name: str):
225
+ if name in _LAZY_IMPORTS:
226
+ module_name, attr_name = _LAZY_IMPORTS[name]
227
+ from importlib import import_module
228
+
229
+ module = import_module(module_name, package=__name__)
230
+ value = getattr(module, attr_name)
231
+ globals()[name] = value
232
+ return value
233
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
234
+
235
+
236
+ def __dir__() -> list[str]:
237
+ return sorted(list(globals().keys()) + list(_LAZY_IMPORTS.keys()))
238
+
239
+
240
+ # 包函数
241
+ def get_version() -> str:
242
+ """获取版本号"""
243
+ return __version__
244
+
245
+
246
+ def info() -> str:
247
+ """显示包信息"""
248
+ return f"""
249
+ CETNext v{__version__}
250
+ 作者: {__author__}
251
+ GitHub: https://github.com/mifongjvav/CETNext/
252
+
253
+ 导入方式:
254
+ import CETNext
255
+ from CETNext import * # 不推荐
256
+
257
+ 命令行使用:
258
+ python main.py check-token
259
+
260
+ 注意事项:
261
+ 请合理使用本工具,遵守编程猫平台的使用规则。
262
+ 开发者不对滥用本工具造成的后果负责。
263
+ """
264
+
265
+
266
+ def about() -> str:
267
+ """显示关于信息"""
268
+ return f"""
269
+ CETNext v{__version__}
270
+ 作者: {__author__}
271
+ GitHub: https://github.com/mifongjvav/CETNext/
272
+ """
@@ -0,0 +1,20 @@
1
+ # __main__.py
2
+ import logging
3
+ import coloredlogs
4
+
5
+ from CETNext.cli import cli
6
+
7
+ logging.basicConfig(
8
+ filename='latest.log',
9
+ filemode='w',
10
+ encoding='utf-8',
11
+ level=logging.INFO,
12
+ format="%(asctime)s - %(levelname)s - %(funcName)s: %(message)s"
13
+ )
14
+ coloredlogs.install(level="INFO", fmt="%(asctime)s - %(levelname)s - %(funcName)s: %(message)s")
15
+
16
+ def main():
17
+ cli()
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -0,0 +1,352 @@
1
+ """
2
+ API调用
3
+ """
4
+ # 版权所有 (C) 2026 Argon
5
+ # 根据 Apache 2.0 许可证发布
6
+ #
7
+ # 修改声明:
8
+ # 本文件基于原作者 WangZixu(2025)的作品修改而来。
9
+ # 主要修改内容:
10
+ # 将 requests 改为 playwright
11
+ # 将 UA 改为常量
12
+ # 优化史山
13
+ # 修改日期:2026-05-05
14
+
15
+ import logging
16
+ import atexit
17
+ import time
18
+ import json
19
+ import threading
20
+ import queue
21
+ import uuid
22
+ from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
23
+
24
+ _browser = None
25
+ _context = None
26
+ _playwright = None
27
+
28
+ _request_queue = queue.Queue()
29
+ _response_queues = {}
30
+ _worker_thread = None
31
+ _browser_ready = threading.Event()
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ USER_AGENT = (
37
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
38
+ "(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"
39
+ )
40
+
41
+ _default_headless = True
42
+
43
+
44
+ def set_default_headless(value: bool):
45
+ """设置全局无头浏览器模式"""
46
+ global _default_headless
47
+ _default_headless = value
48
+
49
+
50
+ def _init_browser(headless: bool = None):
51
+ global _playwright, _browser, _context
52
+ if _browser is not None:
53
+ _browser_ready.set()
54
+ return
55
+
56
+ try:
57
+ logger.info("正在启动 Playwright...")
58
+ _playwright = sync_playwright().start()
59
+ logger.info("正在启动 Chromium 浏览器...")
60
+ _browser = _playwright.chromium.launch(
61
+ headless=headless,
62
+ args=[
63
+ "--disable-blink-features=AutomationControlled",
64
+ "--no-sandbox",
65
+ "--disable-gpu",
66
+ ],
67
+ )
68
+ logger.info("正在创建浏览器上下文...")
69
+ _context = _browser.new_context(
70
+ user_agent=USER_AGENT,
71
+ viewport={"width": 1920, "height": 1080},
72
+ locale="zh-CN",
73
+ timezone_id="Asia/Shanghai",
74
+ )
75
+ _context.add_init_script("""
76
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
77
+ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
78
+ Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] });
79
+ window.chrome = { runtime: {}, loadTimes: function(){}, csi: function(){}, app: {} };
80
+ """)
81
+ logger.info("浏览器初始化完成")
82
+ _browser_ready.set()
83
+ except Exception:
84
+ logger.exception("浏览器初始化失败,请 playwright install chromium 安装浏览器")
85
+ _browser_ready.set()
86
+ raise
87
+
88
+
89
+ def _worker(headless: bool = None):
90
+ global _context, _browser, _playwright
91
+ _init_browser(headless)
92
+ try:
93
+ while True:
94
+ logger.debug("Worker 等待任务...")
95
+ task = _request_queue.get()
96
+ if task is None:
97
+ logger.info("Worker 收到退出信号")
98
+ break
99
+
100
+ request_id, method, url, headers, retry, post_data = task
101
+ logger.info(f"处理请求 [{request_id[:8]}] {method} {url}")
102
+ result = None
103
+
104
+ for i in range(retry):
105
+ page = None
106
+ try:
107
+ logger.debug(f"第 {i + 1} 次尝试: 创建新页面")
108
+ page = _context.new_page()
109
+ logger.debug("新页面已创建,正在发起请求")
110
+
111
+ request_options = {
112
+ "method": method,
113
+ "headers": headers or {},
114
+ "timeout": 30000,
115
+ }
116
+ if post_data is not None:
117
+ request_options["data"] = json.dumps(
118
+ post_data, ensure_ascii=False
119
+ )
120
+ request_options["headers"]["Content-Type"] = "application/json"
121
+
122
+ response = page.request.fetch(url, **request_options)
123
+ status_code = response.status
124
+ response_text = response.text()
125
+ logger.debug(f"请求完成,状态码: {status_code}")
126
+
127
+ # 记录非 200 状态码的内容
128
+ if status_code != 200:
129
+ logger.warning(
130
+ f"非 200 状态码: {status_code}, 返回内容预览: {response_text[:500]}"
131
+ )
132
+
133
+ # 检查 WAF 拦截
134
+ if (
135
+ "renderData" not in response_text
136
+ and "aliyun_waf" not in response_text
137
+ ):
138
+ result = PlaywrightResponse(status_code, response_text, url)
139
+ logger.info(f"请求成功: {status_code}")
140
+ break
141
+
142
+ logger.warning(
143
+ f"请求被 WAF 拦截,状态码: {status_code}, 返回内容预览: {response_text[:500]}"
144
+ )
145
+ time.sleep((i + 1) * 2)
146
+
147
+ except PlaywrightTimeoutError:
148
+ logger.warning(f"请求超时,重试 {i + 1}/{retry}")
149
+ time.sleep((i + 1) * 2)
150
+ except Exception as e:
151
+ # 尝试从异常中提取响应内容
152
+ error_msg = str(e)
153
+ response_preview = ""
154
+ if hasattr(e, "response") and e.response:
155
+ try:
156
+ response_preview = e.response.text()[:500]
157
+ except Exception:
158
+ response_preview = "<无法读取响应内容>"
159
+ logger.warning(
160
+ f"请求异常: {error_msg}, 响应预览: {response_preview}, 重试 {i + 1}/{retry}"
161
+ )
162
+ time.sleep((i + 1) * 2)
163
+ finally:
164
+ if page:
165
+ try:
166
+ page.close()
167
+ logger.debug("页面已关闭")
168
+ except Exception:
169
+ pass
170
+
171
+ if request_id in _response_queues:
172
+ _response_queues[request_id].put(result)
173
+ logger.debug(f"结果已返回给请求者 [{request_id[:8]}]")
174
+ finally:
175
+ logger.info("正在关闭浏览器…")
176
+ if _context:
177
+ try:
178
+ _context.close()
179
+ except Exception:
180
+ pass
181
+ if _browser:
182
+ try:
183
+ _browser.close()
184
+ except Exception:
185
+ pass
186
+ if _playwright:
187
+ try:
188
+ _playwright.stop()
189
+ except Exception:
190
+ pass
191
+ _context = _browser = _playwright = None
192
+ _browser_ready.clear()
193
+
194
+
195
+ def _start_worker(headless: bool = None):
196
+ global _worker_thread
197
+ if _worker_thread is None or not _worker_thread.is_alive():
198
+ _worker_thread = threading.Thread(target=_worker, args=(headless,), daemon=True)
199
+ _worker_thread.start()
200
+ if not _browser_ready.wait(timeout=30):
201
+ raise RuntimeError("浏览器启动超时")
202
+ if _browser is None:
203
+ raise RuntimeError("浏览器初始化失败")
204
+
205
+
206
+ def _close_browser():
207
+ global _worker_thread
208
+ if _request_queue:
209
+ _request_queue.put(None)
210
+ if _worker_thread and _worker_thread.is_alive():
211
+ _worker_thread.join(timeout=10)
212
+
213
+
214
+ class PlaywrightResponse:
215
+ def __init__(self, status_code, text, url):
216
+ self._status_code = status_code
217
+ self._text = text
218
+ self.url = url
219
+ self.ok = 200 <= status_code < 300
220
+
221
+ @property
222
+ def status_code(self):
223
+ return self._status_code
224
+
225
+ @property
226
+ def text(self):
227
+ return self._text
228
+
229
+ def json(self):
230
+ return json.loads(self._text)
231
+
232
+ @property
233
+ def code(self):
234
+ return self._status_code
235
+
236
+
237
+ def _request(method, url, headers=None, retry=3, headless: bool = None, **kwargs):
238
+ _start_worker(headless)
239
+ post_data = kwargs.get("json")
240
+ request_id = str(uuid.uuid4())
241
+ result_queue = queue.Queue()
242
+ _response_queues[request_id] = result_queue
243
+ _request_queue.put((request_id, method, url, headers, retry, post_data))
244
+
245
+ try:
246
+ result = result_queue.get(timeout=60)
247
+ except queue.Empty:
248
+ logger.error(f"主线程等待超时: {method} {url}")
249
+ result = None
250
+ finally:
251
+ _response_queues.pop(request_id, None)
252
+ return result
253
+
254
+
255
+ def get_headers(token=None, include_auth=True):
256
+ headers = {
257
+ "Content-Type": "application/json",
258
+ "Accept": "application/json, text/plain, */*",
259
+ "Accept-Encoding": "gzip, deflate, br, zstd",
260
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
261
+ "Origin": "https://shequ.codemao.cn",
262
+ "Referer": "https://shequ.codemao.cn/",
263
+ "User-Agent": USER_AGENT,
264
+ "sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"',
265
+ "sec-ch-ua-mobile": "?0",
266
+ "sec-ch-ua-platform": '"Windows"',
267
+ }
268
+ if include_auth and token:
269
+ headers["authorization"] = token
270
+ return headers
271
+
272
+
273
+ def GetAPI(Path, Token, headless=None):
274
+ if headless is None:
275
+ headless = _default_headless
276
+ return _request(
277
+ "GET", f"https://api.codemao.cn{Path}", get_headers(Token), headless
278
+ )
279
+
280
+
281
+ def GetWithoutTokenAPI(Path, headless=None):
282
+ if headless is None:
283
+ headless = _default_headless
284
+ return _request(
285
+ "GET",
286
+ f"https://api.codemao.cn{Path}",
287
+ get_headers(include_auth=False),
288
+ headless,
289
+ )
290
+
291
+
292
+ def PostAPI(Path, PostData, Token, headless=None):
293
+ if headless is None:
294
+ headless = _default_headless
295
+ return _request(
296
+ "POST",
297
+ f"https://api.codemao.cn{Path}",
298
+ get_headers(Token),
299
+ json=PostData,
300
+ headless=headless,
301
+ )
302
+
303
+
304
+ def PostWithoutTokenAPI(Path, PostData, headless=None):
305
+ if headless is None:
306
+ headless = _default_headless
307
+ return _request(
308
+ "POST",
309
+ f"https://api.codemao.cn{Path}",
310
+ get_headers(include_auth=False),
311
+ json=PostData,
312
+ headless=headless,
313
+ )
314
+
315
+
316
+ def PostEduAPI(Path, PostData, Token, headless=None):
317
+ if headless is None:
318
+ headless = _default_headless
319
+ h = get_headers(Token)
320
+ h.update(
321
+ {
322
+ "Origin": "https://eduzone.codemao.cn",
323
+ "Referer": "https://eduzone.codemao.cn/",
324
+ "authorization": f"Bearer {Token}",
325
+ }
326
+ )
327
+ return _request(
328
+ "POST", f"https://eduzone.codemao.cn{Path}", h, json=PostData, headless=headless
329
+ )
330
+
331
+
332
+ def PutAPI(Path, Token, headless=None):
333
+ if headless is None:
334
+ headless = _default_headless
335
+ return _request(
336
+ "PUT", f"https://api.codemao.cn{Path}", get_headers(Token), headless=headless
337
+ )
338
+
339
+
340
+ def DeleteAPI(Path, Token, headless=None):
341
+ if headless is None:
342
+ headless = _default_headless
343
+ return _request(
344
+ "DELETE", f"https://api.codemao.cn{Path}", get_headers(Token), headless=headless
345
+ )
346
+
347
+
348
+ def close():
349
+ _close_browser()
350
+
351
+
352
+ atexit.register(close)