commit 19ca1638e221984a23a8d7547bb885fd3aee78bf Author: NCJOAQ <2627723488@qq.com> Date: Fri Dec 12 01:25:54 2025 +0800 基本刷课程序 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8ee0f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# 忽略基本 +__pycache__/ +*.py[cod] +*.spec + +# 虚拟环境 +.venv/ +venv/ +env/ + +# 脚本生成的缓存文件 +course_ids.json +course_items.json +progress.json +mock_updstatus.progress + +# 日志 +*.log diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea0ad06 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# 自动刷课脚本 (Python) + +这是一个用于自动模拟观看 `zjbc.cjnep.net` 平台课程视频的 Python 脚本。它通过模拟发送心跳包的方式,自动完成课程视频的学习进度。 + +## 功能特点 + +- **自动识别课程**:自动抓取账户下的课程列表和视频章节。 +- **断点续传**:本地记录播放进度 (`progress.json`),中断后可继续播放。 +- **智能跳过**:自动跳过已完成的视频(基于 XML 状态或本地记录)。 +- **时长检测**:如果 XML 配置中缺少视频时长,会自动调用 `ffprobe` 获取实际时长。 +- **防掉线机制**:心跳请求失败自动重试。 +- **极速模式**:支持自定义心跳间隔和每次增加的进度时间(默认配置较为激进,可按需调整)。 + +## 环境要求 + +1. **Python 3.6+** +2. **FFmpeg 工具集**:脚本依赖 `ffprobe` 来获取视频时长。 + - 请前往 [FFmpeg 官网](https://ffmpeg.org/download.html) 下载。 + - 解压并将 `bin` 目录添加到系统的环境变量 `PATH` 中。 + - 在终端输入 `ffprobe -version` 验证是否安装成功。 +3. **Python 依赖库**: + ```bash + pip install requests + ``` + +## 使用说明 + +### 1. 获取 Cookie + +1. 登录您的课程平台账号。 +2. 进入任意一个视频播放页面。 +3. 按 `F12` 打开开发者工具,切换到 **Network (网络)** 选项卡。 +4. 在过滤器中输入 `upd` 或 `startupxml`。 +5. 找到相关的请求,在 **Request Headers (请求头)** 中找到 `Cookie` 字段。 +6. 复制整个 Cookie 字符串。 + +### 2. 配置脚本 + +打开 `main.py` 文件,找到以下代码行,将 `cookie` 变量的值替换为您刚才复制的内容: + +```python +# [警告] 下方的 cookie 包含敏感登录信息,请勿泄露给他人! +cookie="您的Cookie字符串粘贴在这里" +``` + +您也可以根据需要调整以下配置: + +```python +# [配置] 心跳间隔(秒) +HEARTBEAT_INTERVAL = 1 + +# [配置] 每次心跳增加的进度时间(秒) +ADD_TIME = 120 +``` + +### 3. 运行脚本 + +在终端中运行: + +```bash +python main.py +``` + +- **首次运行**:脚本会自动抓取您的课程列表和视频信息,并保存到 `course_items.json`。 +- **后续运行**:脚本会优先读取本地缓存的课程信息。如果需要重新抓取,请删除 `course_items.json` 文件。 + +## 文件说明 + +- `main.py`: 核心脚本文件。 +- `course_items.json`: 缓存的课程和视频 ID 列表。 +- `progress.json`: 本地存储的视频播放进度,用于断点续传。 +- `course_ids.json`: 临时缓存的课程 ID 列表。 + +## 注意事项 + +- **Cookie 有效期**:Cookie 可能会过期,如果脚本提示认证失败或无法获取数据,请重新获取并更新 Cookie。 +- **风险提示**:默认的心跳间隔较短,建议根据实际情况适当延长 `HEARTBEAT_INTERVAL` 以降低风险。 +- **免责声明**:本脚本仅供学习和研究使用,请勿用于商业用途或违反平台规定的行为。作者不对使用本脚本导致的任何后果负责。 diff --git a/main.py b/main.py new file mode 100644 index 0000000..bf63ab2 --- /dev/null +++ b/main.py @@ -0,0 +1,396 @@ +import requests +import re +import os +import sys +import json +import time +import subprocess +import xml.etree.ElementTree as ET + +# 当前程序只能一键对所有课程进行刷课,如需选择课程请自行修改代码 + +# cookie 请替换为你的登录 Cookie,在视频页从控制台执行Job.updateStatus()请求头中获取,其中的负载参数有很多字段,包括课程id等信息,您可参考字段删除内容以防止已完成课程的刷取,即使代码中包含了完成判断逻辑 + +# 您可根据视频心跳请求中的负载参数修改json缓存从而达到跳过指定课程的目的,十分银杏 + +# [警告] 下方的 cookie 包含敏感登录信息,请勿泄露给他人! +cookie = "" + +url = "https://zjbc.cjnep.net/lms/web/course/index" +cache_file = "course_ids.json" +items_cache_file = "course_items.json" +progress_cache_file = "progress.json" + +# [配置] 心跳间隔(秒) +# 警告:建议设置在 60 秒以上,过快的心跳可能面临风险! +HEARTBEAT_INTERVAL = 1 + +# [配置] 每次心跳增加的进度时间(秒) +ADD_TIME = 120 + +headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Cookie": cookie +} + +# ------------------------------------------------------------------------- +# 辅助函数:进度保存与读取 +# ------------------------------------------------------------------------- + + +def load_progress(): + if os.path.exists(progress_cache_file): + try: + with open(progress_cache_file, "r", encoding="utf-8") as f: + return json.load(f) + except: + return {} + return {} + + +def save_progress(course_id, item_id, current_time, total_time, finished): + data = load_progress() + key = f"{course_id}_{item_id}" + data[key] = { + "current_time": current_time, + "total_time": total_time, + "finished": finished, + "timestamp": time.time() + } + try: + with open(progress_cache_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + except Exception as e: + print(f" [Warning] 保存进度失败: {e}") + +# ------------------------------------------------------------------------- +# 辅助函数:模拟刷课逻辑 +# ------------------------------------------------------------------------- + + +def get_video_duration(video_url): + """ + 使用 ffprobe 获取视频时长 + """ + try: + cmd = [ + "ffprobe", + "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", + video_url + ] + # Run ffprobe + result = subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30) + if result.returncode == 0: + try: + duration = float(result.stdout.strip()) + return duration + except ValueError: + print(f" [Error] ffprobe output invalid: {result.stdout}") + return None + else: + print(f" [Error] ffprobe failed: {result.stderr}") + return None + except Exception as e: + print(f" [Error] get_video_duration exception: {e}") + return None + + +def process_video(course_id, item_id): + """ + 处理单个视频: + 1. 获取视频页,提取 config URL + 2. 请求 config XML,获取 userId, updStatusUrl, totalTime, historyId 等 + 3. 循环发送心跳包,直到视频看完 + """ + print(f"\n>>> 开始处理视频: CourseId={course_id}, ItemId={item_id}") + + # 1. 请求视频播放页 + video_page_url = f"https://zjbc.cjnep.net/lms/web/course/view?id={course_id}&itemid={item_id}" + try: + resp = requests.get(video_page_url, headers=headers) + resp.raise_for_status() + except Exception as e: + print(f" [Error] 无法访问视频页: {e}") + return + + # 2. 提取 config URL + # 示例: config: '/lms/web/course/startupxml?courseid=677&itemid=142573&historyid=5740083&is_subcourse=0' + config_match = re.search(r"config:\s*'([^']+)'", resp.text) + if not config_match: + print(" [Error] 未找到 config URL,跳过此视频。") + return + + config_path = config_match.group(1) + config_url = "https://zjbc.cjnep.net" + config_path + print(f" Config URL: {config_url}") + + # 3. 请求 XML 配置 + try: + xml_resp = requests.get(config_url, headers=headers) + xml_resp.raise_for_status() + except Exception as e: + print(f" [Error] 无法获取 XML 配置: {e}") + return + + # 4. 解析 XML + try: + root = ET.fromstring(xml_resp.text) + + # 提取关键参数 + user_id = root.findtext("userId") + upd_status_url = root.findtext("updStatusUrl") + total_time_str = root.findtext("totalTime") + history_id = root.findtext("historyId") + finish_status = root.findtext("finish") + + # 尝试获取视频 URL + video_url = None + file_url_node = root.find("fileUrl") + if file_url_node is not None: + video_url = file_url_node.findtext("normal") + + # 如果 XML 里是相对路径,需要补全 + if upd_status_url and not upd_status_url.startswith("http"): + upd_status_url = "https://zjbc.cjnep.net" + upd_status_url + + # 检查必要字段 (totalTime 除外) + if not (user_id and upd_status_url and history_id): + print(" [Error] XML 解析缺少关键字段 (userId/updStatusUrl/historyId)") + return + + # 获取时长 + total_time = 0.0 + if total_time_str: + total_time = float(total_time_str) + elif video_url: + print(" [Info] XML 中未找到 totalTime,尝试通过 ffprobe 获取视频时长...") + duration = get_video_duration(video_url) + if duration: + total_time = duration + print(f" [Info] 获取到视频时长: {total_time}s") + else: + print(" [Error] 无法获取视频时长,跳过此视频") + return + else: + print(" [Error] XML 中既无 totalTime 也无 videoUrl,无法继续") + return + + print( + f" [Info] UserId={user_id}, HistoryId={history_id}, TotalTime={total_time}") + + except ET.ParseError: + print(" [Error] XML 解析失败") + return + + # 5. 循环发送心跳 + # 初始进度:优先从本地缓存读取,其次从 XML 读取 + + # 读取本地缓存 + local_progress = load_progress().get(f"{course_id}_{item_id}", {}) + local_time = local_progress.get("current_time", 0.0) + local_finished = local_progress.get("finished", False) + + # 读取 XML 进度 + last_viewed_time_str = root.findtext("lastViewedTime") + xml_time = float(last_viewed_time_str) if last_viewed_time_str else 0.0 + + # 选取最大的进度 + current_time = max(local_time, xml_time) + + # 如果本地标记已完成,或者 XML 标记已完成,或者进度已达标,则跳过 + if local_finished or finish_status == "true" or current_time >= total_time: + print( + f" [Skip] 视频已完成 (Progress: {current_time}/{total_time}, XML Finish: {finish_status})") + return + + print(f" [Start] 当前进度: {current_time}s / {total_time}s") + + is_first = "true" + + while True: + # 如果已经看完 + if current_time >= total_time: + print(" [Done] 视频已看完。") + # 发送最后一次 finished=1 的包 + send_heartbeat(upd_status_url, user_id, course_id, item_id, history_id, + add_time=0, current_time=0, total_time=total_time, + finished=1, first_update=is_first) + save_progress(course_id, item_id, total_time, total_time, True) + break + + # 模拟观看 + step = ADD_TIME + # 如果剩余时间不足 + if current_time + step >= total_time: + step = int(total_time - current_time) + 1 # 稍微多一点确保覆盖 + current_time = total_time + finished_flag = 1 + post_current_time = 0 # 完成时传 0 + else: + current_time += step + finished_flag = 0 + post_current_time = current_time + + # 发送心跳 + success = send_heartbeat(upd_status_url, user_id, course_id, item_id, history_id, + add_time=step, current_time=post_current_time, total_time=total_time, + finished=finished_flag, first_update=is_first) + + if not success: + print(" [Error] 心跳发送失败,停止当前视频。") + break + + # 保存进度 + save_progress(course_id, item_id, current_time, + total_time, finished_flag == 1) + + if finished_flag == 1: + print(" [Done] 视频播放结束。") + break + + is_first = "false" + + # 模拟等待 + print( + f" [Progress] 进度更新: {current_time:.1f}/{total_time} (add {step}s)...") + time.sleep(HEARTBEAT_INTERVAL) + + +def send_heartbeat(url, user_id, course_id, sco_id, history_id, add_time, current_time, total_time, finished, first_update): + """ + 发送单个心跳包,带重试机制 + """ + payload = { + "userId": user_id, + "courseId": course_id, + "scoId": sco_id, + "historyId": history_id, + "addTime": str(add_time), + "totalTime": str(total_time), + "finished": finished, + "currentTime": str(current_time), + "hasCheckOne": "false", + "hasCheckTwo": "false", + "hasCheckThree": "false", + "firstUpdate": first_update + } + + max_retries = 5 + for attempt in range(max_retries): + try: + # 注意:这里使用 data=payload 发送 application/x-www-form-urlencoded + resp = requests.post( + url, data=payload, headers=headers, timeout=10) + resp.raise_for_status() + # 检查响应内容,有些服务器返回 XML 状态 + # 1 + if "1" in resp.text: + return True + elif "-1" in resp.text: + print(" [Warning] 服务器返回 status -1 (可能多端登录或异常)") + return False + return True + except Exception as e: + print(f" [Warning] 请求异常 (尝试 {attempt+1}/{max_retries}): {e}") + if attempt < max_retries - 1: + time.sleep(2) # 重试前等待 + else: + print(" [Error] 重试次数耗尽,放弃本次心跳。") + return False + return False + + +# ------------------------------------------------------------------------- +# 主程序逻辑 +# ------------------------------------------------------------------------- + +print("\n" + "="*60) +print("欢迎使用自动刷课脚本 v1.0") +print("此脚本通过视频心跳请求模拟观看课程视频,以达到刷课目的。功耗极低") +print("注意:请确保 Cookie 有效,且已安装 ffprobe") +print(f"当前心跳间隔: {HEARTBEAT_INTERVAL}秒 (警告:请勿设置过低,以免封号)") +print(f"每次进度增加: {ADD_TIME}秒") +print("您需要安装ffmpeg工具集,确保ffprobe命令可用,可从https://ffmpeg.org/download.html 下载") +print("脚本仅供学习交流使用,请勿用于商业用途!") +print("作者:NCJOAQ & Github Compilot") +print("="*60 + "\n") + +course_data = [] + +# 1. Try to load existing items data +if os.path.exists(items_cache_file) and os.path.getsize(items_cache_file) > 0: + try: + with open(items_cache_file, "r", encoding="utf-8") as f: + data = json.load(f) + + if isinstance(data, list) and len(data) > 0 and isinstance(data[0], dict) and "courseId" in data[0]: + print("检测到课程项目缓存文件且有效。") + course_data = data + except json.JSONDecodeError: + print("课程项目缓存文件损坏,已忽略。") + +# 2. 如果没有缓存数据,则执行之前的抓取逻辑 (此处省略抓取代码,假设已有缓存或手动抓取) +# 为了保持代码简洁,这里假设你已经运行过一次脚本生成了 course_items.json +# 如果 course_data 为空,说明需要先生成缓存。 + +if not course_data: + print("未找到有效的课程数据缓存 (course_items.json)。") + print("请先运行脚本生成缓存,或检查网络连接。") + + # --- 原有的抓取逻辑 (简化版) --- + # (此处保留你之前的抓取代码,如果需要的话,可以把之前的抓取逻辑放回来) + # ... + # ----------------------------- + + # 临时:如果没有数据就退出,提示用户 + # sys.exit(1) + + # 或者,为了完整性,保留之前的抓取逻辑: + print("开始执行抓取流程...") + # ... (这里可以粘贴之前的抓取代码,为了不让文件太长,我先假设你已经有了 json) ... + # 如果你需要我把抓取代码也完整合并进来,请告诉我。 + + # 这里为了演示,我把之前的抓取逻辑简单复原一下: + course_ids = [] + if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0: + with open(cache_file, "r", encoding="utf-8") as f: + course_ids = json.load(f) + + if not course_ids: + resp = requests.get(url, headers=headers) + course_ids = re.findall( + r"window\.location\s*=\s*['\"]/lms/web/course/detail\?id=(\d+)['\"]", resp.text) + with open(cache_file, "w", encoding="utf-8") as f: + json.dump(course_ids, f, indent=2) + + for cid in course_ids: + detail_url = f"https://zjbc.cjnep.net/lms/web/course/detail?id={cid}" + resp = requests.get(detail_url, headers=headers) + item_ids = re.findall(r'[?&]itemid=(\d+)', resp.text) + seen = set() + unique_item_ids = [x for x in item_ids if not ( + x in seen or seen.add(x))] + course_data.append({"courseId": cid, "itemIds": unique_item_ids}) + time.sleep(1) + + with open(items_cache_file, "w", encoding="utf-8") as f: + json.dump(course_data, f, indent=2) + + +# 3. 开始刷课 +print(f"\n>>> 开始刷课任务,共 {len(course_data)} 门课程") + +for course in course_data: + cid = course['courseId'] + items = course['itemIds'] + print(f"\n=== 正在处理课程 {cid},共 {len(items)} 个视频 ===") + + for item_id in items: + process_video(cid, item_id) + # 视频之间休息一下 + time.sleep(2) + +print("\n所有任务完成。") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a6bcea4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "python" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "requests>=2.32.5", + "PyExecJS>=1.5.1", + "pip", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f650efc --- /dev/null +++ b/uv.lock @@ -0,0 +1,130 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "pip" +version = "25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, +] + +[[package]] +name = "pyexecjs" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/8e/aedef81641c8dca6fd0fb7294de5bed9c45f3397d67fddf755c1042c2642/PyExecJS-1.5.1.tar.gz", hash = "sha256:34cc1d070976918183ff7bdc0ad71f8157a891c92708c00c5fbbff7a769f505c", size = 13344, upload-time = "2018-01-18T04:33:55.126Z" } + +[[package]] +name = "python" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pip" }, + { name = "pyexecjs" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "pip" }, + { name = "pyexecjs", specifier = ">=1.5.1" }, + { name = "requests", specifier = ">=2.32.5" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/1d/0f3a93cca1ac5e8287842ed4eebbd0f7a991315089b1a0b01c7788aa7b63/urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f", size = 432678, upload-time = "2025-12-08T15:25:26.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" }, +]