From 8a8ea0ee19705c443f85a64a89b8cfd024132e0e Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Thu, 15 Jun 2023 16:07:51 +0100 Subject: [PATCH 1/8] `ayon_server.addons` implement `RezRepo` This commit add a new class to the `ayon_server.addons` logic that allows the server to use Rez (https://github.com/AcademySoftwareFoundation/rez) as a package manager, including a default `rezbuild.py` for packages that do not include one, these need to follow the AYON Addon template structure. THIS BREAKS WITH EXISTING ADDONS present in the `/addons` which are not rez packages. --- .gitignore | 3 + ayon_server/addons/rezbuild.py | 112 +++++++++++++++++++++++ ayon_server/addons/rezrepo.py | 160 +++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 276 insertions(+) create mode 100644 ayon_server/addons/rezbuild.py create mode 100644 ayon_server/addons/rezrepo.py diff --git a/.gitignore b/.gitignore index 33c3c085..4f70597d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ test_*.py .python-version .vscode BUILD_DATE +.venv +venv + diff --git a/ayon_server/addons/rezbuild.py b/ayon_server/addons/rezbuild.py new file mode 100644 index 00000000..7f6a11f7 --- /dev/null +++ b/ayon_server/addons/rezbuild.py @@ -0,0 +1,112 @@ +""" AYON Rez Builder + +This expects a certain folder structure: +├── client +│ └── ayon_ +├── package.py +└── server + +Other folders might or might not be present, but they are essentially ignored. + +Will zip up the `client/ayon_` and place it into `private/client.zip` +and copy the contents of `server` into the root of the package. + + +├── addon +│ ├── frontend +│ ├── __init__.py +│ ├── private +│ │ ├── client.zip +│ │ └── pyproject.toml +│ ├── settings +│ └── version.py +├── package.py +└── build.rtx + +MIght be worth checking in the future https://gitlab.com/Pili-Pala/rezbuild +""" +import os +from pathlib import Path +import shutil + +SRC_DIR = Path(os.environ["REZ_BUILD_SOURCE_PATH"]) +DEST_DIR = Path(os.environ["REZ_BUILD_INSTALL_PATH"]) + + +def _recursive_copy(src, dst): + """ Recursively travers a directory and copy the contents to destination + + For soem reason `shutil.copytree` refuses to work, so I had to implement. + """ + for child in src.iterdir(): + if child.is_dir(): + _recursive_copy(child, dst / child.stem) + elif child.is_file(): + dst_file = dst / child.relative_to(src) + print(f"Copying {child} -> {dst_file}") + dst_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copy( + child, + dst_file + ) + + +def install_addon(): + server_dir = SRC_DIR / "server" + + # Copy "server" to "addon" + if server_dir.is_dir(): + print(f"Copying `server` directory contents to {DEST_DIR}") + _recursive_copy(server_dir, DEST_DIR) + + # Zip up the `client`` dir which might not be present + client_dir = SRC_DIR / "client" + + if not client_dir.exists(): + return DEST_DIR + + client_zip_dest_dir = DEST_DIR / "private" / "client" + client_zip_dest_dir.mkdir(parents=True) + client_zip = DEST_DIR / "private" / "client.zip" + + print(f"Copying `client` directory to `private/client` {client_zip_dest_dir}") + # Copy and remove unnecessary files so we can just "zip it up" + _recursive_copy(client_dir, client_zip_dest_dir) + + # Move the `pyproject.toml` next to the `client.zip` + try: + print(f"Moving client's `pyproject.toml` to {DEST_DIR / 'private' / 'pyproject.toml'}") + shutil.move( + str(client_zip_dest_dir / "pyproject.toml"), + str(DEST_DIR / "private" / "pyproject.toml") + ) + except Exception: + print("Client code has no `pyproject.toml`.") + + print(f"Creating an archive of {client_zip_dest_dir}") + shutil.make_archive( + str(client_zip), + "zip", + root_dir=str(client_zip_dest_dir) + ) + + print(f"Removing transient folder {client_zip_dest_dir}") + # Remove transient "client" folder from "private" + shutil.rmtree(f'{client_zip_dest_dir}', ignore_errors=True) + + addon_name = os.environ['REZ_BUILD_PROJECT_NAME'] + addon_version = os.environ['REZ_BUILD_PROJECT_VERSION'] + with open(DEST_DIR / "version.py", "w+") as version_py: + version_py.write(f'__version__ = "{addon_version}"') + + print(f"Finished installing {addon_name}-{addon_version}") + + +if __name__ == "__main__": + try: + install_addon() + except Exception as e: + # shutil.rmtree(str(DEST_DIR)) + raise e + + diff --git a/ayon_server/addons/rezrepo.py b/ayon_server/addons/rezrepo.py new file mode 100644 index 00000000..edd4ad63 --- /dev/null +++ b/ayon_server/addons/rezrepo.py @@ -0,0 +1,160 @@ +""" + Rez implementation of the Addons logic + * Addon Library (`library.py`): A Class that manages addons server definitions. + * Addon Server Definition (`definition.py`): A Class that manages addon's versions. + * Addon Definition (`addon.py`): A Class representing a specific **version** of an addon, + + Library -> Rez Repo + ServerDefinition -> RezFamily + Addon Definition -> BaseServerAddon +""" +from pathlib import Path +import shutil + +from ayon_server.config import ayonconfig +from ayon_server.version import __version__ as ayon_server_version + +from nxtools import logging, log_traceback +from rez.exceptions import PackageFamilyNotFoundError +from rez.packages import get_package +from rez.developer_package import DeveloperPackage +from rez.build_process import create_build_process +from rez.build_system import create_build_system +from rez.package_maker import make_package +# from rez.package_repository import PackageRepository +from rez.packages import get_latest_package +from rez.package_search import get_plugins +from rez.package_remove import remove_package, remove_package_family +from rez.serialise import load_from_file, FileFormat +from rez.utils.resources import ResourcePool +from rezplugins.package_repository.filesystem import FileSystemPackageRepository + + +class RezRepo(FileSystemPackageRepository): + _instance = None + rez_repo_path = ayonconfig.addons_dir + + @classmethod + def get_instance(cls) -> "RezRepo": + if cls._instance is None: + cls._instance = RezRepo() + return cls._instance + + def __init__(self) -> None: + self.restart_requested = False + self.packages = {} + + # Using `super` won't work correctly + FileSystemPackageRepository.__init__(self, self.rez_repo_path, ResourcePool()) + + # AYON server rez meta package + ayon_server_package = get_package( + "ayon_server", ayon_server_version, paths=[self.rez_repo_path] + ) + + if not ayon_server_package: + self._create_ayon_server_package() + + # Find all AYON plugins + self.packages = self._find_ayon_addons() + + if not self.packages: + logging.info("No addons found for 'ayon_server'") + + def _create_ayon_server_package(self): + from rez.package_maker import make_package + + with make_package("ayon_server", self.rez_repo_path) as pkg: + pkg.authors = ["Ynput"] + pkg.description = "Meta package that any Addon has to specify as plugin of." + pkg.has_plugins = True + pkg.version = ayon_server_version + + # We need to restart everytime we install a new package + self.restart_requested = True + + def _find_ayon_addons(self): + """Find Rez packages that are plugins of the `ayon_server` package.""" + ayon_addons = {} + + try: + ayon_server_plugins = get_plugins("ayon_server", [self.rez_repo_path]) + except PackageFamilyNotFoundError as e: + logging.warning(f"Missing 'ayon_server' package in '{self.rez_repo_path}'") + log_traceback(e) + return + + for addon_name in ayon_server_plugins: + package_family = self.get_package_family(addon_name) + + if package_family is None: + print("No package family found") + continue + + ayon_addons[package_family.name] = { + "family": package_family, + "versions": [], + } + package_versions = ayon_addons[package_family.name]["versions"] + # The `FileSystemPackageFamilyResource` does not hold the packages + # So we iterate over them and get the actual `Package` object. + for package in package_family.iter_packages(): + package = get_package( + package.name, package.version, paths=[self.rez_repo_path] + ) + package_versions.append({str(package.version): package}) + + logging.info(f"Found {len(ayon_addons)} 'ayon_server' plugin(s) Rez packages.") + return ayon_addons + + @classmethod + def install_addon(cls, addon_name, package_definition_path): + package_definition_path = Path(package_definition_path) + + if not package_definition_path.exists(): + logging.error( + f"Missing `package.py` in: {package_definition_path}" + ) + return + + working_dir = package_definition_path.parent + ayon_build = Path(__file__).parent / "rezbuild.py" + shutil.copy(ayon_build, working_dir / "rezbuild.py") + + package = DeveloperPackage.from_path(working_dir, format=FileFormat.py) + if getattr(package, "build_command", None) is None: + # If no command is specified we fallback to AYON rezbuild + logging.debug("Setting Package `build_command` to `rezbuild.py`") + package.build_command = "python {root}/rezbuild.py" + + logging.debug(f"Found {package} in {working_dir}") + build_system = create_build_system( + working_dir, + package=package, + buildsys_type="custom", + ) + + # create and execute build process + builder = create_build_process( + "local", + working_dir, + build_system=build_system, + ) + + try: + builder.build(install_path=cls.rez_repo_path, install=True) + except Exception as e: + logging.error(f"Unable to install {package_definition_path}") + log_traceback(e) + + @classmethod + def remove_addon(cls, addon_name, addon_version=None): + if addon_version: + remove_package_family(addon_name, cls.rez_repo_path, force=True) + else: + remove_package(addon_name, addon_version, cls.rez_repo_path) + + @classmethod + def get_latest_addon_version(cls, addon_name): + return get_latest_package(addon_name, paths=[cls.rez_repo_path]) + diff --git a/pyproject.toml b/pyproject.toml index 3d53d45b..ae304a55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ uvicorn = {extras = ["standard"], version = "^0.25"} semver = "^3.0.1" pyjwt = "^2.8.0" cryptography = "^41.0.7" +rez="^2.114" [tool.poetry.dev-dependencies] pytest = "^7.0.0" From 4579194b3d01f9c8843d5ebb0de3cbc8aaa3371c Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Mon, 29 Jan 2024 17:07:04 +0000 Subject: [PATCH 2/8] `ayon_server.addons` Refactor `AddonLibrary` to leverage `RezRepo` With the implementation of the `RezRepo` we deprecate the previous `ServerAddonDefinition` since now we leverage `rez.PackageFamily` to achieve the same results. The class has been refactored to use `RezRepo` for discovery, installing and removing AYON addons. Some methods have been kept (or reworded) but refactored to provide a more streamlined set of results, hopefully for consistency when using the `AddonLibrary`. --- ayon_server/addons/__init__.py | 4 +- ayon_server/addons/definition.py | 127 -------- ayon_server/addons/library.py | 478 ++++++++++++++++++++++--------- 3 files changed, 352 insertions(+), 257 deletions(-) delete mode 100644 ayon_server/addons/definition.py diff --git a/ayon_server/addons/__init__.py b/ayon_server/addons/__init__.py index 0fcbc7df..2bbb945b 100644 --- a/ayon_server/addons/__init__.py +++ b/ayon_server/addons/__init__.py @@ -1,6 +1,6 @@ -__all__ = ["AddonLibrary", "BaseServerAddon", "ServerAddonDefinition", "SSOOption"] +__all__ = ["AddonLibrary", "BaseServerAddon", "SSOOption"] from ayon_server.addons.addon import BaseServerAddon -from ayon_server.addons.definition import ServerAddonDefinition from ayon_server.addons.library import AddonLibrary from ayon_server.addons.models import SSOOption + diff --git a/ayon_server/addons/definition.py b/ayon_server/addons/definition.py deleted file mode 100644 index 412f1568..00000000 --- a/ayon_server/addons/definition.py +++ /dev/null @@ -1,127 +0,0 @@ -import os -from typing import TYPE_CHECKING - -import semver -from nxtools import logging, slugify - -from ayon_server.addons.addon import BaseServerAddon -from ayon_server.addons.utils import classes_from_module, import_module - -if TYPE_CHECKING: - from ayon_server.addons.library import AddonLibrary - - -class ServerAddonDefinition: - title: str | None = None - app_host_name: str | None = None - - def __init__(self, library: "AddonLibrary", addon_dir: str): - self.library = library - self.addon_dir = addon_dir - self.restart_requested = False - self._versions: dict[str, BaseServerAddon] | None = None - - if not self.versions: - logging.warning(f"Addon {self.name} has no versions") - return - - for version in self.versions.values(): - if self.app_host_name is None: - self.app_host_name = version.app_host_name - if self.name is None: - self.name = version.name - - # do we need this check? - if version.app_host_name != self.app_host_name: - raise ValueError( - f"Addon {self.name} has version {version.version} with " - f"mismatched app host name {version.app_host_name} != {self.app_host_name}" - ) - - if version.name != self.name: - raise ValueError( - f"Addon {self.name} has version {version.version} with " - f"mismatched name {version.name} != {self.name}" - ) - - self.title = version.title # Use the latest title - - @property - def dir_name(self) -> str: - return os.path.split(self.addon_dir)[-1] - - @property - def name(self) -> str: - for version in self.versions.values(): - return version.name - raise ValueError("No versions found") - - @property - def friendly_name(self) -> str: - """Return a friendly (human readable) name of the addon.""" - if self.versions: - if self.title: - return self.title - if hasattr(self, "name"): - return self.name.capitalize() - return f"(Empty addon {self.dir_name})" - - @property - def versions(self) -> dict[str, BaseServerAddon]: - if self._versions is None: - self._versions = {} - for version_name in os.listdir(self.addon_dir): - mdir = os.path.join(self.addon_dir, version_name) - mfile = os.path.join(mdir, "__init__.py") - if not os.path.exists(os.path.join(mfile)): - continue - - vname = slugify(f"{self.dir_name}-{version_name}") - try: - module = import_module(vname, mfile) - except AttributeError: - logging.error(f"Addon {vname} is not valid") - continue - - for Addon in classes_from_module(BaseServerAddon, module): - try: - self._versions[Addon.version] = Addon(self, mdir) - except ValueError as e: - logging.error( - f"Error loading addon {vname} versions: {e.args[0]}" - ) - - if self._versions[Addon.version].restart_requested: - logging.warning( - f"Addon {self.name} version {Addon.version} " - "requested server restart" - ) - self.restart_requested = True - - return self._versions - - @property - def latest(self) -> BaseServerAddon | None: - if not self.versions: - return None - versions = list(self.versions.keys()) - max_version = max(versions, key=semver.VersionInfo.parse) - return self.versions[max_version] - - @property - def is_system(self) -> bool: - for version in self.versions.values(): - if version.system: - return True - return False - - def __getitem__(self, item) -> BaseServerAddon: - return self.versions[item] - - def get(self, item, default=None) -> BaseServerAddon | None: - return self.versions.get(item, default) - - def unload_version(self, version: str) -> None: - """Unload the given version of the addon.""" - if self._versions and version in self._versions: - del self._versions[version] diff --git a/ayon_server/addons/library.py b/ayon_server/addons/library.py index 2e33552b..a0df543e 100644 --- a/ayon_server/addons/library.py +++ b/ayon_server/addons/library.py @@ -1,162 +1,384 @@ +import asyncio +from concurrent.futures import ThreadPoolExecutor import os -from typing import Any, ItemsView, Optional - -from nxtools import log_traceback, logging +from pathlib import Path +import shutil +import tempfile +import time +from typing import Dict, Optional +import zipfile from ayon_server.addons.addon import BaseServerAddon -from ayon_server.addons.definition import ServerAddonDefinition +from ayon_server.addons.rezrepo import RezRepo +from ayon_server.addons.utils import classes_from_module, import_module from ayon_server.config import ayonconfig - -# from ayon_server.addons.utils import classes_from_module, import_module +from ayon_server.events import update_event from ayon_server.exceptions import NotFoundException from ayon_server.lib.postgres import Postgres +import aiofiles +import httpx +from nxtools import logging, log_traceback, slugify + class AddonLibrary: - ADDONS_DIR = ayonconfig.addons_dir + """Class to manage AYON addons. + + Attrs: + initialized_addons (dict): Addon name and version with the loaded class as value. + broken_addons (dict): Addon name and version with the reason why they weren't able to be + loaded. + """ _instance = None + initialized_addons = {} + broken_addons = {} @classmethod - def getinstance(cls) -> "AddonLibrary": + def get_instance(cls) -> "AddonLibrary": if cls._instance is None: cls._instance = AddonLibrary() return cls._instance - def __init__(self) -> None: - self.data: dict[str, ServerAddonDefinition] = {} - self.broken_addons: dict[tuple[str, str], dict[str, str]] = {} - self.restart_requested = False - addons_dir = self.get_addons_dir() - if addons_dir is None: - logging.error(f"Addons directory does not exist: {addons_dir}") - return None - - for addon_name in os.listdir(addons_dir): - # ignore hidden directories (such as .git) - if addon_name.startswith("."): - continue - - addon_dir = os.path.join(addons_dir, addon_name) - if not os.path.isdir(addon_dir): - continue - - try: - definition = ServerAddonDefinition(self, addon_dir) - except Exception: - log_traceback(f"Unable to initialize {addon_dir}") - continue - if not definition.versions: - continue - - logging.info("Initializing addon", definition.name) - self.data[definition.name] = definition - if definition.restart_requested: - self.restart_requested = True - - def get_addons_dir(self) -> str | None: - for d in [ayonconfig.addons_dir, "addons"]: - if not os.path.isdir(d): - continue - return d - return None - @classmethod - def addon(cls, name: str, version: str) -> BaseServerAddon: - """Return an instance of the given addon. + def get_addon(cls, addon_name: str, addon_version: str): + """Attempt to initialize a Rez Package as an AYON Addon. - Raise NotFoundException if the addon is not found. + Args: + cls (AddonLibrary): The AddonLibrary. + addon_name (str): The Addon name. + addon_version (str): The Addon version. + + Returns: + addon_class (BaseServerAddon | None): The Addon's main class, if there were no issues + loading. """ - instance = cls.getinstance() - if (definition := instance.data.get(name)) is None: - raise NotFoundException(f"Addon {name} does not exist") - if (addon := definition.versions.get(version)) is None: - raise NotFoundException(f"Addon {name} version {version} does not exist") - return addon + instance = cls.get_instance() - @classmethod - def items(cls) -> ItemsView[str, ServerAddonDefinition]: - instance = cls.getinstance() - return instance.data.items() + try: + # Guard in case we get a version "None" + addon_version = eval(addon_version) + if not addon_version: + return - @classmethod - def get(cls, key: str, default=None) -> ServerAddonDefinition: - instance = cls.getinstance() - return instance.data.get(key, default) + except Exception: + pass - def __getitem__(self, key) -> ServerAddonDefinition: - return self.data[key] + rezrepo = RezRepo().get_instance() + addon_class = None - def __contains__(self, key) -> bool: - return key in self.data + for addon_version_dict in rezrepo.packages.get(addon_name, {}).get("versions", []): + rez_package = addon_version_dict.get(addon_version, None) + addon_name_and_version = slugify(f"{addon_name}-{addon_version}") + if rez_package: + logging.debug(f"Initializing Addon {addon_name}-{addon_version}") - def __iter__(self): - return iter(self.data) + try: + addon_class = cls._init_addon_from_rez_package( + addon_name_and_version, + rez_package + ) + instance.initialized_addons[addon_name_and_version] = addon_class + except Exception as e: + logging.error(f"Unable to initialize Addon {addon_name}-{addon_version}") + log_traceback(e) + instance.broken_addons[addon_name_and_version] = e + else: + instance.broken_addons[addon_name_and_version] = f"No Rez packages found for addon {addon_name_and_version}" + logging.warning(f"Addon {addon_name}-{addon_version} not found.") - async def get_active_versions(self) -> dict[str, dict[str, Optional[str]]]: - production_bundle = await Postgres.fetch( - "SELECT data->'addons' as addons FROM bundles WHERE is_production is true" - ) - staging_bundle = await Postgres.fetch( - "SELECT data->'addons' as addons FROM bundles WHERE is_staging is true" + return addon_class + + @staticmethod + def delete_addon(addon_name, addon_version=None): + """Delete an addon from the server rez repo. + + Args: + addon_name (str): The Addon name. + addon_version (str | Optional): The Addon version. + """ + logging.debug(f"Removing Rez Package: {addon_name} Version: {addon_version}") + RezRepo.remove_addon( + addon_name, + addon_version ) - res: dict[str, dict[str, Optional[str]]] = {} - for addon_name in self.data.keys(): - res[addon_name] = { - "production": None, - "staging": None, + @staticmethod + def get_addons_latest_versions(): + """Find the latest version of an addon, via rez.""" + rezrepo = RezRepo.get_instance() + latest_addons = {} + + for addon_name in rezrepo.packages: + latest_version = RezRepo.get_latest_addon_version() + + latest_addons[addon_name] = latest_version + + return latest_addons + + def _init_addon_from_rez_package(module_name, rez_package): + """Attempt to initialize the Addon's BaseServerAddon sub-class. + + Args: + module_name (str): The moducle name. + rez_package (rez.Package): + """ + module_init_path = Path(rez_package.base) / "__init__.py" + try: + addon_module = import_module(module_name, str(module_init_path)) + except AttributeError as e: + logging.error(f"Addon {module_name} - {module_init_path} is not a valid Python module.") + raise e + + # It makes little sense to allow several addons from one init, + # since they would be separate rez-packages + addon_class = next(iter(classes_from_module(BaseServerAddon, addon_module)), None) + + if not addon_class: + raise NotFoundException( + f"No `BaseServerAddon` subclass found in the package {rez_package}" + ) + + return addon_class(rez_package, rez_package.base) + + @staticmethod + async def get_enabled_addons(bundle_name=None) -> dict[str, dict[str, Optional[str]]]: + """ Get the Addons enabled in the Bundles. + """ + + bundles_query = "SELECT name, is_production, is_staging, data->'addons' as addons FROM bundles" + + if bundle_name: + bundles_query += f" WHERE name = '{bundle_name}'" + + bundles = await Postgres.fetch(bundles_query) + all_addons: dict[str, dict[str, Optional[str | bool]]] = {} + + for bundle in bundles: + all_addons[bundle["name"]] = { + "addons": [], + "production": bundle["is_production"], + "staging": bundle["is_staging"], } - if production_bundle and (addons := production_bundle[0]["addons"]): - if addon_name in addons: - res[addon_name]["production"] = addons[addon_name] - if staging_bundle and (addons := staging_bundle[0]["addons"]): - if addon_name in addons: - res[addon_name]["staging"] = addons[addon_name] - - return res - - async def get_production_addon(self, addon_name: str) -> BaseServerAddon | None: - """Return a production instance of the addon.""" - active_versions = await self.get_active_versions() - if addon_name not in active_versions: - return None - production_version = active_versions[addon_name]["production"] - if production_version is None: - return None - return self[addon_name][production_version] - - async def get_staging_addon(self, addon_name: str) -> BaseServerAddon | None: - """Return a staging instance of the addon.""" - active_versions = await self.get_active_versions() - if addon_name not in active_versions: - return None - staging_version = active_versions[addon_name]["staging"] - if staging_version is None: - return None - return self[addon_name][staging_version] + for addon_name, addon_version in bundle["addons"].items(): + all_addons[bundle["name"]]["addons"].append((addon_name, addon_version)) + + return all_addons + + @staticmethod + async def get_bundle_addons(bundle_name) -> list[tuple[str, BaseServerAddon]]: + """ Return all addons in a given Bundle. + """ + bundle_addons = [] + + library = AddonLibrary().get_instance() + active_versions = await library.get_enabled_addons(bundle_name) + + for active_bundle_name, bundle_dict in active_versions.items(): + for addon in bundle_dict["addons"]: + addon_name_and_version = slugify(f"{addon[0]}-{addon[1]}") + addon = library.initialized_addons.get(addon_name_and_version) + if not addon: + # It's a broken addon + addon = None + + bundle_addons.append((addon_name_and_version, addon)) + + return bundle_addons + + @staticmethod + async def get_variant_addons(variant=None) -> list[tuple[str, BaseServerAddon]]: + if variant is None or variant not in ["production", "staging"]: + variant = "production" + + library = AddonLibrary().get_instance() + + variants_bundle_names = await library.get_variants_bundle_name() + bundle_name = variants_bundle_names.get(variant) + if not bundle_name: + return [] + + addons = await library.get_bundle_addons(bundle_name) + return addons + + @staticmethod + async def get_variants_bundle_name(): + variants_dict = {"production": None, "staging": None} + bundles = await Postgres.fetch( + "SELECT name, is_production, is_staging FROM bundles " + "WHERE is_production = true OR is_staging = true" + ) + for bundle in bundles: + if bundle["is_production"]: + variants_dict["production"] = bundle["name"] + elif bundle["is_staging"]: + variants_dict["staging"] = bundle["name"] + + return variants_dict @classmethod - def unload_addon( - cls, addon_name: str, addon_version: str, reason: dict[str, str] | None = None - ) -> None: - instance = cls.getinstance() - if reason is not None: - instance.broken_addons[(addon_name, addon_version)] = reason - definition = instance.data.get(addon_name) - if definition is None: - return - logging.info("Unloading addon", addon_name, addon_version) - definition.unload_version(addon_version) - - if not definition._versions: - logging.info("Unloading addon", addon_name) - del instance.data[addon_name] + async def initialize_enabled_addons(cls): + instance = cls.get_instance() + enabled_addons_by_bundle = await instance.get_enabled_addons() + + for bundle_name, bundle_dict in enabled_addons_by_bundle.items(): + for addon_name, addon_version in bundle_dict["addons"]: + instance.get_addon(addon_name, addon_version) + + return instance.initialized_addons, instance.broken_addons + + @staticmethod + def get_addon_zip_info(path: str) -> tuple[str, str]: + """Returns the addon name and version from the zip file + + We also perform checks so that the zip is a valid AYON addon. + """ + with zipfile.ZipFile(path, "r") as zip_ref: + names = zip_ref.namelist() + if "package.py" not in names: + raise RuntimeError("Addon package.py not found in zip file") + + if "rezbuild.py" not in names: + logging.warning("Addon rezbuild.py not found in zip file, will use default.") + + if "server/__init__.py" not in names: + raise RuntimeError("Addon __init__.py not found in zip file") + + with zip_ref.open("package.py") as package_manifest: + package_info: Dict[str, str] = {} + exec(package_manifest.read(), package_info, package_info) + addon_name = package_info.get("name") + addon_version = package_info.get("version") + + if not (addon_name and addon_version): + raise RuntimeError("Addon name or version not found in `package.py`") + + return addon_name, addon_version + + @staticmethod + def install_addon_from_zip(addon_name, zip_path: Path | str) -> None: + """ Extract zip and rez install the package. + """ + with tempfile.TemporaryDirectory() as tmpdirname: + with zipfile.ZipFile(zip_path, "r") as zip_ref: + for member in zip_ref.infolist(): + extracted_path = zip_ref.extract(member, tmpdirname) + + # Preserve the file permissions + original_mode = member.external_attr >> 16 + if original_mode: + os.chmod(extracted_path, original_mode) + + rez_package_py = Path(tmpdirname) / "package.py" + rez_build_py = Path(tmpdirname) / "rezbuild.py" + + if not rez_package_py.exists(): + raise RuntimeError("Zip {} is missing the `package.py`.") + + if not rez_build_py.exists(): + default_rez_build_py = Path(__file__).parent / "rezbuild.py" + shutil.copyfile(default_rez_build_py, rez_build_py) + + return RezRepo.install_addon( + addon_name, + rez_package_py + ) @classmethod - def is_broken(cls, addon_name: str, addon_version: str) -> dict[str, Any] | None: - instance = cls.getinstance() - if summary := instance.broken_addons.get((addon_name, addon_version), None): - return summary - return None + async def install_addon( + cls, + event_id: str, + zip_path: str, + addon_name: str, + addon_version: str, + ): + """Unpack the addon from the zip file and install it + + Unpacking is done in a separate thread to avoid blocking the main thread + (unzipping is a synchronous operation and it is also cpu-bound) + + After the addon is unpacked, the event is finalized and the zip file is removed. + """ + + await update_event( + event_id, + description=f"Installing addon {addon_name} {addon_version}", + status="in_progress", + ) + + loop = asyncio.get_event_loop() + isntance = cls.get_instance() + + try: + with ThreadPoolExecutor() as executor: + task = loop.run_in_executor( + executor, + isntance.install_addon_from_zip, + addon_name, + zip_path + ) + rez_package = await asyncio.gather(task) + except Exception as e: + logging.error(f"Error while installing addon: {e}") + log_traceback(e) + await update_event( + event_id, + description=f"Error while installing addon: {e}", + status="failed", + ) + + try: + os.remove(zip_path) + except Exception as e: + logging.error(f"Error while removing zip file: {e}") + log_traceback(e) + + await update_event( + event_id, + description=f"Addon {addon_name} {addon_version} installed.", + status="finished", + ) + + @classmethod + async def install_addon_from_url(cls, event_id: str, url: str) -> None: + """Download the addon zip file from the URL and install it""" + + await update_event( + event_id, + description=f"Downloading addon from URL {url}", + status="in_progress", + ) + + isntance = cls.get_instance() + + # Download the zip file + # we do not use download_file() here because using NamedTemporaryFile + # is much more convenient than manually creating a temporary file + + file_size = 0 + last_time = 0.0 + + i = 0 + with tempfile.NamedTemporaryFile() as temporary_file: + zip_path = temporary_file.name + async with httpx.AsyncClient(timeout=ayonconfig.http_timeout) as client: + async with client.stream("GET", url) as response: + file_size = int(response.headers.get("content-length", 0)) + async with aiofiles.open(zip_path, "wb") as f: + async for chunk in response.aiter_bytes(): + await f.write(chunk) + i += len(chunk) + + if file_size and (time.time() - last_time > 1): + percent = int(i / file_size * 100) + await update_event( + event_id, + progress=int(percent / 2), + store=False, + ) + last_time = time.time() + + addon_name, addon_version = isntance.get_addon_zip_info(zip_path) + isntance.install_addon(event_id, zip_path, addon_name, addon_version) + From c307c02802913986ab5406f1bda8457344d23ceb Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Mon, 29 Jan 2024 17:16:24 +0000 Subject: [PATCH 3/8] Refactor `api` modules to use the new `AddonLibrary` Modified all instances where we accessed the AddonLibrary to ensure we get the same result as previously and that everything works given the new class. --- api/addons/__init__.py | 53 +++++++++------------ api/addons/common.py | 8 ++-- api/addons/delete_addon.py | 46 +----------------- api/addons/install.py | 4 +- api/addons/project_settings.py | 12 ++--- api/addons/site_settings.py | 8 ++-- api/addons/studio_settings.py | 12 ++--- api/bundles/bundles.py | 21 +++++---- api/market/common.py | 10 ++-- api/services/services.py | 4 +- api/settings/legacy_settings.py | 8 +--- api/settings/settings.py | 83 +++++++++++++-------------------- api/system/info.py | 20 +++----- 13 files changed, 99 insertions(+), 190 deletions(-) diff --git a/api/addons/__init__.py b/api/addons/__init__.py index d5d111f5..f0b49fc1 100644 --- a/api/addons/__init__.py +++ b/api/addons/__init__.py @@ -3,7 +3,7 @@ from fastapi import Query, Request, Response from nxtools import logging -from ayon_server.addons import AddonLibrary +from ayon_server.addons import AddonLibrary, RezRepo from ayon_server.addons.models import SourceInfo from ayon_server.api.dependencies import CurrentUser from ayon_server.exceptions import ( @@ -63,56 +63,45 @@ async def list_addons( ) -> AddonList: """List all available addons.""" - base_url = f"{request.url.scheme}://{request.url.netloc}" + # base_url = f"{request.url.scheme}://{request.url.netloc}" result = [] - library = AddonLibrary.getinstance() + rezrepo = RezRepo.get_instance() # maybe some ttl here? - active_versions = await library.get_active_versions() + active_versions = await AddonLibrary.get_enabled_addons() - # TODO: for each version, return the information - # whether it has settings (and don't show the addon in the settings editor if not) - - for _name, definition in library.data.items(): - vers = active_versions.get(definition.name, {}) + for addon_name, addon_dict in rezrepo.packages.items(): + vers = active_versions.get(addon_name, {}) versions = {} is_system = False - for version, addon in definition.versions.items(): - if addon.system: + addon_title = "" + addon_description = "" + + for version_dict in addon_dict.get("versions", []): + ((version_number, addon_rez_package),) = version_dict.items() + versions[version_number] = VersionInfo() + + if getattr(addon_rez_package, "system", False): if not user.is_admin: continue is_system = True - vinf = { - "has_settings": bool(addon.get_settings_model()), - "has_site_settings": bool(addon.get_site_settings_model()), - "frontend_scopes": addon.frontend_scopes, - } - if details: - vinf["client_pyproject"] = await addon.get_client_pyproject() - - source_info = await addon.get_client_source_info(base_url=base_url) - if source_info is None: - pass - - elif not all(isinstance(x, SourceInfo) for x in source_info): - logging.error(f"Invalid source info for {addon.name} {version}") - source_info = [x for x in source_info if isinstance(x, SourceInfo)] - vinf["client_source_info"] = source_info + if not addon_title: + addon_title = getattr(addon_rez_package, "nice_name", "") - vinf["services"] = addon.services or None - versions[version] = VersionInfo(**vinf) + if not addon_description: + addon_description = getattr(addon_rez_package, "description", "") if not versions: continue result.append( AddonListItem( - name=definition.name, - title=definition.friendly_name, + name=addon_name, + title=addon_title if addon_title else addon_name, versions=versions, - description=definition.__doc__ or "", + description=addon_description if addon_description else "", production_version=vers.get("production"), system=is_system or None, staging_version=vers.get("staging"), diff --git a/api/addons/common.py b/api/addons/common.py index 4b9fc130..ed576c71 100644 --- a/api/addons/common.py +++ b/api/addons/common.py @@ -22,7 +22,7 @@ async def remove_override( variant: str = "production", project_name: str | None = None, ): - if (addon := AddonLibrary.addon(addon_name, addon_version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, addon_version)) is None: raise NotFoundException(f"Addon {addon_name} {addon_version} not found") # TODO: ensure the path is not a part of a group @@ -62,7 +62,7 @@ async def pin_override( variant: str = "production", project_name: str | None = None, ): - if (addon := AddonLibrary.addon(addon_name, addon_version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, addon_version)) is None: raise NotFoundException(f"Addon {addon_name} {addon_version} not found") if project_name: @@ -136,7 +136,7 @@ async def remove_site_override( user_name: str, path: list[str], ): - if (addon := AddonLibrary.addon(addon_name, addon_version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, addon_version)) is None: raise NotFoundException(f"Addon {addon_name} {addon_version} not found") overrides = await addon.get_project_site_overrides(project_name, user_name, site_id) @@ -171,7 +171,7 @@ async def pin_site_override( user_name: str, path: list[str], ): - if (addon := AddonLibrary.addon(addon_name, addon_version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, addon_version)) is None: raise NotFoundException(f"Addon {addon_name} {addon_version} not found") overrides = await addon.get_project_site_overrides(project_name, user_name, site_id) diff --git a/api/addons/delete_addon.py b/api/addons/delete_addon.py index b472c692..d2f2f77d 100644 --- a/api/addons/delete_addon.py +++ b/api/addons/delete_addon.py @@ -8,44 +8,9 @@ from ayon_server.api.responses import EmptyResponse from ayon_server.exceptions import AyonException, ForbiddenException, NotFoundException -# from ayon_server.lib.postgres import Postgres from .router import router -async def delete_addon_directory(addon_name: str, addon_version: str | None = None): - """Delete an addon or addon version""" - - addon_definition = AddonLibrary.get(addon_name) - if addon_definition is None: - raise NotFoundException("Addon not found") - - addon_dir = addon_definition.addon_dir - is_empty = not os.listdir(addon_dir) - - if not is_empty and addon_version is not None: - addon = addon_definition.versions.get(addon_version) - if addon is None: - raise NotFoundException("Addon version not found") - - version_dir = addon.addon_dir - try: - await aioshutil.rmtree(version_dir) - except Exception as e: - raise AyonException( - f"Failed to delete {addon_name} {addon_version} directory: {e}" - ) - AddonLibrary.unload_addon(addon_name, addon_version, {"error": "Addon deleted"}) - - is_empty = not os.listdir(addon_dir) - - if (addon_version is None) or is_empty: - try: - await aioshutil.rmtree(addon_dir) - except Exception as e: - raise AyonException(f"Failed to delete {addon_name} directory: {e}") - AddonLibrary.data.pop(addon_name, None) - - @router.delete("/{addon_name}", tags=["Addons"]) async def delete_addon( user: CurrentUser, @@ -57,11 +22,7 @@ async def delete_addon( if not user.is_admin: raise ForbiddenException("Only admins can delete addons") - await delete_addon_directory(addon_name) - - if purge: - pass - # TODO: implement purge + AddonLibrary.delete_addon_from_server(addon_name) @router.delete("/{addon_name}/{addon_version}", tags=["Addons"]) @@ -76,8 +37,5 @@ async def delete_addon_version( if not user.is_admin: raise ForbiddenException("Only admins can delete addons") - await delete_addon_directory(addon_name, addon_version) + AddonLibrary.delete_addon_from_server(addon_name, addon_version) - if purge: - pass - # TODO: implement purge diff --git a/api/addons/install.py b/api/addons/install.py index e6160e96..d0414242 100644 --- a/api/addons/install.py +++ b/api/addons/install.py @@ -11,7 +11,7 @@ from ayon_server.events import dispatch_event, update_event from ayon_server.exceptions import ForbiddenException from ayon_server.installer import background_installer -from ayon_server.installer.addons import get_addon_zip_info +from ayon_server.addons import AddonLibrary from ayon_server.lib.postgres import Postgres from ayon_server.types import Field, OPModel @@ -97,7 +97,7 @@ async def upload_addon_zip_file( # Get addon name and version from the zip file - addon_name, addon_version = get_addon_zip_info(temp_path) + addon_name, addon_version = AddonLibrary.get_addon_zip_info(temp_path) # We don't create the event before we know that the zip file is valid # and contains an addon. If it doesn't, an exception is raised before diff --git a/api/addons/project_settings.py b/api/addons/project_settings.py index 6e6a047d..af0f9f1d 100644 --- a/api/addons/project_settings.py +++ b/api/addons/project_settings.py @@ -40,8 +40,7 @@ async def get_addon_project_settings_schema( site: str | None = Query(None, regex="^[a-z0-9-]+$"), ) -> dict[str, Any]: """Return the JSON schema of the addon settings.""" - - if (addon := AddonLibrary.addon(addon_name, version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, version)) is None: raise NotFoundException(f"Addon {addon_name} {version} not found") model = addon.get_settings_model() @@ -77,7 +76,7 @@ async def get_addon_project_settings( variant: str = Query("production"), site: str | None = Query(None, regex="^[a-z0-9-]+$"), ) -> dict[str, Any]: - if (addon := AddonLibrary.addon(addon_name, version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, version)) is None: raise NotFoundException(f"Addon {addon_name} {version} not found") if site: @@ -101,7 +100,7 @@ async def get_addon_project_overrides( variant: str = Query("production"), site: str | None = Query(None, regex="^[a-z0-9-]+$"), ): - addon = AddonLibrary.addon(addon_name, version) + addon = AddonLibrary.get_addon(addon_name, version) studio_settings = await addon.get_studio_settings(variant=variant) if studio_settings is None: return {} @@ -142,8 +141,7 @@ async def set_addon_project_settings( site: str | None = Query(None, regex="^[a-z0-9-]+$"), ) -> EmptyResponse: """Set the studio overrides of the given addon.""" - - addon = AddonLibrary.addon(addon_name, version) + addon = AddonLibrary.get_addon(addon_name, version) model = addon.get_settings_model() if model is None: raise BadRequestException(f"Addon {addon_name} has no settings") @@ -242,7 +240,7 @@ async def delete_addon_project_overrides( site: str | None = Query(None, regex="^[a-z0-9-]+$"), ): # Ensure the addon and the project exist - _ = AddonLibrary.addon(addon_name, version) + _ = AddonLibrary.get_addon(addon_name, version) _ = await ProjectEntity.load(project_name) if not site: diff --git a/api/addons/site_settings.py b/api/addons/site_settings.py index ceaf0172..1d9f5909 100644 --- a/api/addons/site_settings.py +++ b/api/addons/site_settings.py @@ -21,8 +21,7 @@ async def get_addon_site_settings_schema( user: CurrentUser, ) -> dict[str, Any]: """Return the JSON schema of the addon site settings.""" - - if (addon := AddonLibrary.addon(addon_name, version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, version)) is None: raise NotFoundException(f"Addon {addon_name} {version} not found") model = addon.get_site_settings_model() @@ -54,8 +53,7 @@ async def get_addon_site_settings( site: str = Query(...), ) -> dict[str, Any]: """Return the JSON schema of the addon site settings.""" - - if (addon := AddonLibrary.addon(addon_name, version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, version)) is None: raise NotFoundException(f"Addon {addon_name} {version} not found") model = addon.get_site_settings_model() @@ -84,7 +82,7 @@ async def set_addon_site_settings( user: CurrentUser, site: str = Query(..., title="Site ID", regex="^[a-z0-9-]+$"), ) -> EmptyResponse: - if (addon := AddonLibrary.addon(addon_name, version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, version)) is None: raise NotFoundException(f"Addon {addon_name} {version} not found") model = addon.get_site_settings_model() diff --git a/api/addons/studio_settings.py b/api/addons/studio_settings.py index 3783c19d..e59d5d97 100644 --- a/api/addons/studio_settings.py +++ b/api/addons/studio_settings.py @@ -30,8 +30,7 @@ async def get_addon_settings_schema( variant: str = Query("production"), ) -> dict[str, Any]: """Return the JSON schema of the addon settings.""" - - if (addon := AddonLibrary.addon(addon_name, addon_version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, addon_version)) is None: raise NotFoundException(f"Addon {addon_name} {addon_version} not found") model = addon.get_settings_model() @@ -63,8 +62,7 @@ async def get_addon_studio_settings( variant: str = Query("production"), ) -> dict[str, Any]: """Return the settings (including studio overrides) of the given addon.""" - - if (addon := AddonLibrary.addon(addon_name, addon_version)) is None: + if (addon := AddonLibrary.get_addon(addon_name, addon_version)) is None: raise NotFoundException(f"Addon {addon_name} {addon_version} not found") settings = await addon.get_studio_settings(variant=variant) @@ -86,7 +84,7 @@ async def set_addon_studio_settings( if not user.is_manager: raise ForbiddenException - addon = AddonLibrary.addon(addon_name, addon_version) + addon = AddonLibrary.get_addon(addon_name, addon_version) original = await addon.get_studio_settings(variant=variant) existing = await addon.get_studio_overrides(variant=variant) model = addon.get_settings_model() @@ -143,7 +141,7 @@ async def get_addon_studio_overrides( if not user.is_manager: raise ForbiddenException - addon = AddonLibrary.addon(addon_name, addon_version) + addon = AddonLibrary.get_addon(addon_name, addon_version) settings = await addon.get_studio_settings(variant=variant) if settings is None: return {} @@ -164,7 +162,7 @@ async def delete_addon_studio_overrides( raise ForbiddenException # Ensure addon exists - _ = AddonLibrary.addon(addon_name, addon_version) + _ = get_addon(addon_name, addon_version) await Postgres.execute( """ diff --git a/api/bundles/bundles.py b/api/bundles/bundles.py index 0593b04a..0563cf96 100644 --- a/api/bundles/bundles.py +++ b/api/bundles/bundles.py @@ -245,16 +245,17 @@ async def create_new_bundle( # a bundle with an addon that does not exist if not addon_version: continue - _ = AddonLibrary.addon(addon_name, addon_version) - - for system_addon_name, addon_definition in AddonLibrary.items(): - if addon_definition.is_system: - if system_addon_name not in bundle.addons: - logging.debug( - f"Adding missing system addon {system_addon_name} to bundle {bundle.name}" - ) - if addon_definition.latest: - bundle.addons[system_addon_name] = addon_definition.latest.version + _ = AddonLibrary.get_addon(addon_name, addon_version) + + # TODO reimplement so it looks at the package for this info + # for system_addon_name, addon_definition in rezrepo.packages(): + # if addon_definition.is_system: + # if system_addon_name not in bundle.addons: + # logging.debug( + # f"Adding missing system addon {system_addon_name} to bundle {bundle.name}" + # ) + # if addon_definition.latest: + # bundle.addons[system_addon_name] = addon_definition.latest.version await create_bundle(bundle, user, x_sender) diff --git a/api/market/common.py b/api/market/common.py index 72d77d1b..0093e05f 100644 --- a/api/market/common.py +++ b/api/market/common.py @@ -2,7 +2,7 @@ import httpx -from ayon_server.addons.library import AddonLibrary +from ayon_server.addons import AddonLibrary from ayon_server.config import ayonconfig from ayon_server.exceptions import ForbiddenException from ayon_server.helpers.cloud import get_cloud_api_headers @@ -41,12 +41,8 @@ async def get_local_latest_addon_versions() -> dict[str, str]: Used to check if there are new versions available """ - result = {} - for addon_name, definition in AddonLibrary.items(): - if not definition.latest: - continue - result[addon_name] = definition.latest.version - return result + return AddonLibrary.get_addons_latest_versions() + async def get_local_production_addon_versions() -> dict[str, str]: diff --git a/api/services/services.py b/api/services/services.py index 9dec90b5..76139336 100644 --- a/api/services/services.py +++ b/api/services/services.py @@ -78,8 +78,8 @@ async def spawn_service( if not user.is_admin: raise ForbiddenException("Only admins can spawn services") - library = AddonLibrary.getinstance() - addon = library.addon(payload.addon_name, payload.addon_version) + addon = AddonLibrary.get_addon(payload.addon_name, payload.addon_version) + if payload.service not in addon.services: # TODO: be more verbose raise NotFoundException("This addon does not have this service") diff --git a/api/settings/legacy_settings.py b/api/settings/legacy_settings.py index 1f514c9e..30802391 100644 --- a/api/settings/legacy_settings.py +++ b/api/settings/legacy_settings.py @@ -42,9 +42,7 @@ async def get_all_addons_settings( ) -> AddonSettingsResponse: """Return all addon settings for the project.""" - library = AddonLibrary.getinstance() - - active_versions = await library.get_active_versions() + active_versions = await AddonLibrary.get_enabled_addons() result: dict[str, dict[str, Any]] = {} versions: dict[str, str] = {} @@ -108,9 +106,7 @@ async def get_all_site_settings( return the default settings provided by the model. """ - library = AddonLibrary.getinstance() - - active_versions = await library.get_active_versions() + active_versions = await AddonLibrary.get_enabled_addons() result: dict[str, dict[str, Any]] = {} versions: dict[str, str] = {} diff --git a/api/settings/settings.py b/api/settings/settings.py index 7c1cb8ae..01117cce 100644 --- a/api/settings/settings.py +++ b/api/settings/settings.py @@ -2,12 +2,10 @@ from typing import Any from fastapi import Query -from nxtools import log_traceback, logging +from nxtools import log_traceback, logging, slugify from ayon_server.addons import AddonLibrary from ayon_server.api.dependencies import CurrentUser -from ayon_server.exceptions import NotFoundException -from ayon_server.lib.postgres import Postgres from ayon_server.settings import BaseSettingsModel from ayon_server.types import NAME_REGEX, Field, OPModel @@ -70,73 +68,57 @@ async def get_all_settings( ), variant: str = Query("production"), summary: bool = Query(False, title="Summary", description="Summary mode"), -) -> AllSettingsResponseModel: - if variant not in ("production", "staging"): - query = [ - """ - SELECT name, is_production, is_staging, data->'addons' as addons - FROM bundles WHERE name = $1 - """, - variant, - ] - elif bundle_name is None: - query = [ - f""" - SELECT name, is_production, is_staging, data->'addons' as addons - FROM bundles WHERE is_{variant} IS TRUE - """ - ] +) -> AllSettingsResponseModel | None: + + addons_settings = [] + library = AddonLibrary.get_instance() + + if bundle_name: + addons = await library.get_bundle_addons(bundle_name) else: - query = [ - """ - SELECT name, is_production, is_staging, data->'addons' as addons - FROM bundles WHERE name = $1 - """, - bundle_name, - ] - - brow = await Postgres.fetch(*query) - if not brow: - raise NotFoundException(status_code=404, detail="Bundle not found") - - bundle_name = brow[0]["name"] - addons = brow[0]["addons"] - - addon_result = [] - for addon_name, addon_version in addons.items(): - if addon_version is None: - continue + variants_bundle_names = await library.get_variants_bundle_name() + bundle_name = variants_bundle_names.get(variant) + addons = await library.get_bundle_addons(bundle_name) - try: - addon = AddonLibrary.addon(addon_name, addon_version) - except NotFoundException: + for addon in addons: + addon_name_and_version, addon = addon + + if not addon: + addon_name, addon_version = addon_name_and_version.split("-", 1) logging.warning( f"Addon {addon_name} {addon_version} " - f"declared in {bundle_name} not found" + f"declared in {bundle_name} not initialized." ) - broken_reason = AddonLibrary.is_broken(addon_name, addon_version) + is_broken = library.broken_addons.get(addon_name_and_version, None) - addon_result.append( + broken_reason = {"error": "Addon is not initialized"} + + if is_broken: + broken_reason["traceback"] = str(is_broken) + + addons_settings.append( AddonSettingsItemModel( name=addon_name, title=addon_name, version=addon_version, settings={}, site_settings=None, - is_broken=bool(broken_reason), + is_broken=bool(is_broken), reason=broken_reason, ) ) continue + addon_name = addon.name + addon_version = addon.version # Determine which scopes addon has settings for - model = addon.get_settings_model() has_settings = False has_project_settings = False has_project_site_settings = False has_site_settings = bool(addon.site_settings_model) + if model: has_project_settings = False for field_name, field in model.__fields__.items(): @@ -149,7 +131,6 @@ async def get_all_settings( has_settings = True # Load settings for the addon - site_settings = None settings: BaseSettingsModel | None = None @@ -179,7 +160,7 @@ async def get_all_settings( except Exception: log_traceback(f"Unable to load {addon_name} {addon_version} settings") - addon_result.append( + addons_settings.append( AddonSettingsItemModel( name=addon_name, title=addon_name, @@ -197,7 +178,7 @@ async def get_all_settings( # Add addon to the result - addon_result.append( + addons_settings.append( AddonSettingsItemModel( name=addon_name, title=addon.title if addon.title else addon_name, @@ -221,9 +202,9 @@ async def get_all_settings( ) ) - addon_result.sort(key=lambda x: x.title.lower()) + addons_settings.sort(key=lambda x: x.title.lower()) return AllSettingsResponseModel( bundle_name=bundle_name, - addons=addon_result, + addons=addons_settings, ) diff --git a/api/system/info.py b/api/system/info.py index cbbfa675..9c0f917e 100644 --- a/api/system/info.py +++ b/api/system/info.py @@ -88,21 +88,15 @@ async def get_sso_options(request: Request) -> list[SSOOption]: base_url = "http://localhost:5000" result = [] - library = AddonLibrary.getinstance() - active_versions = await library.get_active_versions() - for _name, definition in library.data.items(): - try: - vers = active_versions.get(definition.name, {}) - except ValueError: - continue - production_version = vers.get("production", None) - if not production_version: - continue + library = AddonLibrary().get_instance() + production_addons = await library.get_variant_addons("production") - try: - addon = definition[production_version] - except KeyError: + for addon in production_addons: + addon_name_and_version, addon = addon + + if not addon: + # Broken Addon continue options = await addon.get_sso_options(base_url) From 2f0d86ecb40784802fd411ede2ff713754791325 Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Mon, 29 Jan 2024 15:43:22 +0000 Subject: [PATCH 4/8] Refactor `ayon_server` modules to use the new `AddonLibrary` and `RezRepo` Modified all modules where the interface with `AddonLibrary` changed or where it was now necessary to use the `RezRepo`. --- ayon_server/addons/__init__.py | 4 +- ayon_server/addons/addon.py | 26 ++-- ayon_server/api/server.py | 214 ++++++++++++++---------------- ayon_server/installer/__init__.py | 6 +- ayon_server/installer/addons.py | 183 ------------------------- ayon_server/metrics/bundles.py | 16 ++- ayon_server/metrics/settings.py | 6 +- 7 files changed, 139 insertions(+), 316 deletions(-) delete mode 100644 ayon_server/installer/addons.py diff --git a/ayon_server/addons/__init__.py b/ayon_server/addons/__init__.py index 2bbb945b..1d9ad624 100644 --- a/ayon_server/addons/__init__.py +++ b/ayon_server/addons/__init__.py @@ -1,6 +1,8 @@ -__all__ = ["AddonLibrary", "BaseServerAddon", "SSOOption"] +__all__ = ["AddonLibrary", "BaseServerAddon", "SSOOption", "RezRepo"] + from ayon_server.addons.addon import BaseServerAddon from ayon_server.addons.library import AddonLibrary from ayon_server.addons.models import SSOOption +from ayon_server.addons.rezrepo import RezRepo diff --git a/ayon_server/addons/addon.py b/ayon_server/addons/addon.py index fe91f8c4..4ded4445 100644 --- a/ayon_server/addons/addon.py +++ b/ayon_server/addons/addon.py @@ -15,7 +15,7 @@ from ayon_server.settings import BaseSettingsModel, apply_overrides if TYPE_CHECKING: - from ayon_server.addons.definition import ServerAddonDefinition + from rez.packages import Package class BaseServerAddon: @@ -23,7 +23,7 @@ class BaseServerAddon: version: str title: str | None = None app_host_name: str | None = None - definition: "ServerAddonDefinition" + rez_package: "Package" endpoints: list[dict[str, Any]] settings_model: Type[BaseSettingsModel] | None = None site_settings_model: Type[BaseSettingsModel] | None = None @@ -31,9 +31,9 @@ class BaseServerAddon: services: dict[str, Any] = {} system: bool = False # Hide settings for non-admins and make the addon mandatory - def __init__(self, definition: "ServerAddonDefinition", addon_dir: str): + def __init__(self, rez_package: "Package", addon_dir: str): assert self.name and self.version - self.definition = definition + self.rez_package = rez_package self.addon_dir = addon_dir self.endpoints = [] self.restart_requested = False @@ -41,12 +41,20 @@ def __init__(self, definition: "ServerAddonDefinition", addon_dir: str): self.initialize() def __repr__(self) -> str: - return f"" + return f"" + + @property + def definition(self) -> "Package": + return self.rez_package @property def friendly_name(self) -> str: """Return the friendly name of the addon.""" - return f"{self.definition.friendly_name} {self.version}" + try: + friendly_name = self.rez_package.friendly_name + except AttributeError: + friendly_name = self.name.capitalize() + return f"{friendly_name} {self.version}" async def is_production(self) -> bool: """Return True if the addon is in production bundle.""" @@ -224,7 +232,7 @@ async def get_studio_overrides(self, variant: str = "production") -> dict[str, A WHERE addon_name = $1 AND addon_version = $2 AND variant = $3 """ - res = await Postgres.fetch(query, self.definition.name, self.version, variant) + res = await Postgres.fetch(query, self.rez_package.name, self.version, variant) if res: return dict(res[0]["data"]) return {} @@ -243,7 +251,7 @@ async def get_project_overrides( try: res = await Postgres.fetch( - query, self.definition.name, self.version, variant + query, self.rez_package.name, self.version, variant ) except Postgres.UndefinedTableError: raise NotFoundException(f"Project {project_name} does not exists") from None @@ -265,7 +273,7 @@ async def get_project_site_overrides( WHERE addon_name = $1 AND addon_version = $2 AND user_name = $3 AND site_id = $4 """, - self.definition.name, + self.rez_package.name, self.version, user_name, site_id, diff --git a/ayon_server/api/server.py b/ayon_server/api/server.py index 1b2af82c..234e4dcb 100644 --- a/ayon_server/api/server.py +++ b/ayon_server/api/server.py @@ -13,7 +13,7 @@ from nxtools import log_to_file, log_traceback, logging, slugify from ayon_server.access.access_groups import AccessGroups -from ayon_server.addons import AddonLibrary +from ayon_server.addons import AddonLibrary, RezRepo from ayon_server.api.messaging import Messaging from ayon_server.api.metadata import app_meta, tags_meta from ayon_server.api.responses import ErrorResponse @@ -302,70 +302,57 @@ def init_api(target_app: fastapi.FastAPI, plugin_dir: str = "api") -> None: route.operation_id = route.name -def init_addon_endpoints(target_app: fastapi.FastAPI) -> None: - library = AddonLibrary.getinstance() - for addon_name, addon_definition in library.items(): - for version in addon_definition.versions: - addon = addon_definition.versions[version] - - if hasattr(addon, "ws"): - target_app.add_api_websocket_route( - f"/api/addons/{addon_name}/{version}/ws", - addon.ws, - name=f"{addon_name}_{version}_ws", - ) +def init_addon_endpoints(target_app: fastapi.FastAPI, addons: list) -> None: + for addon_name, addon in addons.items(): + for endpoint in addon.endpoints: + path = endpoint["path"].lstrip("/") + first_element = path.split("/")[0] + # TODO: site settings? other routes? + if first_element in ["settings", "schema", "overrides"]: + logging.error(f"Unable to assing path to endpoint: {path}") + continue - for endpoint in addon.endpoints: - path = endpoint["path"].lstrip("/") - first_element = path.split("/")[0] - # TODO: site settings? other routes? - if first_element in ["settings", "schema", "overrides"]: - logging.error(f"Unable to assing path to endpoint: {path}") - continue - - path = f"/api/addons/{addon_name}/{version}/{path}" - target_app.add_api_route( - path, - endpoint["handler"], - methods=[endpoint["method"]], - name=endpoint["name"], - tags=[f"{addon_definition.friendly_name} {version}"], - operation_id=slugify( - f"{addon_name}_{version}_{endpoint['name']}", - separator="_", - ), - ) + path = f"/api/addons/{addon_name}/{addon.version}/{path}" + target_app.add_api_route( + path, + endpoint["handler"], + methods=[endpoint["method"]], + name=endpoint["name"], + tags=[f"{addon.title} {addon>addon.version}"], + operation_id=slugify( + f"{addon.name}_{addon.version}_{endpoint['name']}", + separator="_", + ), + ) -def init_addon_static(target_app: fastapi.FastAPI) -> None: +def init_addon_static(target_app: fastapi.FastAPI, addons: list) -> None: """Serve static files for addon frontends.""" - for addon_name, addon_definition in AddonLibrary.items(): - for version in addon_definition.versions: - addon = addon_definition.versions[version] - static_dirs = [] - if (fedir := addon.get_frontend_dir()) is not None: - static_dirs.append("frontend") - target_app.mount( - f"/addons/{addon_name}/{version}/frontend/", - StaticFiles(directory=fedir, html=True), - ) - if (resdir := addon.get_public_dir()) is not None: - static_dirs.append("public") - target_app.mount( - f"/addons/{addon_name}/{version}/public/", - StaticFiles(directory=resdir), - ) - if (resdir := addon.get_private_dir()) is not None: - static_dirs.append("private") - target_app.mount( - f"/addons/{addon_name}/{version}/private/", - AuthStaticFiles(directory=resdir), - ) + for addon_name, addon in addons.items(): + static_dirs = [] + if (fedir := addon.get_frontend_dir()) is not None: + static_dirs.append("frontend") + target_app.mount( + f"/addons/{addon.name}/{addon.version}/frontend/", + StaticFiles(directory=fedir, html=True), + ) + if (resdir := addon.get_public_dir()) is not None: + static_dirs.append("public") + target_app.mount( + f"/addons/{addon.name}/{addon.version}/public/", + StaticFiles(directory=resdir), + ) + if (resdir := addon.get_private_dir()) is not None: + static_dirs.append("private") + target_app.mount( + f"/addons/{addon.name}/{addon.version}/private/", + AuthStaticFiles(directory=resdir), + ) - if static_dirs: - logging.debug( - f"Initialized static dirs for {addon_name}:{version}: {', '.join(static_dirs)}" - ) + if static_dirs: + logging.debug( + f"Initialized static dirs for {addon.name}:{addon.version}: {', '.join(static_dirs)}" + ) def init_frontend(target_app: fastapi.FastAPI, frontend_dir: str) -> None: @@ -427,69 +414,71 @@ async def startup_event() -> None: messaging.start() # Initialize addons - start_event = await dispatch_event("server.started", finished=False) - library = AddonLibrary.getinstance() - addon_records = list(AddonLibrary.items()) - if library.restart_requested: - logging.warning("Restart requested, skipping addon setup") + rezrepo = RezRepo.get_instance() + + # In case we installed the `ayon_server` + if rezrepo.restart_requested: await dispatch_event( "server.restart_requested", description="Server restart requested during addon initialization", ) - return + initialized_addons, broken_addons = await AddonLibrary.initialize_enabled_addons() restart_requested = False bad_addons = {} - for addon_name, addon in addon_records: - for version in addon.versions.values(): - try: - if inspect.iscoroutinefunction(version.pre_setup): - # Since setup may, but does not have to be async, we need to - # silence mypy here. - await version.pre_setup() # type: ignore - else: - version.pre_setup() - if (not restart_requested) and version.restart_requested: - logging.warning( - f"Restart requested during addon {addon_name} pre-setup." - ) - restart_requested = True - except Exception as e: - log_traceback(f"Error during {addon_name} {version.version} pre-setup") - reason = { - "error": str(e), - "traceback": traceback.format_exc(), - } - bad_addons[(addon_name, version.version)] = reason - - for addon_name, addon in addon_records: - for version in addon.versions.values(): - try: - if inspect.iscoroutinefunction(version.setup): - await version.setup() - else: - version.setup() - if (not restart_requested) and version.restart_requested: - logging.warning( - f"Restart requested during addon {addon_name} setup." - ) - restart_requested = True - except Exception as e: - log_traceback(f"Error during {addon_name} {version.version} setup") - reason = { - "error": str(e), - "traceback": traceback.format_exc(), - } - bad_addons[(addon_name, version.version)] = reason + + async def _run_addon_method(addon, method_name): + method_object = getattr(addon, method_name, False) + + try: + if inspect.iscoroutinefunction(method_object): + # Since setup may, but does not have to be async, we need to + # silence mypy here. + await method_object() # type: ignore + else: + method_object() + except Exception as e: + log_traceback(f"Error during {addon} pre-setup") + reason = { + "error": str(e), + "traceback": traceback.format_exc(), + } + return (False, reason) + + return (True, None) + + # Perform Addon's `pre_setup` method + for addon_name, addon_class in initialized_addons.items(): + ran, result = await _run_addon_method(addon_class, "pre_setup") + + if not ran: + bad_addons[(addon_class.name, addon_class.version)] = result + + if (not restart_requested) and addon_class.restart_requested: + logging.warning( + f"Restart requested during addon {addon_name}-{addon_class.version} setup." + ) + restart_requested = True + + # Perform Addon's `pre_setup` method + for addon_name, addon_class in initialized_addons.items(): + ran, result = await _run_addon_method(addon_class, "setup") + + if not ran: + bad_addons[(addon_class.name, addon_class.version)] = result + + if (not restart_requested) and addon_class.restart_requested: + logging.warning( + f"Restart requested during addon {addon_name}-{addon_class.version} setup." + ) + restart_requested = True for _addon_name, _addon_version in bad_addons: logging.error( - f"Addon {_addon_name} {_addon_version} failed to initialize. Unloading." + f"Addon {_addon_name} {_addon_version} failed to initialize." ) - reason = bad_addons[(_addon_name, _addon_version)] - library.unload_addon(_addon_name, _addon_version, reason=reason) if restart_requested: await dispatch_event( @@ -498,10 +487,10 @@ async def startup_event() -> None: ) else: # Initialize endpoints for active addons - init_addon_endpoints(app) + init_addon_endpoints(app, initialized_addons) # Addon static dirs must stay exactly here - init_addon_static(app) + init_addon_static(app, initialized_addons) # Frontend must be initialized last (since it is mounted to /) init_frontend(app, ayonconfig.frontend_dir) @@ -524,3 +513,4 @@ async def shutdown_event() -> None: await messaging.shutdown() await Postgres.shutdown() logging.info("Server stopped", handlers=None) + diff --git a/ayon_server/installer/__init__.py b/ayon_server/installer/__init__.py index 6145b3e8..d603bb86 100644 --- a/ayon_server/installer/__init__.py +++ b/ayon_server/installer/__init__.py @@ -4,7 +4,7 @@ from ayon_server.background.background_worker import BackgroundWorker from ayon_server.events import update_event -from ayon_server.installer.addons import install_addon_from_url, unpack_addon +from ayon_server.addons import AddonLibrary from ayon_server.installer.dependency_packages import download_dependency_package from ayon_server.installer.installers import download_installer from ayon_server.lib.postgres import Postgres @@ -48,7 +48,7 @@ async def process_event(self, event_id: str): logging.info(f"Background installer: processing {topic} event: {event_id}") if topic == "addon.install": - await unpack_addon( + await AddonLibrary.install_addon( event_id, summary["zip_path"], summary["addon_name"], @@ -56,7 +56,7 @@ async def process_event(self, event_id: str): ) elif topic == "addon.install_from_url": - await install_addon_from_url(event_id, summary["url"]) + await AddonLibrary.install_addon_from_url(event_id, summary["url"]) elif topic == "dependency_package.install_from_url": await download_dependency_package(event_id, summary["url"]) diff --git a/ayon_server/installer/addons.py b/ayon_server/installer/addons.py deleted file mode 100644 index ef26e339..00000000 --- a/ayon_server/installer/addons.py +++ /dev/null @@ -1,183 +0,0 @@ -import asyncio -import json -import os -import shutil -import tempfile -import time -import zipfile -from concurrent.futures import ThreadPoolExecutor - -import aiofiles -import httpx -from nxtools import logging - -from ayon_server.config import ayonconfig -from ayon_server.events import update_event - - -def get_addon_zip_info(path: str) -> tuple[str, str]: - """Returns the addon name and version from the zip file""" - with zipfile.ZipFile(path, "r") as zip_ref: - names = zip_ref.namelist() - if "manifest.json" not in names: - raise RuntimeError("Addon manifest not found in zip file") - - if "addon/__init__.py" not in names: - raise RuntimeError("Addon __init__.py not found in zip file") - - with zip_ref.open("manifest.json") as manifest_file: - manifest = json.load(manifest_file) - - addon_name = manifest.get("addon_name") - addon_version = manifest.get("addon_version") - - if not (addon_name and addon_version): - raise RuntimeError("Addon name or version not found in manifest") - return addon_name, addon_version - - -def unpack_addon_sync(zip_path: str, addon_name: str, addon_version) -> None: - addon_root_dir = ayonconfig.addons_dir - os.makedirs(addon_root_dir, exist_ok=True) - target_dir = os.path.join(addon_root_dir, addon_name, addon_version) - - with tempfile.TemporaryDirectory(dir=addon_root_dir) as tmpdirname: - with zipfile.ZipFile(zip_path, "r") as zip_ref: - for member in zip_ref.infolist(): - extracted_path = zip_ref.extract(member, tmpdirname) - - # Preserve the file permissions - original_mode = member.external_attr >> 16 - if original_mode: - os.chmod(extracted_path, original_mode) - - if os.path.isdir(target_dir): - logging.info(f"Removing existing addon {addon_name} {addon_version}") - shutil.rmtree(target_dir) - - # move the extracted files to the target directory - shutil.move(os.path.join(tmpdirname, "addon"), target_dir) - - -async def unpack_addon( - event_id: str, - zip_path: str, - addon_name: str, - addon_version: str, -): - """Unpack the addon from the zip file and install it - - Unpacking is done in a separate thread to avoid blocking the main thread - (unzipping is a synchronous operation and it is also cpu-bound) - - After the addon is unpacked, the event is finalized and the zip file is removed. - """ - - await update_event( - event_id, - description=f"Unpacking addon {addon_name} {addon_version}", - status="in_progress", - ) - - loop = asyncio.get_event_loop() - - try: - with ThreadPoolExecutor() as executor: - task = loop.run_in_executor( - executor, - unpack_addon_sync, - zip_path, - addon_name, - addon_version, - ) - await asyncio.gather(task) - except Exception as e: - logging.error(f"Error while unpacking addon: {e}") - await update_event( - event_id, - description=f"Error while unpacking addon: {e}", - status="failed", - ) - - try: - os.remove(zip_path) - except Exception as e: - logging.error(f"Error while removing zip file: {e}") - - await update_event( - event_id, - description=f"Addon {addon_name} {addon_version} installed", - status="finished", - ) - - -async def install_addon_from_url(event_id: str, url: str) -> None: - """Download the addon zip file from the URL and install it""" - - await update_event( - event_id, - description=f"Downloading addon from URL {url}", - status="in_progress", - ) - - # Download the zip file - # we do not use download_file() here because using NamedTemporaryFile - # is much more convenient than manually creating a temporary file - - file_size = 0 - last_time = 0.0 - - i = 0 - with tempfile.NamedTemporaryFile(dir=ayonconfig.addons_dir) as temporary_file: - zip_path = temporary_file.name - async with httpx.AsyncClient(timeout=ayonconfig.http_timeout) as client: - async with client.stream("GET", url) as response: - file_size = int(response.headers.get("content-length", 0)) - async with aiofiles.open(zip_path, "wb") as f: - async for chunk in response.aiter_bytes(): - await f.write(chunk) - i += len(chunk) - - if file_size and (time.time() - last_time > 1): - percent = int(i / file_size * 100) - await update_event( - event_id, - progress=int(percent / 2), - store=False, - ) - last_time = time.time() - - # Get the addon name and version from the zip file - - addon_name, addon_version = get_addon_zip_info(zip_path) - await update_event( - event_id, - description=f"Installing addon {addon_name} {addon_version}", - status="in_progress", - summary={ - "addon_name": addon_name, - "addon_version": addon_version, - "url": url, - }, - progress=50, - ) - - # Unpack the addon - - loop = asyncio.get_event_loop() - - with ThreadPoolExecutor() as executor: - task = loop.run_in_executor( - executor, - unpack_addon_sync, - zip_path, - addon_name, - addon_version, - ) - await asyncio.gather(task) - - await update_event( - event_id, - description=f"Addon {addon_name} {addon_version} installed", - status="finished", - ) diff --git a/ayon_server/metrics/bundles.py b/ayon_server/metrics/bundles.py index 92480c98..23b5d7a7 100644 --- a/ayon_server/metrics/bundles.py +++ b/ayon_server/metrics/bundles.py @@ -1,4 +1,4 @@ -from ayon_server.addons.library import AddonLibrary +from ayon_server.addons import RezRepo from ayon_server.lib.postgres import Postgres from ayon_server.types import Field, OPModel @@ -50,8 +50,12 @@ async def get_installed_addons(saturated: bool) -> list[tuple[str, str]]: addons which are actually used in the production bundle. """ - result = [] - for addon_name, definition in AddonLibrary.items(): - for version in definition.versions.keys(): - result.append((addon_name, version)) - return result + rezrepo = RezRepo.get_instance() + installed_packages = [] + for package, package_dict in rezrepo.packages.items(): + for version in package_dict.get("versions"): + [[version_number, rez_package]] = version.items() + installed_packages.append((str(package), str(version_number))) + + return installed_packages + diff --git a/ayon_server/metrics/settings.py b/ayon_server/metrics/settings.py index f162607d..07aba2d9 100644 --- a/ayon_server/metrics/settings.py +++ b/ayon_server/metrics/settings.py @@ -1,4 +1,4 @@ -from ayon_server.addons.library import AddonLibrary +from ayon_server.addons import AddonLibrary from ayon_server.lib.postgres import Postgres from ayon_server.settings.overrides import list_overrides from ayon_server.types import Field, OPModel @@ -44,7 +44,9 @@ async def get_studio_settings_overrides(saturated: bool) -> list[SettingsOverrid data = row["data"] try: - addon = AddonLibrary.addon(addon_name, addon_version) + addon = AddonLibrary.get_addon(addon_name, addon_version) + if not addon: + continue except Exception: continue From 111bafaf1be8b5300274d3f19a92eb3c12cfcb1d Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Wed, 14 Feb 2024 15:15:08 +0000 Subject: [PATCH 5/8] Update ayon_server/addons/rezbuild.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- ayon_server/addons/rezbuild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_server/addons/rezbuild.py b/ayon_server/addons/rezbuild.py index 7f6a11f7..74af5b4f 100644 --- a/ayon_server/addons/rezbuild.py +++ b/ayon_server/addons/rezbuild.py @@ -2,7 +2,7 @@ This expects a certain folder structure: ├── client -│ └── ayon_ +│ └── # Example: ayon_core ├── package.py └── server From fb366283e887d24f61bebef129866c3ab369f87a Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Wed, 14 Feb 2024 15:15:22 +0000 Subject: [PATCH 6/8] Update ayon_server/addons/rezbuild.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- ayon_server/addons/rezbuild.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ayon_server/addons/rezbuild.py b/ayon_server/addons/rezbuild.py index 74af5b4f..45b69448 100644 --- a/ayon_server/addons/rezbuild.py +++ b/ayon_server/addons/rezbuild.py @@ -12,14 +12,14 @@ and copy the contents of `server` into the root of the package. -├── addon -│ ├── frontend +├── frontend +│ ├── dist +├── server │ ├── __init__.py -│ ├── private -│ │ ├── client.zip -│ │ └── pyproject.toml -│ ├── settings -│ └── version.py +│ └── settings +├── private +│ ├── client.zip +│ └── pyproject.toml ├── package.py └── build.rtx From 9590d7683670a878a334e7901322d78f3832672a Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Wed, 14 Feb 2024 15:15:42 +0000 Subject: [PATCH 7/8] Update api/system/info.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- api/system/info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/system/info.py b/api/system/info.py index 9c0f917e..088b1f0e 100644 --- a/api/system/info.py +++ b/api/system/info.py @@ -89,7 +89,7 @@ async def get_sso_options(request: Request) -> list[SSOOption]: result = [] - library = AddonLibrary().get_instance() + library = AddonLibrary.get_instance() production_addons = await library.get_variant_addons("production") for addon in production_addons: From 6b75a2bd6f5863f7c1fb3880c521b4aaf996e577 Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Wed, 14 Feb 2024 15:15:58 +0000 Subject: [PATCH 8/8] Update ayon_server/addons/rezbuild.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- ayon_server/addons/rezbuild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_server/addons/rezbuild.py b/ayon_server/addons/rezbuild.py index 45b69448..a8ae1155 100644 --- a/ayon_server/addons/rezbuild.py +++ b/ayon_server/addons/rezbuild.py @@ -8,7 +8,7 @@ Other folders might or might not be present, but they are essentially ignored. -Will zip up the `client/ayon_` and place it into `private/client.zip` +Will zip up the `client/` and place it into `private/client.zip` and copy the contents of `server` into the root of the package.