├── .python-version
├── requirements.txt
├── img
├── DrissionPageMCP-logo.png
├── install_to_Cursor1.png
├── install_to_cursor2.png
├── install_to_vscode0.png
├── install_to_vscode1.png
└── install_to_vscode2.png
├── .gitignore
├── pyproject.toml
├── 提示词.txt
├── MCP安装教程.md
├── ToolBox.py
├── README.md
├── domTreeToJSON.js
├── CodeBox.py
├── DrissionPage使用教程.md
├── DrissionPage_code_guide.md
├── main-2.py
├── main-1.py
├── main.py
└── uv.lock
/.python-version:
--------------------------------------------------------------------------------
1 | 3.13
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | drissionpage>=4.1.0.18
2 | fastmcp>=2.4.0
3 | uv>=0.1.0
--------------------------------------------------------------------------------
/img/DrissionPageMCP-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/DrissionPageMCP-logo.png
--------------------------------------------------------------------------------
/img/install_to_Cursor1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_Cursor1.png
--------------------------------------------------------------------------------
/img/install_to_cursor2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_cursor2.png
--------------------------------------------------------------------------------
/img/install_to_vscode0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_vscode0.png
--------------------------------------------------------------------------------
/img/install_to_vscode1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_vscode1.png
--------------------------------------------------------------------------------
/img/install_to_vscode2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxhzhwxhzh/DrissionPageMCP/HEAD/img/install_to_vscode2.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 | test_*.py
9 | test.py
10 |
11 | # Virtual environments
12 | .venv
13 | .vscode
14 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "drssionpagemcp"
3 | version = "0.1.0"
4 | description = "A Python package for the Drissionpage MCP"
5 | readme = "README.md"
6 | requires-python = ">=3.13"
7 | dependencies = [
8 | "drissionpage>=4.1.0.18",
9 | "fastmcp>=2.4.0",
10 | ]
11 |
12 |
13 | [[tool.uv.index]]
14 | url = "https://pypi.tuna.tsinghua.edu.cn/simple"
--------------------------------------------------------------------------------
/提示词.txt:
--------------------------------------------------------------------------------
1 |
2 | #####################################
3 | 1.用DrissionPageMCP访问 https://gitee.com/login?redirect_to_url=%2F 这个网址,点击 点此注册 的那个链接,然后填写网页,参考数据,另外 网页中的复选框都打上勾
4 | sanguo_user = {
5 | "用户名": "诸葛亮",
6 | “个人空间地址”:“aaabbb”
7 | "密码": "LonelyWolf36计",
8 | "手机号": "13333554444"
9 | }
10 |
11 |
12 | 2.把你整个流程中所有用到的元素列个表格,表头有 元素名,xpath,操作动作,动作内容,定位失败的元素不要
13 |
14 | 3.把你整个操作流程,整理成一个python 脚本文件,用到了DrissionPage这个库,我可以运行这个py文件重复整个流程,运行失败的流程不要写进去
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ###############################################################提示2
24 |
25 | 访问酷我这个网址 https://www.kuwo.cn/search/list
26 | 开始监听网页接受的音乐数据包
27 | 搜索 最后一次的温柔
28 | 然后点击 播放全部 按钮
29 | 把监听到的歌曲下载到当前工作空间目录
30 |
31 |
32 |
33 | ####################################################
34 |
35 | 1.访问 百度图片 网站 ,
36 | 2.搜索 迪丽热巴, 然后对网页进行截图,把截图保存到当前工作目录,名字叫 迪丽热巴截图
37 |
38 | 3. 然后 访问 https://picui.cn/upload,点击网页中的上传元素,刚才保存的截图上传上去
39 | 4.最后 点击 上传这张图片
40 |
41 |
42 | #######################################
43 |
44 | 参考当前目录下 DrissionPage使用教程.md 这个教程,写一个python脚本到当前目录,可以让我重复你刚才的操作流程
--------------------------------------------------------------------------------
/MCP安装教程.md:
--------------------------------------------------------------------------------
1 | # DrissionPageMCP 简单操作指南
2 | 快速完成项目搭建、测试与配置,共6步。
3 |
4 |
5 | ## 1. 装 uv(包管理工具)
6 | 打开命令行,输下面的命令:
7 | ```bash
8 | pip install uv
9 | ```
10 |
11 |
12 | ## 2. 克隆项目仓库
13 | 继续输命令,拉取项目文件:
14 | ```bash
15 | git clone https://github.com/wxhzhwxhzh/DrissionPageMCP
16 | ```
17 |
18 |
19 | ## 3. 进入项目文件夹
20 | 输命令切换到项目目录(后续操作都在这):
21 | ```bash
22 | cd DrissionPageMCP
23 | ```
24 |
25 |
26 | ## 4. 装项目需要的库
27 | 输命令自动安装依赖:
28 | ```bash
29 | uv sync
30 | ```
31 |
32 |
33 | ## 5. 测试程序能不能跑
34 | 输命令启动主程序,出现 *DrissionPage MCP server is running...* 说明程序正常:
35 | ```bash
36 | uv run main.py
37 | ```
38 |
39 |
40 | ## 6. 写 MCP 配置文件
41 | 1. MCP对应的JSON配置文件的"mcpServers"项新增子项"DrssionPageMCP";
42 | 2. 把 `D:\test4\DrissionPageMCP` 改成你电脑上 `main.py` 所在的绝对路径(比如你项目放 `E:\DrissionPageMCP`,就改这个)
43 | 3. Windows中路径用双反斜杠 '\\\\',Linux中用单反斜杠 '/';
44 |
45 | ```json
46 | {
47 | "mcpServers": {
48 | "DrissionPageMCP": {
49 | "type": "stdio",
50 | "command": "uv",
51 | "args": ["--directory", "D:\\test4\\DrissionPageMCP", "run", "main.py"]
52 | }
53 | }
54 | }
55 | ```
56 |
57 | ## 如何找到MCP配置文件
58 | [查看帮助](./README.md)
59 |
--------------------------------------------------------------------------------
/ToolBox.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #!/usr/bin/env python
3 |
4 | import sqlite3
5 | import json
6 | import os
7 |
8 | def save_dict_to_sqlite(data, db_path='data.db', table_name='my_table'):
9 | """
10 | 将字典或JSON字符串保存到SQLite数据库中。
11 |
12 | 参数:
13 | data (dict or list of dict or str): 字典、列表字典,或JSON字符串。
14 | db_path (str): SQLite 数据库文件路径。
15 | table_name (str): 要创建的表名。
16 | """
17 | # 如果是 JSON 字符串,则解析为 Python 对象
18 | if isinstance(data, str):
19 | data = json.loads(data)
20 |
21 | # 如果是单个字典,则转成列表
22 | if isinstance(data, dict):
23 | data = [data]
24 |
25 | if not isinstance(data, list) or not all(isinstance(item, dict) for item in data):
26 | raise ValueError("输入必须是字典、字典列表,或 JSON 字符串。")
27 |
28 | # 提取字段名(以第一个字典为准)
29 | columns = data[0].keys()
30 |
31 | # 构建字段定义
32 | col_defs = ', '.join([f'"{col}" TEXT' for col in columns])
33 |
34 | # 连接数据库
35 | conn = sqlite3.connect(db_path)
36 | cursor = conn.cursor()
37 |
38 | # 创建表(如果不存在)
39 | cursor.execute(f'DROP TABLE IF EXISTS "{table_name}"')
40 | cursor.execute(f'CREATE TABLE "{table_name}" ({col_defs})')
41 |
42 | # 插入数据
43 | placeholders = ', '.join(['?' for _ in columns])
44 | insert_query = f'INSERT INTO "{table_name}" ({", ".join(columns)}) VALUES ({placeholders})'
45 | for row in data:
46 | values = tuple(str(row.get(col, '')) for col in columns)
47 | cursor.execute(insert_query, values)
48 |
49 | # 提交并关闭连接
50 | conn.commit()
51 | conn.close()
52 |
53 | return (f"数据已保存到 {db_path} 的表 {table_name} 中。")
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DrissionPage MCP Server -- 骚神出品
2 |
3 | 基于DrissionPage和FastMCP的浏览器自动化MCP服务器,提供丰富的浏览器操作API供AI调用。
4 |
5 | ## 项目简介
6 | 
7 |
8 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。
9 |
10 | ### 主要特性
11 |
12 | - 支持浏览器的打开、关闭和连接管理
13 | - 提供丰富的页面元素操作方法
14 | - 支持 JavaScript 代码执行
15 | - 支持 CDP 协议操作
16 | - 提供便捷的文件下载功能
17 | - 支持键盘按键模拟
18 | - 支持页面截图功能
19 | - 增加 网页后台监听数据包的功能
20 | - 增加自动上传下载文件功能
21 |
22 | #### Python要求
23 | - Python >= 3.9
24 | - pip(最新版本)
25 | - uv (最新版本)
26 |
27 |
28 | #### 浏览器要求
29 | - Chrome 浏览器(推荐 90 及以上版本)
30 |
31 |
32 | #### 必需的Python包
33 | - drissionpage >= 4.1.0.18
34 | - fastmcp >= 2.4.0
35 | - uv
36 |
37 | ## 安装说明
38 | - 把本仓库git clone到本地,核心文件是main.py:
39 | - 首先要进行[💖MCP安装环境准备工作](./MCP安装教程.md)
40 |
41 | ### 安装到Cursor编辑器
42 |
43 | 
44 | 
45 |
46 | ### 安装到vscode编辑器
47 |
48 | 
49 | 
50 | 
51 |
52 |
53 | 请将以下配置代码粘贴到编辑器的`mcpServers`设置中(请填写`你自己电脑上 main.py 文件的绝对路径`):
54 |
55 | ```json
56 | {
57 | "mcpServers": {
58 | "DrissionPageMCP": {
59 | "type": "stdio",
60 | "command": "uv",
61 | "args": ["--directory", "D:\\test10\\DrissionPageMCP", "run", "main.py"]
62 | }
63 | }
64 | }
65 | ```
66 | 新增mcp配置 ,填写下面的配置:
67 | ``` json
68 | "DrissionPageMCP": {
69 | "type": "stdio",
70 | "command": "uv",
71 | "args": ["--directory", "D:\\test10\\DrissionPageMCP", "run", "main.py"]
72 | }
73 | ```
74 |
75 | 注意事项:
76 | - 请根据实际路径修改`args`中的路径
77 | - Windows中路径中的反斜杠需要转义(使用`\\`)
78 | - 确保`uv`命令在系统PATH中可用
79 | - [《MCP安装参考教程》](https://docs.trae.ai/ide/model-context-protocol)
80 |
81 |
82 | ## 调试命令
83 |
84 | 调试
85 | ```
86 | npx -y @modelcontextprotocol/inspector uv run D:\\test10\\DrissionPageMCP\\main.py
87 | ```
88 | 或者
89 | ```
90 | mcp dev D:\\test10\\DrissionPageMCP\\main.py
91 | ```
92 |
93 | ## 更新日志
94 | ### v0.1.3
95 | 增加 自动上传下载文件功能
96 | ### v0.1.2
97 | 增加 网页后台监听数据包的功能
98 |
99 | ### v0.1.0
100 |
101 | - 初始版本发布
102 | - 实现基本的浏览器控制功能
103 | - 提供元素操作 API
104 |
--------------------------------------------------------------------------------
/domTreeToJSON.js:
--------------------------------------------------------------------------------
1 | function isVisuallyHidden(node) {
2 | if (!(node instanceof Element)) return true;
3 |
4 | const invisibleTags = ['script', 'style', 'meta', 'link', 'template', 'noscript'];
5 | const tagName = node.nodeName.toLowerCase();
6 |
7 | if (invisibleTags.includes(tagName)) return true;
8 |
9 | const style = getComputedStyle(node);
10 | const hiddenByStyle = (
11 | style.display === 'none' ||
12 | style.visibility === 'hidden' ||
13 | style.opacity === '0'
14 | );
15 |
16 | const hasNoSize = node.offsetWidth === 0 && node.offsetHeight === 0;
17 |
18 | return hiddenByStyle || hasNoSize;
19 | }
20 |
21 | function domTreeToJson(node = document.body, tagCounters = {}) {
22 | const getNodeLabel = (node) => {
23 | let name = node.nodeName.toLowerCase();
24 | if (node.id) name += `#${node.id}`;
25 | if (node.className && typeof node.className === 'string') {
26 | const classList = node.className.trim().split(/\s+/).join('.');
27 | if (classList) name += `.${classList}`;
28 | }
29 | const text = node.textContent?.trim().replace(/\s+/g, ' ') || '';
30 | const content = text ? ` content='${text.slice(0, 5)}${text.length > 5 ? "…" : ""}'` : '';
31 | return `${name}/` + content;
32 | };
33 |
34 | if (isVisuallyHidden(node)) {
35 | return {}; // 过滤不可见节点
36 | }
37 |
38 | const tagName = node.nodeName.toLowerCase();
39 | tagCounters[tagName] = (tagCounters[tagName] || 0);
40 | const nodeKey = `${tagName}${tagCounters[tagName]++}`;
41 |
42 | const children = Array.from(node.children)
43 | .map(child => domTreeToJson(child, tagCounters))
44 | .filter(childJson => Object.keys(childJson).length > 0); // 去掉空节点
45 |
46 | if (children.length === 0) {
47 | return { [nodeKey]: getNodeLabel(node) };
48 | } else {
49 | const childJson = {};
50 | children.forEach(child => Object.assign(childJson, child));
51 | return { [nodeKey]: childJson };
52 | }
53 | }
54 |
55 | function buildDomJsonTree(root = document.body) {
56 | const topTag = root.nodeName.toLowerCase();
57 | const result = {};
58 | result[topTag] = domTreeToJson(root);
59 | return result;
60 | }
61 |
62 | // 用法示例
63 | const domJson = buildDomJsonTree();
64 |
65 | // console.log(domJson)
66 | //console.log(JSON.stringify(domJson, null, 2));
67 | return JSON.stringify(domJson, null, 2)
68 |
--------------------------------------------------------------------------------
/CodeBox.py:
--------------------------------------------------------------------------------
1 | domTreeToJson = '''
2 |
3 | function isVisuallyHidden(node) {
4 | if (!(node instanceof Element)) return true;
5 |
6 | const invisibleTags = ['script', 'style', 'meta', 'link', 'template', 'noscript'];
7 | const tagName = node.nodeName.toLowerCase();
8 |
9 | if (invisibleTags.includes(tagName)) return true;
10 |
11 | const style = getComputedStyle(node);
12 | const hiddenByStyle = (
13 | style.display === 'none' ||
14 | style.visibility === 'hidden' ||
15 | style.opacity === '0'
16 | );
17 |
18 | const hasNoSize = node.offsetWidth === 0 && node.offsetHeight === 0;
19 |
20 | return hiddenByStyle || hasNoSize;
21 | }
22 |
23 | function domTreeToJson(node = document.body, tagCounters = {}) {
24 | const getNodeLabel = (node) => {
25 | let name = node.nodeName.toLowerCase();
26 | if (node.id) name += `#${node.id}`;
27 | if (node.className && typeof node.className === 'string') {
28 | const classList = node.className.trim().split(/\s+/).join('.');
29 | if (classList) name += `.${classList}`;
30 | }
31 | const text = node.textContent?.trim().replace(/\s+/g, ' ') || '';
32 | const content = text ? ` content='${text.slice(0, 5)}${text.length > 5 ? "…" : ""}'` : '';
33 | return `${name}/` + content;
34 | };
35 |
36 | if (isVisuallyHidden(node)) {
37 | return {}; // 过滤不可见节点
38 | }
39 |
40 | const tagName = node.nodeName.toLowerCase();
41 | tagCounters[tagName] = (tagCounters[tagName] || 0);
42 | const nodeKey = `${tagName}${tagCounters[tagName]++}`;
43 |
44 | const children = Array.from(node.children)
45 | .map(child => domTreeToJson(child, tagCounters))
46 | .filter(childJson => Object.keys(childJson).length > 0); // 去掉空节点
47 |
48 | if (children.length === 0) {
49 | return { [nodeKey]: getNodeLabel(node) };
50 | } else {
51 | const childJson = {};
52 | children.forEach(child => Object.assign(childJson, child));
53 | return { [nodeKey]: childJson };
54 | }
55 | }
56 |
57 | function buildDomJsonTree(root = document.body) {
58 | const topTag = root.nodeName.toLowerCase();
59 | const result = {};
60 | result[topTag] = domTreeToJson(root);
61 | return result;
62 | }
63 |
64 | // 用法示例
65 | const domJson = buildDomJsonTree();
66 |
67 | // console.log(domJson)
68 | //console.log(JSON.stringify(domJson, null, 2));
69 | return JSON.stringify(domJson, null, 2)
70 |
71 | '''
--------------------------------------------------------------------------------
/DrissionPage使用教程.md:
--------------------------------------------------------------------------------
1 |
2 | # DrissionPage 使用教程
3 |
4 | > 📌 DrissionPage® 是一个基于 Python 的网页自动化工具,能控制浏览器,功能强大,语法简洁优雅,代码量少,对新手友好.支持:Chromium 内核浏览器(如 Chrome 和 Edge)
5 |
6 | ---
7 |
8 | ## ✨ 安装
9 |
10 | ```bash
11 | pip install -U DrissionPage
12 | ```
13 |
14 | 如需使用浏览器功能,请安装对应的浏览器驱动(如 ChromeDriver)并确保版本匹配。
15 |
16 | ---
17 |
18 |
19 |
20 | ## 🛠 启动或者连接浏览器(带配置),然后打开一个网页
21 | 默认状态下,程序会自动在系统内查找 Chrome 路径
22 |
23 | ```python
24 |
25 | #!/usr/bin/env python
26 | # -*- coding:utf-8 -*-
27 | #-导入库
28 | from DrissionPage import Chromium, ChromiumOptions
29 | # 创建配置对象
30 | co = ChromiumOptions()
31 | co.headless(True)
32 | co.set_argument('--start-maximized')
33 | # 设置debug port
34 | co.set_local_port('9222')
35 | # 设置浏览器路径,不设置则默认是谷歌浏览器
36 | co.set_browser_path(r'C:\chrome.exe')
37 |
38 | # 创建浏览器对象
39 | browser = Chromium(co)
40 | tab = browser.latest_tab
41 |
42 | #访问网页..
43 | tab.get("https://www.baidu.com/")
44 | print(tab.title)
45 |
46 | ```
47 |
48 | ---
49 |
50 | ## 📄 自动登录gitee网站
51 |
52 | ```python
53 | # 使用默认模式访问网页
54 | from DrissionPage import Chromium
55 |
56 | # 启动或接管浏览器,并获取标签页对象
57 | tab = Chromium().latest_tab
58 | # 跳转到登录页面
59 | tab.get('https://gitee.com/login')
60 |
61 | # 定位到账号文本框,获取文本框元素
62 | ele = tab.ele('#user_login')
63 | # 输入对文本框输入账号
64 | ele.input('您的账号')
65 | # 定位到密码文本框并输入密码
66 | tab.ele('#user_password').input('您的密码')
67 | # 点击登录按钮
68 | tab.ele('@value=登 录').click()
69 | ```
70 |
71 | ---
72 |
73 | ## 🔍 查找元素
74 |
75 | 支持 CSS 选择器、XPath 和自定义选择器:
76 |
77 | ```python
78 | # 查找tag为div的元素
79 | ele = tab.ele('tag:div') # 原写法
80 | ele = tab('t:div') # 简化写法
81 |
82 | # 用xpath查找元素
83 | ele = tab.ele('xpath://****') # 原写法
84 | ele = tab('x://****') # 简化写法
85 |
86 | # 查找text为'something'的元素
87 | ele = tab.ele('text=something') # 原写法
88 | ele = tab('tx=something') # 简化写法
89 | ```
90 |
91 | ---
92 |
93 | ## ✏️ 获取电商网站的评论
94 |
95 | ```python
96 |
97 | #!/usr/bin/env python
98 | # -*- coding:utf-8 -*-#
99 |
100 | # 电脑内需要提取安装谷歌浏览器或者其他chromium内核的浏览器 比如 edge浏览器 qq浏览器 360浏览器
101 |
102 |
103 | import time
104 | from DrissionPage import Chromium
105 | from loguru import logger
106 |
107 | # 设置日志记录到文件
108 | logger.add("JD_comment.log", format="{time} {message}")
109 |
110 | # 初始化浏览器
111 | browser = Chromium()
112 |
113 | # 打开京东首页
114 | main_tab = browser.new_tab('https://www.jd.com/')
115 |
116 | # 获取搜索框并输入关键词
117 | search_input = main_tab.ele('tag:input@@id=key')
118 | search_input.input('小米手机')
119 |
120 | # 点击搜索按钮
121 | main_tab('tag:button@@aria-label=搜索').click()
122 |
123 | # 获取搜索结果列表
124 | search_results = main_tab.eles('t:li@@class=gl-item')
125 |
126 | # 打印每个搜索结果的文本
127 | # for result in search_results:
128 | # logger.info(result)
129 |
130 | # 点击搜索结果中的第二个商品以打开商品详情页
131 | product_detail_tab = search_results[1].ele('t:a').click.for_new_tab()
132 | # 点击评论标签页
133 | product_detail_tab.ele('@data-anchor=#comment').click()
134 |
135 | # 获取并打印商品评论
136 | def get_comments(tab):
137 | for comment in tab.eles('t:div@@class=comment-item'):
138 | # logger.info(comment)
139 |
140 | logger.info(comment('.comment-con').text) # 记录评论内容
141 | if recomment:=comment.ele('.recomment',timeout=2):
142 | logger.error(recomment.text)
143 |
144 | time.sleep(2)
145 |
146 | # 获取第一页评论并点击下一页
147 | get_comments(product_detail_tab)
148 | product_detail_tab.ele('t:a@@rel=2').click()
149 |
150 | # 循环获取剩余页码的评论
151 | for _ in range(4):
152 | get_comments(product_detail_tab)
153 | product_detail_tab.ele('下一页').click()
154 | ```
155 |
156 | ---
157 |
158 | ## 📦 截图
159 |
160 | ```python
161 | # 对整页截图并保存
162 | tab.get_screenshot(path='tmp', name='pic.jpg', full_page=True)
163 | ```
164 |
165 | ---
166 |
167 | ## 📂 文件下载
168 |
169 | ```python
170 | from DrissionPage import Chromium
171 |
172 | tab = Chromium().latest_tab
173 | tab.set.download_path('save_path') # 设置文件保存路径
174 | tab.set.download_file_name('file_name') # 设置重命名文件名
175 | tab('t:a').click() # 点击一个会触发下载的链接
176 | tab.wait.download_begin() # 等待下载开始
177 | tab.wait.downloads_done() # 等待下载结束
178 | ```
179 |
180 | ---
181 |
182 | ## 📤 文件上传
183 |
184 | ```python
185 | # 设置要上传的文件路径
186 | tab.set.upload_files('demo.txt')
187 | # 点击触发文件选择框按钮
188 | btn_ele.click()
189 | # 等待路径填入
190 | tab.wait.upload_paths_inputted()
191 | ```
192 | ---
193 |
194 | ## 📤 浏览器等待
195 |
196 | ```python
197 | tab.wait(10) # 等待10秒
198 | ```
199 |
200 | ---
201 | ## 📤 关闭浏览器
202 |
203 | ```python
204 | browser.quit()
205 | ```
206 | ## 📤 关闭标签页
207 | ```python
208 | tab.close()
209 | ```
210 |
211 | ---
212 | ## 📤 访问指定小红书页面,监听并下载评论json数据
213 | ```python
214 | #!/usr/bin/env python
215 | # -*- coding: utf-8 -*-
216 |
217 | """
218 | 小红书评论数据下载脚本
219 |
220 | 功能:访问指定小红书页面,监听并下载评论json数据
221 | """
222 |
223 | import json
224 | import time
225 | from DrissionPage import Chromium, ChromiumOptions
226 | from loguru import logger
227 |
228 | # 配置日志
229 | logger.add("xiaohongshu_comment.log", format="{time} {level} {message}")
230 |
231 | def download_xiaohongshu_comments(url: str, output_file: str):
232 | """
233 | 下载小红书评论数据
234 |
235 | :param url: 小红书页面URL
236 | :param output_file: 输出文件路径
237 | """
238 | try:
239 | # 配置浏览器选项
240 | co = ChromiumOptions()
241 | co.headless(False) # 显示浏览器窗口
242 | co.set_local_port('9222') # 设置调试端口
243 |
244 | # 创建浏览器对象
245 | browser = Chromium(co)
246 | tab = browser.latest_tab
247 |
248 | # 监听网络请求
249 | logger.info("开始监听评论数据...")
250 | tab.listen.start('comment') # 监听包含comment的请求
251 | logger.info(f"正在访问页面: {url}")
252 | tab.get(url)
253 | tab.eles(".title")[4].click() # 点击视频
254 |
255 | # 等待足够时间获取数据
256 | time.sleep(4)
257 |
258 | # 获取监听到的数据
259 | data = tab.listen.wait()
260 | if not data:
261 | logger.error("未获取到评论数据")
262 | return
263 |
264 | with open(output_file, 'w', encoding='utf-8') as f:
265 | json.dump(data.response.body, f, ensure_ascii=False, indent=4)
266 |
267 | logger.success(f"评论数据已保存到: {output_file}")
268 |
269 | except Exception as e:
270 | logger.error(f"发生错误: {str(e)}")
271 | finally:
272 | # 关闭浏览器
273 | if 'browser' in locals():
274 | browser.quit()
275 |
276 | if __name__ == '__main__':
277 | # 配置参数
278 | target_url = 'https://www.xiaohongshu.com/explore/'
279 | output_path = "comments_data.json"
280 | # 执行下载
281 | download_xiaohongshu_comments(target_url, output_path)
282 | ```
283 |
284 | ---
285 |
286 |
287 |
288 |
289 | ## 🔗 官方资源
290 |
291 | - GitHub 项目主页: [https://github.com/g1879/DrissionPage](https://github.com/g1879/DrissionPage)
292 | ---
293 |
294 |
295 |
296 | > 本教程适合初学者快速上手,更多高级功能请参考官方文档。
297 | https://drissionpage.cn/
--------------------------------------------------------------------------------
/DrissionPage_code_guide.md:
--------------------------------------------------------------------------------
1 |
2 | # DrissionPage 代码语法规范指南教程
3 |
4 | 📌 DrissionPage® 是一个基于 Python 的网页自动化工具,能控制浏览器,功能强大,语法简洁优雅,代码量少,对新手友好.支持:Chromium 内核浏览器(如 Chrome 和 Edge)
5 |
6 | ---
7 |
8 | ## ✨ 安装
9 |
10 | ```bash
11 | pip install -U DrissionPage
12 | ```
13 |
14 |
15 | ---
16 |
17 |
18 |
19 | ## 🛠 启动或者连接浏览器(带配置),然后打开一个网页
20 | 默认状态下,程序会自动在系统内查找 Chrome 路径
21 |
22 | ```python
23 |
24 | #!/usr/bin/env python
25 | # -*- coding:utf-8 -*-
26 | #-导入库
27 | from DrissionPage import Chromium, ChromiumOptions
28 | # 创建配置对象
29 | co = ChromiumOptions()
30 | co.headless(True)
31 | co.set_argument('--start-maximized')
32 | # 设置debug port
33 | co.set_local_port('9222')
34 | # 设置浏览器路径,不设置则默认是谷歌浏览器
35 | co.set_browser_path(r'C:\chrome.exe')
36 |
37 | # 创建浏览器实例,浏览器实例不能直接打开网页
38 | browser = Chromium(co)
39 |
40 | tab = browser.latest_tab #获取标签页实例,标签页实例可以直接get打开网页
41 |
42 | #访问网页..
43 | tab.get("https://www.baidu.com/")
44 | print(tab.title)
45 |
46 | ```
47 |
48 | ---
49 |
50 | ## 📄 自动登录gitee网站
51 |
52 | ```python
53 | # 使用默认模式访问网页
54 | from DrissionPage import Chromium
55 |
56 | # 启动或接管浏览器,并获取标签页对象
57 | tab = Chromium().latest_tab
58 | # 跳转到登录页面
59 | tab.get('https://gitee.com/login')
60 |
61 | # 定位到账号文本框,获取文本框元素
62 | ele = tab.ele('#user_login')
63 | # 输入对文本框输入账号
64 | ele.input('您的账号')
65 | # 定位到密码文本框并输入密码
66 | tab.ele('#user_password').input('您的密码')
67 | # 点击登录按钮
68 | tab.ele('@value=登 录').click()
69 | ```
70 |
71 | ---
72 |
73 | ## 🔍 查找元素
74 |
75 | 支持 CSS 选择器、XPath 和自定义选择器:
76 |
77 | ```python
78 | # 查找tag为div的元素
79 | ele = tab.ele('tag:div') # 原写法
80 | ele = tab('t:div') # 简化写法
81 |
82 | # 用xpath查找元素
83 | ele = tab.ele('xpath://****') # 原写法
84 | ele = tab('x://****') # 简化写法
85 |
86 | # 查找text为'something'的元素
87 | ele = tab.ele('text=something') # 原写法
88 | ele = tab('tx=something') # 简化写法
89 | ```
90 |
91 | ---
92 |
93 | ## ✏️ 获取电商网站的评论
94 |
95 | ```python
96 |
97 | #!/usr/bin/env python
98 | # -*- coding:utf-8 -*-#
99 |
100 | # 电脑内需要提取安装谷歌浏览器或者其他chromium内核的浏览器 比如 edge浏览器 qq浏览器 360浏览器
101 |
102 |
103 | import time
104 | from DrissionPage import Chromium
105 | from loguru import logger
106 |
107 | # 设置日志记录到文件
108 | logger.add("JD_comment.log", format="{time} {message}")
109 |
110 | # 初始化浏览器
111 | browser = Chromium()
112 |
113 | # 打开京东首页
114 | main_tab = browser.new_tab('https://www.jd.com/')
115 |
116 | # 获取搜索框并输入关键词
117 | search_input = main_tab.ele('tag:input@@id=key')
118 | search_input.input('小米手机')
119 |
120 | # 点击搜索按钮
121 | main_tab('tag:button@@aria-label=搜索').click()
122 |
123 | # 获取搜索结果列表
124 | search_results = main_tab.eles('t:li@@class=gl-item')
125 |
126 | # 打印每个搜索结果的文本
127 | # for result in search_results:
128 | # logger.info(result)
129 |
130 | # 点击搜索结果中的第二个商品以打开商品详情页
131 | product_detail_tab = search_results[1].ele('t:a').click.for_new_tab()
132 | # 点击评论标签页
133 | product_detail_tab.ele('@data-anchor=#comment').click()
134 |
135 | # 获取并打印商品评论
136 | def get_comments(tab):
137 | for comment in tab.eles('t:div@@class=comment-item'):
138 | # logger.info(comment)
139 |
140 | logger.info(comment('.comment-con').text) # 记录评论内容
141 | if recomment:=comment.ele('.recomment',timeout=2):
142 | logger.error(recomment.text)
143 |
144 | time.sleep(2)
145 |
146 | # 获取第一页评论并点击下一页
147 | get_comments(product_detail_tab)
148 | product_detail_tab.ele('t:a@@rel=2').click()
149 |
150 | # 循环获取剩余页码的评论
151 | for _ in range(4):
152 | get_comments(product_detail_tab)
153 | product_detail_tab.ele('下一页').click()
154 | ```
155 |
156 | ---
157 |
158 | ## 📦 截图
159 |
160 | ```python
161 | # 对整页截图并保存
162 | tab.get_screenshot(path='tmp', name='pic.jpg', full_page=True)
163 | ```
164 |
165 | ---
166 |
167 | ## 📂 文件下载
168 |
169 | ```python
170 | from DrissionPage import Chromium
171 |
172 | tab = Chromium().latest_tab
173 | tab.set.download_path('save_path') # 设置文件保存路径
174 | tab.set.download_file_name('file_name') # 设置重命名文件名
175 | tab('t:a').click() # 点击一个会触发下载的链接
176 | tab.wait.download_begin() # 等待下载开始
177 | tab.wait.downloads_done() # 等待下载结束
178 | ```
179 |
180 | ---
181 |
182 | ## 📤 文件上传
183 |
184 | ```python
185 | # 设置要上传的文件路径
186 | tab.set.upload_files('demo.txt')
187 | # 点击触发文件选择框按钮
188 | btn_ele.click()
189 | # 等待路径填入
190 | tab.wait.upload_paths_inputted()
191 | ```
192 | ---
193 |
194 | ## 📤 浏览器等待
195 |
196 | ```python
197 | tab.wait(10) # 等待10秒
198 | ```
199 |
200 | ---
201 | ## 📤 关闭浏览器
202 |
203 | ```python
204 | browser.quit()
205 | ```
206 | ## 📤 关闭标签页
207 | ```python
208 | tab.close()
209 | ```
210 |
211 | ---
212 | ## 📤 访问指定小红书页面,监听并下载评论json数据
213 | ```python
214 | #!/usr/bin/env python
215 | # -*- coding: utf-8 -*-
216 |
217 | """
218 | 小红书评论数据下载脚本
219 |
220 | 功能:访问指定小红书页面,监听并下载评论json数据
221 | """
222 |
223 | import json
224 | import time
225 | from DrissionPage import Chromium, ChromiumOptions
226 | from loguru import logger
227 |
228 | # 配置日志
229 | logger.add("xiaohongshu_comment.log", format="{time} {level} {message}")
230 |
231 | def download_xiaohongshu_comments(url: str, output_file: str):
232 | """
233 | 下载小红书评论数据
234 |
235 | :param url: 小红书页面URL
236 | :param output_file: 输出文件路径
237 | """
238 | try:
239 | # 配置浏览器选项
240 | co = ChromiumOptions()
241 | co.headless(False) # 显示浏览器窗口
242 | co.set_local_port('9222') # 设置调试端口
243 |
244 | # 创建浏览器对象
245 | browser = Chromium(co)
246 | tab = browser.latest_tab
247 |
248 | # 监听网络请求
249 | logger.info("开始监听评论数据...")
250 | tab.listen.start('comment') # 监听包含comment的请求
251 | logger.info(f"正在访问页面: {url}")
252 | tab.get(url)
253 | tab.eles(".title")[4].click() # 点击视频
254 |
255 | # 等待足够时间获取数据
256 | time.sleep(4)
257 |
258 | # 获取监听到的数据
259 | data = tab.listen.wait()
260 | if not data:
261 | logger.error("未获取到评论数据")
262 | return
263 |
264 | with open(output_file, 'w', encoding='utf-8') as f:
265 | json.dump(data.response.body, f, ensure_ascii=False, indent=4)
266 |
267 | logger.success(f"评论数据已保存到: {output_file}")
268 |
269 | except Exception as e:
270 | logger.error(f"发生错误: {str(e)}")
271 | finally:
272 | # 关闭浏览器
273 | if 'browser' in locals():
274 | browser.quit()
275 |
276 | if __name__ == '__main__':
277 | # 配置参数
278 | target_url = 'https://www.xiaohongshu.com/explore/'
279 | output_path = "comments_data.json"
280 | # 执行下载
281 | download_xiaohongshu_comments(target_url, output_path)
282 | ```
283 |
284 | ---
285 |
286 |
287 |
288 |
289 | ## 🔗 官方资源
290 |
291 | - GitHub 项目主页: [https://github.com/g1879/DrissionPage](https://github.com/g1879/DrissionPage)
292 | ---
293 |
294 |
295 |
296 | > 本教程适合初学者快速上手,更多高级功能请参考官方文档。
297 | https://drissionpage.cn/
298 |
--------------------------------------------------------------------------------
/main-2.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | from typing import Any,Literal
5 | import re
6 | from pathlib import Path
7 | from DrissionPage import Chromium,ChromiumOptions
8 | from mcp.server.fastmcp import FastMCP,Image,Context
9 |
10 | from DrissionPage.items import SessionElement, ChromiumElement, ShadowRoot, NoneElement, ChromiumTab, MixTab, ChromiumFrame
11 | from DrissionPage.common import Keys
12 |
13 |
14 | 提示='''
15 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。
16 | 点击元素前,需要先获取页面所有可点击元素的信息,使用get_all_clickable_elements()方法。
17 | 输入元素前,需要先获取页面所有可输入元素的信息,使用get_all_input_elements()方法。
18 |
19 | '''
20 |
21 |
22 |
23 | #region DrissionPageMCP
24 | class DrissionPageMCP():
25 | def __init__(self):
26 | self.browser = None
27 | self.session = None
28 | self.current_tab = None
29 | self.current_frame = None
30 | self.current_shadow_root = None
31 | self.cdp_event_data = []
32 | self.response_listener_data=[]
33 |
34 | def test(self):
35 | return "test"
36 | def get_version(self)-> str:
37 | """ 获取版本号"""
38 | return "1.0.3"
39 | async def connect_or_open_browser(self, config: dict={'debug_port':9222}) -> dict:
40 | """
41 | 用DrissionPage 打开或接管已打开的浏览器,参数通过字典传递。
42 | 必要参数:
43 | config (dict): 可选键包括 、debug_port、browser_path、headless
44 | 返回:
45 | dict: 浏览器信息
46 | """
47 | co = ChromiumOptions()
48 | if config.get("debug_port"):
49 | co.set_local_port(config["debug_port"])
50 | if config.get("browser_path"):
51 | co.set_browser_path(config["browser_path"])
52 | if config.get("headless", False):
53 | co.headless(True)
54 |
55 | self.browser = Chromium(co)
56 | tab = self.browser.latest_tab
57 |
58 | return {
59 | "browser_address": self.browser._chromium_options.address,
60 | "latest_tab_title": tab.title,
61 | "latest_tab_id": tab.tab_id,
62 | }
63 | async def new_tab(self, url: str) -> str:
64 | """用DrissionPage 控制的浏览器,打开新标签页并 打开一个网址"""
65 | tab = self.browser.new_tab(url)
66 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree()}
67 |
68 | def wait(self, a:int) :
69 | """等待a秒"""
70 | self.browser.latest_tab.wait(a)
71 | return f"等待{a}秒成功"
72 |
73 | async def get(self,url:str)->str:
74 | """在当前标签页打开一个网址"""
75 | if not self.browser:
76 | await self.connect_or_open_browser()
77 | # return "请先打开或者连接浏览器"
78 | self.lastest_tab.get(url)
79 | tab=self.browser.latest_tab
80 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree()}
81 |
82 |
83 | #region 上传和下载
84 | def download_file(self, url: str, path: str, rename: str) -> str:
85 | """控制浏览器下载文件到指定路径
86 |
87 | Args:
88 | url (str): 文件的URL地址
89 | path (str): 文件保存的路径
90 | rename (str): 重命名文件名
91 |
92 | Returns:
93 | str: 下载结果信息
94 | """
95 | tab = self.lastest_tab
96 | result = tab.download(file_url=url, save_path=path, rename=rename)
97 | return str(result)
98 |
99 | def upload_file(self, file_path: str) -> str:
100 | """点击当网页上的 元素触发上传文件的操作,上传file_path文件到当前网页
101 |
102 | Args:
103 | file_path (str): 要上传的文件路径
104 |
105 | Returns:
106 | str: 上传结果信息,如果元素不存在则返回错误信息
107 | """
108 | x="//input[@type='file']"
109 | t:ChromiumTab=self.lastest_tab
110 | if e:= t(f"xpath:{x}"):
111 | t.set.upload_files(file_path)
112 | e.click(by_js=True)
113 | t.wait.upload_paths_inputted()
114 | return f"{file_path} 上传成功 {e}"
115 | else:
116 | return f"元素{x}不存在,无法触发上传文件"
117 |
118 |
119 |
120 | @property
121 | def lastest_tab(self) -> ChromiumTab:
122 | """获取最新标签页"""
123 | return self.browser.latest_tab
124 |
125 | def send_enter(self) -> str:
126 | """向当前页面发送 enter 回车键"""
127 | tab = self.browser.latest_tab
128 | try:
129 | result = tab.actions.type(Keys.ENTER)
130 | return f"{tab.title} 网页发送 enter 回车键成功"
131 | except Exception as e:
132 | return f"{tab.title} 网页发送 enter 回车键失败"
133 |
134 | def getInputElementsInfo(self) -> list:
135 | """获取当前标签页的所有可进行输入操作的元素,对元素进行输入操作前优先使用这个方法"""
136 | tab = self.browser.latest_tab
137 | js_code='''
138 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button'));
139 | return inputElements.filter(el => !el.disabled); // 排除禁用的元素
140 | '''
141 | elements = tab.run_js(js_code)
142 | return elements
143 |
144 | def click_by_xpath(self, xpath: str) -> dict:
145 | """通过xpath点击当前标签页中某个元素,最好先获取页面dom信息,再决定Xpath的写法"""
146 |
147 | locator = f"xpath:{xpath}"
148 | element = self.browser.latest_tab.ele(locator, timeout=3)
149 | result = {"locator": locator, "element": str(element), "click_result": element.click()}
150 | return result
151 |
152 | def click_by_containing_text(self, content: str, index: int = None) :
153 | """
154 | 根据包含指定文本的方式点击网页元素。
155 |
156 | 参数:
157 | content: 要查找的文本内容。
158 | index: 当匹配到多个元素时指定要点击的索引,默认不指定。
159 |
160 | 返回:
161 | 点击结果说明,或错误提示。
162 | """
163 |
164 | # 获取包含指定文本的所有元素,等待最多 3 秒
165 | elements = self.browser.latest_tab.eles(content, timeout=3)
166 |
167 | # 如果没有匹配到任何元素,返回错误提示
168 | if len(elements) == 0:
169 | return f"元素{content}不存在,需要getInputElementsInfo先获取元素信息"
170 |
171 | # 如果只找到一个元素,直接点击它
172 | if len(elements) == 1:
173 | self.lastest_tab(content).click()
174 | return f" 点击成功"
175 |
176 | # 如果找到多个元素
177 | if len(elements) > 1:
178 | # 如果未指定 index,提示用户提供索引
179 | if index is None:
180 | return f"元素{content}存在多个,请调整 index 参数,index=0表示第一个元素,{elements}"
181 | else:
182 | # 根据指定索引点击对应的元素
183 | elements[index].click()
184 | return f" 点击成功"
185 |
186 |
187 |
188 | def input_by_xapth(self, xpath: str, input_value: str, clear_first: bool = True) :
189 | """通过xpath给当前标签页中某个元素输入内容,最好先判断元素是否存在
190 |
191 | Args:
192 | xpath (str): 元素的XPath表达式
193 | input_value (str): 要输入的内容
194 | clear_first (bool): 是否先清除已有内容,默认为True
195 |
196 | Returns:
197 | Any: 输入操作的结果,如果元素不存在则返回错误信息
198 | """
199 | locator = f"xpath:{xpath}"
200 | if e := self.browser.latest_tab.ele(locator, timeout=4):
201 | result = {"locator": locator, "result": e.input(input_value, clear=clear_first)}
202 | return result
203 | else:
204 | return f"元素{locator}不存在,需要getInputElementsInfo先获取元素信息"
205 |
206 | def get_body_text(self) -> str:
207 | """获取当前标签页的body的文本内容"""
208 |
209 | tab = self.browser.latest_tab
210 | body_text = tab('t:body').text
211 | return body_text
212 | def run_js(self, js_code: str) :
213 | """
214 | 在当前标签页中运行JavaScript代码并返回执行结果
215 | 查找网页元素,获取元素信息,操作网页元素优先使用这个方法
216 |
217 | Args:
218 | js_code (str): 要执行的JavaScript代码
219 |
220 | Returns:
221 | Any: JavaScript代码执行结果
222 |
223 | Note:
224 | 想要获取执行的js代码的返回值,可以在js_code中使用return语句。
225 | 想要获取异步函数的返回值,可以参考下面代码
226 | return (async (url) => {
227 | const response = await fetch(url);
228 | const data = await response.json();
229 | return data;
230 | })("https://www.baidu.com/");
231 | """
232 | tab = self.browser.latest_tab
233 | result = tab.run_js(js_code)
234 | return result
235 |
236 | def run_cdp( self,cmd, **cmd_args) :
237 | """在当前标签页中运行谷歌CDP协议代码并获取结果
238 |
239 | Args:
240 |
241 | cmd: CDP协议命令
242 | **cmd_args: CDP命令参数
243 |
244 | Returns:
245 | Any: CDP命令执行结果
246 |
247 | Note:
248 | 举例1说明 run_cdp('Page.stopLoading')
249 | 举例2说明 run_cdp('Page.navigate', url='https://example.com')
250 | """
251 | result=self.browser.latest_tab.run_cdp(cmd, **cmd_args)
252 | return result
253 | def listen_cdp_event(self,event_name: str) :
254 | """设置监听CDP事件
255 |
256 | 应该先运行cdp 命令 激活对应的域,比如 Network.enable
257 | """
258 | # b=Chromium(debug_port)
259 | def r(**event):
260 | self.cdp_event_data.append({"event_name": event_name, "event_data": event})
261 |
262 | try:
263 | self.browser.latest_tab.driver.set_callback(event_name, r)
264 | return f"CDP event callback for '{event_name}' set successfully."
265 | except Exception as e:
266 | return e
267 |
268 | def get_cdp_event_data(self) -> list:
269 | """获取CDP事件回调函数收集到的数据"""
270 | return self.cdp_event_data
271 |
272 |
273 |
274 | #region 监听网页接收的数据包
275 |
276 | def get_url_with_response_listener(self,
277 | tab_url: str,
278 | mimeType: Literal[
279 | # 文本类
280 | "text/html",
281 | "text/css",
282 | "text/javascript",
283 | "application/javascript",
284 | "text/plain",
285 | "text/xml",
286 | "text/csv",
287 | "application/json",
288 |
289 | # 应用类
290 | "application/octet-stream",
291 | "application/zip",
292 | "application/pdf",
293 | "multipart/form-data",
294 | "application/xml",
295 |
296 | # 图片类
297 | "image/jpeg",
298 | "image/png",
299 | "image/gif",
300 | "image/webp",
301 | "image/svg+xml",
302 | "image/x-icon",
303 |
304 | # 音视频类
305 | "audio/mpeg",
306 | "audio/ogg",
307 | "video/mp4",
308 | "video/webm",
309 | "video/ogg"
310 | ],
311 | url_include: str = "."
312 | ) :
313 | '''
314 | 开启一个新的标签页,设置监听,访问tab_url,
315 | tab_url: 被监听的标签页的url
316 | mimeType: 需要监听的接收的数据包的mimeType类型
317 | url_include: 需要监听的接收的数据包的url包含的关键字
318 | refresh: 是否刷新页面,
319 | '''
320 | t = self.browser.new_tab(tab_url)
321 |
322 | t.run_cdp("Network.enable")
323 |
324 | def r(**event):
325 | _url = event.get("response", {}).get("url", "")
326 | _mimeType = event.get("response", {}).get("mimeType", "")
327 |
328 | if mimeType in _mimeType and url_include in _url:
329 | self.response_listener_data.append({
330 | "event_name": "Network.responseReceived",
331 | "event_data": event
332 | })
333 |
334 | t.driver.set_callback("Network.responseReceived", r)
335 | t.get(tab_url)
336 |
337 | return f"开启监听{tab_url}, 数据包url包含关键字:{url_include},mimeType:{mimeType}"
338 |
339 |
340 |
341 | def response_listener_stop(self,clear_data:bool=False) -> str:
342 | """关闭监听网页发送的数据包"""
343 | t=self.browser.latest_tab
344 | t.run_cdp("Network.disable")
345 | if clear_data:
346 | self.response_listener_data = []
347 | return f"监听网页发送的数据包关闭成功 ,是否清空数据: {clear_data}"
348 |
349 |
350 | def get_response_listener_data(self) -> list:
351 | """获取监听到的数据,返回数据列表"""
352 | return self.response_listener_data
353 |
354 | def get_current_tab_screenshot(self) -> bytes:
355 | """
356 | 获取当前标签页的网页截图
357 |
358 | Returns:
359 | bytes: 截图的二进制数据
360 | """
361 | t:ChromiumTab=self.browser.latest_tab
362 | screenshot=t.get_screenshot(as_bytes='jpeg')
363 | return screenshot
364 |
365 | def get_current_tab_screenshot_as_file(self,path:str=".",name:str="screenshot.png") -> str:
366 | """
367 | 获取当前标签页的屏幕截图并保存为文件
368 |
369 | Args:
370 | path (str): 截图保存路径,默认为当前目录
371 |
372 | Returns:
373 | str: 截图的文件路径
374 | """
375 |
376 | screenshot=self.browser.latest_tab.get_screenshot(path=path,name=name)
377 | return screenshot
378 |
379 | def get_current_tab_info(self) -> dict:
380 | """获取当前标签页的信息,包括url, title, id"""
381 | tab =self.browser.latest_tab
382 | info = {
383 | "url": tab.url,
384 | "title": tab.title,
385 | "id": tab.tab_id,
386 | }
387 | return info
388 |
389 | def send_key(self, key: Literal["Enter, Backspace, HOME, END, PAGE_UP, PAGE_DOWN, DOWN, UP, LEFT, RIGHT, ESC, Ctrl+C, Ctrl+V, Ctrl+A, Delete"]) -> str:
390 | """向当前标签页发送特殊按键"""
391 | tab = self.browser.latest_tab
392 | k={"Enter": Keys.ENTER,
393 | "Backspace": Keys.BACKSPACE,
394 | "HOME": Keys.HOME,
395 | "END": Keys.END,
396 | "PAGE_UP": Keys.PAGE_UP,
397 | "PAGE_DOWN": Keys.PAGE_DOWN,
398 | "DOWN": Keys.DOWN,
399 | "UP": Keys.UP,
400 | "LEFT": Keys.LEFT,
401 | "RIGHT": Keys.RIGHT,
402 | "ESC": Keys.ESCAPE,
403 | "Ctrl+C": Keys.CTRL_C,
404 | "Ctrl+V": Keys.CTRL_V,
405 | "Ctrl+A": Keys.CTRL_A,
406 | "Delete": Keys.DELETE,}
407 | try:
408 | result = tab.actions.type(k.get(key))
409 | return f"{tab.title} 网页发送 {key} 键成功"
410 | except Exception as e:
411 | return f"{tab.title} 网页发送 {key} 键失败"
412 |
413 | def getSimplifiedDomTree(self) -> dict:
414 | """获取当前标签页的简化版DOM树"""
415 | from CodeBox import domTreeToJson
416 | tab = self.browser.latest_tab
417 | dom_tree = tab.run_js(domTreeToJson)
418 | return dom_tree
419 |
420 | #region 拖动
421 |
422 | def move_to(self,xpath:str) -> dict:
423 | """鼠标移动悬停到指定xpath的元素上"""
424 | tab = self.browser.latest_tab
425 | locator = f"xpath:{xpath}"
426 | element = tab.ele(locator, timeout=3)
427 | if element:
428 | element.hover()
429 | result = {"locator": locator, "element": str(element)}
430 | return result
431 | else:
432 | return f"元素{locator}不存在,需要getSimplifiedDomTree先获取元素信息"
433 | def drag(self,xpath:str, offset_x: int, offset_y: int, duration: int = 1000) -> dict:
434 |
435 | """
436 | 将元素拖动到指定偏移位置
437 |
438 | Args:
439 | xpath: 要拖动的元素xpath路径
440 | offset_x: x轴偏移量(像素)
441 | offset_y: y轴偏移量(像素)
442 | duration: 拖动持续时间(毫秒),默认为1000
443 |
444 | Returns:
445 | dict: 包含偏移量和持续时间的字典,格式为{"offset_x": int, "offset_y": int, "duration": int}
446 | 或 str: 当元素不存在时返回错误信息
447 |
448 | Raises:
449 | 无显式抛出异常,但内部可能因元素不存在而返回错误信息
450 | """
451 | tab = self.browser.latest_tab
452 | if e:=tab.ele(f'xpath:{xpath}', timeout=3):
453 | tab.actions.move_to(e).wait(0.5).hold().move(offset_x, offset_y).release()
454 | result = {"offset_x": offset_x, "offset_y": offset_y, "duration": duration}
455 | return result
456 | else:
457 | return f"元素{xpath}不存在,需要getSimplifiedDomTree先获取元素信息"
458 |
459 | #region 初始化mcp
460 | mcp = FastMCP("DrissionPageMCP", log_level="ERROR",instructions=提示)
461 | b=DrissionPageMCP()
462 |
463 | mcp.add_tool(b.get_version)
464 | mcp.add_tool(b.connect_or_open_browser)
465 | mcp.add_tool(b.new_tab)
466 | mcp.add_tool(b.wait)
467 | mcp.add_tool(b.get)
468 | mcp.add_tool(b.download_file)
469 | mcp.add_tool(b.upload_file)
470 | mcp.add_tool(b.send_enter)
471 | mcp.add_tool(b.getInputElementsInfo)
472 | mcp.add_tool(b.click_by_xpath)
473 | mcp.add_tool(b.click_by_containing_text)
474 | mcp.add_tool(b.input_by_xapth)
475 | mcp.add_tool(b.get_body_text)
476 | mcp.add_tool(b.run_js)
477 | mcp.add_tool(b.run_cdp)
478 | mcp.add_tool(b.listen_cdp_event)
479 | mcp.add_tool(b.get_cdp_event_data)
480 | mcp.add_tool(b.get_url_with_response_listener)
481 | mcp.add_tool(b.response_listener_stop)
482 | mcp.add_tool(b.get_response_listener_data)
483 | mcp.add_tool(b.get_current_tab_screenshot)
484 | mcp.add_tool(b.get_current_tab_screenshot_as_file)
485 | mcp.add_tool(b.get_current_tab_info)
486 | mcp.add_tool(b.send_key)
487 | mcp.add_tool(b.getSimplifiedDomTree)
488 |
489 | mcp.add_tool(b.move_to)
490 | mcp.add_tool(b.drag)
491 |
492 | #region 保存数据到sqlite
493 | from ToolBox import save_dict_to_sqlite
494 | mcp.add_tool(save_dict_to_sqlite)
495 |
496 |
497 |
498 |
499 | def main():
500 | # 启动MCP服务器
501 | print("DrissionPage MCP server is running...")
502 | mcp.run(transport='stdio')
503 |
504 |
505 | if __name__ == "__main__":
506 | main()
507 |
--------------------------------------------------------------------------------
/main-1.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # mcp程序说明:通过DrissionPage这个库控制浏览器进行各种网页自动化操作
4 |
5 | # 调试 npx -y @modelcontextprotocol/inspector uv run D:\\test10\\DrssionPageMCP\\main.py
6 | '''
7 | # mcp配置
8 | "DrissionPageMCP": {
9 | "command": "uv",
10 | "args": [
11 | "run",
12 | "D:\\test10\\DrssionPageMCP\\main.py"
13 | ]
14 | }
15 |
16 | '''
17 |
18 | from typing import Any,Literal, LiteralString
19 | import re
20 | from pathlib import Path
21 | from DrissionPage import Chromium,ChromiumOptions
22 | from mcp.server.fastmcp import FastMCP,Image,Context
23 |
24 | from DrissionPage.items import SessionElement, ChromiumElement, ShadowRoot, NoneElement, ChromiumTab, MixTab, ChromiumFrame
25 | from DrissionPage.common import Keys
26 |
27 | # from PIL import Image as PILImage
28 | import io
29 |
30 | # Initialize FastMCP server
31 | # mcp = FastMCP("DrissionPageMCP", log_level="ERROR")
32 | 提示='''
33 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。
34 | 点击元素前,需要先获取页面所有可点击元素的信息,使用get_all_clickable_elements()方法。
35 | 输入元素前,需要先获取页面所有可输入元素的信息,使用get_all_input_elements()方法。
36 |
37 | '''
38 |
39 | mcp = FastMCP("DrissionPageMCP", log_level="ERROR",instructions=提示)
40 |
41 |
42 |
43 |
44 | # Browser:Chromium=None
45 | class DP:
46 | browser:Chromium=None
47 | cdp_event_data=[]
48 | listener_data=[]
49 | Drissionpage_python_code=None
50 | mime_types = [
51 | # 文本类
52 | "text/html",
53 | "text/css",
54 | "text/javascript",
55 | "application/javascript",
56 | "text/plain",
57 | "text/xml",
58 | "text/csv",
59 | "application/json",
60 |
61 | # 应用类
62 | "application/octet-stream",
63 | "application/zip",
64 | "application/pdf",
65 | "application/x-www-form-urlencoded",
66 | "multipart/form-data",
67 | "application/xml",
68 |
69 | # 图片类
70 | "image/jpeg",
71 | "image/png",
72 | "image/gif",
73 | "image/webp",
74 | "image/svg+xml",
75 | "image/x-icon",
76 |
77 | # 音视频类
78 | "audio/mpeg",
79 | "audio/ogg",
80 | "video/mp4",
81 | "video/webm",
82 | "video/ogg"
83 | ]
84 |
85 |
86 | @mcp.resource("dir://cwd")
87 | def get_current_directory() -> str:
88 | """返回当前工作目录的路径"""
89 | return str(Path.cwd())
90 |
91 |
92 |
93 | @mcp.resource("elments://tagname={tagname}")
94 | def get_input_elements(tagname:str) -> list:
95 | """返回页面中所有tagname的元素信息"""
96 | if DP.browser is None:
97 | return "没有打开浏览器"
98 | aa=DP.browser.latest_tab.eles("t:"+tagname)
99 | return aa
100 |
101 |
102 |
103 | @mcp.resource("browser://{port}/info")
104 | def browser_info(port:int) -> dict:
105 | """获取浏览器的信息"""
106 | b=Chromium(port)
107 | a={'browser_address':b._chromium_options.address,
108 | "latest_tab_title":b.latest_tab.title,
109 | "latest_tab_id":b.latest_tab.tab_id,
110 | }
111 | return a
112 |
113 | @mcp.tool()
114 | def convert_elemnet_to_drissionpage(element: str) -> str:
115 | """把元素转换为drissionpage格式的字符串"""
116 | e= Use.raw(element)
117 | return e
118 |
119 | # @mcp.tool()
120 | def get_all_clickable_elements() -> list:
121 | """获取当前标签页的所有可点击元素"""
122 | tab = DP.browser.latest_tab
123 | js_code = '''
124 | const clickableElements = Array.from(document.querySelectorAll('a, button, input[type="button"], input[type="submit"], [onclick]'));
125 | return clickableElements.map(el => el.outerHTML);
126 | '''
127 | elements = tab.run_js(js_code)
128 | return elements
129 | mcp.add_tool(
130 | name="get_all_clickable_elements",
131 | description="获取当前标签页的所有可点击元素",
132 | fn=get_all_clickable_elements,
133 | )
134 |
135 | @mcp.tool()
136 | def get_all_input_elements() -> list:
137 | """获取当前标签页的所有可输入元素"""
138 | tab = DP.browser.latest_tab
139 | js_code = '''
140 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button'));
141 | return inputElements.filter(el => !el.disabled).map(el => el.outerHTML);
142 | '''
143 | elements = tab.run_js(js_code)
144 | return elements
145 |
146 | #region 处理数据
147 | @mcp.tool()
148 | async def process_data( ctx: Context) -> dict:
149 | """
150 | 从指定的资源 URI 读取数据,报告处理进度,并请求客户端的 LLM 对数据进行摘要。
151 |
152 | 参数:
153 |
154 | - ctx (Context): FastMCP 提供的上下文对象,包含日志记录、资源访问、进度报告等功能。
155 |
156 | 返回:
157 | - dict: 包含数据长度和摘要的字典。
158 | """
159 | data_uri="browser://9222/info"
160 | # 使用 ctx.info() 记录信息日志,通知客户端正在处理的数据 URI
161 | await ctx.info(f"Processing data from {data_uri}")
162 |
163 | # 使用 ctx.read_resource() 读取指定 URI 的资源内容
164 | resource = await ctx.read_resource(data_uri)
165 | # 如果资源存在,提取第一个资源的内容;否则,设置为空字符串
166 | data = resource[0].content if resource else ""
167 |
168 | # 使用 ctx.report_progress() 报告处理进度为 50%
169 | await ctx.report_progress(progress=50, total=100)
170 |
171 |
172 |
173 | # 使用 ctx.report_progress() 报告处理进度为 100%
174 | await ctx.report_progress(progress=100, total=100)
175 |
176 | # 返回包含数据长度和摘要的字典
177 | return {
178 | "length": len(data)
179 |
180 | }
181 |
182 | #region 注册提示
183 | @mcp.prompt()
184 | def ask_about_topic(topic: str) -> str:
185 | """生成一个请求解释某个主题的用户消息。"""
186 | return f"Can you please explain the concept of '{topic}'?"
187 |
188 |
189 |
190 | @mcp.tool()
191 | async def test(text: str) -> str:
192 | """生成一个请求对文本进行摘要的用户消息。"""
193 | a=await mcp.get_prompt("ask_about_topic", {"topic": text})
194 |
195 | return a
196 |
197 |
198 |
199 | #region 连接浏览器
200 | @mcp.tool()
201 | async def connect_or_open_browser(params: dict,ctx: Context) -> int:
202 | """
203 | 用DrissionPage 打开或者接管已打开的浏览器,参数通过字典传递。如果url不为空则打开指定网址。
204 |
205 | 参数:
206 | params (dict): 包含以下可选键的字典:
207 | - url (str, 可选): 要打开的网址。如果未提供则不自动打开页面。
208 | - debug_port (int, 可选): 调试端口,默认9222。
209 | - browser_path (str, 可选): 浏览器可执行文件路径。
210 | - headless (bool, 可选): 是否以无头模式启动浏览器,默认False。
211 | - use_system_user_path (bool, 可选): 是否使用系统默认用户配置,默认False。
212 |
213 | 返回:
214 | srt: 浏览器各种信息
215 | """
216 | """用DrissionPage 控制接管浏览器,参数通过字典传递。如果url不为空则打开指定网址"""
217 | url = params.get("url")
218 | debug_port = params.get("debug_port", 9222)
219 | browser_path = params.get("browser_path")
220 | headless = params.get("headless", False)
221 | use_system_user_path = params.get("use_system_user_path", False)
222 |
223 | co = ChromiumOptions()
224 | co.set_local_port(debug_port)
225 | if browser_path:
226 | co.set_browser_path(browser_path)
227 | if headless:
228 | co.headless(True)
229 | if use_system_user_path:
230 | co.use_system_user_path(True)
231 |
232 | b = Chromium(co)
233 | tab = b.latest_tab
234 | DP.browser = b
235 | if url:
236 | tab.get(url)
237 | info2= await ctx.read_resource(f"browser://{debug_port}/info")
238 | info={
239 | "browser_address": b._chromium_options.address,
240 | "latest_tab_title": tab.title,
241 | "latest_tab_id": tab.tab_id,
242 | "ctx":[ctx.client_id,ctx.model_computed_fields,ctx.request_id,ctx.request_context]
243 | }
244 | # info1=await ctx.info(str(info))
245 |
246 | return info
247 |
248 | @mcp.tool()
249 | def new_tab(url: str="") -> str:
250 | """用DrissionPage 控制的浏览器,打开新标签页并 打开一个网址"""
251 | t=DP.browser.new_tab( url)
252 | return f'{t.title} {t.tab_id} {t.url} 已经打开'
253 |
254 | @mcp.tool()
255 | def download_file(url: str, path: str, rename: str ) -> str:
256 | """下载文件到指定路径
257 |
258 | Args:
259 | url (str): 文件的URL地址
260 | path (str): 文件保存的路径
261 | rename (str, optional): 重命名文件名.
262 |
263 | Returns:
264 | str: 下载结果信息
265 | """
266 | tab = DP.browser.latest_tab
267 | result = tab.download(file_url=url, save_path=path, rename=rename)
268 |
269 |
270 | return result
271 |
272 | @mcp.tool()
273 |
274 | def send_enter() -> str:
275 | """向当前页面发送 enter 回车键"""
276 |
277 | tab=DP.browser.latest_tab
278 | try:
279 | result=tab.actions.type(Keys.ENTER)
280 | return f"{tab.title} 网页发送 enter 回车键成功"
281 | except Exception as e:
282 | return f"{tab.title} 网页发送 enter 回车键失败"
283 |
284 |
285 | @mcp.tool()
286 | def is_element_exist(element_xpath: str, content_include_keyword: str) -> str:
287 | """通过xpath或者文本节点是否包含关键词判断标签页中某个元素是否存在"""
288 |
289 | xpath_locator = f"xpath:{element_xpath}"
290 |
291 | if elements := DP.browser.latest_tab.eles(xpath_locator, timeout=2):
292 | return elements
293 | if elements := DP.browser.latest_tab.eles(content_include_keyword, timeout=2):
294 | return elements
295 |
296 | return "没找到元素"
297 | @mcp.tool()
298 | def getInputElementsInfo() -> list:
299 | """获取当前标签页的所有可进行输入操作的元素,对元素进行输入操作前优先使用这个方法"""
300 | tab = DP.browser.latest_tab
301 | js_code='''
302 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button'));
303 | return inputElements.filter(el => !el.disabled); // 排除禁用的元素
304 | '''
305 | aaa=tab.run_js(js_code)
306 | return aaa
307 |
308 |
309 | #region 点击元素
310 | @mcp.tool()
311 | def element_click(element_xpath: str) -> Any:
312 | """通过xpath点击标签页中某个元素,最好先判断元素是否存在"""
313 |
314 | l=f"xpath:{element_xpath}"
315 | e=DP.browser.latest_tab.ele(l,timeout=3)
316 | result={"locator":l,"element":e,"click_result":e.click()}
317 | return result
318 |
319 |
320 | #region 元素输入
321 | @mcp.tool()
322 | def element_input(element_xpath: str,input_value: str) -> Any:
323 | """通过xpath给标签页中某个元素输入内容,最好先判断元素是否存在"""
324 | # b=Chromium(debug_port)
325 | locator=f"xpath:{element_xpath}"
326 | if e:=DP.browser.latest_tab.ele(locator,timeout=4):
327 | result={"locator":locator,"result":e.input(input_value)}
328 | return result
329 | else:
330 | return f"元素{locator}不存在,需要getInputElementsInfo先获取元素信息"
331 |
332 |
333 | @mcp.tool()
334 | def get_current_tab_element_html(element_xpath: str) -> str:
335 | """获取当前标签页的某个元素的html"""
336 | # b=Chromium(debug_port)
337 | elem=DP.browser.latest_tab.ele(f"xpath:{element_xpath}")
338 | if elem:
339 | return elem.run_js('return this.outerHTML')
340 | else:
341 | return "No element found"
342 |
343 | @mcp.tool()
344 | def get_body_text() -> str:
345 | """获取当前标签页的body的文本内容"""
346 |
347 | tab:ChromiumTab=DP.browser.latest_tab
348 | body_text=tab('t:body').text
349 | return body_text
350 |
351 |
352 | @mcp.tool()
353 | def run_js(js_code: str) -> Any:
354 | """
355 | 在当前标签页中运行JavaScript代码并返回执行结果
356 | 查找网页元素,获取元素信息,操作网页元素优先使用这个方法
357 |
358 | Args:
359 |
360 | js_code (str): 要执行的JavaScript代码
361 |
362 | Returns:
363 | Any: JavaScript代码执行结果
364 |
365 | Note:
366 | 想要获取执行的js代码的返回值,可以在js_code中使用return语句。
367 | 想要获取异步函数的返回值,可以参考下面代码
368 | return (async (url) => {
369 | const response = await fetch(url);
370 | const data = await response.json();
371 | return data;
372 | })("https://www.baidu.com/");
373 | """
374 |
375 | # b=Chromium(debug_port)
376 |
377 | result=DP.browser.latest_tab.run_js(js_code)
378 | return result
379 | @mcp.tool()
380 | def run_cdp( cmd, **cmd_args) -> Any:
381 | """在当前标签页中运行谷歌CDP协议代码并获取结果
382 |
383 | Args:
384 |
385 | cmd: CDP协议命令
386 | **cmd_args: CDP命令参数
387 |
388 | Returns:
389 | Any: CDP命令执行结果
390 |
391 | Note:
392 | 举例1说明 run_cdp('Page.stopLoading')
393 | 举例2说明 run_cdp('Page.navigate', url='https://example.com')
394 | """
395 | """在当前标签页中运行谷歌CDP协议代码并获取结果"""
396 | # b=Chromium(debug_port)
397 | result=DP.browser.latest_tab.run_cdp(cmd, **cmd_args)
398 | return result
399 |
400 | @mcp.tool()
401 | def on_cdp_event(event_name: str) -> any:
402 | """设置监听CDP事件
403 |
404 | 应该先运行cdp 命令 激活对应的域,比如 Network.enable
405 | """
406 | # b=Chromium(debug_port)
407 | def r(**event):
408 | DP.cdp_event_data.append({"event_name": event_name, "event_data": event})
409 |
410 | try:
411 | DP.browser.latest_tab.driver.set_callback(event_name, r)
412 | return f"CDP event callback for '{event_name}' set successfully."
413 | except Exception as e:
414 | return e
415 |
416 | @mcp.tool()
417 | def get_cdp_event_data() -> list:
418 | """获取CDP事件回调函数收集到的数据"""
419 | return DP.cdp_event_data
420 |
421 | #region 监听网页接收的数据包
422 | @mcp.tool()
423 | def response_received_listener(
424 | mimeType: Literal[
425 | # 文本类
426 | "text/html",
427 | "text/css",
428 | "text/javascript",
429 | "application/javascript",
430 | "text/plain",
431 | "text/xml",
432 | "text/csv",
433 | "application/json",
434 |
435 | # 应用类
436 | "application/octet-stream",
437 | "application/zip",
438 | "application/pdf",
439 | "multipart/form-data",
440 | "application/xml",
441 |
442 | # 图片类
443 | "image/jpeg",
444 | "image/png",
445 | "image/gif",
446 | "image/webp",
447 | "image/svg+xml",
448 | "image/x-icon",
449 |
450 | # 音视频类
451 | "audio/mpeg",
452 | "audio/ogg",
453 | "video/mp4",
454 | "video/webm",
455 | "video/ogg"
456 | ],
457 | url_include: str = "."
458 | ) -> any:
459 | '''
460 | 开启监听网页接收的数据包,
461 | mimeType: 需要监听的接收的数据包的mimeType类型
462 | url_include: 需要监听的接收的数据包的url包含的关键字
463 | '''
464 | t = DP.browser.latest_tab
465 |
466 | if mimeType not in DP.mime_types:
467 | # 如果mimeType不在列表中,返回错误信息
468 | return f"{mimeType} 错误!请在{DP.mime_types}列表中选择mimeType类型"
469 |
470 | t.run_cdp("Network.enable")
471 |
472 | def r(**event):
473 | _url = event.get("response", {}).get("url", "")
474 | _mimeType = event.get("response", {}).get("mimeType", "")
475 |
476 | if mimeType in _mimeType and url_include in _url:
477 | DP.listener_data.append({
478 | "event_name": "Network.responseReceived",
479 | "event_data": event
480 | })
481 |
482 | t.driver.set_callback("Network.responseReceived", r)
483 |
484 | return f"开启监听网页接收的数据包, url包含关键字:{url_include},mimeType:{mimeType}"
485 |
486 |
487 | @mcp.tool()
488 | def response_received_listener_stop():
489 | """关闭监听网页发送的数据包"""
490 | t=DP.browser.latest_tab
491 | t.run_cdp("Network.disable")
492 | return f"监听网页发送的数据包关闭成功 Network.disable"
493 |
494 | @mcp.tool()
495 | def get_response_received_listener_data() -> list:
496 | """获取监听到的数据,返回数据列表"""
497 | return DP.listener_data
498 |
499 |
500 | #region 截图
501 | @mcp.tool()
502 | def get_current_tab_screenshot() -> bytes:
503 | """
504 | 获取当前标签页的网页截图
505 |
506 |
507 | Returns:
508 | bytes: 截图的二进制数据
509 | """
510 | """获取当前标签页的屏幕截图 """
511 | t:ChromiumTab=DP.browser.latest_tab
512 | screenshot=t.get_screenshot(as_bytes='jpeg')
513 | i=Image(data=screenshot,format="jpeg")
514 | return i
515 | @mcp.tool()
516 |
517 | def get_current_tab_screenshot_as_file(path:str=".",name:str="screenshot.png") -> str:
518 | """
519 | 获取当前标签页的屏幕截图并保存为文件
520 |
521 | Args:
522 | path (str): 截图保存路径,默认为当前目录
523 |
524 | Returns:
525 | str: 截图的文件路径
526 | """
527 |
528 | screenshot=DP.browser.latest_tab.get_screenshot(path=path,name=name)
529 | return screenshot
530 |
531 |
532 | @mcp.tool()
533 | def get_current_tab_info(debug_port: int) -> int:
534 | """获取当前标签页的信息,包括url, title, id"""
535 | b=Chromium(debug_port)
536 | tab=b.latest_tab
537 | info={
538 | "url":tab.url,
539 | "title":tab.title,
540 | "id":tab.tab_id,
541 | }
542 | return info
543 |
544 | @mcp.tool()
545 | def get_tab_list(debug_port: int) -> list:
546 | """获取当前浏览器的所有标签页的信息,包括url, title, id"""
547 | b=Chromium(debug_port)
548 | tabs=b.get_tabs
549 | tab_list=[]
550 | for tab in tabs:
551 | info={
552 | "url":tab.url,
553 | "title":tab.title,
554 | "id":tab.id,
555 | }
556 | tab_list.append(info)
557 | return tab_list
558 |
559 |
560 |
561 |
562 | @mcp.tool()
563 | def page_down( ) -> Any:
564 | """向当前标签页发送按键 page_down"""
565 |
566 | tab=DP.browser.latest_tab
567 | result=tab.actions.type(Keys.PAGE_DOWN)
568 | return result
569 |
570 | @mcp.tool()
571 | def page_up( ) -> Any:
572 | """向当前标签页发送按键 page_up"""
573 |
574 | tab=DP.browser.latest_tab
575 | result=tab.actions.type(Keys.PAGE_UP)
576 | return result
577 |
578 | @mcp.tool()
579 | def arrow_down( ) -> Any:
580 | """向当前标签页发送按键 arrow_down"""
581 |
582 | tab=DP.browser.latest_tab
583 | result=tab.actions.type(Keys.DOWN)
584 | return result
585 | @mcp.tool()
586 | def arrow_up( ) -> Any:
587 | """向当前标签页发送按键 arrow_up"""
588 |
589 | tab=DP.browser.latest_tab
590 | result=tab.actions.type(Keys.UP)
591 | return result
592 |
593 | @mcp.tool()
594 | def wait(a:int) -> Any:
595 | """网页等待a秒"""
596 |
597 | tab=DP.browser.latest_tab
598 | result=tab.wait(a)
599 | return result
600 |
601 | @mcp.tool()
602 | def get_dom_tree(depth:int) -> str:
603 | """获取当前标签页的DOM树结构信息
604 |
605 | Args:
606 | depth (int): 指定获取DOM树的深度
607 |
608 | Returns:
609 | str: 返回DOM树的JSON结构数据
610 | """
611 | """获取当前标签页的DOM树的信息"""
612 |
613 | tab=DP.browser.latest_tab
614 | tab.run_cdp("DOM.enable")
615 | # 获取DOM树
616 | result=tab.run_cdp("DOM.getDocument",depth=depth)
617 | return result
618 |
619 | # 合并重复的元素获取接口,统一用 get_elements_by_tagname,并保留兼容性接口
620 | @mcp.tool()
621 | def get_elements_info_by_tagname(tagname: str) -> list:
622 | """获取当前标签页中所有指定tagname的元素信息,定位元素时优先使用 ,参数是tagname"""
623 | tab = DP.browser.latest_tab
624 | elements = tab.eles(f'tag:{tagname}')
625 | return elements
626 |
627 |
628 |
629 | @mcp.tool()
630 | def get_input_elements_info() -> list:
631 | """获取当前标签页中所有input元素标签的信息,定位元素时优先使用"""
632 | return get_elements_info_by_tagname('input')
633 |
634 | @mcp.tool()
635 | def get_button_elements_info() -> list:
636 | """获取当前标签页中所有button元素标签的信息,定位元素时优先使用"""
637 | return get_elements_info_by_tagname('button')
638 |
639 | @mcp.tool()
640 | def get_a_elements_info() -> list:
641 | """获取当前标签页中所有a元素标签的信息,定位元素时优先使用"""
642 | return get_elements_info_by_tagname('a')
643 |
644 | # 根据关键词,获取当前标签中的所有文本节点包含这个关键词的元素列表,返回元素列表
645 | @mcp.tool()
646 | def get_elements_info_by_keyword(keyword: str) -> list:
647 | """根据关键词获取当前标签页中包含该关键词的文本节点的所有元素列表 定位元素时优先使用
648 |
649 | Args:
650 | keyword (str): 要搜索的关键词
651 |
652 | Returns:
653 | list: 返回包含关键词的元素列表
654 | """
655 | tab = DP.browser.latest_tab
656 | # 获取所有文本节点
657 | text_nodes = tab('t:body').eles(keyword)
658 | # 筛选包含关键词的文本节点
659 | return text_nodes
660 |
661 |
662 | class Use:
663 | @staticmethod
664 | def extract_text(s):
665 | # 直接使用正则表达式提取并返回结果
666 | return ''.join(re.findall(r'(?<=>)(.+?)(?=<)', s))
667 |
668 | @staticmethod
669 | def extract_attrs_value(input_string):
670 | # 直接返回匹配结果
671 | return re.findall(r'"[^"]+"', input_string)
672 |
673 | @staticmethod
674 | def extract_attrs_name(input_string):
675 | # 改进正则表达式以更精确地匹配属性名
676 | return re.findall(r'\b\w+(?==")', input_string)
677 |
678 | @staticmethod
679 | def extract_innertext(input_string):
680 | # 使用正则表达式简化内部文本提取
681 | match = re.search(r'>(.*?)<', input_string)
682 | return match.group(1) if match else ''
683 |
684 | @staticmethod
685 | def raw(input_str):
686 | input_str = input_str.strip()
687 | tag_name_match = re.match(r'<(\w+)', input_str)
688 | tag_name = tag_name_match.group(1) if tag_name_match else ''
689 |
690 | tag_attr_values = Use.extract_attrs_value(input_str)
691 | tag_attr_names = Use.extract_attrs_name(input_str)
692 |
693 | attr_all = ''.join([f'@@{name}={value}' for name, value in zip(tag_attr_names, tag_attr_values)])
694 | attr_all = attr_all.replace('"', '')
695 |
696 | txt = Use.extract_text(input_str)
697 | tag_txt = f'@@text()={txt}' if txt else ''
698 |
699 | transformed_str = f'tag:{tag_name}{attr_all}{tag_txt}'
700 | print(transformed_str)
701 | return transformed_str
702 |
703 |
704 | def main():
705 | # 启动MCP服务器
706 | print("DrissionPage MCP server is running...")
707 | mcp.run(transport='stdio')
708 |
709 |
710 | if __name__ == "__main__":
711 | main()
712 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | from typing import Any,Literal
5 | import re
6 | from pathlib import Path
7 | from DrissionPage import Chromium,ChromiumOptions
8 | from mcp.server.fastmcp import FastMCP,Image,Context
9 |
10 | from DrissionPage.items import SessionElement, ChromiumElement, ShadowRoot, NoneElement, ChromiumTab, MixTab, ChromiumFrame
11 | from DrissionPage.common import Keys
12 |
13 |
14 | 提示='''
15 | DrissionPage MCP 是一个基于 DrissionPage 和 FastMCP 的浏览器自动化MCP server服务器,它提供了一系列强大的浏览器操作 API,让您能够轻松通过AI实现网页自动化操作。
16 | 点击元素前,需要先获取页面所有可点击元素的信息,使用get_all_clickable_elements()方法。
17 | 输入元素前,需要先获取页面所有可输入元素的信息,使用get_all_input_elements()方法。
18 |
19 | '''
20 |
21 |
22 |
23 | #region DrissionPageMCP
24 | class DrissionPageMCP():
25 | def __init__(self):
26 | self.browser = None
27 | self.session = None
28 | self.current_tab = None
29 | self.current_frame = None
30 | self.current_shadow_root = None
31 | self.cdp_event_data = []
32 | self.response_listener_data=[]
33 |
34 | def test(self):
35 | return "test"
36 | def get_DrissionPage_code_guide(self)-> str:
37 | """ 获取 DrissionPage 代码指南"""
38 | with open(Path(__file__).parent / "DrissionPage_code_guide.md", "r", encoding="utf-8") as f:
39 | return f.read()
40 | # return "1.0.3"
41 | def get_version(self)-> str:
42 | """ 获取版本号"""
43 | return "1.0.5"
44 | async def connect_or_open_browser(self, config: dict={'debug_port':9222}) -> dict:
45 | """
46 | 用DrissionPage 打开或接管已打开的浏览器,参数通过字典传递。
47 | 必要参数:
48 | config (dict): 可选键包括 、debug_port、browser_path、headless
49 | 返回:
50 | dict: 浏览器信息
51 | """
52 | co = ChromiumOptions()
53 | if config.get("debug_port"):
54 | co.set_local_port(config["debug_port"])
55 | if config.get("browser_path"):
56 | co.set_browser_path(config["browser_path"])
57 | if config.get("headless", False):
58 | co.headless(True)
59 |
60 | self.browser = Chromium(co)
61 | tab = self.browser.latest_tab
62 |
63 | return {
64 | "browser_address": self.browser._chromium_options.address,
65 | "latest_tab_title": tab.title,
66 | "latest_tab_id": tab.tab_id,
67 | "等价Python代码":f'''
68 | from DrissionPage import Chromium, ChromiumOptions
69 | form DrissionPage.common import Keys
70 | # 创建配置对象
71 | co = ChromiumOptions()
72 | co.set_local_port({config["debug_port"]})
73 | # 创建浏览器对象,浏览器对象不能打开网址,只有标签页对象才能打开网址
74 | browser = Chromium(co)
75 | # 获取最新标签页
76 | tab = browser.latest_tab
77 | '''
78 | }
79 |
80 | async def new_tab(self, url: str) -> str:
81 | """用DrissionPage 控制的浏览器,打开新标签页并 打开一个网址"""
82 | tab = self.browser.new_tab(url)
83 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree(),
84 | "等价Python代码":f'''
85 | tab = browser.new_tab('{url}')
86 | ''' }
87 |
88 | def wait(self, a:int) :
89 | """等待a秒"""
90 | self.browser.latest_tab.wait(a)
91 | return {"rsult":f"等待{a}秒成功", "等价Python代码":f"tab.wait({a})"}
92 |
93 | async def get(self,url:str)->str:
94 | """在当前标签页打开一个网址"""
95 | if not self.browser:
96 | await self.connect_or_open_browser()
97 | # return "请先打开或者连接浏览器"
98 | self.lastest_tab.get(url)
99 | tab=self.browser.latest_tab
100 | return {"title": tab.title, "tab_id": tab.tab_id, "url": tab.url,"dom":self.getSimplifiedDomTree(),"等价Python代码":f'''tab.get('{url}')'''}
101 |
102 |
103 |
104 | #region 上传和下载
105 | def download_file(self, url: str, path: str, rename: str) -> str:
106 | """控制浏览器下载文件到指定路径
107 |
108 | Args:
109 | url (str): 文件的URL地址
110 | path (str): 文件保存的路径
111 | rename (str): 重命名文件名
112 |
113 | Returns:
114 | str: 下载结果信息
115 | """
116 | tab = self.lastest_tab
117 | result = tab.download(file_url=url, save_path=path, rename=rename)
118 | return str(result)
119 |
120 | def upload_file(self, file_path: str) -> str:
121 | """点击当网页上的 元素触发上传文件的操作,上传file_path文件到当前网页
122 |
123 | Args:
124 | file_path (str): 要上传的文件路径
125 |
126 | Returns:
127 | str: 上传结果信息,如果元素不存在则返回错误信息
128 | """
129 | x="//input[@type='file']"
130 | t:ChromiumTab=self.lastest_tab
131 | if e:= t(f"xpath:{x}"):
132 | t.set.upload_files(file_path)
133 | e.click(by_js=True)
134 | t.wait.upload_paths_inputted()
135 | return f"{file_path} 上传成功 {e}"
136 | else:
137 | return f"元素{x}不存在,无法触发上传文件"
138 |
139 |
140 |
141 | @property
142 | def lastest_tab(self) -> ChromiumTab:
143 | """获取最新标签页"""
144 | return self.browser.latest_tab
145 |
146 | def send_enter(self) -> str:
147 | """向当前页面发送 enter 回车键"""
148 | tab = self.browser.latest_tab
149 | try:
150 | result = tab.actions.type(Keys.ENTER)
151 | return {"result":f'{tab.title} 网页发送 enter 回车键成功', "等价Python代码":f"tab.actions.type(Keys.ENTER)"}
152 | except Exception as e:
153 | return f"{tab.title} 网页发送 enter 回车键失败"
154 |
155 | def getInputElementsInfo(self) -> list:
156 | """获取当前标签页的所有可进行输入操作的元素,对元素进行输入操作前优先使用这个方法"""
157 | tab = self.browser.latest_tab
158 | js_code='''
159 | const inputElements = Array.from(document.querySelectorAll('input, select, textarea, button'));
160 | return inputElements.filter(el => !el.disabled); // 排除禁用的元素
161 | '''
162 | elements = tab.run_js(js_code)
163 | return elements
164 |
165 | def click_by_xpath(self, xpath: str) -> dict:
166 | """通过xpath点击当前标签页中某个元素,最好先获取页面dom信息,再决定Xpath的写法"""
167 |
168 | locator = f"xpath:{xpath}"
169 | element = self.browser.latest_tab.ele(locator, timeout=3)
170 | result = {"locator": locator, "element": str(element), "click_result": element.click(), "等价Python代码":f"tab.ele('{locator}', timeout=3).click()"}
171 | return result
172 |
173 | def click_by_containing_text(self, content: str, index: int = None) :
174 | """
175 | 根据包含指定文本的方式点击网页元素。
176 |
177 | 参数:
178 | content: 要查找的文本内容。
179 | index: 当匹配到多个元素时指定要点击的索引,默认不指定。
180 |
181 | 返回:
182 | 点击结果说明,或错误提示。
183 | """
184 |
185 | # 获取包含指定文本的所有元素,等待最多 3 秒
186 | elements = self.browser.latest_tab.eles(content, timeout=3)
187 |
188 | # 如果没有匹配到任何元素,返回错误提示
189 | if len(elements) == 0:
190 | return f"元素{content}不存在,需要getInputElementsInfo先获取元素信息"
191 |
192 | # 如果只找到一个元素,直接点击它
193 | if len(elements) == 1:
194 | self.lastest_tab(content).click()
195 | return f" 点击成功"
196 |
197 | # 如果找到多个元素
198 | if len(elements) > 1:
199 | # 如果未指定 index,提示用户提供索引
200 | if index is None:
201 | return f"元素{content}存在多个,请调整 index 参数,index=0表示第一个元素,{elements}"
202 | else:
203 | # 根据指定索引点击对应的元素
204 | elements[index].click()
205 | return f" 点击成功"
206 |
207 |
208 |
209 | def input_by_xapth(self, xpath: str, input_value: str, clear_first: bool = True) :
210 | """通过xpath给当前标签页中某个元素输入内容,最好先判断元素是否存在
211 |
212 | Args:
213 | xpath (str): 元素的XPath表达式
214 | input_value (str): 要输入的内容
215 | clear_first (bool): 是否先清除已有内容,默认为True
216 |
217 | Returns:
218 | Any: 输入操作的结果,如果元素不存在则返回错误信息
219 | """
220 | locator = f"xpath:{xpath}"
221 | if e := self.browser.latest_tab.ele(locator, timeout=4):
222 | result = {"locator": locator, "result": e.input(input_value, clear=clear_first), "等价Python代码":f"tab.ele('{locator}', timeout=4).input({input_value}, clear={clear_first})"}
223 | return result
224 | else:
225 | return f"元素{locator}不存在,需要getInputElementsInfo先获取元素信息"
226 |
227 | def get_body_text(self) -> str:
228 | """获取当前标签页的body的文本内容"""
229 |
230 | tab = self.browser.latest_tab
231 | body_text = tab('t:body').text
232 | r={"body_text":body_text,"等价Python代码":f"tab('t:body').text"}
233 | return r
234 | def run_js(self, js_code: str) :
235 | """
236 | 在当前标签页中运行JavaScript代码并返回执行结果
237 | 查找网页元素,获取元素信息,操作网页元素优先使用这个方法
238 |
239 | Args:
240 | js_code (str): 要执行的JavaScript代码
241 |
242 | Returns:
243 | Any: JavaScript代码执行结果
244 |
245 | Note:
246 | 想要获取执行的js代码的返回值,可以在js_code中使用return语句。
247 | 想要获取异步函数的返回值,可以参考下面代码
248 | return (async (url) => {
249 | const response = await fetch(url);
250 | const data = await response.json();
251 | return data;
252 | })("https://www.baidu.com/");
253 | """
254 | tab = self.browser.latest_tab
255 | result = tab.run_js(js_code)
256 | r={"result":result,"等价Python代码":f"r=tab.run_js('{js_code}')"}
257 | return r
258 |
259 |
260 | def run_cdp( self,cmd, **cmd_args) :
261 | """在当前标签页中运行谷歌CDP协议代码并获取结果
262 |
263 | Args:
264 |
265 | cmd: CDP协议命令
266 | **cmd_args: CDP命令参数
267 |
268 | Returns:
269 | Any: CDP命令执行结果
270 |
271 | Note:
272 | 举例1说明 run_cdp('Page.stopLoading')
273 | 举例2说明 run_cdp('Page.navigate', url='https://example.com')
274 | """
275 | result=self.browser.latest_tab.run_cdp(cmd, **cmd_args)
276 | return result
277 | def listen_cdp_event(self,event_name: str) :
278 | """设置监听CDP事件
279 |
280 | 应该先运行cdp 命令 激活对应的域,比如 Network.enable
281 | """
282 | # b=Chromium(debug_port)
283 | def r(**event):
284 | self.cdp_event_data.append({"event_name": event_name, "event_data": event})
285 |
286 | try:
287 | self.browser.latest_tab.driver.set_callback(event_name, r)
288 | return f"CDP event callback for '{event_name}' set successfully."
289 | except Exception as e:
290 | return e
291 |
292 | def get_cdp_event_data(self) -> list:
293 | """获取CDP事件回调函数收集到的数据"""
294 | return self.cdp_event_data
295 |
296 |
297 |
298 | #region 监听网页接收的数据包
299 |
300 | def get_url_with_response_listener(self,
301 | tab_url: str,
302 | mimeType: Literal[
303 | # 文本类
304 | "text/html",
305 | "text/css",
306 | "text/javascript",
307 | "application/javascript",
308 | "text/plain",
309 | "text/xml",
310 | "text/csv",
311 | "application/json",
312 |
313 | # 应用类
314 | "application/octet-stream",
315 | "application/zip",
316 | "application/pdf",
317 | "multipart/form-data",
318 | "application/xml",
319 |
320 | # 图片类
321 | "image/jpeg",
322 | "image/png",
323 | "image/gif",
324 | "image/webp",
325 | "image/svg+xml",
326 | "image/x-icon",
327 |
328 | # 音视频类
329 | "audio/mpeg",
330 | "audio/ogg",
331 | "video/mp4",
332 | "video/webm",
333 | "video/ogg"
334 | ],
335 | url_include: str = "."
336 | ) :
337 | '''
338 | 开启一个新的标签页,设置监听,访问tab_url,
339 | tab_url: 被监听的标签页的url
340 | mimeType: 需要监听的接收的数据包的mimeType类型
341 | url_include: 需要监听的接收的数据包的url包含的关键字
342 | refresh: 是否刷新页面,
343 | '''
344 | t = self.browser.new_tab(tab_url)
345 |
346 | t.run_cdp("Network.enable")
347 |
348 | def r(**event):
349 | _url = event.get("response", {}).get("url", "")
350 | _mimeType = event.get("response", {}).get("mimeType", "")
351 |
352 | if mimeType in _mimeType and url_include in _url:
353 | self.response_listener_data.append({
354 | "event_name": "Network.responseReceived",
355 | "event_data": event
356 | })
357 |
358 | t.driver.set_callback("Network.responseReceived", r)
359 | t.get(tab_url)
360 |
361 | return f"开启监听{tab_url}, 数据包url包含关键字:{url_include},mimeType:{mimeType}"
362 |
363 |
364 |
365 | def response_listener_stop(self,clear_data:bool=False) -> str:
366 | """关闭监听网页发送的数据包"""
367 | t=self.browser.latest_tab
368 | t.run_cdp("Network.disable")
369 | if clear_data:
370 | self.response_listener_data = []
371 | return f"监听网页发送的数据包关闭成功 ,是否清空数据: {clear_data}"
372 |
373 |
374 | def get_response_listener_data(self) -> list:
375 | """获取监听到的数据,返回数据列表"""
376 | return self.response_listener_data
377 |
378 | def get_current_tab_screenshot(self) -> bytes:
379 | """
380 | 获取当前标签页的网页截图
381 |
382 | Returns:
383 | bytes: 截图的二进制数据
384 | """
385 | t:ChromiumTab=self.browser.latest_tab
386 | screenshot=t.get_screenshot(as_bytes='jpeg')
387 | return screenshot
388 |
389 | def get_current_tab_screenshot_as_file(self,path:str=".",name:str="screenshot.png") -> str:
390 | """
391 | 获取当前标签页的屏幕截图并保存为文件
392 |
393 | Args:
394 | path (str): 截图保存路径,默认为当前目录
395 |
396 | Returns:
397 | str: 截图的文件路径
398 | """
399 |
400 | screenshot=self.browser.latest_tab.get_screenshot(path=path,name=name)
401 | return screenshot
402 |
403 | def get_current_tab_info(self) -> dict:
404 | """获取当前标签页的信息,包括url, title, id"""
405 | tab =self.browser.latest_tab
406 | info = {
407 | "url": tab.url,
408 | "title": tab.title,
409 | "id": tab.tab_id,
410 | }
411 | return info
412 |
413 | def send_key(self, key: Literal["Enter, Backspace, HOME, END, PAGE_UP, PAGE_DOWN, DOWN, UP, LEFT, RIGHT, ESC, Ctrl+C, Ctrl+V, Ctrl+A, Delete"]) -> str:
414 | """向当前标签页发送特殊按键"""
415 | tab = self.browser.latest_tab
416 | k={"Enter": Keys.ENTER,
417 | "Backspace": Keys.BACKSPACE,
418 | "HOME": Keys.HOME,
419 | "END": Keys.END,
420 | "PAGE_UP": Keys.PAGE_UP,
421 | "PAGE_DOWN": Keys.PAGE_DOWN,
422 | "DOWN": Keys.DOWN,
423 | "UP": Keys.UP,
424 | "LEFT": Keys.LEFT,
425 | "RIGHT": Keys.RIGHT,
426 | "ESC": Keys.ESCAPE,
427 | "Ctrl+C": Keys.CTRL_C,
428 | "Ctrl+V": Keys.CTRL_V,
429 | "Ctrl+A": Keys.CTRL_A,
430 | "Delete": Keys.DELETE,}
431 | try:
432 | result = tab.actions.type(k.get(key))
433 | return f"{tab.title} 网页发送 {key} 键成功"
434 | except Exception as e:
435 | return f"{tab.title} 网页发送 {key} 键失败"
436 |
437 | def getSimplifiedDomTree2(self) -> dict:
438 | """获取当前标签页的简化版DOM树"""
439 | from CodeBox import domTreeToJson
440 | tab = self.browser.latest_tab
441 | dom_tree = tab.run_js(domTreeToJson)
442 | return dom_tree
443 |
444 | def getSimplifiedDomTree(self):
445 | tab=self.browser.latest_tab
446 | tab.run_cdp('Accessibility.enable')
447 | tree = tab.run_cdp('Accessibility.getFullAXTree')
448 | tempAXTree = AXTreeFormatter(tree)
449 | tempAXTree.parse_tree()
450 | return tempAXTree.all_data
451 |
452 | #region 拖动
453 |
454 | def move_to(self,xpath:str) -> dict:
455 | """鼠标移动悬停到指定xpath的元素上"""
456 | tab = self.browser.latest_tab
457 | locator = f"xpath:{xpath}"
458 | element = tab.ele(locator, timeout=3)
459 | if element:
460 | element.hover()
461 | result = {"locator": locator, "element": str(element)}
462 | return result
463 | else:
464 | return f"元素{locator}不存在,需要getSimplifiedDomTree先获取元素信息"
465 | def drag(self,xpath:str, offset_x: int, offset_y: int, duration: int = 1000) -> dict:
466 |
467 | """
468 | 将元素拖动到指定偏移位置
469 |
470 | Args:
471 | xpath: 要拖动的元素xpath路径
472 | offset_x: x轴偏移量(像素)
473 | offset_y: y轴偏移量(像素)
474 | duration: 拖动持续时间(毫秒),默认为1000
475 |
476 | Returns:
477 | dict: 包含偏移量和持续时间的字典,格式为{"offset_x": int, "offset_y": int, "duration": int}
478 | 或 str: 当元素不存在时返回错误信息
479 |
480 | Raises:
481 | 无显式抛出异常,但内部可能因元素不存在而返回错误信息
482 | """
483 | tab = self.browser.latest_tab
484 | if e:=tab.ele(f'xpath:{xpath}', timeout=3):
485 | tab.actions.move_to(e).wait(0.5).hold().move(offset_x, offset_y).release()
486 | result = {"offset_x": offset_x, "offset_y": offset_y, "duration": duration}
487 | return result
488 | else:
489 | return f"元素{xpath}不存在,需要getSimplifiedDomTree先获取元素信息"
490 |
491 | #region AXtree
492 | class AXTreeFormatter:
493 | def __init__(self, raw_data):
494 | self.nodes_map = {}
495 | self.root_id = None
496 | self._build_lookup(raw_data.get('nodes', []))
497 | self.all_data = ""
498 |
499 | def _build_lookup(self, nodes_list):
500 | all_child_ids = set()
501 |
502 | for node in nodes_list:
503 | self.nodes_map[node['nodeId']] = node
504 | if 'childIds' in node:
505 | for child_id in node['childIds']:
506 | all_child_ids.add(child_id)
507 |
508 | for node_id in self.nodes_map:
509 | if node_id not in all_child_ids:
510 | self.root_id = node_id
511 | break
512 |
513 | def _get_value(self, obj, key):
514 | """获取属性值"""
515 | if not obj:
516 | return None
517 | target = obj.get(key)
518 | if isinstance(target, dict) and 'value' in target:
519 | return target['value']
520 | return target
521 |
522 | def _get_properties(self, node):
523 | """提取 tagName, id, class"""
524 | result = {}
525 | properties = node.get('properties', [])
526 |
527 | for prop in properties:
528 | name = prop.get('name')
529 | value = prop.get('value', {})
530 |
531 | if isinstance(value, dict):
532 | prop_value = value.get('value')
533 | else:
534 | prop_value = value
535 |
536 | # 只提取我们关心的属性
537 | if name in ['htmlTag', 'id', 'class']:
538 | result[name] = prop_value
539 |
540 | return result
541 |
542 | def _format_node(self, node):
543 | """格式化节点信息"""
544 | role = self._get_value(node, 'role') or "Unknown"
545 | name = self._get_value(node, 'name') or ""
546 |
547 | props = self._get_properties(node)
548 | tag = props.get('htmlTag', '')
549 | node_id = props.get('id', '')
550 | node_class = props.get('class', '')
551 |
552 | # 构建简洁的输出 - 优先显示 HTML 标签
553 | parts = []
554 |
555 | if tag:
556 | # 如果有标签名,就显示标签,role 放在后面(可选)
557 | parts.append(f"<{tag}>")
558 | # 如果 role 不是 generic,也显示出来
559 | if role != 'generic':
560 | parts.append(f"[{role}]")
561 | else:
562 | # 没有标签就显示 role
563 | parts.append(role)
564 |
565 | if node_id:
566 | parts.append(f"#{node_id}")
567 |
568 | if node_class:
569 | # class 太长就截断
570 | if len(node_class) > 40:
571 | node_class = node_class[:37] + "..."
572 | parts.append(f".{node_class}")
573 |
574 | if name:
575 | # 文本内容太长就截断
576 | if len(name) > 50:
577 | name = name[:47] + "..."
578 | parts.append(f'"{name}"')
579 |
580 | return " ".join(parts)
581 |
582 | def print_tree(self, node_id=None, level=0):
583 | if node_id is None:
584 | node_id = self.root_id
585 |
586 | node = self.nodes_map.get(node_id)
587 | if not node:
588 | return
589 |
590 | indent = " " * level
591 | self.all_data += f"{indent}- {self._format_node(node)}\n"
592 |
593 | # 递归子节点
594 | for child_id in node.get('childIds', []):
595 | self.print_tree(child_id, level + 1)
596 | def parse_tree(self, node_id=None, level=0):
597 | if node_id is None:
598 | node_id = self.root_id
599 |
600 | node = self.nodes_map.get(node_id)
601 | if not node:
602 | return
603 |
604 | indent = " " * level
605 | self.all_data += f"{indent}- {self._format_node(node)}\n"
606 |
607 | # 递归子节点
608 | for child_id in node.get('childIds', []):
609 | self.parse_tree(child_id, level + 1)
610 |
611 |
612 |
613 |
614 |
615 | #region 初始化mcp
616 | mcp = FastMCP("DrissionPageMCP", log_level="ERROR",instructions=提示)
617 | b=DrissionPageMCP()
618 |
619 | mcp.add_tool(b.get_version)
620 | mcp.add_tool(b.get_DrissionPage_code_guide)
621 | mcp.add_tool(b.connect_or_open_browser)
622 | mcp.add_tool(b.new_tab)
623 | mcp.add_tool(b.wait)
624 | mcp.add_tool(b.get)
625 | mcp.add_tool(b.download_file)
626 | mcp.add_tool(b.upload_file)
627 | mcp.add_tool(b.send_enter)
628 | mcp.add_tool(b.getInputElementsInfo)
629 | mcp.add_tool(b.click_by_xpath)
630 | mcp.add_tool(b.click_by_containing_text)
631 | mcp.add_tool(b.input_by_xapth)
632 | mcp.add_tool(b.get_body_text)
633 | mcp.add_tool(b.run_js)
634 | mcp.add_tool(b.run_cdp)
635 | mcp.add_tool(b.listen_cdp_event)
636 | mcp.add_tool(b.get_cdp_event_data)
637 | mcp.add_tool(b.get_url_with_response_listener)
638 | mcp.add_tool(b.response_listener_stop)
639 | mcp.add_tool(b.get_response_listener_data)
640 | mcp.add_tool(b.get_current_tab_screenshot)
641 | mcp.add_tool(b.get_current_tab_screenshot_as_file)
642 | mcp.add_tool(b.get_current_tab_info)
643 | mcp.add_tool(b.send_key)
644 | mcp.add_tool(b.getSimplifiedDomTree)
645 |
646 | mcp.add_tool(b.move_to)
647 | mcp.add_tool(b.drag)
648 |
649 | #region 保存数据到sqlite
650 | from ToolBox import save_dict_to_sqlite
651 | mcp.add_tool(save_dict_to_sqlite)
652 |
653 |
654 |
655 |
656 | def main():
657 | # 启动MCP服务器
658 | print("DrissionPage MCP server is running...")
659 | mcp.run(transport='stdio')
660 |
661 |
662 | if __name__ == "__main__":
663 | main()
664 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 2
3 | requires-python = ">=3.13"
4 |
5 | [[package]]
6 | name = "annotated-types"
7 | version = "0.7.0"
8 | source = { registry = "https://pypi.org/simple" }
9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
10 | wheels = [
11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
12 | ]
13 |
14 | [[package]]
15 | name = "anyio"
16 | version = "4.9.0"
17 | source = { registry = "https://pypi.org/simple" }
18 | dependencies = [
19 | { name = "idna" },
20 | { name = "sniffio" },
21 | ]
22 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
23 | wheels = [
24 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
25 | ]
26 |
27 | [[package]]
28 | name = "certifi"
29 | version = "2025.4.26"
30 | source = { registry = "https://pypi.org/simple" }
31 | sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
32 | wheels = [
33 | { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
34 | ]
35 |
36 | [[package]]
37 | name = "charset-normalizer"
38 | version = "3.4.2"
39 | source = { registry = "https://pypi.org/simple" }
40 | sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
41 | wheels = [
42 | { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
43 | { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
44 | { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
45 | { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
46 | { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
47 | { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
48 | { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
49 | { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
50 | { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
51 | { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
52 | { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
53 | { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
54 | { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
55 | { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
56 | ]
57 |
58 | [[package]]
59 | name = "click"
60 | version = "8.2.1"
61 | source = { registry = "https://pypi.org/simple" }
62 | dependencies = [
63 | { name = "colorama", marker = "sys_platform == 'win32'" },
64 | ]
65 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
66 | wheels = [
67 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
68 | ]
69 |
70 | [[package]]
71 | name = "colorama"
72 | version = "0.4.6"
73 | source = { registry = "https://pypi.org/simple" }
74 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
75 | wheels = [
76 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
77 | ]
78 |
79 | [[package]]
80 | name = "cssselect"
81 | version = "1.3.0"
82 | source = { registry = "https://pypi.org/simple" }
83 | sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" }
84 | wheels = [
85 | { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" },
86 | ]
87 |
88 | [[package]]
89 | name = "datarecorder"
90 | version = "3.6.2"
91 | source = { registry = "https://pypi.org/simple" }
92 | dependencies = [
93 | { name = "openpyxl" },
94 | ]
95 | sdist = { url = "https://files.pythonhosted.org/packages/b4/a1/43d52eada4e1364f8d1a5da99b855798f945228ffb7dfb199f675c1d2c2b/DataRecorder-3.6.2.tar.gz", hash = "sha256:8c9024736692af68b947fd884589e685c4f27bc29d031b81164457b09c60e1e5", size = 29567, upload-time = "2024-10-15T08:58:36.923Z" }
96 | wheels = [
97 | { url = "https://files.pythonhosted.org/packages/33/05/f0064d93a0b922ea7ad5d40a0dfaac45de37f783a85a2900e6ce8a01dbc3/DataRecorder-3.6.2-py3-none-any.whl", hash = "sha256:41ad022c4c1db58a0f4236f6a4991d1f98cd76ec049484b172bbb3040455e5ff", size = 37539, upload-time = "2024-10-15T08:58:35.419Z" },
98 | ]
99 |
100 | [[package]]
101 | name = "downloadkit"
102 | version = "2.0.7"
103 | source = { registry = "https://pypi.org/simple" }
104 | dependencies = [
105 | { name = "datarecorder" },
106 | { name = "requests" },
107 | ]
108 | sdist = { url = "https://files.pythonhosted.org/packages/6b/35/9bbaed54f5d170b1fd8bcfdd3ce5e24ea71f4cac5026a37e6b21ab69726e/DownloadKit-2.0.7.tar.gz", hash = "sha256:601e423d1d4d0bd3e933524d06c913e744577d455990ec20afc3fb1f79aea9a7", size = 17488, upload-time = "2024-11-30T01:24:30.539Z" }
109 | wheels = [
110 | { url = "https://files.pythonhosted.org/packages/1b/89/979ad2407eb8e0a2ac67461b7bec7db8627218fe3ba287db49636ca0a60c/DownloadKit-2.0.7-py3-none-any.whl", hash = "sha256:55b142d26b4e7ae01fff2181991c92aca9b895f75e2cd8a54f4a3b48c50b90b4", size = 21706, upload-time = "2024-11-30T01:24:17.599Z" },
111 | ]
112 |
113 | [[package]]
114 | name = "drissionpage"
115 | version = "4.1.0.18"
116 | source = { registry = "https://pypi.org/simple" }
117 | dependencies = [
118 | { name = "click" },
119 | { name = "cssselect" },
120 | { name = "downloadkit" },
121 | { name = "lxml" },
122 | { name = "psutil" },
123 | { name = "requests" },
124 | { name = "tldextract" },
125 | { name = "websocket-client" },
126 | ]
127 | sdist = { url = "https://files.pythonhosted.org/packages/3f/54/9e99d96c7a5909a7f2ecc7bdc2978702ca5df20b45ea7f331b306c7e9b57/drissionpage-4.1.0.18.tar.gz", hash = "sha256:ea3193c628bed6f6f11b401d1e07161355ed3060d9c9ce12163df381bd09bf32", size = 206670, upload-time = "2025-03-24T15:39:23.039Z" }
128 | wheels = [
129 | { url = "https://files.pythonhosted.org/packages/e6/83/7ed71f4e0c8d60c9aefb24f0bb95f8a8b14e090718cf3fff8a0083d33164/DrissionPage-4.1.0.18-py3-none-any.whl", hash = "sha256:5492189161e6bde036737aab0874dec7723c38153cb22815f24dba88a2fdfa57", size = 256899, upload-time = "2025-03-24T15:39:21.261Z" },
130 | ]
131 |
132 | [[package]]
133 | name = "drssionpagemcp"
134 | version = "0.1.0"
135 | source = { virtual = "." }
136 | dependencies = [
137 | { name = "drissionpage" },
138 | { name = "fastmcp" },
139 | ]
140 |
141 | [package.metadata]
142 | requires-dist = [
143 | { name = "drissionpage", specifier = ">=4.1.0.18" },
144 | { name = "fastmcp", specifier = ">=2.4.0" },
145 | ]
146 |
147 | [[package]]
148 | name = "et-xmlfile"
149 | version = "2.0.0"
150 | source = { registry = "https://pypi.org/simple" }
151 | sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
152 | wheels = [
153 | { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
154 | ]
155 |
156 | [[package]]
157 | name = "exceptiongroup"
158 | version = "1.3.0"
159 | source = { registry = "https://pypi.org/simple" }
160 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
161 | wheels = [
162 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
163 | ]
164 |
165 | [[package]]
166 | name = "fastmcp"
167 | version = "2.4.0"
168 | source = { registry = "https://pypi.org/simple" }
169 | dependencies = [
170 | { name = "exceptiongroup" },
171 | { name = "httpx" },
172 | { name = "mcp" },
173 | { name = "openapi-pydantic" },
174 | { name = "python-dotenv" },
175 | { name = "rich" },
176 | { name = "typer" },
177 | { name = "websockets" },
178 | ]
179 | sdist = { url = "https://files.pythonhosted.org/packages/50/11/2ccd6219eb65692a298e764fa84a15fd756e03c811c7ea217129d6ca545f/fastmcp-2.4.0.tar.gz", hash = "sha256:a08d812939d16c0d4490bdbdaf17ab136f1bdaa8ddcc14a37e33335727343c05", size = 1020290, upload-time = "2025-05-21T19:58:46.836Z" }
180 | wheels = [
181 | { url = "https://files.pythonhosted.org/packages/ab/a4/26396706c1b48ebd051da4f7d321594207364077a5c308827722536b927c/fastmcp-2.4.0-py3-none-any.whl", hash = "sha256:fccd0768028a31eec488707fb6bbe4f8659f84ca0c206c4c32dd33947c0faae9", size = 101108, upload-time = "2025-05-21T19:58:45.738Z" },
182 | ]
183 |
184 | [[package]]
185 | name = "filelock"
186 | version = "3.18.0"
187 | source = { registry = "https://pypi.org/simple" }
188 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
189 | wheels = [
190 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
191 | ]
192 |
193 | [[package]]
194 | name = "h11"
195 | version = "0.16.0"
196 | source = { registry = "https://pypi.org/simple" }
197 | sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
198 | wheels = [
199 | { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
200 | ]
201 |
202 | [[package]]
203 | name = "httpcore"
204 | version = "1.0.9"
205 | source = { registry = "https://pypi.org/simple" }
206 | dependencies = [
207 | { name = "certifi" },
208 | { name = "h11" },
209 | ]
210 | sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
211 | wheels = [
212 | { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
213 | ]
214 |
215 | [[package]]
216 | name = "httpx"
217 | version = "0.28.1"
218 | source = { registry = "https://pypi.org/simple" }
219 | dependencies = [
220 | { name = "anyio" },
221 | { name = "certifi" },
222 | { name = "httpcore" },
223 | { name = "idna" },
224 | ]
225 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
226 | wheels = [
227 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
228 | ]
229 |
230 | [[package]]
231 | name = "httpx-sse"
232 | version = "0.4.0"
233 | source = { registry = "https://pypi.org/simple" }
234 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" }
235 | wheels = [
236 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" },
237 | ]
238 |
239 | [[package]]
240 | name = "idna"
241 | version = "3.10"
242 | source = { registry = "https://pypi.org/simple" }
243 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
244 | wheels = [
245 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
246 | ]
247 |
248 | [[package]]
249 | name = "lxml"
250 | version = "5.4.0"
251 | source = { registry = "https://pypi.org/simple" }
252 | sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
253 | wheels = [
254 | { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
255 | { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
256 | { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
257 | { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
258 | { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
259 | { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
260 | { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
261 | { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
262 | { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
263 | { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
264 | { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
265 | { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
266 | { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
267 | { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
268 | { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
269 | { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
270 | { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
271 | ]
272 |
273 | [[package]]
274 | name = "markdown-it-py"
275 | version = "3.0.0"
276 | source = { registry = "https://pypi.org/simple" }
277 | dependencies = [
278 | { name = "mdurl" },
279 | ]
280 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
281 | wheels = [
282 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
283 | ]
284 |
285 | [[package]]
286 | name = "mcp"
287 | version = "1.9.0"
288 | source = { registry = "https://pypi.org/simple" }
289 | dependencies = [
290 | { name = "anyio" },
291 | { name = "httpx" },
292 | { name = "httpx-sse" },
293 | { name = "pydantic" },
294 | { name = "pydantic-settings" },
295 | { name = "python-multipart" },
296 | { name = "sse-starlette" },
297 | { name = "starlette" },
298 | { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
299 | ]
300 | sdist = { url = "https://files.pythonhosted.org/packages/bc/8d/0f4468582e9e97b0a24604b585c651dfd2144300ecffd1c06a680f5c8861/mcp-1.9.0.tar.gz", hash = "sha256:905d8d208baf7e3e71d70c82803b89112e321581bcd2530f9de0fe4103d28749", size = 281432, upload-time = "2025-05-15T18:51:06.615Z" }
301 | wheels = [
302 | { url = "https://files.pythonhosted.org/packages/a5/d5/22e36c95c83c80eb47c83f231095419cf57cf5cca5416f1c960032074c78/mcp-1.9.0-py3-none-any.whl", hash = "sha256:9dfb89c8c56f742da10a5910a1f64b0d2ac2c3ed2bd572ddb1cfab7f35957178", size = 125082, upload-time = "2025-05-15T18:51:04.916Z" },
303 | ]
304 |
305 | [[package]]
306 | name = "mdurl"
307 | version = "0.1.2"
308 | source = { registry = "https://pypi.org/simple" }
309 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
310 | wheels = [
311 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
312 | ]
313 |
314 | [[package]]
315 | name = "openapi-pydantic"
316 | version = "0.5.1"
317 | source = { registry = "https://pypi.org/simple" }
318 | dependencies = [
319 | { name = "pydantic" },
320 | ]
321 | sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
322 | wheels = [
323 | { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
324 | ]
325 |
326 | [[package]]
327 | name = "openpyxl"
328 | version = "3.1.5"
329 | source = { registry = "https://pypi.org/simple" }
330 | dependencies = [
331 | { name = "et-xmlfile" },
332 | ]
333 | sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
334 | wheels = [
335 | { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
336 | ]
337 |
338 | [[package]]
339 | name = "psutil"
340 | version = "7.0.0"
341 | source = { registry = "https://pypi.org/simple" }
342 | sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
343 | wheels = [
344 | { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
345 | { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
346 | { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
347 | { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
348 | { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
349 | { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
350 | { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
351 | ]
352 |
353 | [[package]]
354 | name = "pydantic"
355 | version = "2.11.4"
356 | source = { registry = "https://pypi.org/simple" }
357 | dependencies = [
358 | { name = "annotated-types" },
359 | { name = "pydantic-core" },
360 | { name = "typing-extensions" },
361 | { name = "typing-inspection" },
362 | ]
363 | sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" }
364 | wheels = [
365 | { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" },
366 | ]
367 |
368 | [[package]]
369 | name = "pydantic-core"
370 | version = "2.33.2"
371 | source = { registry = "https://pypi.org/simple" }
372 | dependencies = [
373 | { name = "typing-extensions" },
374 | ]
375 | sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
376 | wheels = [
377 | { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
378 | { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
379 | { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
380 | { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
381 | { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
382 | { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
383 | { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
384 | { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
385 | { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
386 | { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
387 | { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
388 | { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
389 | { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
390 | { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
391 | { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
392 | { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
393 | { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
394 | ]
395 |
396 | [[package]]
397 | name = "pydantic-settings"
398 | version = "2.9.1"
399 | source = { registry = "https://pypi.org/simple" }
400 | dependencies = [
401 | { name = "pydantic" },
402 | { name = "python-dotenv" },
403 | { name = "typing-inspection" },
404 | ]
405 | sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
406 | wheels = [
407 | { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
408 | ]
409 |
410 | [[package]]
411 | name = "pygments"
412 | version = "2.19.1"
413 | source = { registry = "https://pypi.org/simple" }
414 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
415 | wheels = [
416 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
417 | ]
418 |
419 | [[package]]
420 | name = "python-dotenv"
421 | version = "1.1.0"
422 | source = { registry = "https://pypi.org/simple" }
423 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
424 | wheels = [
425 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
426 | ]
427 |
428 | [[package]]
429 | name = "python-multipart"
430 | version = "0.0.20"
431 | source = { registry = "https://pypi.org/simple" }
432 | sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
433 | wheels = [
434 | { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
435 | ]
436 |
437 | [[package]]
438 | name = "requests"
439 | version = "2.32.3"
440 | source = { registry = "https://pypi.org/simple" }
441 | dependencies = [
442 | { name = "certifi" },
443 | { name = "charset-normalizer" },
444 | { name = "idna" },
445 | { name = "urllib3" },
446 | ]
447 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
448 | wheels = [
449 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
450 | ]
451 |
452 | [[package]]
453 | name = "requests-file"
454 | version = "2.1.0"
455 | source = { registry = "https://pypi.org/simple" }
456 | dependencies = [
457 | { name = "requests" },
458 | ]
459 | sdist = { url = "https://files.pythonhosted.org/packages/72/97/bf44e6c6bd8ddbb99943baf7ba8b1a8485bcd2fe0e55e5708d7fee4ff1ae/requests_file-2.1.0.tar.gz", hash = "sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658", size = 6891, upload-time = "2024-05-21T16:28:00.24Z" }
460 | wheels = [
461 | { url = "https://files.pythonhosted.org/packages/d7/25/dd878a121fcfdf38f52850f11c512e13ec87c2ea72385933818e5b6c15ce/requests_file-2.1.0-py2.py3-none-any.whl", hash = "sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c", size = 4244, upload-time = "2024-05-21T16:27:57.733Z" },
462 | ]
463 |
464 | [[package]]
465 | name = "rich"
466 | version = "14.0.0"
467 | source = { registry = "https://pypi.org/simple" }
468 | dependencies = [
469 | { name = "markdown-it-py" },
470 | { name = "pygments" },
471 | ]
472 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
473 | wheels = [
474 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
475 | ]
476 |
477 | [[package]]
478 | name = "shellingham"
479 | version = "1.5.4"
480 | source = { registry = "https://pypi.org/simple" }
481 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
482 | wheels = [
483 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
484 | ]
485 |
486 | [[package]]
487 | name = "sniffio"
488 | version = "1.3.1"
489 | source = { registry = "https://pypi.org/simple" }
490 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
491 | wheels = [
492 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
493 | ]
494 |
495 | [[package]]
496 | name = "sse-starlette"
497 | version = "2.3.5"
498 | source = { registry = "https://pypi.org/simple" }
499 | dependencies = [
500 | { name = "anyio" },
501 | { name = "starlette" },
502 | ]
503 | sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511, upload-time = "2025-05-12T18:23:52.601Z" }
504 | wheels = [
505 | { url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233, upload-time = "2025-05-12T18:23:50.722Z" },
506 | ]
507 |
508 | [[package]]
509 | name = "starlette"
510 | version = "0.46.2"
511 | source = { registry = "https://pypi.org/simple" }
512 | dependencies = [
513 | { name = "anyio" },
514 | ]
515 | sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
516 | wheels = [
517 | { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
518 | ]
519 |
520 | [[package]]
521 | name = "tldextract"
522 | version = "5.3.0"
523 | source = { registry = "https://pypi.org/simple" }
524 | dependencies = [
525 | { name = "filelock" },
526 | { name = "idna" },
527 | { name = "requests" },
528 | { name = "requests-file" },
529 | ]
530 | sdist = { url = "https://files.pythonhosted.org/packages/97/78/182641ea38e3cfd56e9c7b3c0d48a53d432eea755003aa544af96403d4ac/tldextract-5.3.0.tar.gz", hash = "sha256:b3d2b70a1594a0ecfa6967d57251527d58e00bb5a91a74387baa0d87a0678609", size = 128502, upload-time = "2025-04-22T06:19:37.491Z" }
531 | wheels = [
532 | { url = "https://files.pythonhosted.org/packages/67/7c/ea488ef48f2f544566947ced88541bc45fae9e0e422b2edbf165ee07da99/tldextract-5.3.0-py3-none-any.whl", hash = "sha256:f70f31d10b55c83993f55e91ecb7c5d84532a8972f22ec578ecfbe5ea2292db2", size = 107384, upload-time = "2025-04-22T06:19:36.304Z" },
533 | ]
534 |
535 | [[package]]
536 | name = "typer"
537 | version = "0.15.3"
538 | source = { registry = "https://pypi.org/simple" }
539 | dependencies = [
540 | { name = "click" },
541 | { name = "rich" },
542 | { name = "shellingham" },
543 | { name = "typing-extensions" },
544 | ]
545 | sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641, upload-time = "2025-04-28T21:40:59.204Z" }
546 | wheels = [
547 | { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253, upload-time = "2025-04-28T21:40:56.269Z" },
548 | ]
549 |
550 | [[package]]
551 | name = "typing-extensions"
552 | version = "4.13.2"
553 | source = { registry = "https://pypi.org/simple" }
554 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
555 | wheels = [
556 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
557 | ]
558 |
559 | [[package]]
560 | name = "typing-inspection"
561 | version = "0.4.1"
562 | source = { registry = "https://pypi.org/simple" }
563 | dependencies = [
564 | { name = "typing-extensions" },
565 | ]
566 | sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
567 | wheels = [
568 | { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
569 | ]
570 |
571 | [[package]]
572 | name = "urllib3"
573 | version = "2.4.0"
574 | source = { registry = "https://pypi.org/simple" }
575 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
576 | wheels = [
577 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
578 | ]
579 |
580 | [[package]]
581 | name = "uvicorn"
582 | version = "0.34.2"
583 | source = { registry = "https://pypi.org/simple" }
584 | dependencies = [
585 | { name = "click" },
586 | { name = "h11" },
587 | ]
588 | sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" }
589 | wheels = [
590 | { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" },
591 | ]
592 |
593 | [[package]]
594 | name = "websocket-client"
595 | version = "1.8.0"
596 | source = { registry = "https://pypi.org/simple" }
597 | sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" }
598 | wheels = [
599 | { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" },
600 | ]
601 |
602 | [[package]]
603 | name = "websockets"
604 | version = "15.0.1"
605 | source = { registry = "https://pypi.org/simple" }
606 | sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
607 | wheels = [
608 | { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
609 | { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
610 | { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
611 | { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
612 | { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
613 | { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
614 | { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
615 | { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
616 | { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
617 | { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
618 | { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
619 | { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
620 | ]
621 |
--------------------------------------------------------------------------------