基本刷课程序

This commit is contained in:
2025-12-12 01:25:54 +08:00
commit 19ca1638e2
6 changed files with 634 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
# 忽略基本
__pycache__/
*.py[cod]
*.spec
# 虚拟环境
.venv/
venv/
env/
# 脚本生成的缓存文件
course_ids.json
course_items.json
progress.json
mock_updstatus.progress
# 日志
*.log

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

78
README.md Normal file
View File

@@ -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` 以降低风险。
- **免责声明**:本脚本仅供学习和研究使用,请勿用于商业用途或违反平台规定的行为。作者不对使用本脚本导致的任何后果负责。

396
main.py Normal file
View File

@@ -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 状态
# <root><status>1</status></root>
if "<status>1</status>" in resp.text:
return True
elif "<status>-1</status>" 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所有任务完成。")

11
pyproject.toml Normal file
View File

@@ -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",
]

130
uv.lock generated Normal file
View File

@@ -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" },
]