mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-15 14:22:55 +08:00
✨ 添加仓库目录多获取渠道
This commit is contained in:
parent
8615eb20d4
commit
7288d5bdba
@ -2,6 +2,7 @@ from typing import cast
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import pytest
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
from respx import MockRouter
|
from respx import MockRouter
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
@ -14,7 +15,9 @@ from tests.config import BotId, UserId, GroupId, MessageId
|
|||||||
from tests.builtin_plugins.plugin_store.utils import init_mocked_api
|
from tests.builtin_plugins.plugin_store.utils import init_mocked_api
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("package_api", ["jsd", "gh"])
|
||||||
async def test_add_plugin_basic(
|
async def test_add_plugin_basic(
|
||||||
|
package_api: str,
|
||||||
app: App,
|
app: App,
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
mocked_api: MockRouter,
|
mocked_api: MockRouter,
|
||||||
@ -32,6 +35,11 @@ async def test_add_plugin_basic(
|
|||||||
new=tmp_path / "zhenxun",
|
new=tmp_path / "zhenxun",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if package_api != "jsd":
|
||||||
|
mocked_api["zhenxun_bot_plugins_metadata"].respond(404)
|
||||||
|
if package_api != "gh":
|
||||||
|
mocked_api["zhenxun_bot_plugins_tree"].respond(404)
|
||||||
|
|
||||||
plugin_id = 1
|
plugin_id = 1
|
||||||
|
|
||||||
async with app.test_matcher(_matcher) as ctx:
|
async with app.test_matcher(_matcher) as ctx:
|
||||||
@ -61,12 +69,13 @@ async def test_add_plugin_basic(
|
|||||||
)
|
)
|
||||||
assert mocked_api["basic_plugins"].called
|
assert mocked_api["basic_plugins"].called
|
||||||
assert mocked_api["extra_plugins"].called
|
assert mocked_api["extra_plugins"].called
|
||||||
assert mocked_api["zhenxun_bot_plugins_metadata"].called
|
|
||||||
assert mocked_api["search_image_plugin_file_init"].called
|
assert mocked_api["search_image_plugin_file_init"].called
|
||||||
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
|
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("package_api", ["jsd", "gh"])
|
||||||
async def test_add_plugin_basic_is_not_dir(
|
async def test_add_plugin_basic_is_not_dir(
|
||||||
|
package_api: str,
|
||||||
app: App,
|
app: App,
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
mocked_api: MockRouter,
|
mocked_api: MockRouter,
|
||||||
@ -84,6 +93,11 @@ async def test_add_plugin_basic_is_not_dir(
|
|||||||
new=tmp_path / "zhenxun",
|
new=tmp_path / "zhenxun",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if package_api != "jsd":
|
||||||
|
mocked_api["zhenxun_bot_plugins_metadata"].respond(404)
|
||||||
|
if package_api != "gh":
|
||||||
|
mocked_api["zhenxun_bot_plugins_tree"].respond(404)
|
||||||
|
|
||||||
plugin_id = 0
|
plugin_id = 0
|
||||||
|
|
||||||
async with app.test_matcher(_matcher) as ctx:
|
async with app.test_matcher(_matcher) as ctx:
|
||||||
@ -113,12 +127,13 @@ async def test_add_plugin_basic_is_not_dir(
|
|||||||
)
|
)
|
||||||
assert mocked_api["basic_plugins"].called
|
assert mocked_api["basic_plugins"].called
|
||||||
assert mocked_api["extra_plugins"].called
|
assert mocked_api["extra_plugins"].called
|
||||||
assert mocked_api["zhenxun_bot_plugins_metadata"].called
|
|
||||||
assert mocked_api["jitang_plugin_file"].called
|
assert mocked_api["jitang_plugin_file"].called
|
||||||
assert (mock_base_path / "plugins" / "alapi" / "jitang.py").is_file()
|
assert (mock_base_path / "plugins" / "alapi" / "jitang.py").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("package_api", ["jsd", "gh"])
|
||||||
async def test_add_plugin_extra(
|
async def test_add_plugin_extra(
|
||||||
|
package_api: str,
|
||||||
app: App,
|
app: App,
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
mocked_api: MockRouter,
|
mocked_api: MockRouter,
|
||||||
@ -136,6 +151,11 @@ async def test_add_plugin_extra(
|
|||||||
new=tmp_path / "zhenxun",
|
new=tmp_path / "zhenxun",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if package_api != "jsd":
|
||||||
|
mocked_api["zhenxun_github_sub_metadata"].respond(404)
|
||||||
|
if package_api != "gh":
|
||||||
|
mocked_api["zhenxun_github_sub_tree"].respond(404)
|
||||||
|
|
||||||
plugin_id = 3
|
plugin_id = 3
|
||||||
|
|
||||||
async with app.test_matcher(_matcher) as ctx:
|
async with app.test_matcher(_matcher) as ctx:
|
||||||
@ -165,7 +185,6 @@ async def test_add_plugin_extra(
|
|||||||
)
|
)
|
||||||
assert mocked_api["basic_plugins"].called
|
assert mocked_api["basic_plugins"].called
|
||||||
assert mocked_api["extra_plugins"].called
|
assert mocked_api["extra_plugins"].called
|
||||||
assert mocked_api["github_sub_plugin_metadata"].called
|
|
||||||
assert mocked_api["github_sub_plugin_file_init"].called
|
assert mocked_api["github_sub_plugin_file_init"].called
|
||||||
assert (mock_base_path / "plugins" / "github_sub" / "__init__.py").is_file()
|
assert (mock_base_path / "plugins" / "github_sub" / "__init__.py").is_file()
|
||||||
|
|
||||||
|
|||||||
@ -17,14 +17,24 @@ def get_content_bytes(file: str) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def init_mocked_api(mocked_api: MockRouter) -> None:
|
def init_mocked_api(mocked_api: MockRouter) -> None:
|
||||||
mocked_api.get(
|
|
||||||
"https://data.jsdelivr.com/v1/packages/gh/xuanerwa/zhenxun_github_sub@main",
|
|
||||||
name="github_sub_plugin_metadata",
|
|
||||||
).respond(json=get_response_json("github_sub_plugin_metadata.json"))
|
|
||||||
mocked_api.get(
|
mocked_api.get(
|
||||||
"https://data.jsdelivr.com/v1/packages/gh/zhenxun-org/zhenxun_bot_plugins@main",
|
"https://data.jsdelivr.com/v1/packages/gh/zhenxun-org/zhenxun_bot_plugins@main",
|
||||||
name="zhenxun_bot_plugins_metadata",
|
name="zhenxun_bot_plugins_metadata",
|
||||||
).respond(json=get_response_json("zhenxun_bot_plugins_metadata.json"))
|
).respond(json=get_response_json("zhenxun_bot_plugins_metadata.json"))
|
||||||
|
mocked_api.get(
|
||||||
|
"https://data.jsdelivr.com/v1/packages/gh/xuanerwa/zhenxun_github_sub@main",
|
||||||
|
name="zhenxun_github_sub_metadata",
|
||||||
|
).respond(json=get_response_json("zhenxun_github_sub_metadata.json"))
|
||||||
|
|
||||||
|
mocked_api.get(
|
||||||
|
"https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/git/trees/main?recursive=1",
|
||||||
|
name="zhenxun_bot_plugins_tree",
|
||||||
|
).respond(json=get_response_json("zhenxun_bot_plugins_tree.json"))
|
||||||
|
mocked_api.get(
|
||||||
|
"https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/main?recursive=1",
|
||||||
|
name="zhenxun_github_sub_tree",
|
||||||
|
).respond(json=get_response_json("zhenxun_github_sub_tree.json"))
|
||||||
|
|
||||||
mocked_api.head(
|
mocked_api.head(
|
||||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/main/plugins.json",
|
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/main/plugins.json",
|
||||||
name="head_basic_plugins",
|
name="head_basic_plugins",
|
||||||
|
|||||||
1372
tests/response/plugin_store/zhenxun_bot_plugins_tree.json
Normal file
1372
tests/response/plugin_store/zhenxun_bot_plugins_tree.json
Normal file
File diff suppressed because it is too large
Load Diff
38
tests/response/plugin_store/zhenxun_github_sub_tree.json
Normal file
38
tests/response/plugin_store/zhenxun_github_sub_tree.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"sha": "438298b9e88f9dafa7020e99d7c7b4c98f93aea6",
|
||||||
|
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/438298b9e88f9dafa7020e99d7c7b4c98f93aea6",
|
||||||
|
"tree": [
|
||||||
|
{
|
||||||
|
"path": "LICENSE",
|
||||||
|
"mode": "100644",
|
||||||
|
"type": "blob",
|
||||||
|
"sha": "f288702d2fa16d3cdf0035b15a9fcbc552cd88e7",
|
||||||
|
"size": 35149,
|
||||||
|
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/blobs/f288702d2fa16d3cdf0035b15a9fcbc552cd88e7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"mode": "100644",
|
||||||
|
"type": "blob",
|
||||||
|
"sha": "e974cfc9b973d4a041f03e693ea20563a933b7ca",
|
||||||
|
"size": 955,
|
||||||
|
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/blobs/e974cfc9b973d4a041f03e693ea20563a933b7ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "github_sub",
|
||||||
|
"mode": "040000",
|
||||||
|
"type": "tree",
|
||||||
|
"sha": "0f7d76bcf472e2ab0610fa542b067633d6e3ae7e",
|
||||||
|
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/0f7d76bcf472e2ab0610fa542b067633d6e3ae7e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "github_sub/__init__.py",
|
||||||
|
"mode": "100644",
|
||||||
|
"type": "blob",
|
||||||
|
"sha": "7d17fd49fe82fa3897afcef61b2c694ed93a4ba3",
|
||||||
|
"size": 7551,
|
||||||
|
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/blobs/7d17fd49fe82fa3897afcef61b2c694ed93a4ba3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"truncated": false
|
||||||
|
}
|
||||||
@ -20,3 +20,8 @@ JSD_PACKAGE_API_FORMAT = (
|
|||||||
"https://data.jsdelivr.com/v1/packages/gh/{owner}/{repo}@{branch}"
|
"https://data.jsdelivr.com/v1/packages/gh/{owner}/{repo}@{branch}"
|
||||||
)
|
)
|
||||||
"""jsdelivr包地址格式"""
|
"""jsdelivr包地址格式"""
|
||||||
|
|
||||||
|
GIT_API_TREES_FORMAT = (
|
||||||
|
"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
|
||||||
|
)
|
||||||
|
"""git api trees地址格式"""
|
||||||
|
|||||||
@ -12,18 +12,13 @@ from zhenxun.utils.image_utils import RowStyle, BuildImage, ImageTemplate
|
|||||||
from zhenxun.builtin_plugins.auto_update.config import REQ_TXT_FILE_STRING
|
from zhenxun.builtin_plugins.auto_update.config import REQ_TXT_FILE_STRING
|
||||||
from zhenxun.builtin_plugins.plugin_store.models import (
|
from zhenxun.builtin_plugins.plugin_store.models import (
|
||||||
FileInfo,
|
FileInfo,
|
||||||
FileType,
|
|
||||||
RepoInfo,
|
RepoInfo,
|
||||||
JsdPackageInfo,
|
TreesInfo,
|
||||||
|
PackageApi,
|
||||||
StorePluginInfo,
|
StorePluginInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .config import (
|
from .config import BASE_PATH, EXTRA_GITHUB_URL, DEFAULT_GITHUB_URL
|
||||||
BASE_PATH,
|
|
||||||
EXTRA_GITHUB_URL,
|
|
||||||
DEFAULT_GITHUB_URL,
|
|
||||||
JSD_PACKAGE_API_FORMAT,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def row_style(column: str, text: str) -> RowStyle:
|
def row_style(column: str, text: str) -> RowStyle:
|
||||||
@ -42,67 +37,6 @@ def row_style(column: str, text: str) -> RowStyle:
|
|||||||
return style
|
return style
|
||||||
|
|
||||||
|
|
||||||
def full_files_path(
|
|
||||||
jsd_package_info: JsdPackageInfo, module_path: str, is_dir: bool = True
|
|
||||||
) -> list[FileInfo]:
|
|
||||||
"""
|
|
||||||
获取文件路径
|
|
||||||
|
|
||||||
参数:
|
|
||||||
jsd_package_info: JsdPackageInfo
|
|
||||||
module_path: 模块路径
|
|
||||||
is_dir: 是否为目录
|
|
||||||
|
|
||||||
返回:
|
|
||||||
list[FileInfo]: 文件路径
|
|
||||||
"""
|
|
||||||
paths: list[str] = module_path.split(".")
|
|
||||||
cur_files: list[FileInfo] = jsd_package_info.files
|
|
||||||
for path in paths:
|
|
||||||
for cur_file in cur_files:
|
|
||||||
if (
|
|
||||||
cur_file.type == FileType.DIR
|
|
||||||
and cur_file.name == path
|
|
||||||
and cur_file.files
|
|
||||||
and (is_dir or path != paths[-1])
|
|
||||||
):
|
|
||||||
cur_files = cur_file.files
|
|
||||||
break
|
|
||||||
if not is_dir and path == paths[-1] and cur_file.name.split(".")[0] == path:
|
|
||||||
return cur_files
|
|
||||||
else:
|
|
||||||
raise ValueError(f"模块路径 {module_path} 不存在")
|
|
||||||
return cur_files
|
|
||||||
|
|
||||||
|
|
||||||
def recurrence_files(
|
|
||||||
files: list[FileInfo], dir_path: str, is_dir: bool = True
|
|
||||||
) -> list[str]:
|
|
||||||
"""
|
|
||||||
递归获取文件路径
|
|
||||||
|
|
||||||
参数:
|
|
||||||
files: 文件列表
|
|
||||||
dir_path: 目录路径
|
|
||||||
is_dir: 是否为目录
|
|
||||||
|
|
||||||
返回:
|
|
||||||
list[str]: 文件路径
|
|
||||||
"""
|
|
||||||
paths = []
|
|
||||||
for file in files:
|
|
||||||
if is_dir and file.type == FileType.DIR and file.files:
|
|
||||||
paths.extend(
|
|
||||||
recurrence_files(file.files, f"{dir_path}/{file.name}", is_dir)
|
|
||||||
)
|
|
||||||
elif file.type == FileType.FILE:
|
|
||||||
if dir_path.endswith(file.name):
|
|
||||||
paths.append(dir_path)
|
|
||||||
elif is_dir:
|
|
||||||
paths.append(f"{dir_path}/{file.name}")
|
|
||||||
return paths
|
|
||||||
|
|
||||||
|
|
||||||
def install_requirement(plugin_path: Path):
|
def install_requirement(plugin_path: Path):
|
||||||
requirement_files = ["requirement.txt", "requirements.txt"]
|
requirement_files = ["requirement.txt", "requirements.txt"]
|
||||||
requirement_paths = [plugin_path / file for file in requirement_files]
|
requirement_paths = [plugin_path / file for file in requirement_files]
|
||||||
@ -274,40 +208,30 @@ class ShopManage:
|
|||||||
)
|
)
|
||||||
return f"插件 {plugin_key} 安装成功! 重启后生效"
|
return f"插件 {plugin_key} 安装成功! 重启后生效"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_repo_package_info_of_jsd(cls, repo_info: RepoInfo) -> JsdPackageInfo:
|
|
||||||
"""获取插件包信息
|
|
||||||
|
|
||||||
参数:
|
|
||||||
repo_info: 仓库信息
|
|
||||||
|
|
||||||
返回:
|
|
||||||
JsdPackageInfo: 插件包信息
|
|
||||||
"""
|
|
||||||
jsd_package_url: str = JSD_PACKAGE_API_FORMAT.format(
|
|
||||||
owner=repo_info.owner, repo=repo_info.repo, branch=repo_info.branch
|
|
||||||
)
|
|
||||||
res = await AsyncHttpx.get(url=jsd_package_url)
|
|
||||||
if res.status_code != 200:
|
|
||||||
raise ValueError(f"下载错误, code: {res.status_code}")
|
|
||||||
return JsdPackageInfo(**res.json())
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def install_plugin_with_repo(
|
async def install_plugin_with_repo(
|
||||||
cls, github_url: str, module_path: str, is_dir: bool, is_external: bool = False
|
cls, github_url: str, module_path: str, is_dir: bool, is_external: bool = False
|
||||||
):
|
):
|
||||||
|
package_api: PackageApi
|
||||||
|
files: list[str]
|
||||||
|
package_info: FileInfo | TreesInfo
|
||||||
repo_info = RepoInfo.parse_github_url(github_url)
|
repo_info = RepoInfo.parse_github_url(github_url)
|
||||||
logger.debug(f"成功获取仓库信息: {repo_info}", "插件管理")
|
logger.debug(f"成功获取仓库信息: {repo_info}", "插件管理")
|
||||||
jsd_package_info: JsdPackageInfo = await cls.get_repo_package_info_of_jsd(
|
for package_api in PackageApi:
|
||||||
repo_info=repo_info
|
try:
|
||||||
|
package_info = await package_api.value.parse_repo_info(repo_info)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"获取插件文件失败: {e} | API类型: {package_api.value}", "插件管理"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError("所有API获取插件文件失败,请检查网络连接")
|
||||||
|
files = package_info.get_files(
|
||||||
|
module_path=module_path.replace(".", "/") + ("" if is_dir else ".py"),
|
||||||
|
is_dir=is_dir,
|
||||||
)
|
)
|
||||||
files = full_files_path(jsd_package_info, module_path, is_dir)
|
|
||||||
files = recurrence_files(
|
|
||||||
files,
|
|
||||||
module_path.replace(".", "/") + ("" if is_dir else ".py"),
|
|
||||||
is_dir,
|
|
||||||
)
|
|
||||||
logger.debug(f"获取插件文件列表: {files}", "插件管理")
|
|
||||||
download_urls = [
|
download_urls = [
|
||||||
await repo_info.get_download_url_with_path(file) for file in files
|
await repo_info.get_download_url_with_path(file) for file in files
|
||||||
]
|
]
|
||||||
@ -321,12 +245,8 @@ class ShopManage:
|
|||||||
else:
|
else:
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
plugin_path = base_path / "/".join(module_path.split("."))
|
plugin_path = base_path / "/".join(module_path.split("."))
|
||||||
req_files = recurrence_files(
|
req_files = package_info.get_files(REQ_TXT_FILE_STRING, False)
|
||||||
jsd_package_info.files, REQ_TXT_FILE_STRING, False
|
req_files.extend(package_info.get_files("requirement.txt", False))
|
||||||
)
|
|
||||||
req_files.extend(
|
|
||||||
recurrence_files(jsd_package_info.files, "requirement.txt", False)
|
|
||||||
)
|
|
||||||
logger.debug(f"获取插件依赖文件列表: {req_files}", "插件管理")
|
logger.debug(f"获取插件依赖文件列表: {req_files}", "插件管理")
|
||||||
req_download_urls = [
|
req_download_urls = [
|
||||||
await repo_info.get_download_url_with_path(file) for file in req_files
|
await repo_info.get_download_url_with_path(file) for file in req_files
|
||||||
@ -357,11 +277,11 @@ class ShopManage:
|
|||||||
返回:
|
返回:
|
||||||
str: 返回消息
|
str: 返回消息
|
||||||
"""
|
"""
|
||||||
data = await cls.__get_data()
|
data: dict[str, StorePluginInfo] = await cls.__get_data()
|
||||||
if plugin_id < 0 or plugin_id >= len(data):
|
if plugin_id < 0 or plugin_id >= len(data):
|
||||||
return "插件ID不存在..."
|
return "插件ID不存在..."
|
||||||
plugin_key = list(data.keys())[plugin_id]
|
plugin_key = list(data.keys())[plugin_id]
|
||||||
plugin_info = data[plugin_key]
|
plugin_info = data[plugin_key] # type: ignore
|
||||||
path = BASE_PATH
|
path = BASE_PATH
|
||||||
if plugin_info.github_url:
|
if plugin_info.github_url:
|
||||||
path = BASE_PATH / "plugins"
|
path = BASE_PATH / "plugins"
|
||||||
@ -388,7 +308,7 @@ class ShopManage:
|
|||||||
返回:
|
返回:
|
||||||
BuildImage | str: 返回消息
|
BuildImage | str: 返回消息
|
||||||
"""
|
"""
|
||||||
data = await cls.__get_data()
|
data: dict[str, StorePluginInfo] = await cls.__get_data()
|
||||||
plugin_list = await cls.get_loaded_plugins("module", "version")
|
plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list}
|
suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list}
|
||||||
filtered_data = [
|
filtered_data = [
|
||||||
@ -431,7 +351,7 @@ class ShopManage:
|
|||||||
返回:
|
返回:
|
||||||
str: 返回消息
|
str: 返回消息
|
||||||
"""
|
"""
|
||||||
data = await cls.__get_data()
|
data: dict[str, StorePluginInfo] = await cls.__get_data()
|
||||||
if plugin_id < 0 or plugin_id >= len(data):
|
if plugin_id < 0 or plugin_id >= len(data):
|
||||||
return "插件ID不存在..."
|
return "插件ID不存在..."
|
||||||
plugin_key = list(data.keys())[plugin_id]
|
plugin_key = list(data.keys())[plugin_id]
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
from strenum import StrEnum
|
from strenum import StrEnum
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from zhenxun.utils.enum import PluginType
|
from zhenxun.utils.enum import PluginType
|
||||||
from zhenxun.utils.http_utils import AsyncHttpx
|
from zhenxun.utils.http_utils import AsyncHttpx
|
||||||
|
|
||||||
from .config import GITHUB_REPO_URL_PATTERN
|
from .config import (
|
||||||
|
GIT_API_TREES_FORMAT,
|
||||||
|
JSD_PACKAGE_API_FORMAT,
|
||||||
|
GITHUB_REPO_URL_PATTERN,
|
||||||
|
)
|
||||||
|
|
||||||
type2name: dict[str, str] = {
|
type2name: dict[str, str] = {
|
||||||
"NORMAL": "普通插件",
|
"NORMAL": "普通插件",
|
||||||
@ -40,11 +47,7 @@ class RepoInfo(BaseModel):
|
|||||||
|
|
||||||
owner: str
|
owner: str
|
||||||
repo: str
|
repo: str
|
||||||
branch: str | None
|
branch: str = "main"
|
||||||
|
|
||||||
@validator("branch", pre=True, always=True)
|
|
||||||
def _set_default_branch(cls, v):
|
|
||||||
return "main" if v is None else v
|
|
||||||
|
|
||||||
async def get_download_url_with_path(self, path: str):
|
async def get_download_url_with_path(self, path: str):
|
||||||
url_format = await self.get_fastest_format()
|
url_format = await self.get_fastest_format()
|
||||||
@ -53,7 +56,7 @@ class RepoInfo(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def parse_github_url(cls, github_url: str) -> "RepoInfo":
|
def parse_github_url(cls, github_url: str) -> "RepoInfo":
|
||||||
if matched := GITHUB_REPO_URL_PATTERN.match(github_url):
|
if matched := GITHUB_REPO_URL_PATTERN.match(github_url):
|
||||||
return RepoInfo(**matched.groupdict())
|
return RepoInfo(**{k: v for k, v in matched.groupdict().items() if v})
|
||||||
raise ValueError("github地址格式错误")
|
raise ValueError("github地址格式错误")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -83,20 +86,168 @@ class FileType(StrEnum):
|
|||||||
|
|
||||||
FILE = "file"
|
FILE = "file"
|
||||||
DIR = "directory"
|
DIR = "directory"
|
||||||
|
PACKAGE = "gh"
|
||||||
|
|
||||||
|
|
||||||
class FileInfo(BaseModel):
|
class BaseInfo(BaseModel, ABC):
|
||||||
|
"""基础信息类"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
async def parse_repo_info(cls, repo_info: RepoInfo) -> "BaseInfo": ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_files(cls, module_path: str, is_dir) -> list[str]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class FileInfo(BaseInfo):
|
||||||
"""文件信息"""
|
"""文件信息"""
|
||||||
|
|
||||||
type: FileType
|
type: FileType
|
||||||
name: str
|
name: str
|
||||||
files: list["FileInfo"] | None
|
files: list["FileInfo"] = []
|
||||||
|
|
||||||
|
def recurrence_files(self, dir_path: str, is_dir: bool = True) -> list[str]:
|
||||||
|
"""
|
||||||
|
递归获取文件路径
|
||||||
|
|
||||||
|
参数:
|
||||||
|
files: 文件列表
|
||||||
|
dir_path: 目录路径
|
||||||
|
is_dir: 是否为目录
|
||||||
|
|
||||||
|
返回:
|
||||||
|
list[str]: 文件路径
|
||||||
|
"""
|
||||||
|
if not is_dir and dir_path.endswith(self.name):
|
||||||
|
return [dir_path]
|
||||||
|
if self.files is None:
|
||||||
|
raise ValueError("文件列表为空")
|
||||||
|
paths = []
|
||||||
|
for file in self.files:
|
||||||
|
if is_dir and file.type == FileType.DIR and file.files:
|
||||||
|
paths.extend(self.recurrence_files(f"{dir_path}/{file.name}", is_dir))
|
||||||
|
elif file.type == FileType.FILE:
|
||||||
|
if is_dir:
|
||||||
|
paths.append(f"{dir_path}/{file.name}")
|
||||||
|
elif dir_path.endswith(file.name):
|
||||||
|
paths.append(dir_path)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
def full_files_path(self, module_path: str, is_dir: bool = True) -> "FileInfo":
|
||||||
|
"""
|
||||||
|
获取文件路径
|
||||||
|
|
||||||
|
参数:
|
||||||
|
module_path: 模块路径
|
||||||
|
is_dir: 是否为目录
|
||||||
|
|
||||||
|
返回:
|
||||||
|
list[FileInfo]: 文件路径
|
||||||
|
"""
|
||||||
|
paths: list[str] = module_path.split("/")
|
||||||
|
if not is_dir:
|
||||||
|
paths = paths[:-1]
|
||||||
|
cur_file: FileInfo = self
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
for file in cur_file.files:
|
||||||
|
if file.type == FileType.DIR and file.name == path and file.files:
|
||||||
|
cur_file = file
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"模块路径 {module_path} 不存在")
|
||||||
|
return cur_file
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def parse_repo_info(cls, repo_info: RepoInfo) -> "FileInfo":
|
||||||
|
"""解析仓库信息"""
|
||||||
|
|
||||||
|
"""获取插件包信息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
repo_info: 仓库信息
|
||||||
|
|
||||||
|
返回:
|
||||||
|
FileInfo: 插件包信息
|
||||||
|
"""
|
||||||
|
jsd_package_url: str = JSD_PACKAGE_API_FORMAT.format(
|
||||||
|
owner=repo_info.owner, repo=repo_info.repo, branch=repo_info.branch
|
||||||
|
)
|
||||||
|
res = await AsyncHttpx.get(url=jsd_package_url)
|
||||||
|
if res.status_code != 200:
|
||||||
|
raise ValueError(f"下载错误, code: {res.status_code}")
|
||||||
|
return FileInfo(**res.json())
|
||||||
|
|
||||||
|
def get_files(self, module_path: str, is_dir: bool = True) -> list[str]:
|
||||||
|
"""获取文件路径"""
|
||||||
|
|
||||||
|
file = self.full_files_path(module_path, is_dir)
|
||||||
|
files = file.recurrence_files(
|
||||||
|
module_path,
|
||||||
|
is_dir,
|
||||||
|
)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
class JsdPackageInfo(BaseModel):
|
class TreeType(StrEnum):
|
||||||
"""jsd包信息"""
|
"""树类型"""
|
||||||
|
|
||||||
type: str
|
FILE = "blob"
|
||||||
name: str
|
DIR = "tree"
|
||||||
version: str
|
|
||||||
files: list[FileInfo]
|
|
||||||
|
class Tree(BaseModel):
|
||||||
|
"""树"""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
mode: str
|
||||||
|
type: TreeType
|
||||||
|
sha: str
|
||||||
|
size: int | None
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class TreesInfo(BaseInfo):
|
||||||
|
"""树信息"""
|
||||||
|
|
||||||
|
sha: str
|
||||||
|
url: str
|
||||||
|
tree: list[Tree]
|
||||||
|
|
||||||
|
def export_files(self, module_path: str) -> list[str]:
|
||||||
|
"""导出文件路径"""
|
||||||
|
return [
|
||||||
|
file.path
|
||||||
|
for file in self.tree
|
||||||
|
if file.type == TreeType.FILE and file.path.startswith(module_path)
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def parse_repo_info(cls, repo_info: RepoInfo) -> "TreesInfo":
|
||||||
|
"""获取仓库树
|
||||||
|
|
||||||
|
参数:
|
||||||
|
repo_info: 仓库信息
|
||||||
|
|
||||||
|
返回:
|
||||||
|
TreesInfo: 仓库树信息
|
||||||
|
"""
|
||||||
|
git_tree_url: str = GIT_API_TREES_FORMAT.format(
|
||||||
|
owner=repo_info.owner, repo=repo_info.repo, branch=repo_info.branch
|
||||||
|
)
|
||||||
|
res = await AsyncHttpx.get(url=git_tree_url)
|
||||||
|
if res.status_code != 200:
|
||||||
|
raise ValueError(f"下载错误, code: {res.status_code}")
|
||||||
|
return TreesInfo(**res.json())
|
||||||
|
|
||||||
|
def get_files(self, module_path: str, is_dir: bool = True) -> list[str]:
|
||||||
|
"""获取文件路径"""
|
||||||
|
return self.export_files(module_path)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageApi(Enum):
|
||||||
|
"""插件包接口"""
|
||||||
|
|
||||||
|
GITHUB = TreesInfo
|
||||||
|
JSDELIVR = FileInfo
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user