mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-10 18:06:48 +08:00
fix: 支持 Windows Python 3.14t 运行依赖 (#6492)
* fix(runtime): support Windows free-threaded dependencies * fix(runtime): close Windows profile validation gaps * chore(architecture): refresh runtime dependency baseline
This commit is contained in:
@@ -7,9 +7,11 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
|
- 'app/__init__.py'
|
||||||
- 'app/doctor/dependencies.py'
|
- 'app/doctor/dependencies.py'
|
||||||
- 'app/foundation/environment.py'
|
- 'app/foundation/environment.py'
|
||||||
- 'app/runtime/dependencies.py'
|
- 'app/runtime/dependencies.py'
|
||||||
|
- 'scripts/verify_runtime_profile.py'
|
||||||
- 'docker/Dockerfile'
|
- 'docker/Dockerfile'
|
||||||
- 'docker/**'
|
- 'docker/**'
|
||||||
- '.github/workflows/dependency-compat.yml'
|
- '.github/workflows/dependency-compat.yml'
|
||||||
@@ -19,9 +21,11 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
|
- 'app/__init__.py'
|
||||||
- 'app/doctor/dependencies.py'
|
- 'app/doctor/dependencies.py'
|
||||||
- 'app/foundation/environment.py'
|
- 'app/foundation/environment.py'
|
||||||
- 'app/runtime/dependencies.py'
|
- 'app/runtime/dependencies.py'
|
||||||
|
- 'scripts/verify_runtime_profile.py'
|
||||||
- 'docker/Dockerfile'
|
- 'docker/Dockerfile'
|
||||||
- 'docker/**'
|
- 'docker/**'
|
||||||
- '.github/workflows/dependency-compat.yml'
|
- '.github/workflows/dependency-compat.yml'
|
||||||
@@ -46,26 +50,43 @@ jobs:
|
|||||||
- name: Linux x64
|
- name: Linux x64
|
||||||
runner: ubuntu-24.04
|
runner: ubuntu-24.04
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
runtime-group: runtime-standard
|
||||||
|
expected-profile: standard
|
||||||
expected-system: Linux
|
expected-system: Linux
|
||||||
expected-machine: x86_64
|
expected-machine: x86_64
|
||||||
- name: Linux ARM64
|
- name: Linux ARM64
|
||||||
runner: ubuntu-24.04-arm
|
runner: ubuntu-24.04-arm
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
runtime-group: runtime-standard
|
||||||
|
expected-profile: standard
|
||||||
expected-system: Linux
|
expected-system: Linux
|
||||||
expected-machine: aarch64
|
expected-machine: aarch64
|
||||||
- name: macOS Intel
|
- name: macOS Intel
|
||||||
runner: macos-15-intel
|
runner: macos-15-intel
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
runtime-group: runtime-standard
|
||||||
|
expected-profile: standard
|
||||||
expected-system: Darwin
|
expected-system: Darwin
|
||||||
expected-machine: x86_64
|
expected-machine: x86_64
|
||||||
- name: macOS ARM
|
- name: macOS ARM
|
||||||
runner: macos-15
|
runner: macos-15
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
runtime-group: runtime-standard
|
||||||
|
expected-profile: standard
|
||||||
expected-system: Darwin
|
expected-system: Darwin
|
||||||
expected-machine: arm64
|
expected-machine: arm64
|
||||||
- name: Windows x64
|
- name: Windows x64
|
||||||
runner: windows-2025
|
runner: windows-2025
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
|
runtime-group: runtime-standard
|
||||||
|
expected-profile: standard
|
||||||
|
expected-system: Windows
|
||||||
|
expected-machine: AMD64
|
||||||
|
- name: Windows x64 free-threaded
|
||||||
|
runner: windows-2025
|
||||||
|
python-version: '3.14t'
|
||||||
|
runtime-group: runtime-free-threaded
|
||||||
|
expected-profile: free-threaded
|
||||||
expected-system: Windows
|
expected-system: Windows
|
||||||
expected-machine: AMD64
|
expected-machine: AMD64
|
||||||
|
|
||||||
@@ -82,22 +103,41 @@ jobs:
|
|||||||
pyproject.toml
|
pyproject.toml
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
||||||
- name: Install locked runtime dependencies
|
- name: Expose PostgreSQL build tools
|
||||||
run: uv sync --locked --inexact --no-dev --python ${{ matrix.python-version }}
|
if: matrix.expected-profile == 'free-threaded'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if (-not $env:PGBIN -or -not (Test-Path -LiteralPath $env:PGBIN -PathType Container)) {
|
||||||
|
throw 'Windows free-threaded profile requires a valid PGBIN directory'
|
||||||
|
}
|
||||||
|
"$env:PGBIN" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
- name: Verify environment and core imports
|
- name: Install locked runtime dependencies
|
||||||
|
run: >-
|
||||||
|
uv sync --locked --inexact --no-default-groups
|
||||||
|
--group ${{ matrix.runtime-group }}
|
||||||
|
--python ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Verify runtime profile
|
||||||
env:
|
env:
|
||||||
|
AUTO_UPDATE_RESOURCE: 'false'
|
||||||
EXPECTED_SYSTEM: ${{ matrix.expected-system }}
|
EXPECTED_SYSTEM: ${{ matrix.expected-system }}
|
||||||
EXPECTED_MACHINE: ${{ matrix.expected-machine }}
|
EXPECTED_MACHINE: ${{ matrix.expected-machine }}
|
||||||
|
EXPECTED_PROFILE: ${{ matrix.expected-profile }}
|
||||||
|
CONFIG_DIR: ${{ runner.temp }}/moviepilot-runtime-smoke
|
||||||
|
MOVIEPILOT_SAFE_MODE: 'true'
|
||||||
|
SUPERUSER_PASSWORD: moviepilot-ci-only
|
||||||
run: >-
|
run: >-
|
||||||
uv run --locked --no-sync python -c
|
uv run --locked --no-sync python -m scripts.verify_runtime_profile
|
||||||
"import os, platform;
|
--expected-profile ${{ matrix.expected-profile }}
|
||||||
assert platform.system() == os.environ['EXPECTED_SYSTEM'], (platform.system(), os.environ['EXPECTED_SYSTEM']);
|
--expected-system ${{ matrix.expected-system }}
|
||||||
assert platform.machine() == os.environ['EXPECTED_MACHINE'], (platform.machine(), os.environ['EXPECTED_MACHINE']);
|
--expected-machine ${{ matrix.expected-machine }}
|
||||||
import alembic, fastapi, pydantic, pydantic_settings, sqlalchemy, starlette, uvicorn"
|
|
||||||
|
|
||||||
- name: Verify locked project consistency
|
- name: Verify locked project consistency
|
||||||
run: uv sync --locked --offline --inexact --no-dev --check --python ${{ matrix.python-version }}
|
run: >-
|
||||||
|
uv sync --locked --offline --inexact --no-default-groups
|
||||||
|
--group ${{ matrix.runtime-group }}
|
||||||
|
--check --python ${{ matrix.python-version }}
|
||||||
|
|
||||||
docker-dependencies:
|
docker-dependencies:
|
||||||
name: Docker dependencies / ${{ matrix.platform }}
|
name: Docker dependencies / ${{ matrix.platform }}
|
||||||
|
|||||||
@@ -1,7 +1,30 @@
|
|||||||
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.foundation.environment import (
|
||||||
|
is_free_threaded_runtime,
|
||||||
|
is_windows,
|
||||||
|
)
|
||||||
from app.runtime.compat.imports import install_legacy_import_hook
|
from app.runtime.compat.imports import install_legacy_import_hook
|
||||||
|
|
||||||
|
_windows_dll_directory_handles: list[object] = []
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_free_threaded_windows_native_dependencies() -> None:
|
||||||
|
"""为 Windows free-threaded 运行时注册外部原生依赖目录。"""
|
||||||
|
if not is_windows() or not is_free_threaded_runtime():
|
||||||
|
return
|
||||||
|
configured_path = os.getenv("PGBIN")
|
||||||
|
if not configured_path:
|
||||||
|
return
|
||||||
|
directory = Path(configured_path)
|
||||||
|
if not directory.is_dir():
|
||||||
|
return
|
||||||
|
_windows_dll_directory_handles.append(
|
||||||
|
getattr(os, "add_dll_directory")(str(directory))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _filter_third_party_startup_warnings() -> None:
|
def _filter_third_party_startup_warnings() -> None:
|
||||||
"""
|
"""
|
||||||
@@ -20,5 +43,6 @@ def _filter_third_party_startup_warnings() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_configure_free_threaded_windows_native_dependencies()
|
||||||
_filter_third_party_startup_warnings()
|
_filter_third_party_startup_warnings()
|
||||||
install_legacy_import_hook()
|
install_legacy_import_hook()
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
|||||||
|
|
||||||
| 指标 | 当前值 | 解释 |
|
| 指标 | 当前值 | 解释 |
|
||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
| 宿主 Python 模块 / 内部依赖边 | 906 / 7,612 | `dependency-baseline.json` 当前快照 |
|
| 宿主 Python 模块 / 内部依赖边 | 907 / 7,618 | `dependency-baseline.json` 当前快照 |
|
||||||
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
|
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
|
||||||
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
||||||
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
||||||
|
|||||||
@@ -722,8 +722,8 @@ flowchart LR
|
|||||||
|
|
||||||
| 指标 | 当前值 |
|
| 指标 | 当前值 |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
| Python 模块 | 906 |
|
| Python 模块 | 907 |
|
||||||
| 内部导入边 | 7,612 |
|
| 内部导入边 | 7,618 |
|
||||||
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
|
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
|
||||||
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
|
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
|
||||||
| Direct egress | 55(债务已清零,55 条精确 containment) |
|
| Direct egress | 55(债务已清零,55 条精确 containment) |
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ profile,依赖名称、版本和 source 语义全部由 `pyproject.toml` 与 `
|
|||||||
| CRC 加速 | `crcmod-plus` 2.3.1 | `crcmod-plus` 2.3.1 | 两套镜像统一使用继续维护且兼容 `crcmod` 导入接口的实现。`oss2` 的陈旧元数据仍声明不再维护的 `crcmod`,由 uv 在解析时排除该传递依赖;宿主不在运行时映射、卸载或替插件兼容旧分发包。 |
|
| CRC 加速 | `crcmod-plus` 2.3.1 | `crcmod-plus` 2.3.1 | 两套镜像统一使用继续维护且兼容 `crcmod` 导入接口的实现。`oss2` 的陈旧元数据仍声明不再维护的 `crcmod`,由 uv 在解析时排除该传递依赖;宿主不在运行时映射、卸载或替插件兼容旧分发包。 |
|
||||||
| `lxml` | 6.1.2 | 7.0.0b1 | V3t 暂用提供目标 ABI 的预发布版本,是当前最高风险项。稳定版提供 `cp314t` wheel 后,需通过 XML、HTML、RSS、站点解析、并发和内存验证再替换。 |
|
| `lxml` | 6.1.2 | 7.0.0b1 | V3t 暂用提供目标 ABI 的预发布版本,是当前最高风险项。稳定版提供 `cp314t` wheel 后,需通过 XML、HTML、RSS、站点解析、并发和内存验证再替换。 |
|
||||||
| PostgreSQL 同步驱动 | `psycopg2-binary` 2.x | `psycopg[c]` 3.3.4 | 当前分叉同时受 ABI 与实测性能影响,不要求仅为版本统一而收敛。若上游能力或性能变化,必须重跑三方案 PostgreSQL A/B 后再决策。异步路径继续使用 `asyncpg`。 |
|
| PostgreSQL 同步驱动 | `psycopg2-binary` 2.x | `psycopg[c]` 3.3.4 | 当前分叉同时受 ABI 与实测性能影响,不要求仅为版本统一而收敛。若上游能力或性能变化,必须重跑三方案 PostgreSQL A/B 后再决策。异步路径继续使用 `asyncpg`。 |
|
||||||
|
| Windows PostgreSQL 客户端 | `psycopg2-binary` 自包含 libpq | `psycopg[c]` 链接本机 libpq | Windows 3.14t 源码环境必须安装 PostgreSQL 客户端开发工具并提供有效 `PGBIN`;启动时只为 free-threaded 解释器注册该 DLL 目录。上游发布自包含的 `cp314t` Windows wheel 后,应优先移除此宿主前提。 |
|
||||||
|
| Windows Docker SDK | `pywin32` 312 | 不安装 `pywin32` | 标准 Windows 保留 Docker named-pipe transport;V3t 仅提供不依赖 pywin32 的 Docker SDK 路径。Docker 或 pywin32 提供兼容 `cp314t` 的组合后,重新验证 named-pipe 再解除排除。 |
|
||||||
| `orjson` | 3.12.0 wheel | 同版本源码构建 | V3t 使用同一锁定版本并启用 free-threaded 构建变量。上游发布覆盖 Linux amd64/arm64 的稳定 `cp314t` wheel 后可删除本地构建要求。 |
|
| `orjson` | 3.12.0 wheel | 同版本源码构建 | V3t 使用同一锁定版本并启用 free-threaded 构建变量。上游发布覆盖 Linux amd64/arm64 的稳定 `cp314t` wheel 后可删除本地构建要求。 |
|
||||||
| 中文转换 | `moviepilot-rust.zhconv_fast()` | `moviepilot-rust.zhconv_fast()` | 从 0.3.3 起两套 ABI 使用同一 MediaWiki 转换实现;主程序统一经 `app.foundation.text.convert()`,插件统一经 SDK,不再安装独立 `zhconv-rs`。 |
|
| 中文转换 | `moviepilot-rust.zhconv_fast()` | `moviepilot-rust.zhconv_fast()` | 从 0.3.3 起两套 ABI 使用同一 MediaWiki 转换实现;主程序统一经 `app.foundation.text.convert()`,插件统一经 SDK,不再安装独立 `zhconv-rs`。 |
|
||||||
| 站点资源 | `cpython-314` | `cpython-314t` | 资源文件必须按解释器 ABI 独立构建和选取,不能让 V3t 复用普通 CPython 扩展,也不能影响 V2 的历史 ABI 制品。 |
|
| 站点资源 | `cpython-314` | `cpython-314t` | 资源文件必须按解释器 ABI 独立构建和选取,不能让 V3t 复用普通 CPython 扩展,也不能影响 V2 的历史 ABI 制品。 |
|
||||||
|
|||||||
+2
-2
@@ -62,7 +62,6 @@ dependencies = [
|
|||||||
"pydantic>=2.13.4,<3.0.0",
|
"pydantic>=2.13.4,<3.0.0",
|
||||||
"pydantic-settings>=2.14.2,<3.0.0",
|
"pydantic-settings>=2.14.2,<3.0.0",
|
||||||
"pyjwt~=2.13.0",
|
"pyjwt~=2.13.0",
|
||||||
"pympler~=1.1",
|
|
||||||
"pyotp~=2.9.0",
|
"pyotp~=2.9.0",
|
||||||
"pyparsing~=3.3.2",
|
"pyparsing~=3.3.2",
|
||||||
"pyquery~=2.0.1",
|
"pyquery~=2.0.1",
|
||||||
@@ -74,7 +73,6 @@ dependencies = [
|
|||||||
"pytz~=2026.2",
|
"pytz~=2026.2",
|
||||||
"pyvirtualdisplay~=3.0",
|
"pyvirtualdisplay~=3.0",
|
||||||
"pywebpush~=2.3.0",
|
"pywebpush~=2.3.0",
|
||||||
"pywin32==312 ; sys_platform == 'win32'",
|
|
||||||
"pyyaml~=6.0.3",
|
"pyyaml~=6.0.3",
|
||||||
"qbittorrent-api==2026.6.0",
|
"qbittorrent-api==2026.6.0",
|
||||||
"redis~=8.1.0",
|
"redis~=8.1.0",
|
||||||
@@ -117,6 +115,7 @@ runtime-standard = [
|
|||||||
"bcrypt~=4.3.0",
|
"bcrypt~=4.3.0",
|
||||||
"lxml~=6.1.2",
|
"lxml~=6.1.2",
|
||||||
"psycopg2-binary~=2.9.12",
|
"psycopg2-binary~=2.9.12",
|
||||||
|
"pywin32==312 ; sys_platform == 'win32'",
|
||||||
]
|
]
|
||||||
runtime-free-threaded = [
|
runtime-free-threaded = [
|
||||||
"Brotli==1.2.0",
|
"Brotli==1.2.0",
|
||||||
@@ -144,6 +143,7 @@ conflicts = [
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
exclude-dependencies = [
|
exclude-dependencies = [
|
||||||
|
{ package = { name = "docker" }, dependencies = ["pywin32"] },
|
||||||
{ package = { name = "oss2" }, dependencies = ["crcmod"] },
|
{ package = { name = "oss2" }, dependencies = ["crcmod"] },
|
||||||
]
|
]
|
||||||
environments = [
|
environments = [
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
"""跨平台 CI 使用的运行依赖 profile 验证入口。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
from importlib import import_module
|
||||||
|
from importlib.util import find_spec
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from packaging.requirements import InvalidRequirement, Requirement
|
||||||
|
from packaging.utils import canonicalize_name
|
||||||
|
|
||||||
|
from app.doctor import dependencies as dependency_doctor
|
||||||
|
from app.runtime.dependencies import runtime_excluded_dependency_pairs
|
||||||
|
|
||||||
|
_UV_MISSING_DEPENDENCY_PATTERN = re.compile(
|
||||||
|
r"The package `(?P<package>[^`]+)` requires `(?P<requirement>[^`]+)`, "
|
||||||
|
r"but [^\r\n;]+"
|
||||||
|
)
|
||||||
|
_UV_PIP_CHECK_SUMMARY_PATTERNS = (
|
||||||
|
re.compile(r"^Using Python .+ environment at: .+$"),
|
||||||
|
re.compile(r"^Checked \d+ packages? in .+$"),
|
||||||
|
re.compile(r"^Found \d+ incompatibilit(?:y|ies)$"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _uv_health_errors(message: str, project_file: Path) -> set[str]:
|
||||||
|
"""返回未被项目 uv 排除策略覆盖的依赖健康诊断。"""
|
||||||
|
excluded_pairs = runtime_excluded_dependency_pairs(project_file)
|
||||||
|
errors: set[str] = set()
|
||||||
|
for line in {item.strip() for item in message.splitlines() if item.strip()}:
|
||||||
|
matches = list(_UV_MISSING_DEPENDENCY_PATTERN.finditer(line))
|
||||||
|
if not matches:
|
||||||
|
if not any(pattern.fullmatch(line) for pattern in _UV_PIP_CHECK_SUMMARY_PATTERNS):
|
||||||
|
errors.add(line)
|
||||||
|
continue
|
||||||
|
for match in matches:
|
||||||
|
try:
|
||||||
|
dependency_name = Requirement(match.group("requirement")).name
|
||||||
|
except InvalidRequirement:
|
||||||
|
errors.add(match.group(0))
|
||||||
|
continue
|
||||||
|
pair = (
|
||||||
|
canonicalize_name(match.group("package")),
|
||||||
|
canonicalize_name(dependency_name),
|
||||||
|
)
|
||||||
|
if pair not in excluded_pairs:
|
||||||
|
errors.add(match.group(0))
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def verify_uv_environment(project_file: Path = Path("pyproject.toml")) -> None:
|
||||||
|
"""执行 uv 元数据健康检查,仅接受项目显式声明的排除边。"""
|
||||||
|
uv = shutil.which("uv")
|
||||||
|
if not uv:
|
||||||
|
raise RuntimeError("未找到 uv 可执行文件")
|
||||||
|
result = subprocess.run(
|
||||||
|
[uv, "pip", "check", "--python", sys.executable],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return
|
||||||
|
errors = _uv_health_errors(
|
||||||
|
"\n".join((result.stdout, result.stderr)),
|
||||||
|
project_file.resolve(),
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
raise RuntimeError("uv 依赖健康检查失败:" + " | ".join(sorted(errors)))
|
||||||
|
|
||||||
|
|
||||||
|
def verify_platform_profile(
|
||||||
|
*,
|
||||||
|
expected_profile: str,
|
||||||
|
expected_system: str,
|
||||||
|
expected_machine: str,
|
||||||
|
) -> None:
|
||||||
|
"""验证运行平台、解释器 ABI 和 Windows Docker 能力边界。"""
|
||||||
|
current_system = platform.system()
|
||||||
|
current_machine = platform.machine()
|
||||||
|
if current_system != expected_system:
|
||||||
|
raise RuntimeError(f"运行系统不匹配:{current_system} != {expected_system}")
|
||||||
|
if current_machine != expected_machine:
|
||||||
|
raise RuntimeError(f"机器架构不匹配:{current_machine} != {expected_machine}")
|
||||||
|
|
||||||
|
free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
|
||||||
|
expected_free_threaded = expected_profile == "free-threaded"
|
||||||
|
if free_threaded != expected_free_threaded:
|
||||||
|
raise RuntimeError(f"解释器 profile 不匹配:free-threaded={free_threaded}")
|
||||||
|
if sys._is_gil_enabled() is expected_free_threaded:
|
||||||
|
raise RuntimeError("解释器 GIL 状态与运行 profile 不匹配")
|
||||||
|
|
||||||
|
import_module("docker")
|
||||||
|
if find_spec("pympler") is not None:
|
||||||
|
raise RuntimeError("运行环境仍包含已移除的 Pympler")
|
||||||
|
if current_system != "Windows":
|
||||||
|
return
|
||||||
|
|
||||||
|
docker_transport = import_module("docker.transport")
|
||||||
|
win32_modules = ("pywintypes", "win32api", "win32file", "win32pipe")
|
||||||
|
installed_win32_modules = {
|
||||||
|
module_name for module_name in win32_modules if find_spec(module_name) is not None
|
||||||
|
}
|
||||||
|
if expected_free_threaded:
|
||||||
|
if installed_win32_modules:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Windows free-threaded profile 意外安装 pywin32:"
|
||||||
|
+ ", ".join(sorted(installed_win32_modules))
|
||||||
|
)
|
||||||
|
if getattr(docker_transport, "NpipeHTTPAdapter", None) is not None:
|
||||||
|
raise RuntimeError("Windows free-threaded profile 意外启用了 Docker named-pipe 能力")
|
||||||
|
return
|
||||||
|
|
||||||
|
missing_modules = set(win32_modules) - installed_win32_modules
|
||||||
|
if missing_modules:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Windows standard profile 缺少 pywin32 模块:"
|
||||||
|
+ ", ".join(sorted(missing_modules))
|
||||||
|
)
|
||||||
|
for module_name in win32_modules:
|
||||||
|
import_module(module_name)
|
||||||
|
if getattr(docker_transport, "NpipeHTTPAdapter", None) is None:
|
||||||
|
raise RuntimeError("Windows standard profile 缺少 Docker named-pipe 能力")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_application_lifecycle() -> None:
|
||||||
|
"""在隔离配置下运行 FastAPI lifespan 并验证 readiness。"""
|
||||||
|
from app.testing.bootstrap import ensure_sites_stub, isolate_config_dir
|
||||||
|
|
||||||
|
isolate_config_dir()
|
||||||
|
ensure_sites_stub()
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.factory import create_app
|
||||||
|
|
||||||
|
with TestClient(create_app()) as client:
|
||||||
|
response = client.get("/health/ready")
|
||||||
|
if response.status_code != 200 or response.json() != {"status": "ready"}:
|
||||||
|
raise RuntimeError(f"应用 readiness 验证失败:{response.text}")
|
||||||
|
if sysconfig.get_config_var("Py_GIL_DISABLED") == 1 and sys._is_gil_enabled():
|
||||||
|
raise RuntimeError("应用启动后启用了 GIL")
|
||||||
|
|
||||||
|
|
||||||
|
def main(
|
||||||
|
*,
|
||||||
|
expected_profile: str,
|
||||||
|
expected_system: str,
|
||||||
|
expected_machine: str,
|
||||||
|
) -> None:
|
||||||
|
"""验证锁定环境、原生能力和应用生命周期。"""
|
||||||
|
verify_platform_profile(
|
||||||
|
expected_profile=expected_profile,
|
||||||
|
expected_system=expected_system,
|
||||||
|
expected_machine=expected_machine,
|
||||||
|
)
|
||||||
|
dependency_doctor.main(full=True)
|
||||||
|
verify_uv_environment()
|
||||||
|
if expected_system == "Windows":
|
||||||
|
verify_application_lifecycle()
|
||||||
|
if sysconfig.get_config_var("Py_GIL_DISABLED") == 1 and sys._is_gil_enabled():
|
||||||
|
raise RuntimeError("完整运行依赖验证后启用了 GIL")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--expected-profile",
|
||||||
|
required=True,
|
||||||
|
choices=("standard", "free-threaded"),
|
||||||
|
)
|
||||||
|
parser.add_argument("--expected-system", required=True)
|
||||||
|
parser.add_argument("--expected-machine", required=True)
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
main(
|
||||||
|
expected_profile=arguments.expected_profile,
|
||||||
|
expected_system=arguments.expected_system,
|
||||||
|
expected_machine=arguments.expected_machine,
|
||||||
|
)
|
||||||
+10
-3
@@ -1119,9 +1119,11 @@
|
|||||||
"runtime_only": true
|
"runtime_only": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"edge_count": 7612,
|
"edge_count": 7618,
|
||||||
"edge_sha256": "15b211a9bc9400fa8a28480223372039344ae8e584bd1ac769359a808e4610be",
|
"edge_sha256": "be924b781df81299b96c7580034b5c27bebeb97285871586b6f0b51f51a83d9b",
|
||||||
"edges": [
|
"edges": [
|
||||||
|
"app -> app.foundation",
|
||||||
|
"app -> app.foundation.environment",
|
||||||
"app -> app.runtime",
|
"app -> app.runtime",
|
||||||
"app -> app.runtime.compat",
|
"app -> app.runtime.compat",
|
||||||
"app -> app.runtime.compat.imports",
|
"app -> app.runtime.compat.imports",
|
||||||
@@ -1267,6 +1269,7 @@
|
|||||||
"app.adapters.system.plugin.package -> app.runtime",
|
"app.adapters.system.plugin.package -> app.runtime",
|
||||||
"app.adapters.system.plugin.package -> app.runtime.execution",
|
"app.adapters.system.plugin.package -> app.runtime.execution",
|
||||||
"app.adapters.system.plugin.package -> app.runtime.log",
|
"app.adapters.system.plugin.package -> app.runtime.log",
|
||||||
|
"app.adapters.system.plugin.package -> app.runtime.native_dependencies",
|
||||||
"app.adapters.system.plugin.package -> app.runtime.settings",
|
"app.adapters.system.plugin.package -> app.runtime.settings",
|
||||||
"app.adapters.system.resource -> app.adapters",
|
"app.adapters.system.resource -> app.adapters",
|
||||||
"app.adapters.system.resource -> app.adapters.network",
|
"app.adapters.system.resource -> app.adapters.network",
|
||||||
@@ -3918,6 +3921,7 @@
|
|||||||
"app.application.plugin.install -> app.runtime",
|
"app.application.plugin.install -> app.runtime",
|
||||||
"app.application.plugin.install -> app.runtime.execution",
|
"app.application.plugin.install -> app.runtime.execution",
|
||||||
"app.application.plugin.install -> app.runtime.log",
|
"app.application.plugin.install -> app.runtime.log",
|
||||||
|
"app.application.plugin.install -> app.runtime.native_dependencies",
|
||||||
"app.application.plugin.install -> app.schemas",
|
"app.application.plugin.install -> app.schemas",
|
||||||
"app.application.plugin.install -> app.schemas.exception",
|
"app.application.plugin.install -> app.schemas.exception",
|
||||||
"app.application.plugin.install -> app.schemas.plugin",
|
"app.application.plugin.install -> app.schemas.plugin",
|
||||||
@@ -7699,6 +7703,8 @@
|
|||||||
"app.runtime.extensions.service_config -> app.schemas",
|
"app.runtime.extensions.service_config -> app.schemas",
|
||||||
"app.runtime.extensions.service_config -> app.schemas.system",
|
"app.runtime.extensions.service_config -> app.schemas.system",
|
||||||
"app.runtime.extensions.service_config -> app.schemas.types",
|
"app.runtime.extensions.service_config -> app.schemas.types",
|
||||||
|
"app.runtime.native_dependencies -> app.runtime",
|
||||||
|
"app.runtime.native_dependencies -> app.runtime.log",
|
||||||
"app.runtime.observability -> app.runtime",
|
"app.runtime.observability -> app.runtime",
|
||||||
"app.runtime.observability -> app.runtime.deprecation",
|
"app.runtime.observability -> app.runtime.deprecation",
|
||||||
"app.runtime.observability -> app.runtime.deprecation.policy",
|
"app.runtime.observability -> app.runtime.deprecation.policy",
|
||||||
@@ -8735,7 +8741,7 @@
|
|||||||
"app.workflow.actions.transfer_file -> app.workflow",
|
"app.workflow.actions.transfer_file -> app.workflow",
|
||||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||||
],
|
],
|
||||||
"module_count": 906,
|
"module_count": 907,
|
||||||
"modules": [
|
"modules": [
|
||||||
"app",
|
"app",
|
||||||
"app.adapters",
|
"app.adapters",
|
||||||
@@ -9505,6 +9511,7 @@
|
|||||||
"app.runtime.localization",
|
"app.runtime.localization",
|
||||||
"app.runtime.log",
|
"app.runtime.log",
|
||||||
"app.runtime.loop",
|
"app.runtime.loop",
|
||||||
|
"app.runtime.native_dependencies",
|
||||||
"app.runtime.observability",
|
"app.runtime.observability",
|
||||||
"app.runtime.progress",
|
"app.runtime.progress",
|
||||||
"app.runtime.rate",
|
"app.runtime.rate",
|
||||||
|
|||||||
@@ -195,3 +195,37 @@ def test_native_dependency_update_probe_has_narrow_automatic_triggers():
|
|||||||
assert workflow["on"]["push"]["paths"] == expected_paths
|
assert workflow["on"]["push"]["paths"] == expected_paths
|
||||||
assert "workflow_dispatch" in workflow["on"]
|
assert "workflow_dispatch" in workflow["on"]
|
||||||
assert set(workflow["jobs"]) == {"probe"}
|
assert set(workflow["jobs"]) == {"probe"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dependency_compatibility_covers_windows_free_threaded_profile():
|
||||||
|
"""依赖门禁必须真实安装并检查 Windows free-threaded profile。"""
|
||||||
|
workflow = _load_workflow("dependency-compat.yml")
|
||||||
|
matrix = workflow["jobs"]["install"]["strategy"]["matrix"]["include"]
|
||||||
|
free_threaded = next(item for item in matrix if item["expected-profile"] == "free-threaded")
|
||||||
|
|
||||||
|
assert free_threaded == {
|
||||||
|
"name": "Windows x64 free-threaded",
|
||||||
|
"runner": "windows-2025",
|
||||||
|
"python-version": "3.14t",
|
||||||
|
"runtime-group": "runtime-free-threaded",
|
||||||
|
"expected-profile": "free-threaded",
|
||||||
|
"expected-system": "Windows",
|
||||||
|
"expected-machine": "AMD64",
|
||||||
|
}
|
||||||
|
install = _step_commands(workflow, "install")
|
||||||
|
assert "--group ${{ matrix.runtime-group }}" in install
|
||||||
|
assert "python -m scripts.verify_runtime_profile" in install
|
||||||
|
assert "MOVIEPILOT_SAFE_MODE" in str(workflow["jobs"]["install"]["steps"])
|
||||||
|
expected_paths = {
|
||||||
|
"app/__init__.py",
|
||||||
|
"scripts/verify_runtime_profile.py",
|
||||||
|
}
|
||||||
|
assert expected_paths <= set(workflow["on"]["pull_request"]["paths"])
|
||||||
|
assert expected_paths <= set(workflow["on"]["push"]["paths"])
|
||||||
|
postgres_step = next(
|
||||||
|
step
|
||||||
|
for step in workflow["jobs"]["install"]["steps"]
|
||||||
|
if step.get("name") == "Expose PostgreSQL build tools"
|
||||||
|
)
|
||||||
|
assert postgres_step["if"] == "matrix.expected-profile == 'free-threaded'"
|
||||||
|
assert "Test-Path -LiteralPath $env:PGBIN -PathType Container" in postgres_step["run"]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import pytest
|
|||||||
from app.doctor import dependencies as dependency_doctor
|
from app.doctor import dependencies as dependency_doctor
|
||||||
from app.foundation import environment
|
from app.foundation import environment
|
||||||
from app.runtime import dependencies
|
from app.runtime import dependencies
|
||||||
|
from scripts import verify_runtime_profile
|
||||||
|
|
||||||
|
|
||||||
def test_free_threaded_runtime_tracks_interpreter_build(monkeypatch):
|
def test_free_threaded_runtime_tracks_interpreter_build(monkeypatch):
|
||||||
@@ -79,6 +80,26 @@ def test_runtime_profiles_share_gil_safe_crcmod_distribution():
|
|||||||
} in document["tool"]["uv"]["exclude-dependencies"]
|
} in document["tool"]["uv"]["exclude-dependencies"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_windows_runtime_profiles_isolate_pywin32_dependency():
|
||||||
|
"""标准 Windows 保留 named-pipe 依赖,free-threaded profile 不解析 pywin32。"""
|
||||||
|
project_file = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
||||||
|
with project_file.open("rb") as file:
|
||||||
|
document = tomllib.load(file)
|
||||||
|
|
||||||
|
dependencies = document["project"]["dependencies"]
|
||||||
|
groups = document["dependency-groups"]
|
||||||
|
assert not any(requirement.lower().startswith("pympler") for requirement in dependencies)
|
||||||
|
assert "pywin32==312 ; sys_platform == 'win32'" in groups["runtime-standard"]
|
||||||
|
assert not any(
|
||||||
|
requirement.lower().startswith("pywin32")
|
||||||
|
for requirement in groups["runtime-free-threaded"]
|
||||||
|
)
|
||||||
|
assert {
|
||||||
|
"package": {"name": "docker"},
|
||||||
|
"dependencies": ["pywin32"],
|
||||||
|
} in document["tool"]["uv"]["exclude-dependencies"]
|
||||||
|
|
||||||
|
|
||||||
def test_standard_runtime_uses_shared_rust_text_capabilities(monkeypatch):
|
def test_standard_runtime_uses_shared_rust_text_capabilities(monkeypatch):
|
||||||
"""标准镜像的文本能力不得重新引入 profile 专属实现。"""
|
"""标准镜像的文本能力不得重新引入 profile 专属实现。"""
|
||||||
imported = []
|
imported = []
|
||||||
@@ -118,6 +139,99 @@ exclude-dependencies = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_profile_probe_accepts_only_declared_uv_exclusions(tmp_path: Path):
|
||||||
|
"""CI 健康检查只能忽略 uv 配置中明确排除的依赖边。"""
|
||||||
|
project_file = tmp_path / "pyproject.toml"
|
||||||
|
project_file.write_text(
|
||||||
|
"""
|
||||||
|
[tool.uv]
|
||||||
|
exclude-dependencies = [
|
||||||
|
{ package = { name = "docker" }, dependencies = ["pywin32"] },
|
||||||
|
]
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
ignored = "The package `docker` requires `pywin32>=304`, but it's not installed"
|
||||||
|
actionable = "The package `demo` requires `missing>=1`, but it's not installed"
|
||||||
|
summary = "\n".join((
|
||||||
|
"Using Python 3.14.7 environment at: .venv",
|
||||||
|
"Checked 189 packages in 3ms",
|
||||||
|
"Found 2 incompatibilities",
|
||||||
|
))
|
||||||
|
|
||||||
|
assert verify_runtime_profile._uv_health_errors(
|
||||||
|
f"{summary}\n{ignored}\n{actionable}",
|
||||||
|
project_file,
|
||||||
|
) == {actionable}
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_profile_probe_loads_standard_windows_named_pipe(monkeypatch):
|
||||||
|
"""标准 Windows 必须真实加载 pywin32 与 Docker named-pipe adapter。"""
|
||||||
|
imported = []
|
||||||
|
win32_modules = {"pywintypes", "win32api", "win32file", "win32pipe"}
|
||||||
|
|
||||||
|
def fake_import_module(name: str):
|
||||||
|
imported.append(name)
|
||||||
|
if name == "docker.transport":
|
||||||
|
return SimpleNamespace(NpipeHTTPAdapter=object())
|
||||||
|
return SimpleNamespace()
|
||||||
|
|
||||||
|
monkeypatch.setattr(verify_runtime_profile.platform, "system", lambda: "Windows")
|
||||||
|
monkeypatch.setattr(verify_runtime_profile.platform, "machine", lambda: "AMD64")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
verify_runtime_profile.sysconfig,
|
||||||
|
"get_config_var",
|
||||||
|
lambda _name: 0,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(verify_runtime_profile.sys, "_is_gil_enabled", lambda: True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
verify_runtime_profile,
|
||||||
|
"find_spec",
|
||||||
|
lambda name: object() if name in win32_modules else None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(verify_runtime_profile, "import_module", fake_import_module)
|
||||||
|
|
||||||
|
verify_runtime_profile.verify_platform_profile(
|
||||||
|
expected_profile="standard",
|
||||||
|
expected_system="Windows",
|
||||||
|
expected_machine="AMD64",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert win32_modules <= set(imported)
|
||||||
|
assert "docker.transport" in imported
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_profile_probe_rejects_pywin32_from_windows_free_threaded(
|
||||||
|
monkeypatch,
|
||||||
|
):
|
||||||
|
"""Windows 3.14t 不得继承标准 ABI 的 pywin32。"""
|
||||||
|
monkeypatch.setattr(verify_runtime_profile.platform, "system", lambda: "Windows")
|
||||||
|
monkeypatch.setattr(verify_runtime_profile.platform, "machine", lambda: "AMD64")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
verify_runtime_profile.sysconfig,
|
||||||
|
"get_config_var",
|
||||||
|
lambda _name: 1,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(verify_runtime_profile.sys, "_is_gil_enabled", lambda: False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
verify_runtime_profile,
|
||||||
|
"find_spec",
|
||||||
|
lambda name: object() if name == "win32api" else None,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
verify_runtime_profile,
|
||||||
|
"import_module",
|
||||||
|
lambda _name: SimpleNamespace(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="意外安装 pywin32"):
|
||||||
|
verify_runtime_profile.verify_platform_profile(
|
||||||
|
expected_profile="free-threaded",
|
||||||
|
expected_system="Windows",
|
||||||
|
expected_machine="AMD64",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_full_dependency_probe_rejects_psycopg_python_fallback(monkeypatch):
|
def test_full_dependency_probe_rejects_psycopg_python_fallback(monkeypatch):
|
||||||
"""V3t 构建不得把 psycopg 纯 Python 实现误认为可发布能力。"""
|
"""V3t 构建不得把 psycopg 纯 Python 实现误认为可发布能力。"""
|
||||||
modules = {
|
modules = {
|
||||||
|
|||||||
@@ -1,8 +1,61 @@
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import app
|
import app
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_registers_pg_bin_for_windows_free_threaded(tmp_path, monkeypatch):
|
||||||
|
"""Windows free-threaded 启动时应注册 PostgreSQL DLL 目录。"""
|
||||||
|
handle = object()
|
||||||
|
registered = []
|
||||||
|
monkeypatch.setattr(app, "is_windows", lambda: True)
|
||||||
|
monkeypatch.setattr(app, "is_free_threaded_runtime", lambda: True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
app.os,
|
||||||
|
"add_dll_directory",
|
||||||
|
lambda path: registered.append(path) or handle,
|
||||||
|
raising=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(app, "_windows_dll_directory_handles", [])
|
||||||
|
monkeypatch.setenv("PGBIN", str(tmp_path))
|
||||||
|
|
||||||
|
app._configure_free_threaded_windows_native_dependencies()
|
||||||
|
|
||||||
|
assert registered == [str(tmp_path)]
|
||||||
|
assert app._windows_dll_directory_handles == [handle]
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_skips_pg_bin_for_standard_runtime(monkeypatch):
|
||||||
|
"""标准 Windows 运行时不应执行 free-threaded 专属 DLL 注册。"""
|
||||||
|
monkeypatch.setattr(app, "is_windows", lambda: True)
|
||||||
|
monkeypatch.setattr(app, "is_free_threaded_runtime", lambda: False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
app.os,
|
||||||
|
"add_dll_directory",
|
||||||
|
lambda _path: pytest.fail("must not register a DLL directory"),
|
||||||
|
raising=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setenv("PGBIN", "C:/PostgreSQL/bin")
|
||||||
|
|
||||||
|
app._configure_free_threaded_windows_native_dependencies()
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_skips_missing_pg_bin_for_windows_free_threaded(monkeypatch):
|
||||||
|
"""无效外部目录不能污染 Windows DLL 搜索路径。"""
|
||||||
|
monkeypatch.setattr(app, "is_windows", lambda: True)
|
||||||
|
monkeypatch.setattr(app, "is_free_threaded_runtime", lambda: True)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
app.os,
|
||||||
|
"add_dll_directory",
|
||||||
|
lambda _path: pytest.fail("must not register a missing DLL directory"),
|
||||||
|
raising=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setenv("PGBIN", "Z:/missing/postgresql/bin")
|
||||||
|
|
||||||
|
app._configure_free_threaded_windows_native_dependencies()
|
||||||
|
|
||||||
|
|
||||||
def test_app_installs_known_oss2_invalid_escape_warning_filter():
|
def test_app_installs_known_oss2_invalid_escape_warning_filter():
|
||||||
"""
|
"""
|
||||||
app 初始化过滤器应覆盖 oss2 的无效转义警告。
|
app 初始化过滤器应覆盖 oss2 的无效转义警告。
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ conflicts = [[
|
|||||||
]]
|
]]
|
||||||
|
|
||||||
[manifest]
|
[manifest]
|
||||||
excludes = [{ package = { name = "oss2" }, dependencies = ["crcmod"] }]
|
excludes = [
|
||||||
|
{ package = { name = "docker" }, dependencies = ["pywin32"] },
|
||||||
|
{ package = { name = "oss2" }, dependencies = ["crcmod"] },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiofiles"
|
name = "aiofiles"
|
||||||
@@ -842,7 +845,6 @@ name = "docker"
|
|||||||
version = "7.1.0"
|
version = "7.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'group-10-moviepilot-runtime-free-threaded' and extra == 'group-10-moviepilot-runtime-standard')" },
|
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
@@ -1701,7 +1703,6 @@ dependencies = [
|
|||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
{ name = "pympler" },
|
|
||||||
{ name = "pyotp" },
|
{ name = "pyotp" },
|
||||||
{ name = "pyparsing" },
|
{ name = "pyparsing" },
|
||||||
{ name = "pyquery" },
|
{ name = "pyquery" },
|
||||||
@@ -1713,7 +1714,6 @@ dependencies = [
|
|||||||
{ name = "pytz" },
|
{ name = "pytz" },
|
||||||
{ name = "pyvirtualdisplay" },
|
{ name = "pyvirtualdisplay" },
|
||||||
{ name = "pywebpush" },
|
{ name = "pywebpush" },
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'group-10-moviepilot-runtime-free-threaded' and extra == 'group-10-moviepilot-runtime-standard')" },
|
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "qbittorrent-api" },
|
{ name = "qbittorrent-api" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
@@ -1762,6 +1762,7 @@ runtime-standard = [
|
|||||||
{ name = "brotli", version = "1.2.0", source = { registry = "https://pypi.org/simple" } },
|
{ name = "brotli", version = "1.2.0", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "lxml", version = "6.1.2", source = { registry = "https://pypi.org/simple" } },
|
{ name = "lxml", version = "6.1.2", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -1821,7 +1822,6 @@ requires-dist = [
|
|||||||
{ name = "pydantic", specifier = ">=2.13.4,<3.0.0" },
|
{ name = "pydantic", specifier = ">=2.13.4,<3.0.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.14.2,<3.0.0" },
|
{ name = "pydantic-settings", specifier = ">=2.14.2,<3.0.0" },
|
||||||
{ name = "pyjwt", specifier = "~=2.13.0" },
|
{ name = "pyjwt", specifier = "~=2.13.0" },
|
||||||
{ name = "pympler", specifier = "~=1.1" },
|
|
||||||
{ name = "pyotp", specifier = "~=2.9.0" },
|
{ name = "pyotp", specifier = "~=2.9.0" },
|
||||||
{ name = "pyparsing", specifier = "~=3.3.2" },
|
{ name = "pyparsing", specifier = "~=3.3.2" },
|
||||||
{ name = "pyquery", specifier = "~=2.0.1" },
|
{ name = "pyquery", specifier = "~=2.0.1" },
|
||||||
@@ -1833,7 +1833,6 @@ requires-dist = [
|
|||||||
{ name = "pytz", specifier = "~=2026.2" },
|
{ name = "pytz", specifier = "~=2026.2" },
|
||||||
{ name = "pyvirtualdisplay", specifier = "~=3.0" },
|
{ name = "pyvirtualdisplay", specifier = "~=3.0" },
|
||||||
{ name = "pywebpush", specifier = "~=2.3.0" },
|
{ name = "pywebpush", specifier = "~=2.3.0" },
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32'", specifier = "==312" },
|
|
||||||
{ name = "pyyaml", specifier = "~=6.0.3" },
|
{ name = "pyyaml", specifier = "~=6.0.3" },
|
||||||
{ name = "qbittorrent-api", specifier = "==2026.6.0" },
|
{ name = "qbittorrent-api", specifier = "==2026.6.0" },
|
||||||
{ name = "redis", specifier = "~=8.1.0" },
|
{ name = "redis", specifier = "~=8.1.0" },
|
||||||
@@ -1882,6 +1881,7 @@ runtime-standard = [
|
|||||||
{ name = "brotli", specifier = "==1.2.0" },
|
{ name = "brotli", specifier = "==1.2.0" },
|
||||||
{ name = "lxml", specifier = "~=6.1.2" },
|
{ name = "lxml", specifier = "~=6.1.2" },
|
||||||
{ name = "psycopg2-binary", specifier = "~=2.9.12" },
|
{ name = "psycopg2-binary", specifier = "~=2.9.12" },
|
||||||
|
{ name = "pywin32", marker = "sys_platform == 'win32'", specifier = "==312" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2504,18 +2504,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e0/b0/3a8040e53df6c5c1e04b0e23ed53fdbeb64f333723a334d313fba2f581ce/pylint-4.0.7-py3-none-any.whl", hash = "sha256:be4a3111557a614411ed1fc89347ce4a8e1013a59e1f33d11485227a02e3304d", size = 539710, upload-time = "2026-08-09T19:13:21.228Z" },
|
{ url = "https://files.pythonhosted.org/packages/e0/b0/3a8040e53df6c5c1e04b0e23ed53fdbeb64f333723a334d313fba2f581ce/pylint-4.0.7-py3-none-any.whl", hash = "sha256:be4a3111557a614411ed1fc89347ce4a8e1013a59e1f33d11485227a02e3304d", size = 539710, upload-time = "2026-08-09T19:13:21.228Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pympler"
|
|
||||||
version = "1.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'group-10-moviepilot-runtime-free-threaded' and extra == 'group-10-moviepilot-runtime-standard')" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/37/c384631908029676d8e7213dd956bb686af303a80db7afbc9be36bc49495/pympler-1.1.tar.gz", hash = "sha256:1eaa867cb8992c218430f1708fdaccda53df064144d1c5656b1e6f1ee6000424", size = 179954, upload-time = "2024-06-28T19:56:06.563Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/4f/a6a2e2b202d7fd97eadfe90979845b8706676b41cbd3b42ba75adf329d1f/Pympler-1.1-py3-none-any.whl", hash = "sha256:5b223d6027d0619584116a0cbc28e8d2e378f7a79c1e5e024f9ff3b673c58506", size = 165766, upload-time = "2024-06-28T19:56:05.087Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyobjc-core"
|
name = "pyobjc-core"
|
||||||
version = "12.2.2"
|
version = "12.2.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user