diff options
| author | AAGaming <aagaming@riseup.net> | 2025-07-28 20:58:59 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-28 20:58:59 -0400 |
| commit | 8f41eb93ef80bfbf3851ce8a82ea0f88c87e6c68 (patch) | |
| tree | 6cbfdf1843dc0aba959e9b76201df149a71964fc | |
| parent | 670ae7d8a7d8fc8efc28ce403865177ebf0b715e (diff) | |
| download | decky-loader-8f41eb93ef80bfbf3851ce8a82ea0f88c87e6c68.tar.gz decky-loader-8f41eb93ef80bfbf3851ce8a82ea0f88c87e6c68.zip | |
Merge commit from forkv3.1.10
* fix incorrect permissions on plugin directories
* chown plugin dirs too
* fix the stupid
* cleanup useless comments
| -rw-r--r-- | backend/decky_loader/browser.py | 35 | ||||
| -rw-r--r-- | backend/decky_loader/enums.py | 5 | ||||
| -rw-r--r-- | backend/decky_loader/helpers.py | 3 | ||||
| -rw-r--r-- | backend/decky_loader/localplatform/localplatformlinux.py | 17 | ||||
| -rw-r--r-- | backend/decky_loader/localplatform/localplatformwin.py | 5 | ||||
| -rw-r--r-- | backend/decky_loader/main.py | 2 | ||||
| -rw-r--r-- | backend/decky_loader/plugin/plugin.py | 23 | ||||
| -rw-r--r-- | backend/decky_loader/plugin/sandboxed_plugin.py | 8 | ||||
| -rw-r--r-- | backend/decky_loader/settings.py | 6 | ||||
| -rw-r--r-- | backend/decky_loader/wsrouter.py | 4 | ||||
| -rw-r--r-- | backend/poetry.lock | 48 | ||||
| -rw-r--r-- | backend/pyproject.toml | 2 |
12 files changed, 94 insertions, 64 deletions
diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py index 4a63f6ec..975a917a 100644 --- a/backend/decky_loader/browser.py +++ b/backend/decky_loader/browser.py @@ -18,9 +18,10 @@ from enum import IntEnum from typing import Dict, List, TypedDict # Local modules -from .localplatform.localplatform import chown, chmod +from .localplatform.localplatform import chown, chmod, get_chown_plugin_path from .loader import Loader, Plugins from .helpers import get_ssl_context, download_remote_binary_to_path +from .enums import UserType from .settings import SettingsManager logger = getLogger("Browser") @@ -60,13 +61,6 @@ class PluginBrowser: return False zip_file = ZipFile(zip) 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 async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str): @@ -101,8 +95,6 @@ class PluginBrowser: rv = False 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: rv = True logger.info(f"No Remote Binaries to Download") @@ -124,6 +116,25 @@ class PluginBrowser: return folder except: 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): if self.loader.watcher: @@ -266,6 +277,7 @@ class PluginBrowser: 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") ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir) + chown_ret = self.set_plugin_dir_permissions(plugin_dir) if ret: logger.info(f"Installed {name} (Version: {version})") if name in self.loader.plugins: @@ -278,6 +290,9 @@ class PluginBrowser: self.settings.setSetting("pluginOrder", current_plugin_order) logger.debug("Plugin %s was added to the pluginOrder setting", name) 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: logger.error("Could not download remote binaries") return diff --git a/backend/decky_loader/enums.py b/backend/decky_loader/enums.py index e7fb4905..4a6296ad 100644 --- a/backend/decky_loader/enums.py +++ b/backend/decky_loader/enums.py @@ -1,9 +1,8 @@ from enum import IntEnum class UserType(IntEnum): - HOST_USER = 1 - EFFECTIVE_USER = 2 - ROOT = 3 + HOST_USER = 1 # usually deck + EFFECTIVE_USER = 2 # usually root class PluginLoadType(IntEnum): LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py index 857503ab..1621652f 100644 --- a/backend/decky_loader/helpers.py +++ b/backend/decky_loader/helpers.py @@ -181,7 +181,8 @@ def get_user_group_id() -> int: # Get the default home path unless a user is specified 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: return await localplatform.service_active(unit_name) diff --git a/backend/decky_loader/localplatform/localplatformlinux.py b/backend/decky_loader/localplatform/localplatformlinux.py index beaa2540..1c8f2ace 100644 --- a/backend/decky_loader/localplatform/localplatformlinux.py +++ b/backend/decky_loader/localplatform/localplatformlinux.py @@ -59,8 +59,6 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = user_str = _get_user()+":"+_get_user_group() elif user == UserType.EFFECTIVE_USER: user_str = _get_effective_user()+":"+_get_effective_user_group() - elif user == UserType.ROOT: - user_str = "root:root" else: raise Exception("Unknown User Type") @@ -87,7 +85,7 @@ def chmod(path : str, permissions : int, recursive : bool = True) -> bool: return True -def folder_owner(path : str) -> UserType|None: +def file_owner(path : str) -> UserType|None: user_owner = _get_user_owner(path) if (user_owner == _get_user()): @@ -106,13 +104,14 @@ def get_home_path(user : UserType = UserType.HOST_USER) -> str: user_name = _get_user() elif user == UserType.EFFECTIVE_USER: user_name = _get_effective_user() - elif user == UserType.ROOT: - pass else: raise Exception("Unknown User Type") return pwd.getpwnam(user_name).pw_dir +def get_effective_username() -> str: + return _get_effective_user() + def get_username() -> str: return _get_user() @@ -121,8 +120,8 @@ def setgid(user : UserType = UserType.HOST_USER): if user == UserType.HOST_USER: user_id = _get_user_group_id() - elif user == UserType.ROOT: - pass + elif user == UserType.EFFECTIVE_USER: + pass # we already are else: raise Exception("Unknown user type") @@ -133,8 +132,8 @@ def setuid(user : UserType = UserType.HOST_USER): if user == UserType.HOST_USER: user_id = _get_user_id() - elif user == UserType.ROOT: - pass + elif user == UserType.EFFECTIVE_USER: + pass # we already are else: raise Exception("Unknown user type") diff --git a/backend/decky_loader/localplatform/localplatformwin.py b/backend/decky_loader/localplatform/localplatformwin.py index 7e3bd31c..8755c943 100644 --- a/backend/decky_loader/localplatform/localplatformwin.py +++ b/backend/decky_loader/localplatform/localplatformwin.py @@ -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: return True # Stubbed -def folder_owner(path : str) -> UserType|None: +def file_owner(path : str) -> UserType|None: return UserType.HOST_USER # Stubbed 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 +def get_effective_username() -> str: + return os.getlogin() + def get_username() -> str: return os.getlogin() diff --git a/backend/decky_loader/main.py b/backend/decky_loader/main.py index 315b7d29..16caca2e 100644 --- a/backend/decky_loader/main.py +++ b/backend/decky_loader/main.py @@ -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 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") if get_chown_plugin_path() == True: diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py index ba23fff9..61de4b1f 100644 --- a/backend/decky_loader/plugin/plugin.py +++ b/backend/decky_loader/plugin/plugin.py @@ -8,7 +8,8 @@ from traceback import format_exc from .sandboxed_plugin import SandboxedPlugin 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 ..helpers import get_homebrew_path, mkdir_as_user @@ -26,9 +27,12 @@ class PluginWrapper: self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value - json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8")) - if path.isfile(path.join(plugin_path, plugin_directory, "package.json")): - package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8")) + plugin_dir_path = path.join(plugin_path, plugin_directory) + plugin_json_path = path.join(plugin_dir_path, "plugin.json") + + 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"] if ("type" in package_json and package_json["type"] == "module"): self.load_type = PluginLoadType.ESMODULE_V1.value @@ -42,6 +46,17 @@ class PluginWrapper: 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.proc: Process | None = None self._socket = LocalSocket() diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py index 7d88719f..71e1d17b 100644 --- a/backend/decky_loader/plugin/sandboxed_plugin.py +++ b/backend/decky_loader/plugin/sandboxed_plugin.py @@ -13,7 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType from ..localplatform.localsocket import LocalSocket from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX from ..enums import UserType -from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport] +from .. import helpers from typing import List, TypeVar, Any @@ -61,10 +61,10 @@ class SandboxedPlugin: if self.passive: return - setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER) - setuid(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.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER) # 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["DECKY_VERSION"] = helpers.get_loader_version() environ["DECKY_USER"] = get_username() diff --git a/backend/decky_loader/settings.py b/backend/decky_loader/settings.py index b5f034aa..73dbe38a 100644 --- a/backend/decky_loader/settings.py +++ b/backend/decky_loader/settings.py @@ -1,7 +1,7 @@ from json import dump, load from os import mkdir, path, listdir, rename 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 .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: - expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT - if folder_owner(settings_directory) != expected_user: + expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER + if file_owner(settings_directory) != expected_user: chown(settings_directory, expected_user, False) self.settings: Dict[str, Any] = {} diff --git a/backend/decky_loader/wsrouter.py b/backend/decky_loader/wsrouter.py index 8a019b44..691abd05 100644 --- a/backend/decky_loader/wsrouter.py +++ b/backend/decky_loader/wsrouter.py @@ -7,7 +7,7 @@ from aiohttp.web import Application, WebSocketResponse, Request, Response, get 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 @@ -29,8 +29,6 @@ class WSMessageExtra(WSMessage): # see wsrouter.ts for typings -DataType = TypeVar("DataType") - Route = Callable[..., Coroutine[Any, Any, Any]] class WSRouter: diff --git a/backend/poetry.lock b/backend/poetry.lock index bd67b3bb..03300f16 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -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]] name = "aiohappyeyeballs" @@ -190,7 +190,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, @@ -678,32 +678,32 @@ files = [ [[package]] name = "pyinstaller" -version = "6.8.0" +version = "6.14.2" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false -python-versions = "<3.13,>=3.8" +python-versions = "<3.14,>=3.8" groups = ["dev"] files = [ - {file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"}, - {file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"}, - {file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"}, - {file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"}, - {file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"}, - {file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"}, - {file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"}, - {file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"}, - {file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"}, - {file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"}, - {file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"}, - {file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"}, + {file = "pyinstaller-6.14.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d77d18bf5343a1afef2772393d7a489d4ec2282dee5bca549803fc0d74b78330"}, + {file = "pyinstaller-6.14.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3fa0c391e1300a9fd7752eb1ffe2950112b88fba9d2743eee2ef218a15f4705f"}, + {file = "pyinstaller-6.14.2-py3-none-manylinux2014_i686.whl", hash = "sha256:077efb2d01d16d9c8fdda3ad52788f0fead2791c5cec9ed6ce058af7e26eb74b"}, + {file = "pyinstaller-6.14.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:fdd2bd020a18736806a6bd5d3c4352f1209b427a96ad6c459d88aec1d90c4f21"}, + {file = "pyinstaller-6.14.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:03862c6b3cf7b16843d24b529f89cd4077cbe467883cd54ce7a81940d6da09d3"}, + {file = "pyinstaller-6.14.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:78827a21ada2a848e98671852d20d74b2955b6e2aaf2359ed13a462e1a603d84"}, + {file = "pyinstaller-6.14.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:185710ab1503dfdfa14c43237d394d96ac183422d588294be42531480dfa6c38"}, + {file = "pyinstaller-6.14.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6c673a7e761bd4a2560cfd5dbe1ccdcfe2dff304b774e6e5242fc5afed953661"}, + {file = "pyinstaller-6.14.2-py3-none-win32.whl", hash = "sha256:1697601aa788e3a52f0b5e620b4741a34b82e6f222ec6e1318b3a1349f566bb2"}, + {file = "pyinstaller-6.14.2-py3-none-win_amd64.whl", hash = "sha256:e10e0e67288d6dcb5898a917dd1d4272aa0ff33f197ad49a0e39618009d63ed9"}, + {file = "pyinstaller-6.14.2-py3-none-win_arm64.whl", hash = "sha256:69fd11ca57e572387826afaa4a1b3d4cb74927d76f231f0308c0bd7872ca5ac1"}, + {file = "pyinstaller-6.14.2.tar.gz", hash = "sha256:142cce0719e79315f0cc26400c2e5c45d9b6b17e7e0491fee444a9f8f16f4917"}, ] [package.dependencies] altgraph = "*" macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} packaging = ">=22.0" -pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} -pyinstaller-hooks-contrib = ">=2024.6" +pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2025.5" pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" @@ -713,14 +713,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2024.7" +version = "2025.8" description = "Community maintained hooks for PyInstaller" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"}, - {file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"}, + {file = "pyinstaller_hooks_contrib-2025.8-py3-none-any.whl", hash = "sha256:8d0b8cfa0cb689a619294ae200497374234bd4e3994b3ace2a4442274c899064"}, + {file = "pyinstaller_hooks_contrib-2025.8.tar.gz", hash = "sha256:3402ad41dfe9b5110af134422e37fc5d421ba342c6cb980bd67cb30b7415641c"}, ] [package.dependencies] @@ -1041,5 +1041,5 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" -python-versions = ">=3.10,<3.13" -content-hash = "3c9488709e61f3aa21ab47ceb9c677927ce770d8e1e327602a1a6afb09164475" +python-versions = ">=3.10,<3.14" +content-hash = "9a331b42c52134230384c1a7348c2856903d82d6007e06cd75eed13842aa21ea" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 38fc304d..27934398 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -14,7 +14,7 @@ include = [ ] [tool.poetry.dependencies] -python = ">=3.10,<3.13" +python = ">=3.10,<3.14" aiohttp = "^3.10.11" aiohttp-jinja2 = "^1.5.1" |
