Compare commits

...
5 Commits
Author SHA1 Message Date
Sergey Palcheh 07ad9294d4 feat: block head tracking when gamepad is connected
Code check / Check code (pull_request) Failing after 2m39s
Package Build - AppImage, Arch, Fedora, Debian / changes (pull_request) Successful in 16s
Package Build - AppImage, Arch, Fedora, Debian / Build AppImage (pull_request) Has been skipped
Package Build - AppImage, Arch, Fedora, Debian / Build Fedora RPM (42) (pull_request) Successful in 1m14s
Package Build - AppImage, Arch, Fedora, Debian / Build Fedora RPM (43) (pull_request) Successful in 5m18s
Package Build - AppImage, Arch, Fedora, Debian / Build Fedora RPM (44) (pull_request) Successful in 1m21s
Package Build - AppImage, Arch, Fedora, Debian / Build Fedora RPM (rawhide) (pull_request) Successful in 1m38s
Package Build - AppImage, Arch, Fedora, Debian / Build Arch Package (pull_request) Successful in 6m16s
Package Build - AppImage, Arch, Fedora, Debian / Build Debian Package (pull_request) Successful in 4m22s
2026-06-28 22:53:53 +05:00
Sergey Palcheh 7af3c29c58 fix(settings): suppress pyright type error for input_manager access 2026-06-28 22:53:52 +05:00
Sergey Palcheh 394958724a fix(packaging): add missing datadir files for head tracking 2026-06-28 22:53:52 +05:00
Sergey Palcheh 19151d5fd3 refactor(head_tracking): replace mediapipe with opencv yunet 2026-06-28 22:34:27 +05:00
Sergey Palcheh e67d52c89a added head tracking 2026-06-28 22:32:52 +05:00
39 changed files with 3404 additions and 93 deletions

No files matched your search

+2 -2
View File
@@ -111,7 +111,7 @@ jobs:
run: |
dnf install -y git rpmdevtools meson ninja-build python3-devel \
systemd-rpm-macros vulkan-loader-devel redhat-rpm-config nodejs npm \
gcc gettext
gcc gettext mingw64-gcc
- name: Setup rpmbuild environment
run: |
@@ -150,7 +150,7 @@ jobs:
run: |
apt-get update
apt-get install -y build-essential debhelper dpkg-dev gettext git \
libvulkan-dev meson ninja-build nodejs npm \
libvulkan-dev meson mingw-w64 ninja-build nodejs npm \
pkgconf python3
- name: Checkout
+2 -2
View File
@@ -137,7 +137,7 @@ jobs:
run: |
dnf install -y git rpmdevtools meson ninja-build python3-devel \
systemd-rpm-macros vulkan-loader-devel redhat-rpm-config nodejs npm \
gcc gettext
gcc gettext mingw64-gcc
- name: Setup rpmbuild environment
run: |
@@ -257,7 +257,7 @@ jobs:
run: |
apt-get update
apt-get install -y build-essential debhelper dpkg-dev gettext git \
libvulkan-dev meson ninja-build nodejs npm \
libvulkan-dev meson mingw-w64 ninja-build nodejs npm \
pkgconf python3
- name: Checkout PR
+3
View File
@@ -41,5 +41,8 @@ Thumbs.db
# Compiled gettext catalogs
portprotonqt/locales/*/LC_MESSAGES/*.mo
# Local MinGW toolchain and built DLL (CI builds these)
.toolchain/
data/NPClient64.dll
# AI
.codex
+2 -2
View File
@@ -6,8 +6,8 @@ arch=('any')
url="https://git.linux-gaming.ru/Linux-Gaming/PortProtonQt"
license=('GPL-3.0')
depends=('python-requests' 'python-babel' 'python-evdev' 'python-pygame' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'python-rapidfuzz' 'python-pefile' 'python-pillow' 'perl-image-exiftool' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'jq' 'file' 'findutils' 'gawk' 'grep' 'tar' 'xz' 'zstd' 'gzip' 'unrar' 'qt6-svg' 'qt6-imageformats' 'pciutils' 'mesa-utils' 'vulkan-icd-loader' 'procps-ng' 'psmisc' '7zip' 'python-dbus-fast' 'sdl3')
makedepends=('meson' 'ninja' 'vulkan-headers' 'gettext')
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'python-rapidfuzz' 'python-pefile' 'python-pillow' 'perl-image-exiftool' 'python-websocket-client' 'python-opencv' 'python-numpy' 'cabextract' 'unzip' 'curl' 'jq' 'file' 'findutils' 'gawk' 'grep' 'tar' 'xz' 'zstd' 'gzip' 'unrar' 'qt6-svg' 'qt6-imageformats' 'pciutils' 'mesa-utils' 'vulkan-icd-loader' 'procps-ng' 'psmisc' '7zip' 'python-dbus-fast' 'sdl3')
makedepends=('meson' 'ninja' 'vulkan-headers' 'gettext' 'mingw-w64-gcc')
conflicts=("portprotonqt-steam-compat")
provides=('portprotonqt-steam-compat')
replaces=('portprotonqt-steam-compat')
+1 -1
View File
@@ -6,7 +6,7 @@ arch=('any')
url="https://git.linux-gaming.ru/Linux-Gaming/PortProtonQt"
license=('GPL-3.0')
depends=('python-requests' 'python-babel' 'python-evdev' 'python-pygame' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'python-rapidfuzz' 'python-pefile' 'python-pillow' 'perl-image-exiftool' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'jq' 'file' 'findutils' 'gawk' 'grep' 'tar' 'xz' 'zstd' 'gzip' 'unrar' 'qt6-svg' 'qt6-imageformats' 'pciutils' 'mesa-utils' 'vulkan-icd-loader' 'procps-ng' 'psmisc' '7zip' 'python-dbus-fast' 'sdl3')
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'python-rapidfuzz' 'python-pefile' 'python-pillow' 'perl-image-exiftool' 'python-websocket-client' 'python-opencv' 'python-numpy' 'cabextract' 'unzip' 'curl' 'jq' 'file' 'findutils' 'gawk' 'grep' 'tar' 'xz' 'zstd' 'gzip' 'unrar' 'qt6-svg' 'qt6-imageformats' 'pciutils' 'mesa-utils' 'vulkan-icd-loader' 'procps-ng' 'psmisc' '7zip' 'python-dbus-fast' 'sdl3')
makedepends=('meson' 'ninja' 'vulkan-headers' 'gettext')
conflicts=("portprotonqt-steam-compat")
provides=('portprotonqt-steam-compat')
+4
View File
@@ -23,6 +23,7 @@ BuildRequires: gettext
BuildRequires: systemd-rpm-macros
BuildRequires: vulkan-loader-devel
BuildRequires: gcc
BuildRequires: mingw64-gcc
Obsoletes: python3-%{pypi_name}-git < %{version}-%{release}
Provides: python3-%{pypi_name}-git = %{version}-%{release}
@@ -42,6 +43,8 @@ Requires: python3-pillow
Requires: python3-pillow-qt
Requires: python3-rapidfuzz
Requires: python3-libarchive-c
Requires: python3-opencv
Requires: python3-numpy
Requires: perl-Image-ExifTool
Requires: SDL3
Requires: qt6-qtsvg
@@ -118,6 +121,7 @@ install -Dpm 0644 ./completions/_portprotonqt -t %{buildroot}%{zsh_completions_d
%{_datadir}/portproton/scripts/
%{_datadir}/portproton/conf/
%{_datadir}/portproton/img/
%{_datadir}/%{pypi_name}/
%{bash_completions_dir}/portprotonqt
%{fish_completions_dir}/portprotonqt.fish
%{zsh_completions_dir}/_portprotonqt
+4
View File
@@ -20,6 +20,7 @@ BuildRequires: gettext
BuildRequires: systemd-rpm-macros
BuildRequires: vulkan-loader-devel
BuildRequires: gcc
BuildRequires: mingw64-gcc
Obsoletes: python3-%{pypi_name} < %{version}-%{release}
Provides: python3-%{pypi_name} = %{version}-%{release}
@@ -39,6 +40,8 @@ Requires: python3-pillow
Requires: python3-pillow-qt
Requires: python3-rapidfuzz
Requires: python3-libarchive-c
Requires: python3-opencv
Requires: python3-numpy
Requires: perl-Image-ExifTool
Requires: SDL3
Requires: qt6-qtsvg
@@ -116,6 +119,7 @@ install -Dpm 0644 ./completions/_portprotonqt -t %{buildroot}%{zsh_completions_d
%{_datadir}/portproton/scripts/
%{_datadir}/portproton/conf/
%{_datadir}/portproton/img/
%{_datadir}/%{pypi_name}/
%{bash_completions_dir}/portprotonqt
%{fish_completions_dir}/portprotonqt.fish
%{zsh_completions_dir}/_portprotonqt
+26
View File
@@ -0,0 +1,26 @@
NPClient64.dll for PortProtonQt
Copyright (C) 2026 PortProtonQt contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
---
The NPClient API implemented in this DLL is based on publicly
available documentation and open-source implementations, including:
- linuxtrack-wine by John Tsiombikas (GPLv3)
https://github.com/jtsiomb/linuxtrack-wine
- opentrack by Stanislaw Halik and contributors (ISC)
https://github.com/opentrack/opentrack
+20
View File
@@ -0,0 +1,20 @@
# Makefile for NPClient64.dll
# Build with: make NPClient64.dll
# Requires mingw-w64 toolchain (x86_64-w64-mingw32-gcc)
CC = x86_64-w64-mingw32-gcc
CFLAGS = -O2 -Wall -shared
LDFLAGS = -Wl,--subsystem,windows
TARGET = NPClient64.dll
SRC = npclient64.c
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(SRC)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
clean:
rm -f $(TARGET)
+210
View File
@@ -0,0 +1,210 @@
/*
* NPClient64.dll - TrackIR-compatible head tracking client for Wine.
*
* Implements the NaturalPoint TrackIR NPClient API, reading head tracking
* data from the Wine shared memory file "facetracknoir-wine-shm" written
* by PortProtonQt.
*
* The NPClient API was reverse-engineered from publicly available
* documentation and open-source implementations. This file is released
* under the GPL-3.0 license as part of PortProtonQt.
*/
#include <windows.h>
#include <stdint.h>
#include <string.h>
#define WINE_SHM_NAME "facetracknoir-wine-shm"
#define WINE_MTX_NAME "facetracknoir-wine-mtx"
#pragma pack(push, 1)
struct WineSHM {
double data[6];
int32_t gameid;
int32_t gameid2;
unsigned char table[8];
BOOL stop;
};
struct NPData {
int16_t status;
int16_t frame;
uint32_t checksum;
float roll, pitch, yaw;
float tx, ty, tz;
float padding[9];
};
struct NPSignature {
char dllsig[200];
char appsig[200];
};
#pragma pack(pop)
static HANDLE hShm = NULL;
static struct WineSHM *shm = NULL;
static HANDLE hMtx = NULL;
static int16_t frame_counter = 0;
static int initialized = 0;
static int transmitting = 0;
static struct NPSignature sigdata = {
"precise head tracking\n put your head into the game\n now go look around\n\n "
"Copyright EyeControl Technologies",
"hardware camera\n software processing data\n track user movement\n\n "
"Copyright EyeControl Technologies"
};
static BOOL impl_create_mapping(void)
{
if (shm != NULL)
return TRUE;
hShm = CreateFileMappingA(INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
sizeof(struct WineSHM),
(LPCSTR)WINE_SHM_NAME);
if (hShm == NULL)
return FALSE;
shm = (struct WineSHM *)MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(struct WineSHM));
hMtx = CreateMutexA(NULL, FALSE, WINE_MTX_NAME);
return shm != NULL;
}
__declspec(dllexport) int __stdcall NP_GetData(struct NPData *data)
{
if (data == NULL)
return 1;
memset(data, 0, sizeof(struct NPData));
if (!initialized || !transmitting)
return 0;
if (impl_create_mapping() == FALSE || shm == NULL)
return 1;
if (hMtx && WaitForSingleObject(hMtx, 16) == WAIT_OBJECT_0) {
double x = shm->data[0];
double y = shm->data[1];
double z = shm->data[2];
double yaw = shm->data[3];
double pitch = shm->data[4];
double roll = shm->data[5];
ReleaseMutex(hMtx);
data->yaw = (float)(-(yaw * 180.0 / 3.14159265358979323846) / 180.0 * 16384.0);
data->pitch = (float)(-(pitch * 180.0 / 3.14159265358979323846) / 180.0 * 16384.0);
data->roll = (float)(-(roll * 180.0 / 3.14159265358979323846) / 180.0 * 16384.0);
data->tx = (float)(-x * 64.0f / 10.0f);
data->ty = (float)(y * 64.0f / 10.0f);
data->tz = (float)(z * 64.0f / 10.0f);
}
data->status = 0;
data->frame = frame_counter++;
data->checksum = 0;
return 0;
}
__declspec(dllexport) int __stdcall NP_GetSignature(struct NPSignature *sig)
{
if (sig == NULL)
return 1;
memcpy(sig, &sigdata, sizeof(sigdata));
return 0;
}
__declspec(dllexport) int __stdcall NP_QueryVersion(short *ver)
{
if (ver == NULL)
return 1;
*ver = 0x0400;
return 0;
}
__declspec(dllexport) int __stdcall NP_ReCenter(void)
{
return 0;
}
__declspec(dllexport) int __stdcall NP_RegisterWindowHandle(void *handle)
{
(void)handle;
return 0;
}
__declspec(dllexport) int __stdcall NP_UnregisterWindowHandle(void)
{
return 0;
}
__declspec(dllexport) int __stdcall NP_RegisterProgramProfileID(short id)
{
(void)id;
initialized = 1;
return 0;
}
__declspec(dllexport) int __stdcall NP_RequestData(short data)
{
(void)data;
return 0;
}
__declspec(dllexport) int __stdcall NP_StopCursor(void)
{
return 0;
}
__declspec(dllexport) int __stdcall NP_StartCursor(void)
{
return 0;
}
__declspec(dllexport) int __stdcall NP_StartDataTransmission(void)
{
transmitting = 1;
return 0;
}
__declspec(dllexport) int __stdcall NP_StopDataTransmission(void)
{
transmitting = 0;
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
(void)hinstDLL;
(void)lpvReserved;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
break;
case DLL_PROCESS_DETACH:
if (shm) {
UnmapViewOfFile(shm);
shm = NULL;
}
if (hShm) {
CloseHandle(hShm);
hShm = NULL;
}
if (hMtx) {
CloseHandle(hMtx);
hMtx = NULL;
}
break;
default:
break;
}
return TRUE;
}
Binary file not shown.
+3
View File
@@ -7,6 +7,7 @@ Build-Depends:
gettext,
libvulkan-dev,
meson,
mingw-w64,
ninja-build,
pkgconf,
python3,
@@ -34,6 +35,8 @@ Depends:
python3-dbus-fast,
python3-evdev,
python3-libarchive-c,
python3-numpy,
python3-opencv,
python3-orjson,
python3-pefile,
python3-pil,
+5
View File
@@ -12,6 +12,11 @@ Copyright:
2026 PortProtonQt contributors
License: GPL-3.0
Files: data/face_detection_yunet_2023mar.onnx
Copyright:
2023 OpenCV team
License: Apache-2.0
License: GPL-3.0
This package is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3.
+1
View File
@@ -11,4 +11,5 @@ usr/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml
usr/share/mime/packages/ru.linux_gaming.PortProtonQt.xml
usr/share/polkit-1/rules.d/ru.linux_gaming.PortProtonQt.rules
usr/share/portproton/
usr/share/portprotonqt/
usr/share/zsh/site-functions/_portprotonqt
+21
View File
@@ -102,6 +102,27 @@ install_data(
install_dir: datadir / 'metainfo',
)
# Install YuNet face detection model for head tracking
install_data(
'data/face_detection_yunet_2023mar.onnx',
install_dir: datadir / meson.project_name(),
)
# Build NPClient64.dll for TrackIR-compatible games (optional, requires mingw-w64)
mingw_cc = find_program('x86_64-w64-mingw32-gcc', required: false)
if mingw_cc.found()
npclient64_dll = custom_target('npclient64_dll',
input: 'build-aux/npclient/npclient64.c',
output: 'NPClient64.dll',
command: [mingw_cc, '-O2', '-Wall', '-shared', '-Wl,--subsystem,windows',
'@INPUT@', '-o', '@OUTPUT@'],
install: true,
install_dir: datadir / meson.project_name(),
)
else
warning('mingw-w64 not found, skipping NPClient64.dll build. Install mingw-w64 to enable TrackIR game support.')
endif
# Install PortProton scripts
install_subdir(
'build-aux/share/portproton/scripts',
+27 -6
View File
@@ -3,7 +3,7 @@
import os
import re
import subprocess
from typing import cast, TYPE_CHECKING
from typing import cast
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, QProcess, QTimer, QUrl
from PySide6.QtGui import QColor, QContextMenuEvent, QDesktopServices, QGuiApplication, QIcon
@@ -26,9 +26,6 @@ from PySide6.QtWidgets import (
QWidget,
)
if TYPE_CHECKING:
from portprotonqt.main_window import MainWindow
from portprotonqt.config import (
get_portproton_location,
get_portproton_scripts_path,
@@ -42,6 +39,7 @@ from portprotonqt.dialogs.dialog_utils import create_dialog_hints_widget, update
from portprotonqt.dialogs.settings_mangohud import MANGOHUD_ENV_KEYS, MangoHudSettingsMixin
from portprotonqt.dialogs.settings_gamescope import GAMESCOPE_ENV_KEYS, GamescopeSettingsMixin
from portprotonqt.dialogs.settings_vkbasalt import VKBASALT_ENV_KEYS, VkBasaltSettingsMixin
from portprotonqt.dialogs.settings_headtracking import HeadTrackingSettingsMixin, HT_ALL_KEYS
from portprotonqt.localization import _, format_setting_name_for_display
from portprotonqt.logger import get_logger
from portprotonqt.preloader import Preloader
@@ -132,6 +130,7 @@ class ExeSettingsDialog(
MangoHudSettingsMixin,
GamescopeSettingsMixin,
VkBasaltSettingsMixin,
HeadTrackingSettingsMixin,
):
"""Dialog for configuring executable-specific settings."""
@@ -179,11 +178,13 @@ class ExeSettingsDialog(
self.init_mangohud_state()
self.init_gamescope_state()
self.init_vkbasalt_state()
self.init_headtracking_state()
self.blocked_keys = set()
self.numa_nodes = {}
self.locale_options = []
self.logical_core_options = []
self._gamepad_tooltip_map = {}
self._closing = False
self.setWindowTitle(_("Exe Settings"))
self.setModal(True)
@@ -203,7 +204,7 @@ class ExeSettingsDialog(
parent_obj = self.parent()
while parent_obj:
if hasattr(parent_obj, 'input_manager'):
self.input_manager = cast("MainWindow", parent_obj).input_manager
self.input_manager = parent_obj.input_manager # type: ignore[union-attr]
self.main_window = parent_obj
parent_obj = parent_obj.parent()
@@ -212,6 +213,7 @@ class ExeSettingsDialog(
if self.input_manager:
self.input_manager.enable_settings_mode(self)
self.input_manager.gamepad_hotplug.connect(self._update_vkbasalt_toggle_key_visibility)
self.input_manager.gamepad_hotplug.connect(self._check_gamepad_and_block_headtracking)
self.hints_widget, self.hints_labels = create_dialog_hints_widget(
self.theme, self.main_window, self.input_manager, context='settings'
@@ -290,6 +292,8 @@ class ExeSettingsDialog(
self.vkbasalt_tab_layout = QVBoxLayout(self.vkbasalt_tab)
self.gamescope_tab = QWidget()
self.gamescope_tab_layout = QVBoxLayout(self.gamescope_tab)
self.headtracking_tab = QWidget()
self.headtracking_tab_layout = QVBoxLayout(self.headtracking_tab)
self.tab_widget.addTab(self.main_tab, _("Main"))
self.tab_widget.addTab(self.advanced_tab, _("Advanced"))
@@ -297,7 +301,9 @@ class ExeSettingsDialog(
self.tab_widget.addTab(self.vkbasalt_tab, "vkBasalt")
if self.gamescope_available:
self.tab_widget.addTab(self.gamescope_tab, "Gamescope")
self.tab_widget.addTab(self.headtracking_tab, _("Head Tracking"))
self.tab_widget.currentChanged.connect(self.on_table_selection_changed)
self.tab_widget.currentChanged.connect(lambda: self._on_headtracking_tab_changed())
self.settings_table = QTableWidget()
self.settings_table.setAlternatingRowColors(True)
@@ -421,6 +427,7 @@ class ExeSettingsDialog(
self.setup_vkbasalt_tab()
if self.gamescope_available:
self.setup_gamescope_tab()
self.setup_headtracking_tab()
self.main_layout.addWidget(self.tab_widget)
@@ -473,6 +480,8 @@ class ExeSettingsDialog(
def on_show_ppdb_finished(self, exit_code, exit_status):
"""Handle --show-ppdb output."""
if self._closing:
return
process = cast(QProcess, self.sender())
self.current_settings = {}
self.blocked_keys = set()
@@ -504,6 +513,7 @@ class ExeSettingsDialog(
or key in GAMESCOPE_ENV_KEYS
or key in VKBASALT_ENV_KEYS
or key in TOGGLE_BOOL_KEYS
or key in HT_ALL_KEYS
):
if val.startswith('"') and val.endswith('"') and len(val) >= 2:
val = val[1:-1]
@@ -564,6 +574,8 @@ class ExeSettingsDialog(
self.populate_vkbasalt()
if self.gamescope_available:
self.populate_gamescope()
self.populate_headtracking()
self._on_headtracking_tab_changed()
self.populate_favorites(select_tab=True)
self.settings_container.setCurrentIndex(1)
@@ -1228,9 +1240,11 @@ class ExeSettingsDialog(
gamescope_changes = []
if self.gamescope_available:
gamescope_changes = self._collect_gamescope_changes()
headtracking_changes = self._collect_headtracking_changes()
changes.extend(mangohud_changes)
changes.extend(self._collect_vkbasalt_changes())
changes.extend(gamescope_changes)
changes.extend(headtracking_changes)
# Check if PW_GAMESCOPE toggle changes are already in the list
has_gamescope_toggle = any(change.startswith("PW_GAMESCOPE=") for change in gamescope_changes)
@@ -1267,14 +1281,17 @@ class ExeSettingsDialog(
QMessageBox.warning(self, _("Error"), _("Failed to apply changes. Check logs."))
logger.error(f"Failed to apply changes: {error_output}")
else:
self.load_current_settings()
self._closing = True
self.cleanup_headtracking()
self.close()
def closeEvent(self, event):
self._closing = True
if hasattr(self, 'keyboard') and self.keyboard.isVisible():
self.keyboard.hide()
if self.input_manager:
self.input_manager.disable_settings_mode()
self.cleanup_headtracking()
super().closeEvent(event)
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
@@ -1474,17 +1491,21 @@ class ExeSettingsDialog(
self.show_gamepad_tooltip(show=False)
def reject(self):
self._closing = True
if hasattr(self, 'keyboard') and self.keyboard.isVisible():
self.keyboard.hide()
self.gamepad_tooltip.setVisible(False)
if self.input_manager:
self.input_manager.disable_settings_mode()
self.cleanup_headtracking()
super().reject()
def accept(self):
self._closing = True
if hasattr(self, 'keyboard') and self.keyboard.isVisible():
self.keyboard.hide()
self.gamepad_tooltip.setVisible(False)
if self.input_manager:
self.input_manager.disable_settings_mode()
self.cleanup_headtracking()
super().accept()
File diff suppressed because it is too large. Load diff
+209
View File
@@ -0,0 +1,209 @@
"""Global hotkey support via evdev without grab (Linux).
Reads keyboard events at the kernel level without grabbing devices,
so all keys (including the hotkey) pass through to games normally.
Works on Wayland, X11, and in games.
"""
import select
import threading
import time
from collections.abc import Callable
from PySide6.QtCore import QObject, Signal
from evdev import InputDevice, ecodes, list_devices
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
# Keys that can be used as global hotkeys
SUPPORTED_HOTKEYS = {
"scrolllock": ecodes.KEY_SCROLLLOCK,
"insert": ecodes.KEY_INSERT,
}
DEFAULT_HOTKEY = "insert"
class GlobalHotkeyEvdev(QObject):
"""Global hotkey using evdev without grab. Works on Wayland and X11."""
activated = Signal(str)
def __init__(self) -> None:
super().__init__()
self._callbacks: dict[str, Callable[[], None]] = {}
self._devices: list[InputDevice] = []
self._thread: threading.Thread | None = None
self._running = False
self._pressed: set[int] = set()
self._hotkey_codes: set[int] = {SUPPORTED_HOTKEYS[DEFAULT_HOTKEY]}
def _find_keyboard_devices(self) -> list[InputDevice]:
"""Find all keyboard input devices."""
devices: list[InputDevice] = []
for path in list_devices():
try:
dev = InputDevice(path)
caps = dev.capabilities()
if ecodes.EV_KEY not in caps:
continue
keys = caps[ecodes.EV_KEY]
# Must have at least one hotkey and basic letter keys
if ecodes.KEY_A in keys and any(h in keys for h in self._hotkey_codes):
devices.append(dev)
logger.debug("Keyboard device: %s (%s)", path, dev.name)
except (OSError, PermissionError):
continue
logger.info("Found %d keyboard device(s)", len(devices))
return devices
def _scan_devices(self) -> None:
"""Close old devices and open new ones."""
for dev in self._devices:
try:
dev.close()
except Exception:
pass
self._devices = self._find_keyboard_devices()
def register(
self,
shortcut_id: str,
_description: str,
callback: Callable[[], None],
hotkey: str | None = None,
) -> bool:
"""Register a global hotkey and start monitoring."""
self._callbacks[shortcut_id] = callback
if hotkey and hotkey.lower() in SUPPORTED_HOTKEYS:
self._hotkey_codes.add(SUPPORTED_HOTKEYS[hotkey.lower()])
# Already running — just updated hotkey codes
if self._running and self._thread is not None and self._thread.is_alive():
logger.info("Global hotkey '%s' (Insert) active", shortcut_id)
return True
self._devices = self._find_keyboard_devices()
if not self._devices:
logger.warning("No keyboard devices found for hotkey")
return False
self._running = True
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
logger.info(
"Global hotkey '%s' (Insert) registered on %d device(s)",
shortcut_id,
len(self._devices),
)
return True
def _run(self) -> None:
"""Main loop: wait for events with select() and dispatch."""
scan_interval = 30.0 # Rescan devices every 30 seconds
min_scan_interval = 5.0 # Minimum time between rescans
last_scan = time.time()
while self._running:
try:
now = time.time()
if now - last_scan > scan_interval:
self._scan_devices()
last_scan = now
if not self._devices:
time.sleep(1.0)
if now - last_scan > min_scan_interval:
self._scan_devices()
last_scan = time.time()
continue
# Build readable file descriptors
readable: list[int] = []
fd_map: dict[int, InputDevice] = {}
for dev in list(self._devices):
try:
fd = dev.fd
if fd is not None and fd >= 0:
readable.append(fd)
fd_map[fd] = dev
except Exception:
continue
if not readable:
time.sleep(0.5)
continue
# Wait for events with timeout
ready, _, _ = select.select(readable, [], [], 0.5)
for fd in ready:
dev = fd_map.get(fd)
if dev is None:
continue
try:
for event in dev.read():
self._handle_event(event)
except (OSError, PermissionError):
# Device disconnected or permission lost
logger.debug("Device %s unreadable, removing", dev.path)
try:
dev.close()
except Exception:
pass
if dev in self._devices:
self._devices.remove(dev)
except Exception as e:
logger.debug("Read error on %s: %s", dev.path, e)
except Exception as e:
logger.error("Hotkey monitor error: %s", e)
time.sleep(1.0)
def _handle_event(self, event) -> None:
"""Process a single input event."""
if event.type != ecodes.EV_KEY:
return
code = event.code
value = event.value
# value: 0 = release, 1 = press, 2 = repeat
if value == 1 and code in self._hotkey_codes:
# Find hotkey name from code and call all registered callbacks
hotkey_name = None
for name, key_code in SUPPORTED_HOTKEYS.items():
if key_code == code:
hotkey_name = name
break
if hotkey_name is None:
return
logger.info("Hotkey %s pressed", hotkey_name)
for shortcut_id, cb in self._callbacks.items():
try:
cb()
except Exception as e:
logger.error("Hotkey callback error for '%s': %s", shortcut_id, e)
for shortcut_id in self._callbacks:
self.activated.emit(shortcut_id)
def unregister(self, shortcut_id: str) -> None:
"""Unregister a global hotkey."""
self._callbacks.pop(shortcut_id, None)
def stop(self) -> None:
"""Stop and cleanup."""
self._running = False
if self._thread is not None:
self._thread.join(timeout=2.0)
self._thread = None
for dev in self._devices:
try:
dev.close()
except Exception:
pass
self._devices = []
logger.info("Global hotkey stopped")
+14
View File
@@ -0,0 +1,14 @@
"""Head tracking module for PortProtonQt."""
from portprotonqt.head_tracking.tracker_base import BaseTracker, PoseData
from portprotonqt.head_tracking.tracker_camera import CameraTracker
from portprotonqt.head_tracking.tracker_manager import TrackerManager
from portprotonqt.head_tracking.tracking_worker import TrackingWorker
__all__ = [
"BaseTracker",
"CameraTracker",
"PoseData",
"TrackerManager",
"TrackingWorker",
]
@@ -0,0 +1,10 @@
"""Output methods for head tracking data."""
from portprotonqt.head_tracking.outputs.flightgear_output import FlightGearOutput
from portprotonqt.head_tracking.outputs.joystick_output import JoystickOutput
from portprotonqt.head_tracking.outputs.mouse_output import MouseOutput
from portprotonqt.head_tracking.outputs.npclient_output import NpclientOutput
from portprotonqt.head_tracking.outputs.shm_output import ShmOutput
from portprotonqt.head_tracking.outputs.udp_output import UdpOutput
__all__ = ["FlightGearOutput", "JoystickOutput", "MouseOutput", "NpclientOutput", "ShmOutput", "UdpOutput"]
@@ -0,0 +1,21 @@
"""Base class for head tracking outputs."""
from abc import ABC, abstractmethod
from portprotonqt.head_tracking.tracker_base import PoseData
class BaseOutput(ABC):
"""Abstract base for head tracking output methods."""
@abstractmethod
def open(self) -> bool:
"""Initialize output. Returns True on success."""
@abstractmethod
def close(self) -> None:
"""Release output resources."""
@abstractmethod
def write(self, pose: PoseData) -> None:
"""Write pose data to output."""
@@ -0,0 +1,75 @@
"""FlightGear UDP output for head tracking.
Sends pose data in FlightGear native protocol format.
Compatible with opentrack FlightGear protocol.
"""
import socket
import struct
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.tracker_base import PoseData
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
# FlightGear datagram format:
# x, y, z in meters (pose * 1e-2, with sign flips)
# h, p, r in degrees (with sign flips)
# status int32 = 1
_FG_FORMAT = "<6dI"
_DEFAULT_HOST = "127.0.0.1"
_DEFAULT_PORT = 5542
class FlightGearOutput(BaseOutput):
"""Send head tracking data to FlightGear via UDP."""
def __init__(self, host: str = _DEFAULT_HOST, port: int = _DEFAULT_PORT) -> None:
self.host = host
self.port = port
self._sock: socket.socket | None = None
def open(self) -> bool:
"""Open UDP socket."""
try:
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
logger.info("FlightGear output opened: %s:%d", self.host, self.port)
return True
except Exception as exc:
logger.error("Failed to open FlightGear UDP socket: %s", exc)
return False
def close(self) -> None:
"""Close UDP socket."""
if self._sock:
try:
self._sock.close()
except Exception:
pass
self._sock = None
logger.info("FlightGear output closed")
def write(self, pose: PoseData) -> None:
"""Send pose data in FlightGear format."""
if self._sock is None:
return
# opentrack conversion:
# x = -headpose[TX] * 1e-2, y = headpose[TY] * 1e-2, z = headpose[TZ] * 1e-2
# h = -headpose[Yaw], p = headpose[Pitch], r = -headpose[Roll]
data = (
-pose.x * 1e-2,
pose.y * 1e-2,
pose.z * 1e-2,
-pose.yaw,
pose.pitch,
-pose.roll,
1, # status
)
packet = struct.pack(_FG_FORMAT, *data)
try:
self._sock.sendto(packet, (self.host, self.port))
except Exception as exc:
logger.warning("FlightGear UDP send failed: %s", exc)
@@ -0,0 +1,136 @@
"""Joystick emulation output for head tracking."""
import threading
import time
import evdev
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.tracker_base import PoseData
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
# Linux input axis range for absolute axes
_AXIS_MIN = -32768
_AXIS_MAX = 32767
def _degrees_to_axis(degrees: float, max_degrees: float = 45.0) -> int:
"""Map degrees to joystick axis value."""
clamped = max(-max_degrees, min(max_degrees, degrees))
return int((clamped / max_degrees) * _AXIS_MAX)
class JoystickOutput(BaseOutput):
"""Emulates joystick axes from head tracking yaw/pitch/roll."""
def __init__(
self,
sens_yaw: float = 1.0,
sens_pitch: float = 1.0,
sens_roll: float = 1.0,
max_angle: float = 45.0,
) -> None:
self.sens_yaw = sens_yaw
self.sens_pitch = sens_pitch
self.sens_roll = sens_roll
self.max_angle = max_angle
self._ui = None
self._running = False
self._thread: threading.Thread | None = None
self._lock = threading.Lock()
self._pose = PoseData()
def open(self) -> bool:
"""Start joystick emulation thread."""
try:
import evdev.uinput
except ImportError:
logger.error("evdev not installed. Install: uv add evdev")
return False
with self._lock:
if self._running:
return True
try:
caps = {
evdev.ecodes.EV_ABS: [
(evdev.ecodes.ABS_X, evdev.AbsInfo(0, _AXIS_MIN, _AXIS_MAX, 0, 0, 0)),
(evdev.ecodes.ABS_Y, evdev.AbsInfo(0, _AXIS_MIN, _AXIS_MAX, 0, 0, 0)),
(evdev.ecodes.ABS_Z, evdev.AbsInfo(0, _AXIS_MIN, _AXIS_MAX, 0, 0, 0)),
(evdev.ecodes.ABS_RX, evdev.AbsInfo(0, _AXIS_MIN, _AXIS_MAX, 0, 0, 0)),
(evdev.ecodes.ABS_RY, evdev.AbsInfo(0, _AXIS_MIN, _AXIS_MAX, 0, 0, 0)),
(evdev.ecodes.ABS_RZ, evdev.AbsInfo(0, _AXIS_MIN, _AXIS_MAX, 0, 0, 0)),
],
evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_TRIGGER],
}
self._ui = evdev.uinput.UInput(
caps, name="PortProtonQt Head Tracking", version=0x3
)
except Exception as exc:
logger.error("Failed to create uinput joystick: %s", exc)
return False
self._running = True
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
logger.info("Joystick output started")
return True
def close(self) -> None:
"""Stop joystick emulation."""
with self._lock:
self._running = False
if self._ui:
try:
self._ui.close()
except Exception:
pass
self._ui = None
if self._thread:
self._thread.join(timeout=1.0)
self._thread = None
logger.info("Joystick output stopped")
def write(self, pose: PoseData) -> None:
"""Update current head pose."""
with self._lock:
self._pose = pose
def _run(self) -> None:
"""Main emulation loop."""
last_values: dict[int, int] = {}
while True:
with self._lock:
if not self._running:
break
pose = self._pose
ui = self._ui
if ui is None:
time.sleep(0.016)
continue
values = {
evdev.ecodes.ABS_X: _degrees_to_axis(pose.yaw * self.sens_yaw, self.max_angle),
evdev.ecodes.ABS_Y: _degrees_to_axis(pose.pitch * self.sens_pitch, self.max_angle),
evdev.ecodes.ABS_Z: _degrees_to_axis(pose.roll * self.sens_roll, self.max_angle),
evdev.ecodes.ABS_RX: _degrees_to_axis(pose.x * self.sens_yaw * 10, self.max_angle),
evdev.ecodes.ABS_RY: _degrees_to_axis(pose.y * self.sens_pitch * 10, self.max_angle),
evdev.ecodes.ABS_RZ: _degrees_to_axis(pose.z * self.sens_roll * 10, self.max_angle),
}
try:
for axis, val in values.items():
if last_values.get(axis) != val:
ui.write(evdev.ecodes.EV_ABS, axis, val)
last_values[axis] = val
ui.syn()
except Exception as exc:
logger.warning("Joystick output write failed: %s", exc)
time.sleep(0.016)
@@ -0,0 +1,128 @@
"""Mouse emulation output for head tracking."""
import math
import threading
import time
from collections.abc import Sequence
import evdev
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.tracker_base import PoseData
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
class MouseOutput(BaseOutput):
"""Emulates mouse movement from head tracking yaw/pitch."""
def __init__(
self,
sens_yaw: float = 10.0,
sens_pitch: float = 10.0,
deadzone: float = 0.05,
) -> None:
self.sens_yaw = sens_yaw
self.sens_pitch = sens_pitch
self.deadzone = deadzone
self._ui = None
self._running = False
self._thread: threading.Thread | None = None
self._lock = threading.Lock()
self._pose = PoseData()
def open(self) -> bool:
"""Start mouse emulation thread."""
try:
import evdev.uinput
except ImportError:
logger.error("evdev not installed. Install: uv add evdev")
return False
with self._lock:
if self._running:
return True
try:
caps: dict[int, Sequence[int]] = {
evdev.ecodes.EV_REL: [evdev.ecodes.REL_X, evdev.ecodes.REL_Y],
evdev.ecodes.EV_KEY: [evdev.ecodes.BTN_LEFT],
}
self._ui = evdev.uinput.UInput(
caps, name="PortProtonQt Head Tracking"
)
except Exception as exc:
logger.error("Failed to create uinput mouse: %s", exc)
return False
self._running = True
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
logger.info("Mouse output started")
return True
def close(self) -> None:
"""Stop mouse emulation."""
with self._lock:
self._running = False
if self._ui:
try:
self._ui.close()
except Exception:
pass
self._ui = None
if self._thread:
self._thread.join(timeout=1.0)
self._thread = None
logger.info("Mouse output stopped")
def write(self, pose: PoseData) -> None:
"""Update current head pose."""
with self._lock:
self._pose = pose
def _run(self) -> None:
"""Main emulation loop."""
accum_x = 0.0
accum_y = 0.0
while True:
with self._lock:
if not self._running:
break
pose = self._pose
ui = self._ui
if ui is None:
time.sleep(0.016)
continue
yaw = pose.yaw if abs(pose.yaw) > self.deadzone else 0.0
pitch = pose.pitch if abs(pose.pitch) > self.deadzone else 0.0
# Reset accumulation when head is centered to prevent drift
if yaw == 0.0:
accum_x = 0.0
if pitch == 0.0:
accum_y = 0.0
# Accumulate fractional movement to avoid jitter at low speeds
accum_x += -math.radians(yaw) * self.sens_yaw
accum_y += math.radians(pitch) * self.sens_pitch
dx = int(accum_x)
dy = int(accum_y)
if dx != 0 or dy != 0:
try:
ui.write(evdev.ecodes.EV_REL, evdev.ecodes.REL_X, dx)
ui.write(evdev.ecodes.EV_REL, evdev.ecodes.REL_Y, dy)
ui.syn()
accum_x -= dx
accum_y -= dy
except Exception as exc:
logger.warning("Mouse output write failed: %s", exc)
time.sleep(0.016)
@@ -0,0 +1,124 @@
"""NPClient64 DLL output for native TrackIR SDK games.
This output writes head tracking data to the Wine SHM file and ensures
that a compatible NPClient64.dll is present in the game's Wine prefix.
Games using the TrackIR SDK (e.g., DCS World) load this DLL to receive
tracking data.
The actual DLL implementation is provided as a separate MinGW-built
binary placed next to the PortProtonQt data files.
"""
import os
import shutil
import subprocess
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.outputs.shm_output import ShmOutput
from portprotonqt.head_tracking.tracker_base import PoseData
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
_DLL_NAME = "NPClient64.dll"
_DLL_SOURCE_CANDIDATES = [
# Development / user data
"data/NPClient64.dll",
# Build output
"builddir/build-aux/npclient/NPClient64.dll",
# Installed system paths
"/usr/share/portprotonqt/NPClient64.dll",
"/usr/local/share/portprotonqt/NPClient64.dll",
]
class NpclientOutput(BaseOutput):
"""Output via NPClient64.dll for TrackIR-native games."""
def __init__(self, prefix_path: str | None = None) -> None:
self.prefix_path = prefix_path
self._shm = ShmOutput()
def open(self) -> bool:
"""Open SHM and deploy DLL if prefix is known."""
if not self._shm.open():
return False
if self.prefix_path:
self._deploy_dll(self.prefix_path)
return True
def close(self) -> None:
"""Close SHM output."""
self._shm.close()
def write(self, pose: PoseData) -> None:
"""Write pose to Wine SHM (DLL reads from there)."""
self._shm.write(pose)
def _deploy_dll(self, prefix_path: str) -> None:
"""Copy NPClient64.dll into Wine prefix and set registry keys."""
source = self._find_dll_source()
if not source:
logger.warning("NPClient64.dll not found; game may not detect tracker")
return
target = os.path.join(prefix_path, "drive_c", "windows", "system32", _DLL_NAME)
try:
os.makedirs(os.path.dirname(target), exist_ok=True)
shutil.copy2(source, target)
logger.info("Deployed %s to %s", _DLL_NAME, target)
except Exception as exc:
logger.warning("Failed to deploy %s to %s: %s", _DLL_NAME, target, exc)
return
self._setup_registry(prefix_path, os.path.dirname(target))
def _setup_registry(self, prefix_path: str, dll_dir: str) -> None:
"""Set Wine registry keys so games find the DLL."""
wine_path = self._find_wine(prefix_path)
if not wine_path:
logger.warning("Wine not found; cannot set NPClient registry keys")
return
env = os.environ.copy()
env["WINEPREFIX"] = prefix_path
# Use forward slashes and add trailing slash as expected by games
dll_dir_fwd = dll_dir.replace("\\", "/")
if not dll_dir_fwd.endswith("/"):
dll_dir_fwd += "/"
keys = [
(r"HKCU\Software\NaturalPoint\NATURALPOINT\NPClient Location", "Path", dll_dir_fwd),
(r"HKCU\Software\Freetrack\FreeTrackClient", "Path", dll_dir_fwd),
]
for key, value_name, value in keys:
try:
subprocess.run(
[wine_path, "reg", "add", key, "/v", value_name, "/d", value, "/f"],
env=env,
check=False,
capture_output=True,
timeout=30,
)
logger.info("Set registry key %s\\%s", key, value_name)
except Exception as exc:
logger.warning("Failed to set registry key %s: %s", key, exc)
@staticmethod
def _find_wine(prefix_path: str) -> str | None:
"""Locate wine binary for the given prefix."""
candidates = ["wine", "/usr/bin/wine", "/usr/local/bin/wine"]
for candidate in candidates:
if shutil.which(candidate):
return candidate
return None
@staticmethod
def _find_dll_source() -> str | None:
"""Locate bundled NPClient64.dll."""
for candidate in _DLL_SOURCE_CANDIDATES:
if os.path.exists(candidate):
return candidate
return None
@@ -0,0 +1,89 @@
"""Wine SHM output for head tracking."""
import math
import mmap
import os
import struct
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.tracker_base import PoseData
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
# Wine SHM protocol constants (TrackIR/FreeTrack compatible)
WINE_SHM_NAME = "/facetracknoir-wine-shm"
_SHM_FORMAT = "6d2i8b?"
_SHM_SIZE = struct.calcsize(_SHM_FORMAT)
class ShmOutput(BaseOutput):
"""Writes head tracking data to Wine shared memory file."""
def __init__(self) -> None:
self._shm: mmap.mmap | None = None
def open(self) -> bool:
"""Open shared memory file compatible with Wine."""
if self._shm is not None:
return True
wine_shm_path = "/dev/shm/facetracknoir-wine-shm"
try:
fd = os.open(wine_shm_path, os.O_CREAT | os.O_RDWR, 0o666)
os.ftruncate(fd, _SHM_SIZE)
self._shm = mmap.mmap(fd, _SHM_SIZE)
os.close(fd)
self._write_pose(PoseData(), gameid=0, stop=False)
logger.info("Wine SHM opened: %s", wine_shm_path)
return True
except Exception as exc:
logger.error("Failed to open Wine SHM: %s", exc)
return False
def close(self) -> None:
"""Close shared memory."""
if self._shm:
try:
self._write_pose(PoseData(), gameid=0, stop=True)
except Exception:
pass
self._shm.close()
self._shm = None
wine_shm_path = "/dev/shm/facetracknoir-wine-shm"
try:
if os.path.exists(wine_shm_path):
os.unlink(wine_shm_path)
except Exception:
pass
logger.info("Wine SHM closed")
def write(self, pose: PoseData, gameid: int = 0) -> None:
"""Write pose data to shared memory."""
self._write_pose(pose, gameid, stop=False)
def _write_pose(
self,
pose: PoseData,
gameid: int,
stop: bool,
) -> None:
"""Internal write."""
if self._shm is None:
return
data = (
pose.x * 10.0,
pose.y * 10.0,
pose.z * 10.0,
math.radians(pose.yaw),
math.radians(pose.pitch),
math.radians(pose.roll),
gameid,
0,
0, 0, 0, 0, 0, 0, 0, 0,
stop,
)
self._shm.seek(0)
self._shm.write(struct.pack(_SHM_FORMAT, *data))
@@ -0,0 +1,71 @@
"""UDP output for head tracking.
Sends 6-DOF pose data as 6 doubles over UDP.
Compatible with opentrack UDP protocol.
"""
import math
import socket
import struct
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.tracker_base import PoseData
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
# opentrack UDP format: 6 doubles (little-endian)
# x, y, z in mm (pose * 10)
# yaw, pitch, roll in radians
_UDP_FORMAT = "<6d"
_DEFAULT_HOST = "127.0.0.1"
_DEFAULT_PORT = 4242
class UdpOutput(BaseOutput):
"""Send head tracking data over UDP."""
def __init__(self, host: str = _DEFAULT_HOST, port: int = _DEFAULT_PORT) -> None:
self.host = host
self.port = port
self._sock: socket.socket | None = None
def open(self) -> bool:
"""Open UDP socket."""
try:
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
logger.info("UDP output opened: %s:%d", self.host, self.port)
return True
except Exception as exc:
logger.error("Failed to open UDP socket: %s", exc)
return False
def close(self) -> None:
"""Close UDP socket."""
if self._sock:
try:
self._sock.close()
except Exception:
pass
self._sock = None
logger.info("UDP output closed")
def write(self, pose: PoseData) -> None:
"""Send pose data over UDP."""
if self._sock is None:
return
data = (
pose.x * 10.0,
pose.y * 10.0,
pose.z * 10.0,
math.radians(pose.yaw),
math.radians(pose.pitch),
math.radians(pose.roll),
)
packet = struct.pack(_UDP_FORMAT, *data)
try:
self._sock.sendto(packet, (self.host, self.port))
except Exception as exc:
logger.warning("UDP send failed: %s", exc)
@@ -0,0 +1,69 @@
"""Base tracker interface for head tracking."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import numpy as np
@dataclass
class PoseData:
"""6-DOF head pose data."""
x: float = 0.0 # Translation X (cm)
y: float = 0.0 # Translation Y (cm)
z: float = 0.0 # Translation Z (cm)
yaw: float = 0.0 # Rotation Yaw (degrees)
pitch: float = 0.0 # Rotation Pitch (degrees)
roll: float = 0.0 # Rotation Roll (degrees)
def apply_offset(self, offset: "PoseData") -> "PoseData":
"""Subtract offset from this pose."""
return PoseData(
x=self.x - offset.x,
y=self.y - offset.y,
z=self.z - offset.z,
yaw=self.yaw - offset.yaw,
pitch=self.pitch - offset.pitch,
roll=self.roll - offset.roll,
)
def apply_sensitivity(self, sens: "PoseData") -> "PoseData":
"""Apply per-axis sensitivity multipliers."""
return PoseData(
x=self.x * sens.x,
y=self.y * sens.y,
z=self.z * sens.z,
yaw=self.yaw * sens.yaw,
pitch=self.pitch * sens.pitch,
roll=self.roll * sens.roll,
)
class BaseTracker(ABC):
"""Abstract base class for head trackers."""
@abstractmethod
def start(self) -> bool:
"""Start tracking. Returns True on success."""
@abstractmethod
def stop(self) -> None:
"""Stop tracking and release resources."""
@abstractmethod
def get_pose(self) -> PoseData | None:
"""Get current head pose. Returns None if tracking lost."""
def get_frame(self) -> tuple["np.ndarray | None", PoseData | None]:
"""Get current camera frame with landmarks drawn and pose.
Default implementation returns no frame; subclasses may override.
"""
return None, self.get_pose()
@abstractmethod
def is_running(self) -> bool:
"""Check if tracker is active."""
@@ -0,0 +1,350 @@
"""Camera-based head tracker using OpenCV YuNet DNN."""
import math
import os
import time
import cv2
import numpy as np
from portprotonqt.head_tracking.tracker_base import BaseTracker, PoseData
from portprotonqt.logger import get_logger
# Module-level shared VideoCapture to avoid V4L2 device busy issues
_shared_cap: cv2.VideoCapture | None = None
_shared_cap_index: int = -1
logger = get_logger(__name__)
def _find_yunet_model() -> str | None:
"""Find YuNet model file in known install locations."""
try:
import portprotonqt
module_dir = os.path.dirname(portprotonqt.__file__)
except ImportError:
module_dir = ""
project_root = os.path.dirname(module_dir) if module_dir else ""
candidates = []
if project_root:
candidates.append(
os.path.join(project_root, "data", "face_detection_yunet_2023mar.onnx")
)
xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share")
for d in xdg_data_dirs.split(":"):
candidates.append(
os.path.join(d, "portprotonqt", "face_detection_yunet_2023mar.onnx")
)
candidates.append("/usr/share/portprotonqt/face_detection_yunet_2023mar.onnx")
candidates.append("/usr/local/share/portprotonqt/face_detection_yunet_2023mar.onnx")
for c in candidates:
if os.path.exists(c):
return c
return None
def _create_detector(model_path: str) -> cv2.FaceDetectorYN | None:
"""Create YuNet face detector from model file."""
try:
detector = cv2.FaceDetectorYN.create(
model=model_path,
config="",
input_size=(320, 320),
score_threshold=0.3,
nms_threshold=0.3,
top_k=5000,
)
return detector
except Exception as exc:
logger.error("Failed to create YuNet detector: %s", exc)
return None
class CameraTracker(BaseTracker):
"""Head tracker using webcam and OpenCV YuNet DNN."""
def __init__(self, camera_index: int = 0) -> None:
self.camera_index = camera_index
self._cap: cv2.VideoCapture | None = None
self._detector: cv2.FaceDetectorYN | None = None
self._running = False
self._last_pose = PoseData()
self._last_face: tuple[int, int, int, int] | None = None
self._last_landmarks: list[tuple[int, int]] | None = None
self._pose_history: list[PoseData] = []
self._history_size = 5
self._lost_frames = 0
self._max_lost_frames = 5
# Calibration: neutral pose at start
self._calibrated = False
self._neutral_yaw = 0.0
self._neutral_pitch = 0.0
self._neutral_roll = 0.0
self._neutral_x = 0.0
self._neutral_y = 0.0
def _get_or_create_detector(self) -> cv2.FaceDetectorYN | None:
"""Get or create shared YuNet detector."""
if self._detector is not None:
return self._detector
model_path = _find_yunet_model()
if not model_path:
logger.error("YuNet model not found")
return None
self._detector = _create_detector(model_path)
return self._detector
def start(self) -> bool:
"""Start camera and initialize YuNet detector."""
global _shared_cap, _shared_cap_index
detector = self._get_or_create_detector()
if detector is None:
return False
if (
_shared_cap is not None
and _shared_cap_index == self.camera_index
and _shared_cap.isOpened()
):
self._cap = _shared_cap
self._running = True
logger.info("Camera tracker reused existing capture")
return True
if _shared_cap is not None:
_shared_cap.release()
_shared_cap = None
for attempt in range(5):
_shared_cap = cv2.VideoCapture(self.camera_index)
if _shared_cap.isOpened():
break
if _shared_cap:
_shared_cap.release()
_shared_cap = None
if attempt < 4:
time.sleep(0.5)
else:
logger.error("Cannot open camera %s after retries", self.camera_index)
return False
_shared_cap_index = self.camera_index
self._cap = _shared_cap
self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
self._running = True
self._calibrated = False
logger.info("Camera tracker started")
return True
def stop(self) -> None:
"""Stop tracking and release camera capture."""
global _shared_cap, _shared_cap_index
self._running = False
self._detector = None
self._cap = None
self._calibrated = False
if _shared_cap is not None:
_shared_cap.release()
_shared_cap = None
_shared_cap_index = -1
logger.info("Camera tracker stopped (capture released)")
def is_running(self) -> bool:
return self._running and self._cap is not None and self._cap.isOpened()
def get_pose(self) -> PoseData | None:
"""Capture frame and compute head pose."""
if not self.is_running() or self._cap is None:
return None
ret, frame = self._cap.read()
if not ret or frame is None:
return None
pose = self._process_frame(frame)
if pose:
self._last_pose = pose
return pose
def get_frame(self) -> tuple[np.ndarray | None, PoseData | None]:
"""Get current camera frame with landmarks drawn and pose."""
if not self.is_running() or self._cap is None:
return None, None
ret, frame = self._cap.read()
if not ret or frame is None:
return None, None
pose = self._process_frame(frame)
if pose:
self._last_pose = pose
if self._last_landmarks:
for x, y in self._last_landmarks:
cv2.circle(frame, (x, y), 2, (0, 255, 0), -1)
return frame, pose
def _detect_face(self, frame: np.ndarray) -> np.ndarray | None:
"""Detect face using YuNet, return face data array."""
if self._detector is None:
return None
h, w = frame.shape[:2]
self._detector.setInputSize((w, h))
retval, faces = self._detector.detect(frame)
if not retval or faces is None or len(faces) == 0:
return None
# Pick face closest to previous position, or highest score
if self._last_face is not None:
lx, ly, lw, lh = self._last_face
lcx = lx + lw / 2
lcy = ly + lh / 2
best = min(
faces,
key=lambda f: (f[0] + f[2] / 2 - lcx) ** 2
+ (f[1] + f[3] / 2 - lcy) ** 2,
)
else:
best = max(faces, key=lambda f: f[14])
return best
def _estimate_pose(
self, frame_w: int, frame_h: int, face: np.ndarray
) -> PoseData | None:
"""Estimate head pose from YuNet face landmarks.
Uses geometric relationships between facial landmarks
rather than solvePnP for stability.
"""
# YuNet: [x, y, w, h, re_x, re_y, le_x, le_y, nt_x, nt_y,
# rcm_x, rcm_y, lcm_x, lcm_y, score]
re_x, re_y = face[4], face[5] # right eye
le_x, le_y = face[6], face[7] # left eye
nt_x = face[8] # nose tip x
# Face center
face_cx = face[0] + face[2] / 2
face_cy = face[1] + face[3] / 2
# Eye center
eye_cx = (re_x + le_x) / 2
# Inter-ocular distance (distance between eyes)
iod = math.sqrt((le_x - re_x) ** 2 + (le_y - re_y) ** 2)
if iod < 1:
return None
# Roll from eye line angle
roll = math.degrees(math.atan2(le_y - re_y, le_x - re_x))
# Yaw from nose position relative to eye center
# When looking right, nose is to the right of eye center
nose_offset_x = nt_x - eye_cx
yaw = math.degrees(math.asin(max(-1.0, min(1.0, nose_offset_x / (iod * 0.8)))))
# Pitch from vertical shift of face center relative to image center
# When looking up, face center moves down on image (nose goes up)
face_center_y = face_cy
image_center_y = frame_h / 2
pitch = (face_center_y - image_center_y) / (frame_h / 2) * 30.0
# X, Y translation from face center offset
dx = face_cx - frame_w / 2
dy = face_cy - frame_h / 2
# Z from face size (inverse of inter-ocular distance)
z = 500.0 / max(iod, 1.0)
# Scale X/Y by Z
fov_half = 30.0
fx_len = frame_w / (2 * math.tan(math.radians(fov_half)))
x = dx * z / fx_len
y = dy * z / fx_len
return PoseData(x=x, y=y, z=z, yaw=yaw, pitch=pitch, roll=roll)
def _process_frame(self, frame: np.ndarray) -> PoseData | None:
"""Process frame and return smoothed pose."""
if self._detector is None:
return None
face = self._detect_face(frame)
if face is None:
self._lost_frames += 1
if self._lost_frames >= self._max_lost_frames:
self._last_face = None
self._last_landmarks = None
return None
self._lost_frames = 0
self._last_face = (int(face[0]), int(face[1]), int(face[2]), int(face[3]))
self._last_landmarks = [
(int(face[i]), int(face[i + 1])) for i in range(4, 14, 2)
]
pose = self._estimate_pose(frame.shape[1], frame.shape[0], face)
if pose is None:
return None
# Auto-calibrate on first valid frame
if not self._calibrated:
self._neutral_yaw = pose.yaw
self._neutral_pitch = pose.pitch
self._neutral_roll = pose.roll
self._neutral_x = pose.x
self._neutral_y = pose.y
self._calibrated = True
logger.info(
"Head tracking calibrated: yaw=%.1f, pitch=%.1f, roll=%.1f",
self._neutral_yaw,
self._neutral_pitch,
self._neutral_roll,
)
# Subtract neutral pose
pose = PoseData(
x=pose.x - self._neutral_x,
y=pose.y - self._neutral_y,
z=pose.z,
yaw=pose.yaw - self._neutral_yaw,
pitch=pose.pitch - self._neutral_pitch,
roll=pose.roll - self._neutral_roll,
)
self._pose_history.append(pose)
if len(self._pose_history) > self._history_size:
self._pose_history.pop(0)
return self._smooth_poses(self._pose_history)
@staticmethod
def _smooth_poses(history: list[PoseData]) -> PoseData:
"""Compute moving average of pose history."""
n = len(history)
if n == 0:
return PoseData()
if n == 1:
return history[0]
return PoseData(
x=sum(p.x for p in history) / n,
y=sum(p.y for p in history) / n,
z=sum(p.z for p in history) / n,
yaw=sum(p.yaw for p in history) / n,
pitch=sum(p.pitch for p in history) / n,
roll=sum(p.roll for p in history) / n,
)
@@ -0,0 +1,190 @@
"""Manager for head tracking lifecycle."""
import threading
import numpy as np
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.outputs.flightgear_output import FlightGearOutput
from portprotonqt.head_tracking.outputs.joystick_output import JoystickOutput
from portprotonqt.head_tracking.outputs.mouse_output import MouseOutput
from portprotonqt.head_tracking.outputs.npclient_output import NpclientOutput
from portprotonqt.head_tracking.outputs.shm_output import ShmOutput
from portprotonqt.head_tracking.outputs.udp_output import UdpOutput
from portprotonqt.head_tracking.tracker_base import BaseTracker, PoseData
from portprotonqt.head_tracking.tracking_worker import TrackingWorker
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
class TrackerManager:
"""Manages tracker and output lifecycle."""
def __init__(self, prefix_path: str | None = None) -> None:
self.tracker: BaseTracker | None = None
self.output: BaseOutput | None = None
self.worker: TrackingWorker | None = None
self._lock = threading.Lock()
self.calibration_offset: PoseData | None = None
self.sensitivity: PoseData = PoseData(1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
self.output_mode: str = "shm"
self.mouse_sens_yaw: float = 10.0
self.mouse_sens_pitch: float = 10.0
self.mouse_deadzone: float = 0.05
self.udp_host: str = "127.0.0.1"
self.udp_port: int = 4242
self.prefix_path = prefix_path
def _create_output(self) -> BaseOutput | None:
"""Create output based on current mode."""
if self.output_mode == "mouse":
return MouseOutput(
sens_yaw=self.mouse_sens_yaw,
sens_pitch=self.mouse_sens_pitch,
deadzone=self.mouse_deadzone,
)
if self.output_mode == "joystick":
return JoystickOutput()
if self.output_mode == "npclient":
return NpclientOutput(prefix_path=self.prefix_path)
if self.output_mode == "udp":
return UdpOutput(host=self.udp_host, port=self.udp_port)
if self.output_mode == "flightgear":
return FlightGearOutput(host=self.udp_host, port=self.udp_port)
return ShmOutput()
def start(self, tracker: BaseTracker) -> bool:
"""Start tracking with given tracker."""
with self._lock:
if self.is_running():
logger.warning("Tracker already running")
return False
if not tracker.start():
return False
output = self._create_output()
if output is None or not output.open():
tracker.stop()
return False
self.tracker = tracker
self.output = output
self.worker = TrackingWorker(
tracker,
output,
calibration_offset=self.calibration_offset,
sensitivity=self.sensitivity,
)
self.worker.start()
logger.info("Tracker manager started (mode=%s)", self.output_mode)
return True
def stop(self) -> None:
"""Stop tracking and cleanup."""
with self._lock:
if self.tracker:
self.tracker.stop()
if self.worker:
self.worker.stop()
self.worker = None
self.tracker = None
if self.output:
self.output.close()
self.output = None
logger.info("Tracker manager stopped")
def is_running(self) -> bool:
"""Check if tracking is active."""
return self.worker is not None and self.worker.is_alive()
def get_preview_frame(self) -> tuple[np.ndarray | None, PoseData | None]:
"""Get preview frame and calibrated pose for UI."""
if self.tracker is None:
return None, None
frame, pose = self.tracker.get_frame()
if pose:
pose = self._apply_calibration_and_sensitivity(pose)
return frame, pose
def _apply_calibration_and_sensitivity(self, pose: PoseData) -> PoseData:
"""Apply calibration offset and sensitivity to pose."""
if self.calibration_offset:
pose = pose.apply_offset(self.calibration_offset)
return pose.apply_sensitivity(self.sensitivity)
def calibrate(self) -> None:
"""Set current pose as zero offset."""
if self.tracker is None:
return
raw = self.tracker.get_pose()
if raw:
self.calibration_offset = raw
if self.worker is not None:
self.worker.calibration_offset = raw
logger.info("Head tracking calibrated: %s", raw)
def set_sensitivity(self, sens: PoseData) -> None:
"""Set per-axis sensitivity multipliers."""
self.sensitivity = sens
if self.worker is not None:
self.worker.sensitivity = sens
def set_output_mode(
self,
mode: str,
mouse_sens_yaw: float = 10.0,
mouse_sens_pitch: float = 10.0,
mouse_deadzone: float = 0.05,
udp_host: str = "127.0.0.1",
udp_port: int = 4242,
) -> None:
"""Set output mode and parameters; restart output if running."""
with self._lock:
changed = (
self.output_mode != mode
or self.mouse_sens_yaw != mouse_sens_yaw
or self.mouse_sens_pitch != mouse_sens_pitch
or self.mouse_deadzone != mouse_deadzone
or self.udp_host != udp_host
or self.udp_port != udp_port
)
self.output_mode = mode
self.mouse_sens_yaw = mouse_sens_yaw
self.mouse_sens_pitch = mouse_sens_pitch
self.mouse_deadzone = mouse_deadzone
self.udp_host = udp_host
self.udp_port = udp_port
if changed and self.is_running() and self.tracker is not None:
# Restart output with new settings
if self.worker:
self.worker.stop()
self.worker = None
if self.output:
self.output.close()
self.output = None
output = self._create_output()
if output is None or not output.open():
logger.error("Failed to restart output in mode %s", mode)
self.tracker.stop()
self.tracker = None
return
self.output = output
self.worker = TrackingWorker(
self.tracker,
output,
calibration_offset=self.calibration_offset,
sensitivity=self.sensitivity,
)
self.worker.start()
logger.info("Tracker output restarted (mode=%s)", mode)
@@ -0,0 +1,51 @@
"""Background worker for head tracking outputs."""
import threading
from portprotonqt.head_tracking.outputs.base_output import BaseOutput
from portprotonqt.head_tracking.tracker_base import BaseTracker, PoseData
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
class TrackingWorker(threading.Thread):
"""Background worker that reads tracker and writes to active output."""
def __init__(
self,
tracker: BaseTracker,
output: BaseOutput,
update_interval: float = 0.016, # ~60 FPS
calibration_offset: PoseData | None = None,
sensitivity: PoseData | None = None,
) -> None:
super().__init__(daemon=True)
self.tracker = tracker
self.output = output
self.update_interval = update_interval
self.calibration_offset = calibration_offset
self.sensitivity = sensitivity or PoseData(1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
self._stop_event = threading.Event()
def stop(self) -> None:
"""Request worker to stop."""
self._stop_event.set()
self.join(timeout=2.0)
def _apply_calibration_and_sensitivity(self, pose: PoseData) -> PoseData:
"""Apply calibration offset and sensitivity."""
if self.calibration_offset:
pose = pose.apply_offset(self.calibration_offset)
return pose.apply_sensitivity(self.sensitivity)
def run(self) -> None:
"""Main worker loop."""
logger.info("Tracking worker started")
while not self._stop_event.is_set():
pose = self.tracker.get_pose()
if pose:
pose = self._apply_calibration_and_sensitivity(pose)
self.output.write(pose)
self._stop_event.wait(self.update_interval)
logger.info("Tracking worker stopped")
+5
View File
@@ -59,6 +59,8 @@ class MainWindowProtocol(Protocol):
current_add_game_dialog: AddGameDialog | None
game_library_manager: Any # GameLibraryManager - using Any to avoid circular import
auto_size_slider: QSlider | None
def toggleHeadtracking(self) -> None:
...
PAD_BUTTON_SOUTH = 1000
PAD_BUTTON_EAST = 1001
@@ -3705,6 +3707,9 @@ class InputManager(QObject):
if key == pygame.K_F11 and not self._is_gamescope_session:
self.toggle_fullscreen.emit(not self._is_fullscreen)
return True
if key == pygame.K_SCROLLLOCK:
self._parent.toggleHeadtracking()
return True
return False
def _handle_pygame_file_explorer_key(self, key: int) -> bool:
+171 -4
View File
@@ -9,7 +9,11 @@ from queue import Empty, Queue
from threading import Thread
from portprotonqt.logger import get_logger
from portprotonqt.icon_extractor import generate_thumbnail
from portprotonqt.dialogs import FileExplorer, ExeSettingsDialog
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog
from portprotonqt.head_tracking.tracker_base import PoseData
from portprotonqt.head_tracking.tracker_camera import CameraTracker
from portprotonqt.head_tracking.tracker_manager import TrackerManager
from portprotonqt.global_hotkey import GlobalHotkeyEvdev
from portprotonqt.game_card import GameCard
from portprotonqt.animations import DetailPageAnimations
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel
@@ -60,9 +64,9 @@ from portprotonqt.tabs.wine_tab import MainWindowWineTabMixin
from portprotonqt.tabs.workers import MainWindowWorkersMixin
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
QMessageBox, QApplication, QPushButton, QCheckBox)
from PySide6.QtCore import Qt, QEvent, QUrl, Signal, QTimer, Slot, QProcess, QFileSystemWatcher, QObject
from PySide6.QtGui import QColor, QDesktopServices, QHideEvent, QShowEvent, QGuiApplication
QDialog, QFormLayout, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller, QSlider, QFrame)
from PySide6.QtCore import Qt, QAbstractAnimation, QEvent, QUrl, Signal, QTimer, Slot, QProcess, QProcessEnvironment, QFileSystemWatcher, QStandardPaths, QObject
from PySide6.QtGui import QIcon, QColor, QDesktopServices, QHideEvent, QShowEvent, QKeySequence, QShortcut, QGuiApplication
from typing import cast
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
@@ -248,6 +252,27 @@ class MainWindow(
self.portproton_location = get_portproton_location()
self.start_sh = get_portproton_start_command()
self.launch_exe = launch_exe # Store launch_exe path
self.headtracking_manager = None
self._headtracking_settings: dict[str, str] | None = None
self._headtracking_exe_path: str | None = None
# Hotkey for toggling head tracking (Insert or Scroll Lock)
self._headtracking_shortcut = QShortcut(QKeySequence("Insert"), self)
self._headtracking_shortcut.activated.connect(self._toggle_headtracking)
# Global hotkey via evdev without grab (works in games on Wayland/X11)
self._global_hotkey = GlobalHotkeyEvdev()
registered = False
for key in ("insert", "scrolllock"):
if self._global_hotkey.register("toggle_headtracking", "Toggle Head Tracking",
self.toggleHeadtracking,
hotkey=key,
):
registered = True
if registered:
logger.info("Global hotkey registered via evdev (Insert, Scroll Lock)")
else:
logger.warning("Failed to register global hotkey via evdev")
self.game_library_manager = GameLibraryManager(self, self.theme, None)
@@ -1893,6 +1918,11 @@ class MainWindow(
self.game_library_manager.update_game_grid(focus_first_card=False)
self._refresh_current_detail_time()
# Stop head tracking
self._stop_headtracking()
self._headtracking_settings = None
self._headtracking_exe_path = None
def _has_running_game_process(self) -> bool:
return any(proc.poll() is None for proc in self.game_processes)
@@ -1953,8 +1983,132 @@ class MainWindow(
self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None
self.resetPlayButton()
# Stop head tracking on manual stop
self._stop_headtracking()
self._headtracking_settings = None
self._headtracking_exe_path = None
return True
def _start_headtracking_if_enabled(self, exe_path: str) -> None:
"""Read head tracking settings for this executable."""
if not self.portproton_location or not self.start_sh:
logger.info("Cannot read HT settings: portproton_location=%s, start_sh=%s",
bool(self.portproton_location), bool(self.start_sh))
return
try:
result = subprocess.run(
self.start_sh + ["cli", "--show-ppdb", exe_path],
capture_output=True,
text=True,
timeout=5,
)
logger.info("PPDB query for %s: rc=%s, stdout=%s", exe_path, result.returncode, result.stdout[:200])
if result.returncode != 0:
return
settings = {}
for line in result.stdout.strip().splitlines():
if "=" in line:
key, value = line.split("=", 1)
settings[key.strip()] = value.strip().strip('"\'')
self._headtracking_settings = settings
self._headtracking_exe_path = exe_path
logger.info("HT settings saved for %s: PW_HEADTRACKING=%s", exe_path, settings.get("PW_HEADTRACKING"))
except Exception as exc:
logger.error("Error reading head tracking settings: %s", exc)
def _gamepad_blocks_headtracking(self) -> bool:
"""Check if a gamepad is connected and should block head tracking."""
return (
getattr(self, "input_manager", None) is not None
and getattr(self.input_manager, "gamepad", None) is not None
)
def _start_headtracking_with_settings(
self, exe_path: str, settings: dict[str, str]
) -> None:
"""Start head tracking with parsed settings."""
if self._gamepad_blocks_headtracking():
logger.info("Head tracking blocked: gamepad is connected")
return
self._stop_headtracking()
mode = settings.get("PW_HEADTRACKING_OUTPUT", "shm")
mouse_sens_x = float(settings.get("PW_HEADTRACKING_MOUSE_SENS_X", "10"))
mouse_sens_y = float(settings.get("PW_HEADTRACKING_MOUSE_SENS_Y", "10"))
mouse_deadzone = float(settings.get("PW_HEADTRACKING_MOUSE_DEADZONE", "0.05"))
prefix_path = None
if self.portproton_location:
prefix_name = get_prefix_name(exe_path)
if prefix_name:
candidate = os.path.join(
self.portproton_location, "data", "prefixes", prefix_name
)
if os.path.isdir(candidate):
prefix_path = candidate
self.headtracking_manager = TrackerManager(prefix_path=prefix_path)
self.headtracking_manager.set_output_mode(
mode,
mouse_sens_yaw=mouse_sens_x,
mouse_sens_pitch=mouse_sens_y,
mouse_deadzone=mouse_deadzone,
)
camera_idx = int(settings.get("PW_HEADTRACKING_CAMERA", "0"))
sens = PoseData(
x=float(settings.get("PW_HEADTRACKING_SENS_X", "1.0")),
y=float(settings.get("PW_HEADTRACKING_SENS_Y", "1.0")),
z=float(settings.get("PW_HEADTRACKING_SENS_Z", "1.0")),
yaw=float(settings.get("PW_HEADTRACKING_SENS_YAW", "1.0")),
pitch=float(settings.get("PW_HEADTRACKING_SENS_PITCH", "1.0")),
roll=float(settings.get("PW_HEADTRACKING_SENS_ROLL", "1.0")),
)
self.headtracking_manager.set_sensitivity(sens)
tracker = CameraTracker(camera_index=camera_idx)
if self.headtracking_manager.start(tracker):
logger.info("Head tracking started for %s (mode=%s)", exe_path, mode)
else:
logger.warning("Failed to start head tracking for %s", exe_path)
self.headtracking_manager = None
def _stop_headtracking(self) -> None:
"""Stop head tracking if running."""
if self.headtracking_manager:
self.headtracking_manager.stop()
self.headtracking_manager = None
logger.info("Head tracking stopped")
def toggleHeadtracking(self) -> None:
"""Toggle head tracking on/off via hotkey (public for InputManager)."""
self._toggle_headtracking()
def _toggle_headtracking(self) -> None:
"""Toggle head tracking on/off via Scroll Lock hotkey."""
logger.info("Toggle headtracking called, manager=%s, settings=%s, exe=%s",
self.headtracking_manager, bool(self._headtracking_settings), self._headtracking_exe_path)
if self.headtracking_manager and self.headtracking_manager.is_running():
self._stop_headtracking()
logger.info("Head tracking toggled off")
return
if not self._headtracking_settings or not self._headtracking_exe_path:
logger.info("Cannot toggle: no settings or exe path")
return
if self._headtracking_settings.get("PW_HEADTRACKING") != "1":
logger.info("Cannot toggle: head tracking is disabled in settings")
return
self._start_headtracking_with_settings(
self._headtracking_exe_path, self._headtracking_settings
)
logger.info("Head tracking toggled on")
def _check_missing_prefix_by_name_before_launch(self, prefix_name: str, env_vars: dict[str, str]) -> None:
"""Check prefix presence and optionally disable default recommended libs."""
if not prefix_name or not self.portproton_location:
@@ -2116,6 +2270,12 @@ class MainWindow(
self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
self.checkProcessTimer.start(500)
# Start head tracking if enabled for this exe
self._headtracking_exe_path = file_to_check
self._start_headtracking_if_enabled(file_to_check)
except Exception as e:
logger.error(f"Failed to launch game {exe_name}: {e}")
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
@@ -2162,6 +2322,13 @@ class MainWindow(
self.disc_image_manager.cleanup_iso_rw_paths()
self._stopBackgroundWorkers()
self._stop_headtracking()
self._headtracking_settings = None
self._headtracking_exe_path = None
# Stop global hotkey
if hasattr(self, "_global_hotkey") and self._global_hotkey:
self._global_hotkey.stop()
# Universal stop and delete timers
timers = [
+21
View File
@@ -10,6 +10,7 @@ install_data(
'downloader.py',
'game_card.py',
'game_library_manager.py',
'global_hotkey.py',
'howlongtobeat_api.py',
'image_utils.py',
'icon_extractor.py',
@@ -71,6 +72,7 @@ install_data(
'dialogs/settings_mangohud.py',
'dialogs/settings_gamescope.py',
'dialogs/settings_vkbasalt.py',
'dialogs/settings_headtracking.py',
'dialogs/proton_manager.py',
'dialogs/prefix_backup.py',
'dialogs/wine_loader.py',
@@ -165,6 +167,25 @@ install_data(
install_dir: pythondir / meson.project_name() / 'config',
)
# Install head_tracking package
install_data(
['head_tracking/__init__.py',
'head_tracking/tracker_base.py',
'head_tracking/tracker_camera.py',
'head_tracking/tracker_manager.py',
'head_tracking/tracking_worker.py',
'head_tracking/outputs/__init__.py',
'head_tracking/outputs/base_output.py',
'head_tracking/outputs/shm_output.py',
'head_tracking/outputs/mouse_output.py',
'head_tracking/outputs/joystick_output.py',
'head_tracking/outputs/npclient_output.py',
'head_tracking/outputs/udp_output.py',
'head_tracking/outputs/flightgear_output.py',
],
install_dir: pythondir / meson.project_name() / 'head_tracking',
)
# Install themes
install_subdir(
'themes',
+3
View File
@@ -168,6 +168,9 @@ class _BootstrapInputHost(MainWindowControlHintsMixin, QWidget):
def handleSystemGamepadAction(self, action: str) -> bool:
return False
def toggleHeadtracking(self) -> None:
return
def _show_portdata_warning(parent: QWidget, theme: Any, text: str, allow_choose: bool = False) -> bool:
message_box = QMessageBox(parent)
message_box.setIcon(QMessageBox.Icon.Warning)
@@ -284,6 +284,14 @@ CONTENT_STYLE = f"""
padding-bottom: 15px;
}}
"""
WARNING_LABEL_STYLE = f"""
QLabel {{
font-family: '{font_family}';
font-size: {font_size_a};
color: {color_warning};
background-color: none;
}}
"""
PREVIEW_WIDGET_STYLE = f"""
QWidget {{
margin-top: 3px;
@@ -75,6 +75,7 @@ color_accent_blue = "#007AFF"
color_preloader = "#007AFF"
color_gamepad_supported = "#00ff00"
color_white = "#ffffff"
color_warning = "#FFD700"
missing_exe_cover_opacity = 0.45
# === Widget State Colors ===
+2
View File
@@ -26,6 +26,8 @@ dependencies = [
"babel>=2.18.0",
"evdev>=1.9.3",
"libarchive-c>=5.3",
"numpy>=2.2.0",
"opencv-python>=4.11.0",
"orjson>=3.11.8",
"pillow>=12.2.0",
"psutil>=7.2.2",
Generated
+308 -76
View File
@@ -1,6 +1,11 @@
version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version == '3.11.*'",
"python_full_version < '3.11'",
]
[[package]]
name = "babel"
@@ -13,11 +18,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.5.20"
version = "2026.6.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
]
[[package]]
@@ -145,62 +150,62 @@ wheels = [
[[package]]
name = "dbus-fast"
version = "5.0.4"
version = "5.0.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b1/c7/490f9ca9da84d1d47e021585cfc6888519f5c12ae48db90cdb4ba631be93/dbus_fast-5.0.4.tar.gz", hash = "sha256:ef372607f967870780c2063e5d39037fdd27e253680aef76687a8dc6cd9413f3", size = 80559, upload-time = "2026-05-23T18:31:31.027Z" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/db/b621610e50b1bc46ff63534d75239553c1bf33256de6096b58214fd9808a/dbus_fast-5.0.22.tar.gz", hash = "sha256:34dc67d7d21a12399828dd13e63b352750580beea54ea7c729e708f2d2905fef", size = 83224, upload-time = "2026-06-05T18:47:59.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/e7/35d25d8652ded1787c198d87a3771a24c71ca04d544365e77e0babd019ba/dbus_fast-5.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7339765555cc40586fa8ee25b5a9b51c6f397c420fd9d42833ee8aec42c86bc", size = 694169, upload-time = "2026-05-23T18:38:34.461Z" },
{ url = "https://files.pythonhosted.org/packages/a0/75/dfef781f2de8c5f97c4360cba5d8abbccf617628d2feae031642e003946d/dbus_fast-5.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8cffe3f31d9e0108c4f0cf59e104037630fddbcf410954d821259e0cd71acf8", size = 842006, upload-time = "2026-05-23T18:38:37.269Z" },
{ url = "https://files.pythonhosted.org/packages/a0/f8/39d2f6a72cd572ccf857ce7567db881113785655a5c25fd95cdf50d32121/dbus_fast-5.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f01e98f38215e1abbb320e0f6e0875c2ed3c17891eef3176fed2c0de8ffb7954", size = 885667, upload-time = "2026-05-23T18:38:38.939Z" },
{ url = "https://files.pythonhosted.org/packages/f7/20/73f8c0b2b17fab681680065a3a3853f992d26dbe00ab966241d114989aed/dbus_fast-5.0.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe8dd98f504af78f92ea4377839d1ca22dca9b1b7242e86a7655e866173b5df8", size = 884493, upload-time = "2026-05-23T18:38:41.528Z" },
{ url = "https://files.pythonhosted.org/packages/fd/75/d46e40d7308599a3c1df7a5b60f6e557f91cd2fbe56efc0ab2acf5a63236/dbus_fast-5.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f819880a12e31f489fa81beedea14c87dd14fcb92817e9b88d2527a8989ac9f", size = 848095, upload-time = "2026-05-23T18:38:43.468Z" },
{ url = "https://files.pythonhosted.org/packages/2c/b5/fa9f4399c78c237dbfdbe3e71eaccd1049e027fba1da312d3612533a96c2/dbus_fast-5.0.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bf8ff7ceb5cac93c0c6869f8dc2ce5a84200337503d66bf83a3b88a9346ea76e", size = 882021, upload-time = "2026-05-23T18:38:45.479Z" },
{ url = "https://files.pythonhosted.org/packages/c0/9d/321c26379acfbfb605a68550a404c829b3498f01563e86091e2408ab1c68/dbus_fast-5.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe088a16d33054607021400043f3daeff566fc0cdd45e789d331d24e99f13e93", size = 891620, upload-time = "2026-05-23T18:38:47.113Z" },
{ url = "https://files.pythonhosted.org/packages/f6/17/9297c320ab6363cbb3c0f349565f78e7e1adc0e6fb356bc37dba16bcfd91/dbus_fast-5.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20c2d83950f2a3b4536f51da0ad51dfa063ffc92dec084f80e9c7661582edeb9", size = 692498, upload-time = "2026-05-23T18:38:49.267Z" },
{ url = "https://files.pythonhosted.org/packages/31/b6/c63d13b45bff8619e3479be45429459728870d697fe037367ef78bb208fd/dbus_fast-5.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aca56a03b2d46263c1a0b6483677dbf1fa84f6516db9e1c032b4d57ad757cfc2", size = 842099, upload-time = "2026-05-23T18:38:51.425Z" },
{ url = "https://files.pythonhosted.org/packages/35/85/5f9162efc2c1e6f9a9ed6732d4e4778a62f851482fb847afc82726d97f6a/dbus_fast-5.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:864b659724fb011356d4a5439551e17e03fa8420e5e14b1ae7cf5baf541c61e4", size = 885967, upload-time = "2026-05-23T18:38:53.591Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c6/704494d8bf28e4b5126a43c21d600af13b3e075c1c53e122e4f95a832174/dbus_fast-5.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d00901d51ba4e5f2eb421f83fb6e4faf9466788fc7d7c4cfaf464bd97737bc52", size = 885377, upload-time = "2026-05-23T18:38:55.236Z" },
{ url = "https://files.pythonhosted.org/packages/e8/82/8dd56dad8a32050a74261ead6bf8536823f2bf35a95cd5e2d50b4e8e585e/dbus_fast-5.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e4d4a0f108555f94ce76c2d9be58a1a0d72f3881b83ce919e418ece575e24e88", size = 848562, upload-time = "2026-05-23T18:38:57.86Z" },
{ url = "https://files.pythonhosted.org/packages/83/aa/b12b378005538f4355a8a376783fa7477a60adb111947a565edf12101675/dbus_fast-5.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0a56f1b933d6f0333653b7968cb5b38ff27e42f50c60a35801f6d27a94cc1fb9", size = 882106, upload-time = "2026-05-23T18:38:59.994Z" },
{ url = "https://files.pythonhosted.org/packages/2c/12/61c28663229b2fa3a8833d74d086c8eb7bad57efa03d9b719a4a72b6f675/dbus_fast-5.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6e28e4d08361697afe0be0815d2e649bdf5bc96bf012db61b3e5c6aa991bd7c3", size = 892440, upload-time = "2026-05-23T18:39:02.345Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d5/8a9dae63f6e68eafed65c2fc23aa1172bd840b93cb564f52035fe5bde409/dbus_fast-5.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06fdd44811c938bf8d10bb8a5d1d09831e337eeff325b17ad7ef45640c2c88c2", size = 689936, upload-time = "2026-05-23T18:39:04.404Z" },
{ url = "https://files.pythonhosted.org/packages/d0/2a/67d7a011dfe5054bca33ff7faee6d60daae343e5d07e3b6f4182e5cbcecf/dbus_fast-5.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d51094e6a80a7bfa6e67c59826a5604c7824e166958e6a029d0020f98287c00e", size = 803359, upload-time = "2026-05-23T18:39:06.39Z" },
{ url = "https://files.pythonhosted.org/packages/bb/15/3ed2ab01ac5fd025378e6528ca4b3067333fcaa56f2f7338111fc879c702/dbus_fast-5.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a3d8f0aa43d34f19588ccdfd12e332258ab6c234b419166fa938bbae8afd52", size = 855303, upload-time = "2026-05-23T18:39:08.041Z" },
{ url = "https://files.pythonhosted.org/packages/cd/ca/88b86f5f065311eee071f30559538acd39b5dfb3b1298c4d7e599accbe8d/dbus_fast-5.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff5f30056c3a68944729b88686551b56b79ec50fe27657e4e172afef4856458", size = 836412, upload-time = "2026-05-23T18:39:09.995Z" },
{ url = "https://files.pythonhosted.org/packages/0b/74/13de0346943bed77e322479a6823293712d3525c953bb130476069a4f6da/dbus_fast-5.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1582355810e2224736a008e277bab8780bd3e6a7bbf865dd9d78fe6cd4aab961", size = 810524, upload-time = "2026-05-23T18:39:12.109Z" },
{ url = "https://files.pythonhosted.org/packages/c6/58/515cafb4ecae652ac8d289f9e7e73fff26e3f4052751f014ccff0c761742/dbus_fast-5.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:176ea1a9c736f65262d923f9f8303824f1d1029d80bbfbdefe37a80e8a50b242", size = 833967, upload-time = "2026-05-23T18:39:13.899Z" },
{ url = "https://files.pythonhosted.org/packages/c4/13/5f369f6f388a19b662785c8e156795dd8f4df08cb49a3e2220464b69d7ee/dbus_fast-5.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:17ff032d12dc4f3dbc3b29cd3756ce9094fd4cfc8f8984efa214072f680f70b1", size = 863709, upload-time = "2026-05-23T18:39:15.799Z" },
{ url = "https://files.pythonhosted.org/packages/43/be/0e88b7c1263583fda6213ef3370e259d19735b2f7f6e8746b77b08fb0080/dbus_fast-5.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:00fe9b762c9974304e50f73abfd75210ae769207b78d8f3a97783d7281108200", size = 685726, upload-time = "2026-05-23T18:39:18.176Z" },
{ url = "https://files.pythonhosted.org/packages/15/e5/609bbd7e8e59753236a06838b5d561498695c457f08883348a7b034399c0/dbus_fast-5.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527edcecb06a6a1676c8e7eb0c167a234701715913c66431377d38da7acaa5b3", size = 800170, upload-time = "2026-05-23T18:39:19.968Z" },
{ url = "https://files.pythonhosted.org/packages/4c/a1/9542cd0fbf86be24aa4c0865667cf8bf742df90c93b5063c3364d10f8dc3/dbus_fast-5.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:556ffba0fd56e9e872ed031dab579dab99c537c702e7c3d28f460c39e686ad8a", size = 850999, upload-time = "2026-05-23T18:39:21.808Z" },
{ url = "https://files.pythonhosted.org/packages/5e/8e/aaf3ff0e66df530807af0381e7a9ec92a8ebd9783d54f00c1cbfaab35dc5/dbus_fast-5.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d7bb1465c2d06ddb6c38f9c7344c5a70beb09adfee8a00344322c7b2a90a5f4", size = 830440, upload-time = "2026-05-23T18:39:23.662Z" },
{ url = "https://files.pythonhosted.org/packages/29/ca/1cbf6e7928134fa642857222c4a684f4ffd5cc0d16fca51416a659cdc9be/dbus_fast-5.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ca2a8e94610705fae090aa1c318b810da9f9df47b990da9b6e06a8005a52fb91", size = 807074, upload-time = "2026-05-23T18:39:25.667Z" },
{ url = "https://files.pythonhosted.org/packages/ef/f2/a5f8e20fdef07a3afdf6e50fb98df5dc266c6ff43346dd66d71cde300dd8/dbus_fast-5.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f4e47a01c1e196d0aac0654850911eade720e9e571f2e68ee363dfeb69699e4e", size = 830159, upload-time = "2026-05-23T18:39:27.465Z" },
{ url = "https://files.pythonhosted.org/packages/01/d7/a2307c0b32c297728d325c05b9978caf7e9a1787d259d5172cfb90946b73/dbus_fast-5.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c3587f50a3089e5cc1a09b9a72cad964b88565f3d6dcfab3681981bca81a3fac", size = 859531, upload-time = "2026-05-23T18:39:29.755Z" },
{ url = "https://files.pythonhosted.org/packages/10/33/1dabccf932cee9408f189306bd6efdfab8cc063fddebcb218fd36d991ce1/dbus_fast-5.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:aa519fd8536ea8d100347f15af8a9d1a2d209fa5d353dadb64dfa13ecf707b3c", size = 692866, upload-time = "2026-05-23T18:39:31.622Z" },
{ url = "https://files.pythonhosted.org/packages/5d/46/a56b19ef369e55fe723c8b33fcdd18b730d77442585bd325972d2c07780f/dbus_fast-5.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6139737eda772a9a5de13368862a15e486d61570be7d3a5e6bd64c7a23131d36", size = 814255, upload-time = "2026-05-23T18:39:33.423Z" },
{ url = "https://files.pythonhosted.org/packages/01/f2/e0397cdfa0aefb5b4c44658bd6ba8ce532b997e81bff3d9714bbd36ee858/dbus_fast-5.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ce788791929923dbcb3eee889c61b8898eed41c77e334ac73bacd24d6e5c68a", size = 856800, upload-time = "2026-05-23T18:39:35.352Z" },
{ url = "https://files.pythonhosted.org/packages/ef/49/d11afde6602cefb2fdb43dd15a1f6b6f36a0e86cd046c99b4bafb071eb30/dbus_fast-5.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e36da263ce03faf48c5f40b9cc33949a8bbb67db1ef09b7cdb89a8b807aaaf7", size = 836628, upload-time = "2026-05-23T18:39:37.152Z" },
{ url = "https://files.pythonhosted.org/packages/8d/d8/0d711a54e4523cad9a4b08753c7c49a32580fe90a2a0949109fa8cdccc1b/dbus_fast-5.0.4-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:2eeb8dc52aa377074133fd8cd15733e8de27c4fef62f9c66be90fe0f4822aaa3", size = 854900, upload-time = "2026-05-23T18:31:27.897Z" },
{ url = "https://files.pythonhosted.org/packages/e6/38/59d700c6540136740b102f66c94147dc707e917d8136e32fc7fcef649032/dbus_fast-5.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d3d3bddcec3df4e213dce8de6e456e8bf4053a5e6dc9544fa13d2803c7ade3e8", size = 820912, upload-time = "2026-05-23T18:39:38.941Z" },
{ url = "https://files.pythonhosted.org/packages/34/58/961d5d6d7c71347bfc6d6e6fdf4501b8773eab46bad02dc0f57d1005ab66/dbus_fast-5.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e2c53a20bc6756d2d70b5a189b2adc76bb5d9856a1abd9564f160cd03f5d63bb", size = 837066, upload-time = "2026-05-23T18:39:40.754Z" },
{ url = "https://files.pythonhosted.org/packages/47/e6/49122a2b58ceca179627b2cae1c6f536336303f00cff2d6ad37c08ce0094/dbus_fast-5.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2b0f0d98fcabc838589eb05b29322d20378bd3c98999ace909ed63bed28a92e8", size = 864390, upload-time = "2026-05-23T18:39:43.091Z" },
{ url = "https://files.pythonhosted.org/packages/17/9b/55aed4a5e2905c3767dfa92b052272fa3459e2a7c46b1a97ca968163c2e9/dbus_fast-5.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2142c9be443711c8e185de57ad19781f6ff231fa2f6ebbe9312734efd7a72762", size = 1356783, upload-time = "2026-05-23T18:39:45.079Z" },
{ url = "https://files.pythonhosted.org/packages/45/8d/df4488525e22a8e843e12656f0718d760528f56709401af979c27a6c151c/dbus_fast-5.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a80a3114dc11fc463ef5b5562de6fa7fb5615acd488efa31a157dd83d510a247", size = 1545147, upload-time = "2026-05-23T18:39:46.933Z" },
{ url = "https://files.pythonhosted.org/packages/fd/75/c18803c88e85164ce60c28f59a7fdb7819901b5e5971bbc505bcd438d02d/dbus_fast-5.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340d36759a18d8b67deeb6bbd60647e0294f92950778a02e753832840b93f899", size = 1620877, upload-time = "2026-05-23T18:39:48.703Z" },
{ url = "https://files.pythonhosted.org/packages/c0/5c/a8154e4ba68c822c96233302551c7280198fd56ad6709baacc804a51f7e8/dbus_fast-5.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbbd41b9a641fc47e92d923331750a28c3a5001947bc1271d2392247c8fba327", size = 829639, upload-time = "2026-05-23T18:39:50.734Z" },
{ url = "https://files.pythonhosted.org/packages/54/6b/13ef6c4fddcd7beeb15e8425e8d90dd5f06d973e959397153d53e12ec385/dbus_fast-5.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00ef02cfdc2423b50756e32e90af8f1c3d63ad41f3af8a3d0f7cea9315e0005", size = 1560002, upload-time = "2026-05-23T18:39:52.571Z" },
{ url = "https://files.pythonhosted.org/packages/e9/54/010f9a9673ca7bf67a393486278a617b9930fead5c04563bf710bb6dc21d/dbus_fast-5.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2ba075dc3c12af888ecae09226dbc2225c75b0e105666e1f0630f95d2ca6ab84", size = 831734, upload-time = "2026-05-23T18:39:54.736Z" },
{ url = "https://files.pythonhosted.org/packages/cf/ad/14b5ed792988db43b257b9c262e39b43d8b1fdb361b96da005a5cd3e5b48/dbus_fast-5.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:210ba9eef1f63d256111207f98c5e2264b946ff8db1bf3c82ab064c7d1fb2dae", size = 1639471, upload-time = "2026-05-23T18:39:57.115Z" },
{ url = "https://files.pythonhosted.org/packages/e5/5c/fa3b060e06e0322ddd5c21a4f63e6c3e59a427a92db1553b930816d40e97/dbus_fast-5.0.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:027b358d55164f722d7432d6734506751c89b5cee8d516e7282eff3eaa763f05", size = 694683, upload-time = "2026-06-05T18:55:38.241Z" },
{ url = "https://files.pythonhosted.org/packages/4b/69/9235af850e2ee0cec23b4e863612492964a9c40b65f3002e4a9f32e1ffa2/dbus_fast-5.0.22-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8244561e503cffcd5e6b1ff6544a44aa0b65b6856f2f64d04345ebf82f8ac48", size = 840236, upload-time = "2026-06-05T18:55:41.007Z" },
{ url = "https://files.pythonhosted.org/packages/1b/d1/08ed17331dd23a8b91b80db9acca48301c4879352024dc7882e2c5893fa2/dbus_fast-5.0.22-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32869678240f233ece5a1137960c0337699dbb39acc02c155929f52543f03fb", size = 884309, upload-time = "2026-06-05T18:55:42.447Z" },
{ url = "https://files.pythonhosted.org/packages/34/ed/02458af54460e55cf26fdf0a778aa9b3217ac8d37e6280aa68e19f176650/dbus_fast-5.0.22-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f74700b1b4fd58acb76f8790e2cd475084d0e59149e0cc035abd239435a339b7", size = 885179, upload-time = "2026-06-05T18:55:43.801Z" },
{ url = "https://files.pythonhosted.org/packages/e9/79/4958c807e59106558a4415212e6be019775f5be9775ffc79c6bc7248b388/dbus_fast-5.0.22-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:246780dcda59d26dd2fb1d9a15991bc8483e9c241cee8713920a39a363ad4d39", size = 847693, upload-time = "2026-06-05T18:55:45.271Z" },
{ url = "https://files.pythonhosted.org/packages/c4/cc/a5322fbc172ee099f98186224cf7e3bf48fc8bcb08270951da8006b556ba/dbus_fast-5.0.22-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:58e4d41f9fea22cfd6dacdf03d53b932bbe15a825af5abad3ca79b91493a15c3", size = 882773, upload-time = "2026-06-05T18:55:46.712Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e7/e068899c1d9a034ed9d5fc8519b8e2247aa8c28a6a67229162db64163b59/dbus_fast-5.0.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d8ca39f3f2ab34f88bb343db87c7cf8b07eefa046b23d8c38b3387f150ac609e", size = 891573, upload-time = "2026-06-05T18:55:48.34Z" },
{ url = "https://files.pythonhosted.org/packages/ab/1d/f6020228675338b7184ce5491e4535fb64c7badc0197151b9776f345a1a1/dbus_fast-5.0.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0926e4cf49989b4ec8233e8fd462eb35a640fcfe81bb75d91675dd47489022b", size = 693158, upload-time = "2026-06-05T18:55:49.874Z" },
{ url = "https://files.pythonhosted.org/packages/ec/84/dfb014de75a3a854dccaae1cce8f840e4312e3efc781768eedd60d25d9ef/dbus_fast-5.0.22-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846f9a6602b4383f989201f7851459fb225a8912cd24b38e63894748545c3040", size = 838836, upload-time = "2026-06-05T18:55:51.51Z" },
{ url = "https://files.pythonhosted.org/packages/70/c2/be41bcc678e97092d44ba22d09ce687f76c955b3367a7e6863377b1cfea5/dbus_fast-5.0.22-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886b43446b6fdc3986befbbb88db1365b14e49dd0a7edf84c2c67ac66c7160a4", size = 883163, upload-time = "2026-06-05T18:55:53Z" },
{ url = "https://files.pythonhosted.org/packages/17/cf/336c08f88fdd813a39fc1603a10a15aec67115e08a895e7c9840df54d4d7/dbus_fast-5.0.22-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a699fca957acc845ddb12b47f741dba23ce147fdb93583e0c7e7bad3e9b2355", size = 886852, upload-time = "2026-06-05T18:55:54.434Z" },
{ url = "https://files.pythonhosted.org/packages/04/f1/7c1aa53f25252a2317f21ad6e8eaa125246d2585ea4611f3f10a6feaeeb1/dbus_fast-5.0.22-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a5b05fd4973862042e5bee2c5e8c5a15297e0b33a975bf25b44becf7bcb3618", size = 846497, upload-time = "2026-06-05T18:55:56.193Z" },
{ url = "https://files.pythonhosted.org/packages/4c/32/a981ef2305f1bf41e538e02fa0cd69614f9043fd00ca965bf3044416c79b/dbus_fast-5.0.22-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:6c4dae5292a7924ec062815c34b49043d8386cd22e165f9fb4012de00997cdf1", size = 882275, upload-time = "2026-06-05T18:55:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/d9/a0/78800a172f4ca32e19a70a36e175f54831f33a497c9644f3a3fa4dce01ea/dbus_fast-5.0.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7d90a52be79acbaef257f3a81d5b9b9dec40f1bad29429ac5c7802684fb9b84", size = 890566, upload-time = "2026-06-05T18:55:59.407Z" },
{ url = "https://files.pythonhosted.org/packages/52/81/ffc155f700c45191673e7f7620a28cbbbf5f116ff74a99f765895baa6f9c/dbus_fast-5.0.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f72b77be63f7bb24cf42936ad10994d40f43fed691f857f7854b5882d6a5227c", size = 690171, upload-time = "2026-06-05T18:56:01.151Z" },
{ url = "https://files.pythonhosted.org/packages/24/06/233b0bc13919474f70320bf389cb81ce02811956d6cf86c45e84679b63c3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f0bcad7f71d2304a68a5b0bc0d24c3fcc14710a2ffcf5f2a27521e3aece71ca", size = 799464, upload-time = "2026-06-05T18:56:02.617Z" },
{ url = "https://files.pythonhosted.org/packages/68/e9/77bc23a6f5aebfb8f2c34489795e8517aed7eca31738438e1a4c4a4891d3/dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9", size = 852687, upload-time = "2026-06-05T18:56:04.433Z" },
{ url = "https://files.pythonhosted.org/packages/ae/04/56769d0936d1273d1801ef574ec426ccb3f61f4b0a7a0eeb9eb2b8ccafa5/dbus_fast-5.0.22-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98de6d2c200d8182e1fd0bdde3206fa556b8fa14ebb752a044cd8daa87b4658c", size = 833814, upload-time = "2026-06-05T18:56:05.87Z" },
{ url = "https://files.pythonhosted.org/packages/06/ca/964f0d39a3be03b12a98f39519d34ad95b74360c7e3adf4cd4907dc25fd6/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b013437b66dc22b8d9aca5e0b0d46bf1980208a143409469fe482d9684a2a717", size = 806891, upload-time = "2026-06-05T18:56:07.623Z" },
{ url = "https://files.pythonhosted.org/packages/f4/25/57fe6ab509ad9da2e190498fa9c37f868e38ac521d940a9edb9ba6b6c657/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:855f15b7f7805171da2b82de1c317d01cfbb9fb8ac61fcc1e8dec54d8c69fab7", size = 830867, upload-time = "2026-06-05T18:56:09.473Z" },
{ url = "https://files.pythonhosted.org/packages/be/2b/da036e9f4aeb776833139575fe0774544aa6cd13ba997eea7fbc4ab99852/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7d1c42963235cfc015a2d2b8c5fe42b65387493b4ad4ce0ec122601c805e6742", size = 860651, upload-time = "2026-06-05T18:56:10.952Z" },
{ url = "https://files.pythonhosted.org/packages/25/1d/ebd02ae707328286c24582ac9189e4ed9c344399bc3b6a4b74e9687088eb/dbus_fast-5.0.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26e26b409e1e6edf5e2a4df8d192625fb38876b074fb5c7d0a5b15c5792e549d", size = 685921, upload-time = "2026-06-05T18:56:12.674Z" },
{ url = "https://files.pythonhosted.org/packages/76/5a/fa81a6685c763ea488ad93228cb6e036adc9af6a560f4c31643691f4cfd8/dbus_fast-5.0.22-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de10ff3b3cb2acb1c09fe17158a470519000d37bb5ee5fd69c4075e81ce8dcf5", size = 798472, upload-time = "2026-06-05T18:56:14.141Z" },
{ url = "https://files.pythonhosted.org/packages/6a/34/6b272e6df60be1aa4d575aa30220175a52c002a649c951d9950bfa3a72d6/dbus_fast-5.0.22-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:979761985fe343c701f2b7575285d6e370123f7231d4656209ef7824bb686bbb", size = 850312, upload-time = "2026-06-05T18:56:16.36Z" },
{ url = "https://files.pythonhosted.org/packages/d8/5c/9045c3595ddcd4069e6b5d051df06bf11a2b022592f721c9acea1e0e4d22/dbus_fast-5.0.22-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb73f1d8374253b7c17d69e902cf2ded1bfb089cb6ae67c10b4e0bdfe1b8fe08", size = 828366, upload-time = "2026-06-05T18:56:17.786Z" },
{ url = "https://files.pythonhosted.org/packages/a7/78/ca6881442b8fa29edbe6d99bec4b535b0b2e2f423075d015ff5b719c4e2c/dbus_fast-5.0.22-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b67a02037eb58bcf9e445df60ea0d9d7346fd334abde3aa62e03c75823b53979", size = 806036, upload-time = "2026-06-05T18:56:19.501Z" },
{ url = "https://files.pythonhosted.org/packages/9e/c6/458728eb1caa26171e6a8ae1d0d99bd29aaeac67ad7824bbd95d7f854a41/dbus_fast-5.0.22-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:83940ea00d7ee2f0c5bcb5d19d7d05e7949e52d467616a0b735d72e7285402ec", size = 828353, upload-time = "2026-06-05T18:56:21.346Z" },
{ url = "https://files.pythonhosted.org/packages/ea/1d/830b1569264780210d44898e5b0d95cffe2830b952c2ee21ea481274cd81/dbus_fast-5.0.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:279d212e9fb262d595af2e4b5b9e951bc00c73a5c8eeb50f158caa13705b9c84", size = 857743, upload-time = "2026-06-05T18:56:22.9Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b6/171f8e92775254e2f42afe9e7501a57ace47f32de2969cb694a375dc9dce/dbus_fast-5.0.22-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:511655d915692f55b8c5f5a535acf80cca9c6d1a35384db7fcfdabaae05dd837", size = 692986, upload-time = "2026-06-05T18:56:24.661Z" },
{ url = "https://files.pythonhosted.org/packages/b3/8c/4eefaabdf538882528164060ae83d9a34f1172b019c32c3254436834e9b1/dbus_fast-5.0.22-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:703e0f8f9af52e8e053394ee2b578042be0c3d8ea2b1488f9db8cb14393cc13f", size = 810835, upload-time = "2026-06-05T18:56:26.356Z" },
{ url = "https://files.pythonhosted.org/packages/f5/cf/fd327dbb40ee67a9331fb587bf78aff2ab1500b35979978a5cacb10d7f8c/dbus_fast-5.0.22-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb1d7e8e65561d0fd438004fd9e0f981c8a862912fed58dd4e29db1936c39d73", size = 855498, upload-time = "2026-06-05T18:56:28.009Z" },
{ url = "https://files.pythonhosted.org/packages/56/33/1709ebc16a4d353ddc4fcd29252e2b9d93bded6422a45fd6df170e0911c1/dbus_fast-5.0.22-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:959fab6420897ab99410e67d6f9f9a7f6f4cedb6014700768f5e2d71dbff5dc6", size = 833510, upload-time = "2026-06-05T18:56:29.806Z" },
{ url = "https://files.pythonhosted.org/packages/b0/fd/89d7c34152900d986b9c78e39cc62aa73eefc22b57b3a8c946d945a85540/dbus_fast-5.0.22-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:eb31c5ff339a7071b914617a69d5b7c6ba7d411da4b01a5f9b5b2fe51e9d1301", size = 853669, upload-time = "2026-06-05T18:47:56.747Z" },
{ url = "https://files.pythonhosted.org/packages/a6/a1/031cc4a89d947f1fe110f663f93dcce9230213b7accaf719790d813def04/dbus_fast-5.0.22-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:856f0543c593f3480e93e67bcd1aa4ddc1d94a6076cfd3ad4e0f5e2b01b33dc3", size = 818486, upload-time = "2026-06-05T18:56:31.72Z" },
{ url = "https://files.pythonhosted.org/packages/36/e2/de8b764fdb947314fb8c2e079b556510194fd100983776845e234a107cc9/dbus_fast-5.0.22-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:96d231d128c1f46f263790335897195dde9dac2f38571782db8ae1d8647bd548", size = 833582, upload-time = "2026-06-05T18:56:33.325Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1f/e5f0dd28d07c4b3f7bafd3357bfa424c8dace355a3dad921fec05db4634b/dbus_fast-5.0.22-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:595bd3ccfd8318cbafff79f33a15709fee3728724fd61d5fa220080d73b574cb", size = 862291, upload-time = "2026-06-05T18:56:35.102Z" },
{ url = "https://files.pythonhosted.org/packages/7c/dd/61086156a1c2d8ffd04d61a232debbaff8ca9fc1cf598476999e1f06164d/dbus_fast-5.0.22-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69a077c296eaab8c30160e861b5514c33d99d67d41d17dbf02e89aae44543b11", size = 1353993, upload-time = "2026-06-05T18:56:36.849Z" },
{ url = "https://files.pythonhosted.org/packages/26/69/5b54654f598ef98e8f94fd5a40929668b1f8fcd76e7fb50de0db73d329da/dbus_fast-5.0.22-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04bac97d0cb754a4d13037d0132517f1df28192d6e0568a0bf6df06623062285", size = 1534804, upload-time = "2026-06-05T18:56:38.804Z" },
{ url = "https://files.pythonhosted.org/packages/24/b7/c00d01699dc87ffc35f143226d3b296372840e2e2bc15101d35df7c74949/dbus_fast-5.0.22-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3eb57d592d84b0bb90e0c077db7ecb61562f49cc9b86a3ef08cbe17243e9cc4f", size = 1613316, upload-time = "2026-06-05T18:56:40.461Z" },
{ url = "https://files.pythonhosted.org/packages/f3/94/ea0db4c1aa6409cb16551b50aa8573e72f64407ca5281b042919ef81ca1c/dbus_fast-5.0.22-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:de4d235d1282ebb3ab65b6cddab84e914c045d92ceb381ddcbdbaf66bf1fb132", size = 822053, upload-time = "2026-06-05T18:56:42.519Z" },
{ url = "https://files.pythonhosted.org/packages/40/e4/a3bb52185b8a8c76bd8aaba3ff4fa8395eea19fbc142122b43dc377b275c/dbus_fast-5.0.22-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:048f34299fbe82d7b87c56f47e8bd83f62339a4517685abc6671d603a55d2c89", size = 1549996, upload-time = "2026-06-05T18:56:44.307Z" },
{ url = "https://files.pythonhosted.org/packages/37/2b/6e405ba92e87d78a689a387809d975f97f8c8748b98efccfacd2b4e1d9f5/dbus_fast-5.0.22-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:92df9fb6d8adeb17b534621c2ee730295bbe1d0c2584d5c82b1db478e3f04e8f", size = 823004, upload-time = "2026-06-05T18:56:46.023Z" },
{ url = "https://files.pythonhosted.org/packages/b8/8f/77135ab8d690030cdb0ebeca879640b5945c4cbf5344ecbc507b4628da24/dbus_fast-5.0.22-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7be4271e38251f1ad726962dec60da887c8ed352d157352e4fc27f56aece5c5d", size = 1629160, upload-time = "2026-06-05T18:56:47.688Z" },
]
[[package]]
name = "distlib"
version = "0.4.0"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
{ url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" },
]
[[package]]
@@ -214,7 +219,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -223,11 +228,11 @@ wheels = [
[[package]]
name = "filelock"
version = "3.29.0"
version = "3.29.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
{ url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" },
]
[[package]]
@@ -241,11 +246,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.16"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" },
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
@@ -275,6 +280,227 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]]
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
]
[[package]]
name = "numpy"
version = "2.4.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.11.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
]
[[package]]
name = "numpy"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12'",
]
sdist = { url = "https://files.pythonhosted.org/packages/e7/05/3d27272d30698dc0ecb7fdfaa41ad70303b444f81722bb99bce1d818638a/numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c", size = 20652461, upload-time = "2026-06-21T20:57:51.95Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/0a/11486d02add7b1384dff7374d124b1cfbb0ee864dcc9f6a2c0380638cf84/numpy-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:489780423903667933b4ed6197b6ec3b75ea5dd17d1d8f0f38d798feb6921561", size = 16789987, upload-time = "2026-06-21T20:56:16.657Z" },
{ url = "https://files.pythonhosted.org/packages/55/b2/285f48640a181947b4587a3766d21ec1eaa7fea833d4b49957e09da467a2/numpy-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ece55976ced6bca95a03ae2839e2e5ccffe8eb6a3e7022415645eb154a81e4e6", size = 11760322, upload-time = "2026-06-21T20:56:19.813Z" },
{ url = "https://files.pythonhosted.org/packages/dd/67/b032db1eb03ca30d16eda3b0c22aaa615338b9263c2fd559d0f29451aca4/numpy-2.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c83b664b0e6eee9594fa920cf0639d8af796606d3fad6cc70180c87e4b97c7be", size = 5319605, upload-time = "2026-06-21T20:56:22.173Z" },
{ url = "https://files.pythonhosted.org/packages/b9/83/03fc7300c7c6b6c84c487b1dc80d322817b95fbd1f4dd57a85e23b7198de/numpy-2.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bf80333980bf37f523341ddd72c783f39d6829ec7736b9eb99086388a2d52cc2", size = 6653628, upload-time = "2026-06-21T20:56:23.914Z" },
{ url = "https://files.pythonhosted.org/packages/82/49/2ec21730bc63ccfda829323f7040a8ed4715b3852ce658689cf74ee96a8c/numpy-2.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a4874217b36d5ac8fc876f52e39df56f8182c88463e9e2dceabf7ca8b7efb8", size = 15153691, upload-time = "2026-06-21T20:56:25.631Z" },
{ url = "https://files.pythonhosted.org/packages/bb/6b/f4a3d0637692c49da8ef99d72d52526f92e0a8d6ac4f0ca9f31441b9d9ea/numpy-2.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaa760137137e8d3c920d27927748215b56014f92667dc9b6c27dfc61249255a", size = 16660066, upload-time = "2026-06-21T20:56:28.009Z" },
{ url = "https://files.pythonhosted.org/packages/3a/2f/c354ec86d1f3f5c19649463b0d39652e160736e5b0a4cd18dff0576715c4/numpy-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7174ce8265fc7f7417d171c9ea8fe905220748893ea67a2a7abe726ec331c4b0", size = 16514638, upload-time = "2026-06-21T20:56:30.26Z" },
{ url = "https://files.pythonhosted.org/packages/06/34/43efdcb319988648580f93c11f1ae82cf7e2faa74925e98e454ae3aa95f8/numpy-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b8c3daaf99de52415d20b42f8e8155c78642cb04207d02f9d317a0dcf1b3fb54", size = 18419647, upload-time = "2026-06-21T20:56:32.41Z" },
{ url = "https://files.pythonhosted.org/packages/71/e2/f5d1676b1d7fb682eb5e9a1641e7ebd2414b3216c370661d1029778908b4/numpy-2.5.0-cp312-cp312-win32.whl", hash = "sha256:6206db0af545d73d068add6d992279145f158428d1da6cc49adc4b630c5d6ee5", size = 6056688, upload-time = "2026-06-21T20:56:34.657Z" },
{ url = "https://files.pythonhosted.org/packages/8f/7c/48f115d1c58a34032facebcd51fdf2d02df2c51d4a46a81dd1197bb2ea6b/numpy-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f2d6873e2940c860a309d21e25b1e69af6aaffdd80aa056b04c16380db1c4f2", size = 12419237, upload-time = "2026-06-21T20:56:36.24Z" },
{ url = "https://files.pythonhosted.org/packages/86/26/2e0882f4044d1b1a1b63e875151fb2393389032022a8b7f5657a7996d3b2/numpy-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:a55e1eb2bca2cfd17a16b213c99dfc8502d47b0d494224d2122277d0400935ca", size = 10339912, upload-time = "2026-06-21T20:56:38.733Z" },
{ url = "https://files.pythonhosted.org/packages/8a/33/07675aaad7f26ea013d5e884d9a0d784b79c6bd7566c333f5a52fa3c610b/numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2", size = 16784890, upload-time = "2026-06-21T20:56:40.799Z" },
{ url = "https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c", size = 11754584, upload-time = "2026-06-21T20:56:43.094Z" },
{ url = "https://files.pythonhosted.org/packages/44/9b/56dd530c367c74ae17411027cea4135ca57e1e0583bf5594cee18bd83217/numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd", size = 5313904, upload-time = "2026-06-21T20:56:45.503Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b0/bcd672edad27ecca7da1f7bb0ce72cd1706a4f2d79ae94990afc97c13e1c/numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd", size = 6648504, upload-time = "2026-06-21T20:56:47.567Z" },
{ url = "https://files.pythonhosted.org/packages/80/9e/15cdfcbd30a1544a46c9e487a00df331c4672450216538705a9e51fa6710/numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e", size = 15150086, upload-time = "2026-06-21T20:56:49.352Z" },
{ url = "https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9", size = 16647250, upload-time = "2026-06-21T20:56:51.542Z" },
{ url = "https://files.pythonhosted.org/packages/3c/81/97060281b602ed07f21b12f4ec409eac1f75a2f91fbc829ed8b2becf3ad4/numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab", size = 16512864, upload-time = "2026-06-21T20:56:55.401Z" },
{ url = "https://files.pythonhosted.org/packages/33/ab/4496208146911f8d8ddb54f68a972aafa6c8d44babcb2ea03b0e5cc87c9d/numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4", size = 18408407, upload-time = "2026-06-21T20:56:57.75Z" },
{ url = "https://files.pythonhosted.org/packages/d4/9f/a4df67c181e4ee8b467aa3332dc2db10fd5c515136831302f3ca48bc0a01/numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988", size = 6054431, upload-time = "2026-06-21T20:57:00.016Z" },
{ url = "https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748", size = 12414420, upload-time = "2026-06-21T20:57:01.815Z" },
{ url = "https://files.pythonhosted.org/packages/eb/4a/25c2906f541e9d9f4c5769764db732e6627be91a13f4724fa10634d77db4/numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60", size = 10339533, upload-time = "2026-06-21T20:57:03.961Z" },
{ url = "https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9", size = 16783807, upload-time = "2026-06-21T20:57:06.253Z" },
{ url = "https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446", size = 11765215, upload-time = "2026-06-21T20:57:08.547Z" },
{ url = "https://files.pythonhosted.org/packages/f7/a0/8400a9c0e3625182347593f5e1f57da9a617a534794805c8df5518154ddc/numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c", size = 5324493, upload-time = "2026-06-21T20:57:11.012Z" },
{ url = "https://files.pythonhosted.org/packages/f6/8c/0d104deaa0401c93395a629ec902891618a2eff76d19229139cb5a887bfc/numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303", size = 6645211, upload-time = "2026-06-21T20:57:12.919Z" },
{ url = "https://files.pythonhosted.org/packages/6a/d9/4a4a628c812750363786afc3d33492709a5cd64b215469c16b0f6c7bb811/numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22", size = 15166004, upload-time = "2026-06-21T20:57:14.717Z" },
{ url = "https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03", size = 16650797, upload-time = "2026-06-21T20:57:16.906Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a0/a0090e6329f4ca5992c07847bb579c5259a19953dc57255bb08793142ffb/numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a", size = 16524647, upload-time = "2026-06-21T20:57:19.165Z" },
{ url = "https://files.pythonhosted.org/packages/5e/7d/6caf27734c42b65837e7461ed0dbbd6b6fc835060c9714ec59d673bb383a/numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21", size = 18411841, upload-time = "2026-06-21T20:57:21.638Z" },
{ url = "https://files.pythonhosted.org/packages/13/dc/26edadbd812536769a82c2e9e002234e33feb5da43061d47a044f6d309b7/numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731", size = 6106361, upload-time = "2026-06-21T20:57:23.844Z" },
{ url = "https://files.pythonhosted.org/packages/f2/9e/4dd1459282229a72d92dece2ae9138e5cac94a72263a7ceb48f37434c925/numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73", size = 12551749, upload-time = "2026-06-21T20:57:25.945Z" },
{ url = "https://files.pythonhosted.org/packages/05/a7/6bc6384c080b86c7f6c85c5bc5b540b24f4f679cd144791d99574e90d462/numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e", size = 10617072, upload-time = "2026-06-21T20:57:28.175Z" },
{ url = "https://files.pythonhosted.org/packages/86/6b/4a2b71d66ada5608ae02b63f150dfad520f6940721cb7f029ad270befc0e/numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b", size = 11881067, upload-time = "2026-06-21T20:57:30.104Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b2/d365eb40a20efb49d67e9feb90494ed8511282ee1f5fa16006675c65397d/numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927", size = 5440290, upload-time = "2026-06-21T20:57:32.193Z" },
{ url = "https://files.pythonhosted.org/packages/fa/5e/e9c03188de5f9b767e46a8fe988bcfd3efad066a4a3fda8b9cb11a93f895/numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826", size = 6748371, upload-time = "2026-06-21T20:57:33.933Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1d/68c186a38a5027bae2c4ddd5ea681fdaf8b4d30fb7301def6d8ad270390f/numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd", size = 15214643, upload-time = "2026-06-21T20:57:35.677Z" },
{ url = "https://files.pythonhosted.org/packages/8c/67/73f67b7c7e20635baae9c4c3ead4ae7326a005900297a6110971abd62eb5/numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6", size = 16690128, upload-time = "2026-06-21T20:57:38.159Z" },
{ url = "https://files.pythonhosted.org/packages/eb/05/d4c1fb0c46d02a27d6b2b8b319a78c90937acec8631c1641874670b31e6f/numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a", size = 16577902, upload-time = "2026-06-21T20:57:40.447Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1d/771c797d50fa26e4888989cccf1d50ee51f530d4e455ad2692dcb64fa711/numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9", size = 18452814, upload-time = "2026-06-21T20:57:42.875Z" },
{ url = "https://files.pythonhosted.org/packages/e8/46/52fc0d2a68d7643f0f149eeea5a5d8ea2a3507056ac8afa83c9212606e8b/numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7", size = 6253168, upload-time = "2026-06-21T20:57:45.101Z" },
{ url = "https://files.pythonhosted.org/packages/2a/be/6c8d1118b5f13b2881dc095d5b345de19c6638b8959c17409b6eff84c8aa/numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa", size = 12736286, upload-time = "2026-06-21T20:57:46.935Z" },
{ url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" },
]
[[package]]
name = "opencv-python"
version = "4.13.0.92"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" },
{ url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" },
{ url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" },
{ url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" },
{ url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" },
{ url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" },
{ url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" },
]
[[package]]
name = "orjson"
version = "3.11.9"
@@ -474,11 +700,11 @@ wheels = [
[[package]]
name = "platformdirs"
version = "4.9.6"
version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
{ url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
]
[[package]]
@@ -499,6 +725,10 @@ dependencies = [
{ name = "dbus-fast" },
{ name = "evdev" },
{ name = "libarchive-c" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "opencv-python" },
{ name = "orjson" },
{ name = "pefile" },
{ name = "pillow" },
@@ -527,6 +757,8 @@ requires-dist = [
{ name = "dbus-fast", specifier = ">=5.0.4" },
{ name = "evdev", specifier = ">=1.9.3" },
{ name = "libarchive-c", specifier = ">=5.3" },
{ name = "numpy", specifier = ">=2.2.0" },
{ name = "opencv-python", specifier = ">=4.11.0" },
{ name = "orjson", specifier = ">=3.11.8" },
{ name = "pefile", specifier = ">=2024.8.26" },
{ name = "pillow", specifier = ">=12.2.0" },
@@ -640,15 +872,15 @@ wheels = [
[[package]]
name = "pyright"
version = "1.1.409"
version = "1.1.411"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" },
{ url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
]
[[package]]
@@ -702,7 +934,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.3"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -713,9 +945,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
@@ -732,15 +964,15 @@ wheels = [
[[package]]
name = "python-discovery"
version = "1.3.1"
version = "1.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" },
{ url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" },
]
[[package]]
@@ -992,14 +1224,14 @@ wheels = [
[[package]]
name = "tqdm"
version = "4.67.3"
version = "4.68.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
{ url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" },
]
[[package]]
@@ -1031,7 +1263,7 @@ wheels = [
[[package]]
name = "virtualenv"
version = "21.3.3"
version = "21.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
@@ -1040,9 +1272,9 @@ dependencies = [
{ name = "python-discovery" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" },
{ url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
]
[[package]]