Merge commit from fork
Builder Win / Build PluginLoader for Win (push) Has been cancelled
Builder / Build PluginLoader (push) Has been cancelled
Push Updated Plugin Stub to Template / copy-stub (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Type Check / Run type checkers (push) Has been cancelled

* fix incorrect permissions on plugin directories

* chown plugin dirs too

* fix the stupid

* cleanup useless comments
This commit is contained in:
AAGaming
2025-07-28 20:58:59 -04:00
committed by GitHub
parent 670ae7d8a7
commit 8f41eb93ef
12 changed files with 94 additions and 64 deletions
+25 -10
View File
@@ -18,9 +18,10 @@ from enum import IntEnum
from typing import Dict, List, TypedDict from typing import Dict, List, TypedDict
# Local modules # Local modules
from .localplatform.localplatform import chown, chmod from .localplatform.localplatform import chown, chmod, get_chown_plugin_path
from .loader import Loader, Plugins from .loader import Loader, Plugins
from .helpers import get_ssl_context, download_remote_binary_to_path from .helpers import get_ssl_context, download_remote_binary_to_path
from .enums import UserType
from .settings import SettingsManager from .settings import SettingsManager
logger = getLogger("Browser") logger = getLogger("Browser")
@@ -60,13 +61,6 @@ class PluginBrowser:
return False return False
zip_file = ZipFile(zip) zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path) zip_file.extractall(self.plugin_path)
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
return False
return True return True
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str): async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
@@ -101,8 +95,6 @@ class PluginBrowser:
rv = False rv = False
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}") raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
chown(self.plugin_path)
chmod(pluginBasePath, 555)
else: else:
rv = True rv = True
logger.info(f"No Remote Binaries to Download") logger.info(f"No Remote Binaries to Download")
@@ -124,6 +116,25 @@ class PluginBrowser:
return folder return folder
except: except:
logger.debug(f"skipping {folder}") logger.debug(f"skipping {folder}")
def set_plugin_dir_permissions(self, plugin_dir: str) -> bool:
plugin_json_path = path.join(plugin_dir, 'plugin.json')
logger.debug(f"Checking plugin.json at {plugin_json_path}")
root_plugin = False
if access(plugin_json_path, R_OK):
with open(plugin_json_path, "r", encoding="utf-8") as f:
plugin_json = json.load(f)
if "flags" in plugin_json and "root" in plugin_json["flags"]:
root_plugin = True
logger.debug("root_plugin %d, dir %s", root_plugin, plugin_dir)
if get_chown_plugin_path():
return chown(plugin_dir, UserType.EFFECTIVE_USER if root_plugin else UserType.HOST_USER, True) and chown(plugin_dir, UserType.EFFECTIVE_USER, False) and chmod(plugin_dir, 755) and chown(plugin_json_path, UserType.EFFECTIVE_USER, False) and chmod(plugin_json_path, 755)
else:
logger.debug("chown disabled by environment")
return True
async def uninstall_plugin(self, name: str): async def uninstall_plugin(self, name: str):
if self.loader.watcher: if self.loader.watcher:
@@ -266,6 +277,7 @@ class PluginBrowser:
plugin_dir = path.join(self.plugin_path, plugin_folder) plugin_dir = path.join(self.plugin_path, plugin_folder)
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote") await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote")
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir) ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
chown_ret = self.set_plugin_dir_permissions(plugin_dir)
if ret: if ret:
logger.info(f"Installed {name} (Version: {version})") logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins: if name in self.loader.plugins:
@@ -278,6 +290,9 @@ class PluginBrowser:
self.settings.setSetting("pluginOrder", current_plugin_order) self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name) logger.debug("Plugin %s was added to the pluginOrder setting", name)
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder) await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
elif not chown_ret:
logger.error("Could not chown plugin")
return
else: else:
logger.error("Could not download remote binaries") logger.error("Could not download remote binaries")
return return
+2 -3
View File
@@ -1,9 +1,8 @@
from enum import IntEnum from enum import IntEnum
class UserType(IntEnum): class UserType(IntEnum):
HOST_USER = 1 HOST_USER = 1 # usually deck
EFFECTIVE_USER = 2 EFFECTIVE_USER = 2 # usually root
ROOT = 3
class PluginLoadType(IntEnum): class PluginLoadType(IntEnum):
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
+2 -1
View File
@@ -181,7 +181,8 @@ def get_user_group_id() -> int:
# Get the default home path unless a user is specified # Get the default home path unless a user is specified
def get_home_path(username: str | None = None) -> str: def get_home_path(username: str | None = None) -> str:
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER) # TODO hardcoded root is kinda a hack
return localplatform.get_home_path(UserType.EFFECTIVE_USER if username == "root" else UserType.HOST_USER)
async def is_systemd_unit_active(unit_name: str) -> bool: async def is_systemd_unit_active(unit_name: str) -> bool:
return await localplatform.service_active(unit_name) return await localplatform.service_active(unit_name)
@@ -59,8 +59,6 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
user_str = _get_user()+":"+_get_user_group() user_str = _get_user()+":"+_get_user_group()
elif user == UserType.EFFECTIVE_USER: elif user == UserType.EFFECTIVE_USER:
user_str = _get_effective_user()+":"+_get_effective_user_group() user_str = _get_effective_user()+":"+_get_effective_user_group()
elif user == UserType.ROOT:
user_str = "root:root"
else: else:
raise Exception("Unknown User Type") raise Exception("Unknown User Type")
@@ -87,7 +85,7 @@ def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True return True
def folder_owner(path : str) -> UserType|None: def file_owner(path : str) -> UserType|None:
user_owner = _get_user_owner(path) user_owner = _get_user_owner(path)
if (user_owner == _get_user()): if (user_owner == _get_user()):
@@ -106,13 +104,14 @@ def get_home_path(user : UserType = UserType.HOST_USER) -> str:
user_name = _get_user() user_name = _get_user()
elif user == UserType.EFFECTIVE_USER: elif user == UserType.EFFECTIVE_USER:
user_name = _get_effective_user() user_name = _get_effective_user()
elif user == UserType.ROOT:
pass
else: else:
raise Exception("Unknown User Type") raise Exception("Unknown User Type")
return pwd.getpwnam(user_name).pw_dir return pwd.getpwnam(user_name).pw_dir
def get_effective_username() -> str:
return _get_effective_user()
def get_username() -> str: def get_username() -> str:
return _get_user() return _get_user()
@@ -121,8 +120,8 @@ def setgid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER: if user == UserType.HOST_USER:
user_id = _get_user_group_id() user_id = _get_user_group_id()
elif user == UserType.ROOT: elif user == UserType.EFFECTIVE_USER:
pass pass # we already are
else: else:
raise Exception("Unknown user type") raise Exception("Unknown user type")
@@ -133,8 +132,8 @@ def setuid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER: if user == UserType.HOST_USER:
user_id = _get_user_id() user_id = _get_user_id()
elif user == UserType.ROOT: elif user == UserType.EFFECTIVE_USER:
pass pass # we already are
else: else:
raise Exception("Unknown user type") raise Exception("Unknown user type")
@@ -7,7 +7,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
def chmod(path : str, permissions : int, recursive : bool = True) -> bool: def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True # Stubbed return True # Stubbed
def folder_owner(path : str) -> UserType|None: def file_owner(path : str) -> UserType|None:
return UserType.HOST_USER # Stubbed return UserType.HOST_USER # Stubbed
def get_home_path(user : UserType = UserType.HOST_USER) -> str: def get_home_path(user : UserType = UserType.HOST_USER) -> str:
@@ -34,6 +34,9 @@ async def service_restart(service_name : str, block : bool = True) -> bool:
return True # Stubbed return True # Stubbed
def get_effective_username() -> str:
return os.getlogin()
def get_username() -> str: def get_username() -> str:
return os.getlogin() return os.getlogin()
+1 -1
View File
@@ -50,7 +50,7 @@ def chown_plugin_dir():
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path) mkdir_as_user(plugin_path)
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555): if not chown(plugin_path, UserType.EFFECTIVE_USER, False) or not chmod(plugin_path, 755, False):
logger.error(f"chown/chmod exited with a non-zero exit code") logger.error(f"chown/chmod exited with a non-zero exit code")
if get_chown_plugin_path() == True: if get_chown_plugin_path() == True:
+19 -4
View File
@@ -8,7 +8,8 @@ from traceback import format_exc
from .sandboxed_plugin import SandboxedPlugin from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType from .messages import MethodCallRequest, SocketMessageType
from ..enums import PluginLoadType from ..enums import PluginLoadType, UserType
from ..localplatform.localplatform import file_owner, chown, chmod, get_chown_plugin_path
from ..localplatform.localsocket import LocalSocket from ..localplatform.localsocket import LocalSocket
from ..helpers import get_homebrew_path, mkdir_as_user from ..helpers import get_homebrew_path, mkdir_as_user
@@ -26,9 +27,12 @@ class PluginWrapper:
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8")) plugin_dir_path = path.join(plugin_path, plugin_directory)
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")): plugin_json_path = path.join(plugin_dir_path, "plugin.json")
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
json = load(open(plugin_json_path, "r", encoding="utf-8"))
if path.isfile(path.join(plugin_dir_path, "package.json")):
package_json = load(open(path.join(plugin_dir_path, "package.json"), "r", encoding="utf-8"))
self.version = package_json["version"] self.version = package_json["version"]
if ("type" in package_json and package_json["type"] == "module"): if ("type" in package_json and package_json["type"] == "module"):
self.load_type = PluginLoadType.ESMODULE_V1.value self.load_type = PluginLoadType.ESMODULE_V1.value
@@ -42,6 +46,17 @@ class PluginWrapper:
self.log = getLogger("plugin") self.log = getLogger("plugin")
if get_chown_plugin_path():
# ensure plugin folder ownership
if file_owner(plugin_dir_path) != UserType.EFFECTIVE_USER:
chown(plugin_dir_path, UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER, True)
chown(plugin_dir_path, UserType.EFFECTIVE_USER, False)
chmod(plugin_dir_path, 755, True)
# fix plugin.json permissions
if file_owner(plugin_json_path) != UserType.EFFECTIVE_USER:
chown(plugin_json_path, UserType.EFFECTIVE_USER, False)
chmod(plugin_json_path, 755, False)
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version) self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
self.proc: Process | None = None self.proc: Process | None = None
self._socket = LocalSocket() self._socket = LocalSocket()
@@ -13,7 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType
from ..localplatform.localsocket import LocalSocket from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
from ..enums import UserType from ..enums import UserType
from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport] from .. import helpers
from typing import List, TypeVar, Any from typing import List, TypeVar, Any
@@ -61,10 +61,10 @@ class SandboxedPlugin:
if self.passive: if self.passive:
return return
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER) setgid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER) setuid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
# export a bunch of environment variables to help plugin developers # export a bunch of environment variables to help plugin developers
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER) environ["HOME"] = get_home_path(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
environ["USER"] = "root" if "root" in self.flags else get_username() environ["USER"] = "root" if "root" in self.flags else get_username()
environ["DECKY_VERSION"] = helpers.get_loader_version() environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = get_username() environ["DECKY_USER"] = get_username()
+3 -3
View File
@@ -1,7 +1,7 @@
from json import dump, load from json import dump, load
from os import mkdir, path, listdir, rename from os import mkdir, path, listdir, rename
from typing import Any, Dict from typing import Any, Dict
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path from .localplatform.localplatform import chown, file_owner, get_chown_plugin_path
from .enums import UserType from .enums import UserType
from .helpers import get_homebrew_path from .helpers import get_homebrew_path
@@ -28,8 +28,8 @@ class SettingsManager:
#If the owner of the settings directory is not the user, then set it as the user: #If the owner of the settings directory is not the user, then set it as the user:
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER
if folder_owner(settings_directory) != expected_user: if file_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False) chown(settings_directory, expected_user, False)
self.settings: Dict[str, Any] = {} self.settings: Dict[str, Any] = {}
+1 -3
View File
@@ -7,7 +7,7 @@ from aiohttp.web import Application, WebSocketResponse, Request, Response, get
from enum import IntEnum from enum import IntEnum
from typing import Callable, Coroutine, Dict, Any, cast, TypeVar from typing import Callable, Coroutine, Dict, Any, cast
from traceback import format_exc from traceback import format_exc
@@ -29,8 +29,6 @@ class WSMessageExtra(WSMessage):
# see wsrouter.ts for typings # see wsrouter.ts for typings
DataType = TypeVar("DataType")
Route = Callable[..., Coroutine[Any, Any, Any]] Route = Callable[..., Coroutine[Any, Any, Any]]
class WSRouter: class WSRouter:
+24 -24
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]] [[package]]
name = "aiohappyeyeballs" name = "aiohappyeyeballs"
@@ -190,7 +190,7 @@ description = "Timeout context manager for asyncio programs"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
markers = "python_version < \"3.11\"" markers = "python_version == \"3.10\""
files = [ files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
@@ -678,32 +678,32 @@ files = [
[[package]] [[package]]
name = "pyinstaller" name = "pyinstaller"
version = "6.8.0" version = "6.14.2"
description = "PyInstaller bundles a Python application and all its dependencies into a single package." description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false optional = false
python-versions = "<3.13,>=3.8" python-versions = "<3.14,>=3.8"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"}, {file = "pyinstaller-6.14.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d77d18bf5343a1afef2772393d7a489d4ec2282dee5bca549803fc0d74b78330"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"}, {file = "pyinstaller-6.14.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3fa0c391e1300a9fd7752eb1ffe2950112b88fba9d2743eee2ef218a15f4705f"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"}, {file = "pyinstaller-6.14.2-py3-none-manylinux2014_i686.whl", hash = "sha256:077efb2d01d16d9c8fdda3ad52788f0fead2791c5cec9ed6ce058af7e26eb74b"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"}, {file = "pyinstaller-6.14.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:fdd2bd020a18736806a6bd5d3c4352f1209b427a96ad6c459d88aec1d90c4f21"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"}, {file = "pyinstaller-6.14.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:03862c6b3cf7b16843d24b529f89cd4077cbe467883cd54ce7a81940d6da09d3"},
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"}, {file = "pyinstaller-6.14.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:78827a21ada2a848e98671852d20d74b2955b6e2aaf2359ed13a462e1a603d84"},
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"}, {file = "pyinstaller-6.14.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:185710ab1503dfdfa14c43237d394d96ac183422d588294be42531480dfa6c38"},
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"}, {file = "pyinstaller-6.14.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6c673a7e761bd4a2560cfd5dbe1ccdcfe2dff304b774e6e5242fc5afed953661"},
{file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"}, {file = "pyinstaller-6.14.2-py3-none-win32.whl", hash = "sha256:1697601aa788e3a52f0b5e620b4741a34b82e6f222ec6e1318b3a1349f566bb2"},
{file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"}, {file = "pyinstaller-6.14.2-py3-none-win_amd64.whl", hash = "sha256:e10e0e67288d6dcb5898a917dd1d4272aa0ff33f197ad49a0e39618009d63ed9"},
{file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"}, {file = "pyinstaller-6.14.2-py3-none-win_arm64.whl", hash = "sha256:69fd11ca57e572387826afaa4a1b3d4cb74927d76f231f0308c0bd7872ca5ac1"},
{file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"}, {file = "pyinstaller-6.14.2.tar.gz", hash = "sha256:142cce0719e79315f0cc26400c2e5c45d9b6b17e7e0491fee444a9f8f16f4917"},
] ]
[package.dependencies] [package.dependencies]
altgraph = "*" altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=22.0" packaging = ">=22.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2024.6" pyinstaller-hooks-contrib = ">=2025.5"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0" setuptools = ">=42.0.0"
@@ -713,14 +713,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]] [[package]]
name = "pyinstaller-hooks-contrib" name = "pyinstaller-hooks-contrib"
version = "2024.7" version = "2025.8"
description = "Community maintained hooks for PyInstaller" description = "Community maintained hooks for PyInstaller"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"}, {file = "pyinstaller_hooks_contrib-2025.8-py3-none-any.whl", hash = "sha256:8d0b8cfa0cb689a619294ae200497374234bd4e3994b3ace2a4442274c899064"},
{file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"}, {file = "pyinstaller_hooks_contrib-2025.8.tar.gz", hash = "sha256:3402ad41dfe9b5110af134422e37fc5d421ba342c6cb980bd67cb30b7415641c"},
] ]
[package.dependencies] [package.dependencies]
@@ -1041,5 +1041,5 @@ propcache = ">=0.2.0"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10,<3.13" python-versions = ">=3.10,<3.14"
content-hash = "3c9488709e61f3aa21ab47ceb9c677927ce770d8e1e327602a1a6afb09164475" content-hash = "9a331b42c52134230384c1a7348c2856903d82d6007e06cd75eed13842aa21ea"
+1 -1
View File
@@ -14,7 +14,7 @@ include = [
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.10,<3.13" python = ">=3.10,<3.14"
aiohttp = "^3.10.11" aiohttp = "^3.10.11"
aiohttp-jinja2 = "^1.5.1" aiohttp-jinja2 = "^1.5.1"