From de358e7248a3a54c6f023ef08ba9d32482517af8 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 11:59:48 +0200 Subject: [PATCH 01/45] Move event definitions --- core/ez/events/__init__.py | 8 ++++---- modules/ezjsx/__main__.py | 3 ++- modules/plugins/{ => ez_plugins}/events.py | 0 modules/web/app/app.py | 3 ++- core/ez/events/http.py => modules/web/events.py | 0 5 files changed, 8 insertions(+), 6 deletions(-) rename modules/plugins/{ => ez_plugins}/events.py (100%) rename core/ez/events/http.py => modules/web/events.py (100%) diff --git a/core/ez/events/__init__.py b/core/ez/events/__init__.py index 0db9ebe..1b1d130 100644 --- a/core/ez/events/__init__.py +++ b/core/ez/events/__init__.py @@ -1,18 +1,18 @@ from utilities.event import Event -from .http import HTTP from .settings import Settings +from web.events import HTTP from web.app.events import App from modules.events import Modules -from plugins.events import Plugins -from ezjsx.events import TreeRenderer +# from plugins.events import Plugins +# from ezjsx.events import TreeRenderer __all__ = [ "Event", "HTTP", "App", "Modules", - "Plugins", + # "Plugins", "Settings", "TreeRenderer" ] diff --git a/modules/ezjsx/__main__.py b/modules/ezjsx/__main__.py index 8012122..e92886d 100644 --- a/modules/ezjsx/__main__.py +++ b/modules/ezjsx/__main__.py @@ -4,7 +4,8 @@ from jsx.renderer import render from jsx.components import Component from jsx.html import Element -from ez.events import HTTP, TreeRenderer +from ez.events import HTTP +from .events import TreeRenderer @ez.on(HTTP.Out) diff --git a/modules/plugins/events.py b/modules/plugins/ez_plugins/events.py similarity index 100% rename from modules/plugins/events.py rename to modules/plugins/ez_plugins/events.py diff --git a/modules/web/app/app.py b/modules/web/app/app.py index b7b1f7c..8b3887a 100644 --- a/modules/web/app/app.py +++ b/modules/web/app/app.py @@ -3,7 +3,8 @@ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from ..response import _EzResponse -from ez.events import HTTP, App +from .events import App +from ..events import HTTP docs_urls = [ diff --git a/core/ez/events/http.py b/modules/web/events.py similarity index 100% rename from core/ez/events/http.py rename to modules/web/events.py From 4fae144905c4b03f97e6f85194ecad76c3ca8f6b Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 12:01:28 +0200 Subject: [PATCH 02/45] Remove module info from __main__.py --- modules/plugins/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/plugins/__main__.py b/modules/plugins/__main__.py index 856be4b..e69de29 100644 --- a/modules/plugins/__main__.py +++ b/modules/plugins/__main__.py @@ -1,2 +0,0 @@ -__title__ = "Plugin Manager" -__version__ = "1.0.0" From 788ebf57ed184a84ccd0df27685a39963bc4c046 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 12:01:54 +0200 Subject: [PATCH 03/45] Add more specific plugin errors --- modules/plugins/errors.py | 49 +++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/modules/plugins/errors.py b/modules/plugins/errors.py index 3eaecdd..9d48ef1 100644 --- a/modules/plugins/errors.py +++ b/modules/plugins/errors.py @@ -1,11 +1,17 @@ -from ez.errors import EZError +from ez import EZError +from typing import TYPE_CHECKING -class EZPluginManagerError(EZError): +if TYPE_CHECKING: + from plugins.machinery.installer import PluginInstallerId + from plugins.machinery.loader import PluginLoaderId + + +class EZPluginError(EZError): ... -class UnknownPluginError(EZPluginManagerError): +class UnknownPluginError(EZPluginError): def __init__(self, plugin_name): self.plugin_name = plugin_name @@ -13,9 +19,44 @@ def __str__(self): return f"Unknown plugin: {self.plugin_name}" -class PluginAlreadyInstalledError(EZPluginManagerError): +class PluginAlreadyInstalledError(EZPluginError): def __init__(self, plugin_name): self.plugin_name = plugin_name def __str__(self): return f"Plugin already installed: {self.plugin_name}" + + +class DuplicateIDError(EZPluginError): + def __init__(self, item, id) -> None: + self.item = item + self.id = id + + def __str__(self): + return f"Duplicate ID of {type(self.item).__name__}: {self.id}" + + +class UnknownPluginInstallerError(EZPluginError): + def __init__(self, installer_id: "PluginInstallerId") -> None: + self._installer_id = installer_id + + def __str__(self): + return f"Unknown plugin installer: {self._installer_id}" + + +class UnknownPluginLoaderError(EZPluginError): + def __init__(self, loader_id: "PluginLoaderId") -> None: + self._loader_id = loader_id + + def __str__(self): + return f"Unknown plugin loader: {self._loader_id}" + + +__all__ = [ + "EZPluginError", + "UnknownPluginError", + "PluginAlreadyInstalledError", + "DuplicateIDError", + "UnknownPluginInstallerError", + "UnknownPluginLoaderError", +] From 304cc4cfe8af15abe3b514150450d07328abc8b1 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 12:02:37 +0200 Subject: [PATCH 04/45] Fix module manager --- modules/modules/manager.py | 50 +++++++++++++++++++++++++++----------- modules/modules/module.py | 6 +---- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/modules/modules/manager.py b/modules/modules/manager.py index 0117985..614d22e 100644 --- a/modules/modules/manager.py +++ b/modules/modules/manager.py @@ -1,3 +1,4 @@ +from types import ModuleType from utilities.semver import SemanticVersion from .module import Module @@ -15,11 +16,17 @@ class ModuleManager: PACKAGE_ENTRY_POINT = "__main__.py" MODULE_PREFIX = "ez.global.modules" + MODULE_NAME_ATTRIBUTE = "__module_name__" def __init__(self, module_dir: Path) -> None: self.module_dir = module_dir self._modules: list[Module] = [] + sys.modules[self.MODULE_PREFIX] = type("", (ModuleType,), { + "__path__": [str(module_dir)], + "__package__": str(self.MODULE_PREFIX) + })(self.MODULE_PREFIX) + def load_modules(self, *, reload: bool = True) -> bool: """ Load all modules from the path given in the constructor. @@ -42,6 +49,7 @@ def load_modules(self, *, reload: bool = True) -> bool: return False if not self.module_dir.is_dir(): return False + for item in self.module_dir.iterdir(): name = item.stem if item.is_dir(): @@ -53,30 +61,44 @@ def load_modules(self, *, reload: bool = True) -> bool: if item.suffix != ".py": continue self._load_module_from(name, item) + + for module in self._modules: + module.entry_point.__spec__.loader.exec_module(module.entry_point) + if self._modules: self._modules.append(Module( "Module Manager", THIS, Path(THIS.__file__), - SemanticVersion.parse(THIS.__version__), - "Module Manager by EZ Framework." )) - return True - return False + + return bool(self._modules) def _load_module_from(self, name: str, path: Path): - spec = spec_from_file_location(self.get_module_full_name(name), str(path)) + full_name = self.get_module_full_name(name) + package_dir = path.parent + + init_file = package_dir / "__init__.py" + if init_file.exists(): + spec = spec_from_file_location( + full_name, + init_file, + submodule_search_locations=[ + str(package_dir) + ]) + module = sys.modules[spec.name] = module_from_spec(spec) + spec.loader.exec_module(module) + + name = getattr(module, self.MODULE_NAME_ATTRIBUTE, name) + + spec = spec_from_file_location( + full_name + '.__main__', + str(path) + ) entry_point = module_from_spec(spec) - spec.loader.exec_module(entry_point) - - if not hasattr(entry_point, "__version__"): - raise Exception(f"Invalid module '{name}' at '{path}'") - - name = getattr(entry_point, "__title__", name) - version = SemanticVersion.parse(entry_point.__version__) - description = getattr(entry_point, "__description__", getattr(entry_point, "__doc__", "")) + sys.modules[spec.name] = entry_point - module = Module(name, entry_point, path, version, description) + module = Module(name, entry_point, path) self._modules.append(module) def reload_module(self, module: Module): diff --git a/modules/modules/module.py b/modules/modules/module.py index 655faee..1b2be26 100644 --- a/modules/modules/module.py +++ b/modules/modules/module.py @@ -2,16 +2,12 @@ from pathlib import Path from types import ModuleType -from utilities.semver import SemanticVersion - @dataclass class Module: name: str entry_point: ModuleType entry_point_path: Path - version: SemanticVersion - description: str def __str__(self) -> str: - return f"{self.name} v{self.version} @ {self.entry_point_path}" + return f"{self.name} @ {self.entry_point_path}" From 81e3cb9ecf8c24b3844f13ebf4d6042a3540ef9e Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 12:02:50 +0200 Subject: [PATCH 05/45] Add `Version` alias --- modules/utilities/version.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules/utilities/version.py diff --git a/modules/utilities/version.py b/modules/utilities/version.py new file mode 100644 index 0000000..280ed8f --- /dev/null +++ b/modules/utilities/version.py @@ -0,0 +1 @@ +from .semver import SemanticVersion as Version From a378e26aca3e6956d26b3945ca07df0181dfc54a Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 12:03:10 +0200 Subject: [PATCH 06/45] Fix plugin system --- modules/plugins/__init__.py | 6 + modules/plugins/builtins/installer.py | 27 +++ modules/plugins/builtins/loader.py | 115 +++++++++++ modules/plugins/builtins/plugin.py | 13 ++ modules/plugins/builtins/plugin_module.py | 18 ++ .../plugins/builtins/plugin_module_loader.py | 16 ++ modules/plugins/config.py | 7 + modules/plugins/ez_plugins/__init__.py | 136 +++++++++++++ modules/plugins/ez_plugins/errors.py | 1 + modules/plugins/ez_plugins/machinery.py | 13 ++ modules/plugins/machinery/installer.py | 45 ++++ modules/plugins/machinery/loader.py | 29 +++ modules/plugins/machinery/manager.py | 192 ++++++++++++++++++ modules/plugins/machinery/utils.py | 36 ++++ modules/plugins/manager.py | 131 +----------- modules/plugins/plugin.py | 21 +- modules/plugins/plugin_info.py | 24 ++- 17 files changed, 700 insertions(+), 130 deletions(-) create mode 100644 modules/plugins/builtins/installer.py create mode 100644 modules/plugins/builtins/loader.py create mode 100644 modules/plugins/builtins/plugin.py create mode 100644 modules/plugins/builtins/plugin_module.py create mode 100644 modules/plugins/builtins/plugin_module_loader.py create mode 100644 modules/plugins/config.py create mode 100644 modules/plugins/ez_plugins/__init__.py create mode 100644 modules/plugins/ez_plugins/errors.py create mode 100644 modules/plugins/ez_plugins/machinery.py create mode 100644 modules/plugins/machinery/installer.py create mode 100644 modules/plugins/machinery/loader.py create mode 100644 modules/plugins/machinery/manager.py create mode 100644 modules/plugins/machinery/utils.py diff --git a/modules/plugins/__init__.py b/modules/plugins/__init__.py index e69de29..3fd3fd9 100644 --- a/modules/plugins/__init__.py +++ b/modules/plugins/__init__.py @@ -0,0 +1,6 @@ +from . import ez_plugins +import ez +ez.extend_ez(ez_plugins, "plugins") + + +__module_name__ = "Plugin Manager" diff --git a/modules/plugins/builtins/installer.py b/modules/plugins/builtins/installer.py new file mode 100644 index 0000000..01f50ee --- /dev/null +++ b/modules/plugins/builtins/installer.py @@ -0,0 +1,27 @@ +from ..errors import EZPluginError +from ..machinery.installer import IPluginInstaller, PluginInstallationResult, PluginInstallerInfo + + +class EZPluginInstallerError(EZPluginError): + ... + + +class InvalidPluginArchive(EZPluginInstallerError): + ... + + +class InvalidPluginManifest(EZPluginInstallerError): + ... + + +class EZPluginInstaller(IPluginInstaller): + info = PluginInstallerInfo( + "ez.plugins.installer", + "EZ Plugin Installer", + ) + + def install(self, path: str) -> PluginInstallationResult: + ... + + def uninstall(self, plugin_id: str) -> None: + ... diff --git a/modules/plugins/builtins/loader.py b/modules/plugins/builtins/loader.py new file mode 100644 index 0000000..da7b71f --- /dev/null +++ b/modules/plugins/builtins/loader.py @@ -0,0 +1,115 @@ +import sys + +from types import ModuleType +from importlib.util import spec_from_file_location, module_from_spec + +from utilities.version import Version + +from ..plugin_info import PluginId, PluginInfo +from ..machinery.loader import IPluginLoader, PluginLoaderInfo + +from .plugin import EZPlugin +from .plugin_module_loader import PluginModuleLoader +from .plugin_module import PluginModule + + +class EZPluginLoader(IPluginLoader): + info = PluginLoaderInfo( + "ez.plugins.loader", + "EZ Plugin Loader", + ) + + EZ_PLUGIN_ENTRY_POINT_FILENAME = "plugin.py" + EZ_PLUGIN_API_ATTRIBUTE_NAME = "__api__" + EZ_PLUGIN_INFO_ATTRIBUTE_NAME = "__info__" + EZ_PLUGIN_PREFIX = "ez.current-site.plugins" + + def __init__(self, plugin_dir: PluginId) -> None: + super().__init__(plugin_dir) + + self._loader = PluginModuleLoader() + + root = sys.modules[self.EZ_PLUGIN_PREFIX] = PluginModule(self.EZ_PLUGIN_PREFIX) + root.__path__ = [str(self.plugin_dir)] + + def load(self, plugin_id: PluginId, plugin: EZPlugin) -> EZPlugin | None: + if plugin is None: + return self._load_plugin(plugin_id) + if not isinstance(plugin, EZPlugin): + # TODO: Add a custom exception for this, deriving from PluginLoaderException + raise TypeError(f"Expected EZPlugin, got {type(plugin)}") + if plugin.is_loaded: + if plugin.enabled: + return None + self._reload_plugin(plugin) + else: + plugin.module = self._load_module(plugin.info.package_name) + + def _load_plugin(self, plugin_id: PluginId) -> EZPlugin: + module = self._load_module(plugin_id) + sys.modules[module.__name__] = module + + plugin_dir = self.plugin_dir / plugin_id + + with open(plugin_dir / "plugin-metadata") as file: + metadata = { + key.strip(): value.strip() + for key, value in (line.split(":", 1) for line in file) + } + metadata["version"] = Version.parse(metadata["version"]) + if "description" not in metadata: + metadata["description"] = module.__doc__ + info = PluginInfo( + **metadata + ) + + module.__info__ = info + module.__loader__ = self.info.id + module.__path__.append(str(plugin_dir)) + + ez_plugin = EZPlugin( + info=info, + loader=self.info, + enabled=True, + api=None, + module=module + ) + + module.__plugin__ = ez_plugin + + self._execute_module(module) + + ez_plugin.api = getattr(module, self.get_api_attribute_name(), None) + + return ez_plugin + + def _load_module(self, plugin_id: PluginId) -> PluginModule: + path = self._get_plugin_path(plugin_id) + spec = spec_from_file_location(self.get_module_full_name(plugin_id), str(path), loader=self._loader) + module = module_from_spec(spec) + return module + + def _execute_module(self, module: ModuleType) -> None: + module.__spec__.loader.exec_module(module) + + def _reload_plugin(self, plugin: EZPlugin) -> None: + self._execute_module(plugin.module) + + def _get_plugin_path(self, plugin_id: PluginId): + return self.plugin_dir / str(plugin_id) / self.get_entry_point_filename() + + @classmethod + def get_entry_point_filename(cls) -> str: + return cls.EZ_PLUGIN_ENTRY_POINT_FILENAME + + @classmethod + def get_api_attribute_name(cls) -> str: + return cls.EZ_PLUGIN_API_ATTRIBUTE_NAME + + @classmethod + def get_info_attribute_name(cls) -> str: + return cls.EZ_PLUGIN_INFO_ATTRIBUTE_NAME + + @classmethod + def get_module_full_name(cls, plugin_id: PluginId) -> str: + return f"{cls.EZ_PLUGIN_PREFIX}.{plugin_id}" diff --git a/modules/plugins/builtins/plugin.py b/modules/plugins/builtins/plugin.py new file mode 100644 index 0000000..6ad42ff --- /dev/null +++ b/modules/plugins/builtins/plugin.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + +from ..plugin import Plugin +from .plugin_module import PluginModule + + +@dataclass +class EZPlugin(Plugin): + module: PluginModule | None + + @property + def is_loaded(self): + return self.module is not None diff --git a/modules/plugins/builtins/plugin_module.py b/modules/plugins/builtins/plugin_module.py new file mode 100644 index 0000000..18d1efc --- /dev/null +++ b/modules/plugins/builtins/plugin_module.py @@ -0,0 +1,18 @@ +from types import ModuleType +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ez.plugins import Plugin, PluginInfo + + +class PluginModule(ModuleType): + __plugin__: "Plugin" + __info__: "PluginInfo" + + def __init__(self, name, doc=None): + super().__init__(name, doc) + self.__path__ = [] + self.__package__ = name + + def __repr__(self): + return f"" diff --git a/modules/plugins/builtins/plugin_module_loader.py b/modules/plugins/builtins/plugin_module_loader.py new file mode 100644 index 0000000..38b445a --- /dev/null +++ b/modules/plugins/builtins/plugin_module_loader.py @@ -0,0 +1,16 @@ +from importlib.abc import Loader + +from .plugin_module import PluginModule + + +class PluginModuleLoader(Loader): + def create_module(self, spec): + module = PluginModule(spec.name, None) + module.__spec__ = spec + return module + + def exec_module(self, module): + with open(module.__spec__.origin, "r") as file: + code = file.read() + + exec(code, vars(module)) diff --git a/modules/plugins/config.py b/modules/plugins/config.py new file mode 100644 index 0000000..1329a53 --- /dev/null +++ b/modules/plugins/config.py @@ -0,0 +1,7 @@ +import ez + + +PLUGINS_DIRECTORY_NAME = "plugins" +PLUGINS_DIRECTORY = ez.SITE_DIR / PLUGINS_DIRECTORY_NAME + +METADATA_FILENAME = "plugin-metadata" diff --git a/modules/plugins/ez_plugins/__init__.py b/modules/plugins/ez_plugins/__init__.py new file mode 100644 index 0000000..bf1696a --- /dev/null +++ b/modules/plugins/ez_plugins/__init__.py @@ -0,0 +1,136 @@ +from typing import Callable + +from ..plugin import Plugin, PluginId, PluginAPI +from ..plugin_info import PluginInfo +from ..machinery.installer import IPluginInstaller, PluginInstallerInfo, PluginInstallerId +from ..machinery.loader import IPluginLoader, PluginLoaderInfo + +from .errors import EZPluginError, UnknownPluginError, DuplicateIDError +from .events import Plugins as PluginEvent + +from ..manager import PLUGIN_MANAGER as __pm +from ..config import METADATA_FILENAME + + +def get_plugins() -> list[Plugin]: + return __pm.get_plugins() + + +def get_plugin(plugin_id: PluginId) -> Plugin: + return __pm.get_plugin(plugin_id) + + +def install(path: str, *, installer: PluginInstallerId = None): + return __pm.install(path, installer=installer) + + +def uninstall(plugin_id: PluginId): + __pm.uninstall(plugin_id) + + +def enable(plugin_id: PluginId) -> bool: + if is_enabled(plugin_id): + return False + return __pm.enable(plugin_id) is None + + +def disable(plugin_id: PluginId) -> bool: + if is_disabled(plugin_id): + return False + return __pm.disable(plugin_id) is None + + +def is_enabled(plugin_id: PluginId) -> bool: + plugin = __pm.get_plugin(plugin) + return plugin.enabled + + +def is_disabled(plugin_id: PluginId) -> bool: + return not is_enabled(plugin_id) + + +def has_api(plugin_id: PluginId) -> bool: + return get_api(plugin_id) is not None + + +def get_api(plugin_id: PluginId) -> PluginAPI | None: + plugin = get_plugin(plugin_id) + return plugin.api + + +def get_plugins_directory() -> str: + return str(__pm.plugin_dir) + + +def get_metadata_filename() -> str: + return METADATA_FILENAME + + +def get_plugin_public_api_module_name() -> str: + ... + + +def expose(plugin: Plugin, api: PluginAPI): + return __pm.expose(plugin, api) + + +def add_installer(installer: IPluginInstaller | Callable[[str], IPluginInstaller]) -> IPluginInstaller: + return __pm.add_installer(installer) + + +def remove_installer(installer: IPluginInstaller) -> bool: + return __pm.remove_installer(installer) + + +def add_loader(loader: IPluginLoader | Callable[[str], IPluginLoader]) -> IPluginLoader: + return __pm.add_loader(loader) + + +def remove_loader(loader: IPluginLoader) -> bool: + return __pm.remove_loader(loader) + + +def get_installers() -> list[PluginInstallerInfo]: + return [ + installer.info for installer in __pm.get_installers() + ] + + +def get_loaders() -> list[PluginLoaderInfo]: + return [ + loader.info for loader in __pm.get_loaders() + ] + + +__all__ = [ + "Plugin", + "IPluginInstaller", + "PluginInstallerInfo", + "IPluginLoader", + "PluginLoaderInfo", + "UnknownPluginError", + "DuplicateIDError", + "PluginInfo", + "EZPluginError", + "PluginEvent", + "get_plugins", + "get_plugin", + "install", + "uninstall", + "enable", + "disable", + "is_enabled", + "is_disabled", + "has_api", + "get_api", + "get_plugins_directory", + "get_metadata_filename", + "get_plugin_public_api_module_name", + "expose", + "add_installer", + "remove_installer", + "add_loader", + "remove_loader", + "get_installers", + "get_loaders", +] diff --git a/modules/plugins/ez_plugins/errors.py b/modules/plugins/ez_plugins/errors.py new file mode 100644 index 0000000..a11fc7d --- /dev/null +++ b/modules/plugins/ez_plugins/errors.py @@ -0,0 +1 @@ +from ..errors import * diff --git a/modules/plugins/ez_plugins/machinery.py b/modules/plugins/ez_plugins/machinery.py new file mode 100644 index 0000000..eaaf2da --- /dev/null +++ b/modules/plugins/ez_plugins/machinery.py @@ -0,0 +1,13 @@ +from ....machinery.installer import IPluginInstaller, PluginInstallerInfo, PluginInstallationResult +from ....machinery.loader import IPluginLoader, PluginLoaderInfo +from ....plugin_info import PluginInfo + + +__all__ = [ + "IPluginInstaller", + "PluginInstallerInfo", + "PluginInstallationResult", + "IPluginLoader", + "PluginLoaderInfo", + "PluginInfo", +] diff --git a/modules/plugins/machinery/installer.py b/modules/plugins/machinery/installer.py new file mode 100644 index 0000000..533fe7b --- /dev/null +++ b/modules/plugins/machinery/installer.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import TypeAlias + +from utilities.version import Version + +from ..plugin_info import PluginId + + +PluginInstallerId: TypeAlias = str + + +@dataclass +class PluginInstallerInfo: + id: PluginInstallerId + name: str + + +@dataclass +class PluginInstallationResult: + installer_id: PluginInstallerId + package_name: str + version: Version + previous_version: Version | None + + @property + def is_upgrade(self) -> bool: + return self.previous_version is not None + + +class IPluginInstaller: + info: PluginInstallerInfo + + def __init__(self, plugin_dir: str) -> None: + self._plugin_dir = Path(plugin_dir) + + @property + def plugin_dir(self) -> Path: + return self._plugin_dir + + def install(self, path: str) -> PluginInstallationResult: + raise NotImplementedError + + def uninstall(self, plugin_id: str) -> None: + raise NotImplementedError diff --git a/modules/plugins/machinery/loader.py b/modules/plugins/machinery/loader.py new file mode 100644 index 0000000..11961f1 --- /dev/null +++ b/modules/plugins/machinery/loader.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import TypeAlias + +from ..plugin import Plugin +from ..plugin_info import PluginId + + +PluginLoaderId: TypeAlias = str + + +@dataclass +class PluginLoaderInfo: + id: PluginLoaderId + name: str + + +class IPluginLoader: + info: PluginLoaderInfo + + def __init__(self, plugin_dir: str) -> None: + self._plugin_dir = Path(plugin_dir) + + @property + def plugin_dir(self) -> Path: + return self._plugin_dir + + def load(self, plugin_id: PluginId, plugin: Plugin | None) -> Plugin | None: + raise NotImplementedError \ No newline at end of file diff --git a/modules/plugins/machinery/manager.py b/modules/plugins/machinery/manager.py new file mode 100644 index 0000000..25f19f0 --- /dev/null +++ b/modules/plugins/machinery/manager.py @@ -0,0 +1,192 @@ +from typing import Callable +from types import ModuleType +from pathlib import Path + +from ..machinery.installer import PluginInstallerId +from ..machinery.loader import PluginLoaderId + +from ..plugin import ( + Plugin, + PluginInfo, + PluginId, +) + +from ..errors import ( + UnknownPluginError, + UnknownPluginInstallerError, + UnknownPluginLoaderError, + DuplicateIDError, +) + +from ..machinery.installer import IPluginInstaller +from ..machinery.loader import IPluginLoader + + +class PluginManager: + EZ_PLUGIN_PREFIX = "ez.current-site.plugins" + EZ_PUBLIC_API_MODULE = "ez_plugins" + + def __init__( + self, + plugin_dir: str, + default_installer: Callable[[str], IPluginInstaller] | IPluginInstaller, + default_loader: Callable[[str], IPluginLoader] | IPluginLoader, + public_api: ModuleType | None = None + ) -> None: + self.plugin_dir = Path(plugin_dir) + self._plugins: dict[PluginId, Plugin] = {} + self._plugin_installers: dict[PluginInstallerId, IPluginInstaller] = {} + self._plugin_loaders: dict[PluginLoaderId, IPluginLoader] = {} + + self._default_plugin_installer = self.add_installer(default_installer) + self._default_plugin_loader = self.add_loader(default_loader) + + self._public_api = public_api or ModuleType(self.EZ_PUBLIC_API_MODULE) + + #region Plugin Public API + + @property + def public_api(self): + return self._public_api + + def enable_public_api(self, alias: str = None): + alias = alias or self.EZ_PUBLIC_API_MODULE + + import sys + + sys.modules[alias] = self._public_api + self._public_api.__ez_name__ = alias + + def disable_public_api(self): + import sys + + del sys.modules[self._public_api.__ez_name__] + + def expose(self, plugin: Plugin, api: object): + if api is None: + raise ValueError("API cannot be None") + + api_name = plugin.info.package_name.replace("-", "_") + if hasattr(self._public_api, api_name): + raise ValueError(f"API name {api_name} already in use") + + plugin.api = api + setattr(self._public_api, api_name, api) + + #endregion + + def get_plugin(self, plugin_id: PluginId) -> Plugin: + try: + return self._plugins[plugin_id] + except KeyError: + raise UnknownPluginError(plugin_id) from None + + def get_plugins(self): + return list(self._plugins.values()) + + #region Installers & Loaders + + def add_installer(self, installer: IPluginInstaller | Callable[[str], IPluginInstaller]) -> IPluginInstaller: + if not isinstance(installer, IPluginInstaller): + installer = installer(self.plugin_dir) + if installer.info.id in self._plugin_installers: + raise DuplicateIDError(installer, installer.info.id) + self._plugin_installers[installer.info.id] = installer + return installer + + def add_loader(self, loader: IPluginLoader | Callable[[str], IPluginLoader]) -> IPluginLoader: + if not isinstance(loader, IPluginInstaller): + loader = loader(self.plugin_dir) + if loader.info.id in self._plugin_loaders: + raise DuplicateIDError(loader, loader.info.id) + self._plugin_loaders[loader.info.id] = loader + return loader + + def remove_installer(self, installer: IPluginInstaller) -> bool: + try: + del self._plugin_installers[installer.info.id] + except KeyError: + return False + return True + + def remove_loader(self, loader: IPluginLoader) -> bool: + try: + del self._plugin_loaders[loader.info.id] + except KeyError: + return False + return True + + def get_installer(self, installer: PluginInstallerId) -> IPluginInstaller: + try: + return self._plugin_installers[installer] + except KeyError: + raise UnknownPluginInstallerError(installer) from None + + def get_loader(self, loader: PluginLoaderId) -> IPluginLoader: + try: + return self._plugin_loaders[loader] + except KeyError: + raise UnknownPluginLoaderError(loader) from None + + def get_installers(self): + return list(self._plugin_installers.values()) + + def get_loaders(self): + return list(self._plugin_loaders.values()) + + #endregion + + #region Installation + + def install(self, path: str, *, installer: PluginInstallerId = None): + if installer is None: + _installer = self._default_plugin_installer + else: + _installer = self.get_installer(installer) + return _installer.install(path) + + def uninstall(self, plugin_id: PluginId): + plugin = self.get_plugin(plugin_id) + installer = self.get_installer(plugin.info.installer_id) + installer.uninstall(plugin_id) + + #endregion + + #region Plugin Status + + def enable(self, plugin_id: PluginId): + ... + + def disable(self, plugin_id: PluginId): + ... + + def load_plugin(self, plugin_id: PluginId): + loader = self._default_plugin_loader + + try: + plugin = self.get_plugin(plugin_id) + except UnknownPluginError: + plugin = loader.load(plugin_id, None) + + if plugin is None: + raise TypeError(f"Plugin loader {loader.info.id} returned None for plugin {plugin_id}") + self._plugins[plugin_id] = plugin + + if plugin.api is not None: + self.expose(plugin, plugin.api) + else: + loader.load(plugin_id, plugin) + + return plugin + + def load_plugins(self, *plugin_ids: PluginId): + for plugin_id in plugin_ids: + self.load_plugin(plugin_id) + + #endregion + + @classmethod + def get_plugin_full_name(cls, plugin: str | PluginInfo) -> str: + if isinstance(plugin, PluginInfo): + plugin = plugin.package_name + return f"{cls.EZ_PLUGIN_PREFIX}.{plugin}" diff --git a/modules/plugins/machinery/utils.py b/modules/plugins/machinery/utils.py new file mode 100644 index 0000000..58cf3fc --- /dev/null +++ b/modules/plugins/machinery/utils.py @@ -0,0 +1,36 @@ +from pathlib import Path + + +class InstallerHelper: + def __init__(self, root: str) -> None: + self._files: list[str] = [] + self.root = Path(root) + + def add_file(self, file: str): + self._files.append(file) + + def _move_file(self, src: str, dest: str, name: str = None, exclude: bool = False): + dest = Path(dest) + dest.mkdir(parents=True, exist_ok=True) + src = Path(src) + src.rename(dest / (name or src.name)) + if not exclude: + self.add_file(str(dest / (name or src.name))) + + + def install_file(self, src: str, dest: str, name: str = None, exclude: bool = False): + self._move_file(src, dest, name, exclude=exclude) + + def install_dir(self, src: str, dest: str, exclude: bool = False): + src = Path(src) + dest = Path(dest) + for file in src.iterdir(): + if file.is_dir(): + self.install_dir(file, dest / file.name, exclude) + else: + self._move_file(file, dest, exclude=exclude) + + def finalize(self, uninstallation_filename: str): + with open(self.root / uninstallation_filename, "w") as file: + for f in self._files: + file.write(f"{f}\n") diff --git a/modules/plugins/manager.py b/modules/plugins/manager.py index 3c0496a..67ebc11 100644 --- a/modules/plugins/manager.py +++ b/modules/plugins/manager.py @@ -1,127 +1,8 @@ -import sys -from utilities.semver import SemanticVersion +from .machinery.manager import PluginManager +from .builtins.installer import EZPluginInstaller +from .builtins.loader import EZPluginLoader -from .plugin_info import PluginInfo -from .model import PluginWebModel -from .plugin import Plugin -from .errors import UnknownPluginError +from .config import PLUGINS_DIRECTORY -from types import ModuleType -from pathlib import Path -from importlib.util import spec_from_file_location, module_from_spec -from importlib import reload - - -class PluginManager: - PLUGIN_PREFIX = "ez.current-site.plugins" - - def __init__(self, plugin_dir: Path) -> None: - self.plugin_dir = plugin_dir - self._plugins: dict[str, Plugin] = {} - - sys.modules[self.PLUGIN_PREFIX] = type("", (ModuleType,), { - "__path__": [str(plugin_dir)] - })(self.PLUGIN_PREFIX) - - def get_plugins(self): - return [ - PluginWebModel.model_validate({ - "name": plugin.name, - "id": plugin.info.dir_name, - "enabled": plugin.enabled, - "version": str(plugin.version), - "description": plugin.info.description - }) - for plugin in self._plugins.values() - ] - - def enable_plugin(self, plugin_name: str): - try: - plugin = self._plugins[plugin_name] - except KeyError: - raise UnknownPluginError(plugin_name) - else: - if plugin.enabled: - return - plugin.enable() - if plugin.is_loaded: - plugin.module.__spec__.loader.exec_module(plugin.module) - else: - self.load_plugin(plugin) - - def disable_plugin(self, plugin_name: str): - try: - plugin = self._plugins[plugin_name] - except KeyError: - raise UnknownPluginError(plugin_name) - else: - if not plugin.enabled: - return - plugin.disable() - - def get_plugin_info(self, plugin_name: str) -> PluginInfo: - return self.get_plugin(plugin_name).info - - def get_plugin(self, plugin_name): - try: - return self._plugins[plugin_name] - except KeyError: - raise UnknownPluginError(plugin_name) - - def add_plugin(self, plugin: PluginInfo) -> None: - self._plugins[plugin.dir_name] = Plugin(plugin, None) - - def load_plugin(self, plugin: str | PluginInfo, _plugin: Plugin | None = None) -> bool: - if isinstance(plugin, str): - try: - plugin = self.get_plugin_info(plugin) - except UnknownPluginError: - dir_name = plugin - else: - dir_name = plugin.dir_name - if dir_name in self._plugins and self._plugins[dir_name].is_loaded: - self.enable_plugin(dir_name) - return True - - spec = spec_from_file_location(self.get_plugin_full_name(dir_name), str(self.plugin_dir / dir_name / "plugin.py")) - module = module_from_spec(spec) - - if not hasattr(module, "__path__"): - module.__path__ = [] - module.__package__ = spec.name - module.__path__.append(str(self.plugin_dir / dir_name)) - sys.modules[module.__name__] = module - - if isinstance(plugin, str): - plugin = PluginInfo(plugin, SemanticVersion(0, 0, 0), "", dir_name) - _plugin = self._plugins[dir_name] = Plugin(plugin, None) - - try: - spec.loader.exec_module(module) - except Exception: - del sys.modules[module.__name__] - del self._plugins[dir_name] - raise - - if isinstance(plugin, str): - if not hasattr(module, "__version__"): - return False - plugin.version = SemanticVersion.parse(module.__version__) - plugin.description = getattr(module, "__description__", getattr(module, "__doc__", "")) - if _plugin is None: - _plugin = Plugin(plugin, None) - _plugin.module = module - _plugin.enable() - self._plugins[dir_name] = _plugin - - return True - - def load_plugins(self, dir_names: list[str]): - for dir_name in dir_names: - self.load_plugin(dir_name) - - @classmethod - def get_plugin_full_name(cls, plugin: str | PluginInfo) -> str: - if isinstance(plugin, PluginInfo): - plugin = plugin.dir_name - return f"{cls.PLUGIN_PREFIX}.{plugin}" +PLUGIN_MANAGER = PluginManager(PLUGINS_DIRECTORY, EZPluginInstaller, EZPluginLoader) +PLUGIN_MANAGER.enable_public_api() diff --git a/modules/plugins/plugin.py b/modules/plugins/plugin.py index 014dd11..5d8ba24 100644 --- a/modules/plugins/plugin.py +++ b/modules/plugins/plugin.py @@ -1,11 +1,20 @@ +from dataclasses import dataclass from types import ModuleType -from .plugin_info import PluginInfo +from typing import TypeAlias, TYPE_CHECKING +from .plugin_info import PluginInfo, PluginId, PackageName + + +if TYPE_CHECKING: + from .machinery.loader import PluginLoaderInfo API_ATTRIBUTE_NAME = "__api__" -class Plugin: +PluginAPI: TypeAlias = object + + +class EZPlugin: info: PluginInfo module: ModuleType | None enabled: bool @@ -40,3 +49,11 @@ def enable(self): def disable(self): self.enabled = False + + +@dataclass +class Plugin: + info: PluginInfo + loader: "PluginLoaderInfo" + enabled: bool + api: PluginAPI | None diff --git a/modules/plugins/plugin_info.py b/modules/plugins/plugin_info.py index db540eb..64341f7 100644 --- a/modules/plugins/plugin_info.py +++ b/modules/plugins/plugin_info.py @@ -1,10 +1,28 @@ from dataclasses import dataclass -from utilities.semver import SemanticVersion +from typing import TypeAlias, TYPE_CHECKING +from utilities.version import Version + + +if TYPE_CHECKING: + from .machinery.installer import PluginInstallerId + + +PluginId: TypeAlias = str +PackageName: TypeAlias = str @dataclass class PluginInfo: name: str - version: SemanticVersion + version: Version description: str - dir_name: str + installer_id: "PluginInstallerId" + package_name: str + + @property + def id(self) -> PluginId: + return self.package_name + + @property + def dir_name(self) -> PackageName: + return self.package_name From 8e3f21545cc28d1a833149f2e499f7b1000e8eef Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 12:03:27 +0200 Subject: [PATCH 07/45] Fix EZ core API file --- core/ez/__init__.py | 135 ++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 55 deletions(-) diff --git a/core/ez/__init__.py b/core/ez/__init__.py index 0a78f7b..0f136fe 100644 --- a/core/ez/__init__.py +++ b/core/ez/__init__.py @@ -42,30 +42,34 @@ from utilities.event import Event from utilities.event_emitter import EventEmitter from web.response import _EzResponse -from plugins.manager import PluginManager -from plugins.installer import PluginInstaller -from plugins.errors import UnknownPluginError -from plugins.plugin_info import PluginInfo +# from plugins.manager import PluginManager +# from plugins.installer import PluginInstaller +# from plugins.errors import UnknownPluginError +# from plugins.plugin_info import PluginInfo from web.app.app import EZApplication -sys.path.remove(str(MODULE_DIR)) -del sys +from ez.errors import EZError +sys.path.remove(str(MODULE_DIR)) #endregion #region EZ Internal + +__path__ = list(__path__) + + class _EZ: ez: "_EZ | None" = None ee: EventEmitter app: FastAPI plugin_events: dict[str, list[tuple[str, Callable]]] - plugin_manager: PluginManager - plugin_installer: PluginInstaller + # plugin_manager: PluginManager + # plugin_installer: PluginInstaller mm: ModuleManager @@ -78,8 +82,8 @@ def __init__(self, app: FastAPI | None = None, ee: EventEmitter | None = None) - self.app = app or EZApplication(redirect_slashes=True) self.plugin_events = {} - self.plugin_manager = PluginManager(PLUGINS_DIR) - self.plugin_installer = PluginInstaller(self.plugin_manager) + # self.plugin_manager = PluginManager(PLUGINS_DIR) + # self.plugin_installer = PluginInstaller(self.plugin_manager) self.mm = ModuleManager(MODULE_DIR) @@ -95,20 +99,24 @@ def remove_plugin_events(self, plugin: str): del self.plugin_events[plugin] def get_plugin_from_handler(self, handler: Callable, __Path=Path) -> str: - if not handler.__module__.startswith(self.plugin_manager.PLUGIN_PREFIX): + PLUGIN_PREFIX = "ez.current-site.plugins" + if not handler.__module__.startswith(PLUGIN_PREFIX): return None - name = handler.__module__.removeprefix(self.plugin_manager.PLUGIN_PREFIX + ".").split(".")[0] - return self.plugin_manager.get_plugin_info(name).dir_name + name = handler.__module__.removeprefix(PLUGIN_PREFIX + ".").split(".")[0] + return name _EZ() -def event_function(f: Callable, *, __wraps=wraps, __ez=_EZ.ez, UnkownPluginError=UnknownPluginError): +def event_function(f: Callable, *, __wraps=wraps, __ez=_EZ.ez): + import ez.plugins + from ez.plugins import UnknownPluginError + plugin = __ez.get_plugin_from_handler(f) if not plugin: return f try: - plugin = __ez.plugin_manager.get_plugin(plugin) + plugin = ez.plugins.get_plugin(plugin) except UnknownPluginError: return f else: @@ -116,9 +124,8 @@ def event_function(f: Callable, *, __wraps=wraps, __ez=_EZ.ez, UnkownPluginError def wrapper(*args, **kwargs): if plugin.enabled: return f(*args, **kwargs) - print(f"Plugin '{plugin.info.dir_name}' is disabled.") return - f.__ez_plugin__ = wrapper.__ez_plugin__ = plugin.info + f.__ez_plugin__ = wrapper.__ez_plugin__ = plugin return wrapper @@ -126,10 +133,21 @@ def is_plugin_event_handler(f): return callable(f) and getattr(f, "__ez_plugin__", None) is not None -def get_plugin_event_handler_info(f) -> PluginInfo: - if not is_plugin_event_handler(f): - raise ValueError(f"Function '{f.__qualname__}' is not a plugin event handler.") - return getattr(f, "__ez_plugin__") +# def get_plugin_event_handler_info(f) -> PluginInfo: +# if not is_plugin_event_handler(f): +# raise ValueError(f"Function '{f.__qualname__}' is not a plugin event handler.") +# return getattr(f, "__ez_plugin__") + + +def extend_ez(module, alias: str = None, *, THIS=sys.modules[__name__]): + import sys + from pathlib import Path + + path = Path(module.__file__) + + name = alias or path.stem + setattr(THIS, alias or path.stem, module) + sys.modules[f"ez.{name}"] = module #endregion @@ -186,52 +204,52 @@ def emit(event: Event, *args, __ez=_EZ.ez, **kwargs): #endregion -#region Plugin System +# #region Plugin System -def get_plugins(__ez=_EZ.ez): - return __ez.plugin_manager.get_plugins() +# def get_plugins(__ez=_EZ.ez): +# return __ez.plugin_manager.get_plugins() -def load_plugin(plugin: str, __ez=_EZ.ez): - return __ez.plugin_manager.load_plugin(plugin) +# def load_plugin(plugin: str, __ez=_EZ.ez): +# return __ez.plugin_manager.load_plugin(plugin) -def load_plugins(__ez=_EZ.ez): - from ez.database.models.plugin import PluginModel +# def load_plugins(__ez=_EZ.ez): +# from ez.database.models.plugin import PluginModel - names = [plugin.dir_name for plugin in PluginModel.filter_by(enabled=True).all()] +# names = [plugin.dir_name for plugin in PluginModel.filter_by(enabled=True).all()] - return __ez.plugin_manager.load_plugins(names) +# return __ez.plugin_manager.load_plugins(names) -def enable_plugin(plugin: str, __ez=_EZ.ez): - return __ez.plugin_manager.enable_plugin(plugin) +# def enable_plugin(plugin: str, __ez=_EZ.ez): +# return __ez.plugin_manager.enable_plugin(plugin) -def disable_plugin(plugin: str, __ez=_EZ.ez): - plugin_info = __ez.plugin_manager.get_plugin_info(plugin) - __ez.remove_plugin_events(plugin_info.dir_name) - return __ez.plugin_manager.disable_plugin(plugin) +# def disable_plugin(plugin: str, __ez=_EZ.ez): +# plugin_info = __ez.plugin_manager.get_plugin_info(plugin) +# __ez.remove_plugin_events(plugin_info.dir_name) +# return __ez.plugin_manager.disable_plugin(plugin) -def install_plugin(path: str | Path, __Path=Path, __ez=_EZ.ez): - if isinstance(path, str): - path = __Path(path) +# def install_plugin(path: str | Path, __Path=Path, __ez=_EZ.ez): +# if isinstance(path, str): +# path = __Path(path) - if path.suffix == ".zip": - return __ez.plugin_installer.install_from_zip(path) - elif path.is_dir(): - return __ez.plugin_installer.install_from_path(path) - else: - raise ValueError("Invalid path to plugin: must be a directory or a zip file.") +# if path.suffix == ".zip": +# return __ez.plugin_installer.install_from_zip(path) +# elif path.is_dir(): +# return __ez.plugin_installer.install_from_path(path) +# else: +# raise ValueError("Invalid path to plugin: must be a directory or a zip file.") -def uninstall_plugin(plugin: str, __ez=_EZ.ez): - return __ez.plugin_installer.uninstall_plugin(plugin) +# def uninstall_plugin(plugin: str, __ez=_EZ.ez): +# return __ez.plugin_installer.uninstall_plugin(plugin) -#endregion +# #endregion #region Module System @@ -400,13 +418,17 @@ def _setup(__ez=_EZ.ez): del Modules - from ez.events import Plugins + from ez.plugins import PluginEvent, __pm - emit(Plugins.WillLoad) - load_plugins() - emit(Plugins.DidLoad) + emit(PluginEvent.WillLoad) + # TODO: WTF???? + __pm.load_plugins( + "test-plugin", + "title-changer" + ) + emit(PluginEvent.DidLoad) - del Plugins + del PluginEvent def _run(setup=_setup): @@ -431,10 +453,10 @@ def run(_run=_run): del _EZ del EZApplication -del PluginManager +# del PluginManager del ModuleManager -del UnknownPluginError +# del UnknownPluginError del FastAPI @@ -448,7 +470,10 @@ def run(_run=_run): del Path +del sys + __all__ = [ + "EZError", "on", "once", "emit", From 5090f066d9323919735698c1e75427f0b2e24b94 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 12:21:02 +0200 Subject: [PATCH 08/45] Add support for plugin entry point --- modules/plugins/builtins/loader.py | 20 ++++++++++++++++---- modules/plugins/machinery/loader.py | 5 ++++- modules/plugins/machinery/manager.py | 13 ++++++++++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/modules/plugins/builtins/loader.py b/modules/plugins/builtins/loader.py index da7b71f..4848171 100644 --- a/modules/plugins/builtins/loader.py +++ b/modules/plugins/builtins/loader.py @@ -5,6 +5,7 @@ from utilities.version import Version +from ..plugin import Plugin from ..plugin_info import PluginId, PluginInfo from ..machinery.loader import IPluginLoader, PluginLoaderInfo @@ -22,6 +23,7 @@ class EZPluginLoader(IPluginLoader): EZ_PLUGIN_ENTRY_POINT_FILENAME = "plugin.py" EZ_PLUGIN_API_ATTRIBUTE_NAME = "__api__" EZ_PLUGIN_INFO_ATTRIBUTE_NAME = "__info__" + EZ_PLUGIN_MAIN_FUNCTION_NAME = "main" EZ_PLUGIN_PREFIX = "ez.current-site.plugins" def __init__(self, plugin_dir: PluginId) -> None: @@ -32,12 +34,10 @@ def __init__(self, plugin_dir: PluginId) -> None: root = sys.modules[self.EZ_PLUGIN_PREFIX] = PluginModule(self.EZ_PLUGIN_PREFIX) root.__path__ = [str(self.plugin_dir)] - def load(self, plugin_id: PluginId, plugin: EZPlugin) -> EZPlugin | None: + def load(self, plugin_id: PluginId, plugin: Plugin) -> EZPlugin | None: if plugin is None: return self._load_plugin(plugin_id) - if not isinstance(plugin, EZPlugin): - # TODO: Add a custom exception for this, deriving from PluginLoaderException - raise TypeError(f"Expected EZPlugin, got {type(plugin)}") + plugin = self._assert_ez_plugin(plugin) if plugin.is_loaded: if plugin.enabled: return None @@ -82,6 +82,13 @@ def _load_plugin(self, plugin_id: PluginId) -> EZPlugin: ez_plugin.api = getattr(module, self.get_api_attribute_name(), None) return ez_plugin + + def run_main(self, plugin: Plugin) -> None: + plugin = self._assert_ez_plugin(plugin) + if plugin.enabled: + main = getattr(plugin.module, self.EZ_PLUGIN_MAIN_FUNCTION_NAME, None) + if callable(main): + main() def _load_module(self, plugin_id: PluginId) -> PluginModule: path = self._get_plugin_path(plugin_id) @@ -98,6 +105,11 @@ def _reload_plugin(self, plugin: EZPlugin) -> None: def _get_plugin_path(self, plugin_id: PluginId): return self.plugin_dir / str(plugin_id) / self.get_entry_point_filename() + def _assert_ez_plugin(self, plugin: Plugin) -> EZPlugin: + if not isinstance(plugin, EZPlugin): + raise TypeError(f"Expected EZPlugin, got {type(plugin)}") + return plugin + @classmethod def get_entry_point_filename(cls) -> str: return cls.EZ_PLUGIN_ENTRY_POINT_FILENAME diff --git a/modules/plugins/machinery/loader.py b/modules/plugins/machinery/loader.py index 11961f1..174d361 100644 --- a/modules/plugins/machinery/loader.py +++ b/modules/plugins/machinery/loader.py @@ -26,4 +26,7 @@ def plugin_dir(self) -> Path: return self._plugin_dir def load(self, plugin_id: PluginId, plugin: Plugin | None) -> Plugin | None: - raise NotImplementedError \ No newline at end of file + raise NotImplementedError + + def run_main(self, plugin: Plugin) -> None: + return None diff --git a/modules/plugins/machinery/manager.py b/modules/plugins/machinery/manager.py index 25f19f0..29d26bd 100644 --- a/modules/plugins/machinery/manager.py +++ b/modules/plugins/machinery/manager.py @@ -179,10 +179,21 @@ def load_plugin(self, plugin_id: PluginId): return plugin - def load_plugins(self, *plugin_ids: PluginId): + def load_plugins(self, *plugin_ids: PluginId, run_main: bool = True): for plugin_id in plugin_ids: self.load_plugin(plugin_id) + if run_main: + self.run_plugins(*plugin_ids) + + def run_plugins(self, *plugin_ids: PluginId): + if not plugin_ids: + plugin_ids = self._plugins.keys() + for plugin_id in plugin_ids: + plugin = self.get_plugin(plugin_id) + loader = self.get_loader(plugin.loader.id) + loader.run_main(plugin) + #endregion @classmethod From 23ae4b2e348e6246dbd33978d7a3f48c0d9285a6 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 22:47:58 +0200 Subject: [PATCH 09/45] Make `installer.info` and `loader.info` class vars --- modules/plugins/machinery/installer.py | 4 ++-- modules/plugins/machinery/loader.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/plugins/machinery/installer.py b/modules/plugins/machinery/installer.py index 533fe7b..031cd12 100644 --- a/modules/plugins/machinery/installer.py +++ b/modules/plugins/machinery/installer.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from pathlib import Path -from typing import TypeAlias +from typing import TypeAlias, ClassVar from utilities.version import Version @@ -29,7 +29,7 @@ def is_upgrade(self) -> bool: class IPluginInstaller: - info: PluginInstallerInfo + info: ClassVar[PluginInstallerInfo] def __init__(self, plugin_dir: str) -> None: self._plugin_dir = Path(plugin_dir) diff --git a/modules/plugins/machinery/loader.py b/modules/plugins/machinery/loader.py index 174d361..58d00b0 100644 --- a/modules/plugins/machinery/loader.py +++ b/modules/plugins/machinery/loader.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from pathlib import Path -from typing import TypeAlias +from typing import TypeAlias, ClassVar from ..plugin import Plugin from ..plugin_info import PluginId @@ -16,7 +16,7 @@ class PluginLoaderInfo: class IPluginLoader: - info: PluginLoaderInfo + info: ClassVar[PluginLoaderInfo] def __init__(self, plugin_dir: str) -> None: self._plugin_dir = Path(plugin_dir) From 7fb042e18ccba35cb8f8423128ab8475f4446ca8 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 23:03:04 +0200 Subject: [PATCH 10/45] Remove comments --- core/ez/__init__.py | 65 +++------------------------------------------ 1 file changed, 4 insertions(+), 61 deletions(-) diff --git a/core/ez/__init__.py b/core/ez/__init__.py index 0f136fe..e0ab01d 100644 --- a/core/ez/__init__.py +++ b/core/ez/__init__.py @@ -42,11 +42,6 @@ from utilities.event import Event from utilities.event_emitter import EventEmitter from web.response import _EzResponse -# from plugins.manager import PluginManager -# from plugins.installer import PluginInstaller -# from plugins.errors import UnknownPluginError -# from plugins.plugin_info import PluginInfo - from web.app.app import EZApplication from ez.errors import EZError @@ -68,8 +63,6 @@ class _EZ: app: FastAPI plugin_events: dict[str, list[tuple[str, Callable]]] - # plugin_manager: PluginManager - # plugin_installer: PluginInstaller mm: ModuleManager @@ -82,8 +75,6 @@ def __init__(self, app: FastAPI | None = None, ee: EventEmitter | None = None) - self.app = app or EZApplication(redirect_slashes=True) self.plugin_events = {} - # self.plugin_manager = PluginManager(PLUGINS_DIR) - # self.plugin_installer = PluginInstaller(self.plugin_manager) self.mm = ModuleManager(MODULE_DIR) @@ -133,10 +124,10 @@ def is_plugin_event_handler(f): return callable(f) and getattr(f, "__ez_plugin__", None) is not None -# def get_plugin_event_handler_info(f) -> PluginInfo: -# if not is_plugin_event_handler(f): -# raise ValueError(f"Function '{f.__qualname__}' is not a plugin event handler.") -# return getattr(f, "__ez_plugin__") +def get_plugin_event_handler_info(f): + if not is_plugin_event_handler(f): + raise ValueError(f"Function '{f.__qualname__}' is not a plugin event handler.") + return getattr(f, "__ez_plugin__") def extend_ez(module, alias: str = None, *, THIS=sys.modules[__name__]): @@ -204,54 +195,6 @@ def emit(event: Event, *args, __ez=_EZ.ez, **kwargs): #endregion -# #region Plugin System - - -# def get_plugins(__ez=_EZ.ez): -# return __ez.plugin_manager.get_plugins() - - -# def load_plugin(plugin: str, __ez=_EZ.ez): -# return __ez.plugin_manager.load_plugin(plugin) - - -# def load_plugins(__ez=_EZ.ez): -# from ez.database.models.plugin import PluginModel - -# names = [plugin.dir_name for plugin in PluginModel.filter_by(enabled=True).all()] - -# return __ez.plugin_manager.load_plugins(names) - - -# def enable_plugin(plugin: str, __ez=_EZ.ez): -# return __ez.plugin_manager.enable_plugin(plugin) - - -# def disable_plugin(plugin: str, __ez=_EZ.ez): -# plugin_info = __ez.plugin_manager.get_plugin_info(plugin) -# __ez.remove_plugin_events(plugin_info.dir_name) -# return __ez.plugin_manager.disable_plugin(plugin) - - -# def install_plugin(path: str | Path, __Path=Path, __ez=_EZ.ez): -# if isinstance(path, str): -# path = __Path(path) - -# if path.suffix == ".zip": -# return __ez.plugin_installer.install_from_zip(path) -# elif path.is_dir(): -# return __ez.plugin_installer.install_from_path(path) -# else: -# raise ValueError("Invalid path to plugin: must be a directory or a zip file.") - - -# def uninstall_plugin(plugin: str, __ez=_EZ.ez): -# return __ez.plugin_installer.uninstall_plugin(plugin) - - -# #endregion - - #region Module System From c921ef7e57296ba057a583435763fe9e084ee717 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 23:46:02 +0200 Subject: [PATCH 11/45] Add URI class and tools --- modules/utilities/uri.py | 147 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 modules/utilities/uri.py diff --git a/modules/utilities/uri.py b/modules/utilities/uri.py new file mode 100644 index 0000000..f6925a1 --- /dev/null +++ b/modules/utilities/uri.py @@ -0,0 +1,147 @@ +from io import StringIO, SEEK_CUR + + +class URIParser: + def __init__(self, source: str) -> None: + self.source = source + self.stream = StringIO(source) + + def parse(self) -> "UnifiedResourceIdentifier": + scheme = self._parse_scheme() + if self.stream.read(2) == "//": + authority = self._parse_authority() + else: + authority = None + self.stream.seek(-1, SEEK_CUR) + path = self._parse_path() + query = self._parse_query() + fragment = self._parse_fragment() + + return UnifiedResourceIdentifier(scheme, authority, path, query, fragment, self.source) + + def _parse_scheme(self) -> str: + scheme = "" + while (char := self.stream.read(1)) != ":": + scheme += char + return scheme + + def _parse_authority(self) -> "URIAuthority": + userinfo = self._parse_userinfo() + host = self._parse_host() + port = self._parse_port() + return URIAuthority(userinfo, host, port) + + def _parse_userinfo(self) -> "URIUserInfo": + username = "" + while (char := self.stream.read(1)) != "@": + username += char + password = "" + while (char := self.stream.read(1)) != ":": + password += char + return URIUserInfo(username, password) + + def _parse_host(self) -> str: + host = "" + while (char := self.stream.read(1)) != ":" and char != "/": + host += char + return host + + def _parse_port(self) -> int: + port = "" + while (char := self.stream.read(1)) != "/": + port += char + return int(port) + + def _parse_path(self) -> str: + path = "" + while (char := self.stream.read(1)) != "?": + path += char + return path + + def _parse_query(self) -> str: + query = "" + while (char := self.stream.read(1)) != "#": + query += char + return query + + def _parse_fragment(self) -> str: + return self.stream.read() + + +class URIUserInfo: + username: str + password: str + + def __init__(self, username: str, password: str) -> None: + self.username = username + self.password = password + + +class URIAuthority: + userinfo: URIUserInfo + host: str + port: int + + def __init__(self, userinfo: URIUserInfo, host: str, port: int) -> None: + self.userinfo = userinfo + self.host = host + self.port = port + + +class URIPath: + SEPARATOR: str = "/" + + raw: str + segments: list[str] + + def __init__(self, raw: str) -> None: + self.raw = raw + self.segments = raw.split(self.SEPARATOR) + + @property + def name(self) -> str: + return self.segments[-1] + + @property + def parent(self) -> "URIPath": + return URIPath(self.SEPARATOR.join(self.segments[:-1])) + + def __str__(self) -> str: + return self.raw + + def __truediv__(self, other: str) -> "URIPath": + return URIPath(self.raw + self.SEPARATOR + other) + + def __rtruediv__(self, other: str) -> "URIPath": + return URIPath(other + self.SEPARATOR + self.raw) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, URIPath): + return NotImplemented + return self.raw == other.raw + + def __hash__(self) -> int: + return hash(self.raw) + + +class UnifiedResourceIdentifier: + scheme: str + authority: URIAuthority | None + path: str + query: str + fragment: str + + raw: str + + def __init__(self, scheme: str, authority: URIAuthority, path: str, query: str, fragment: str, raw: str) -> None: + self.scheme = scheme + self.authority = authority + self.path = path + self.query = query + self.fragment = fragment + self.raw = raw + + @classmethod + def parse(cls, uri: str) -> "UnifiedResourceIdentifier": + parser = URIParser(uri) + return parser.parse() From 8ef346918b557a5bca44d75949208f1735de9352 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 23:46:26 +0200 Subject: [PATCH 12/45] Add core resource types --- modules/resources/__init__.py | 5 +++ modules/resources/machinery/loader.py | 17 +++++++++ modules/resources/manager.py | 23 ++++++++++++ modules/resources/resource.py | 50 +++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 modules/resources/__init__.py create mode 100644 modules/resources/machinery/loader.py create mode 100644 modules/resources/manager.py create mode 100644 modules/resources/resource.py diff --git a/modules/resources/__init__.py b/modules/resources/__init__.py new file mode 100644 index 0000000..4cf0262 --- /dev/null +++ b/modules/resources/__init__.py @@ -0,0 +1,5 @@ +from .resource import Resource, URI + + +def load_resource(uri: URI) -> Resource: + ... diff --git a/modules/resources/machinery/loader.py b/modules/resources/machinery/loader.py new file mode 100644 index 0000000..cdc0ff9 --- /dev/null +++ b/modules/resources/machinery/loader.py @@ -0,0 +1,17 @@ +from typing import TypeAlias + +from ..resource import Resource, ResourceType, URI + + +ResourceLoaderId: TypeAlias = str + + +class ResourceLoaderInfo: + id: ResourceLoaderId + name: str + + + +class IResourceLoader: + def load_resource(self, uri: URI, resource_type: ResourceType | None = None) -> Resource: + raise NotImplementedError diff --git a/modules/resources/manager.py b/modules/resources/manager.py new file mode 100644 index 0000000..11fc715 --- /dev/null +++ b/modules/resources/manager.py @@ -0,0 +1,23 @@ +from .machinery.loader import IResourceLoader, ResourceLoaderId +from .resource import Resource, ResourceType, URI + + +class ResourceManager: + _loaders: dict[ResourceLoaderId, IResourceLoader] + _resources: dict[URI, Resource] + + def __init__(self) -> None: + self._loaders = {} + self._resources = {} + + def load_resource(self, uri: URI, resource_type: ResourceType | None = None, *, enable_caching: bool = True): + try: + return self._resources[uri] + except KeyError: + resource = self._load_resource(uri, resource_type) + if enable_caching: + self._resources[uri] = resource + return resource + + def _load_resource(self, uri: URI, resource_type: ResourceType | None = None): + raise NotImplementedError diff --git a/modules/resources/resource.py b/modules/resources/resource.py new file mode 100644 index 0000000..3c4a077 --- /dev/null +++ b/modules/resources/resource.py @@ -0,0 +1,50 @@ +from typing import TypeAlias, TypeVar, Type + +from utilities.uri import UnifiedResourceIdentifier + + +ResourceTypeId: TypeAlias = str +URI: TypeAlias = UnifiedResourceIdentifier + +ResourceT = TypeVar("ResourceT", bound="Resource") + + +class ResourceType: + _id: ResourceTypeId + _name: str + + def __init__(self, name: str, id: ResourceTypeId) -> None: + self._name = name + self._id = id + + @property + def name(self): + return self._name + + @property + def id(self): + return self._id + + +class Resource: + _type: ResourceType + _uri: URI + + def __init__(self, resource_type: ResourceType, uri: str) -> None: + self._type = resource_type + self._uri = uri + + @property + def type(self): + return self._type + + @property + def uri(self): + return self._uri + + def cast(self, cls: Type[ResourceT], *, strict: bool = True) -> ResourceT: + if not strict: + return self + if not isinstance(self, cls): + raise TypeError(f"Resource.cast(strict=True) could not convert resource of type {type(self).__name__} to {cls.__name__}") + return self From 0cb42ad739878bedf171caeeea399ffd7fc3ffaf Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Mon, 19 Feb 2024 11:51:12 +0200 Subject: [PATCH 13/45] Rename script variable --- scripts/setup.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index f8d2e16..bf351d2 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -76,8 +76,8 @@ function Load { Write-Host `b"OK" -ForegroundColor $GREEN } else { Write-Host -NoNewLine `b"Failed" -ForegroundColor $RED - $error = Receive-Job $job - Write-Host $error + $jobError = Receive-Job $job + Write-Host $jobError return 1 } } From fb4ceca66c1e8503f882a55c2be148545e60317a Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Mon, 19 Feb 2024 12:54:07 +0200 Subject: [PATCH 14/45] Fix ez.plugins.machinery imports --- modules/plugins/ez_plugins/machinery.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/plugins/ez_plugins/machinery.py b/modules/plugins/ez_plugins/machinery.py index eaaf2da..ca69536 100644 --- a/modules/plugins/ez_plugins/machinery.py +++ b/modules/plugins/ez_plugins/machinery.py @@ -1,6 +1,6 @@ -from ....machinery.installer import IPluginInstaller, PluginInstallerInfo, PluginInstallationResult -from ....machinery.loader import IPluginLoader, PluginLoaderInfo -from ....plugin_info import PluginInfo +from ..machinery.installer import IPluginInstaller, PluginInstallerInfo, PluginInstallationResult +from ..machinery.loader import IPluginLoader, PluginLoaderInfo +from ..plugin_info import PluginInfo __all__ = [ From 68a06ccc97575706c53462c461699cd5a3289871 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Mon, 19 Feb 2024 13:50:47 +0200 Subject: [PATCH 15/45] Temporary fix making the info types inherit BaseModel --- modules/plugins/builtins/installer.py | 6 +++--- modules/plugins/builtins/loader.py | 8 ++++---- modules/plugins/machinery/installer.py | 17 ++++++++++------- modules/plugins/machinery/loader.py | 5 +++-- modules/plugins/plugin_info.py | 14 ++++++++------ modules/utilities/semver.py | 24 +++++++++++++----------- 6 files changed, 41 insertions(+), 33 deletions(-) diff --git a/modules/plugins/builtins/installer.py b/modules/plugins/builtins/installer.py index 01f50ee..55df3e9 100644 --- a/modules/plugins/builtins/installer.py +++ b/modules/plugins/builtins/installer.py @@ -15,9 +15,9 @@ class InvalidPluginManifest(EZPluginInstallerError): class EZPluginInstaller(IPluginInstaller): - info = PluginInstallerInfo( - "ez.plugins.installer", - "EZ Plugin Installer", + info = PluginInstallerInfo.model_construct( + id="ez.plugins.installer", + name="EZ Plugin Installer", ) def install(self, path: str) -> PluginInstallationResult: diff --git a/modules/plugins/builtins/loader.py b/modules/plugins/builtins/loader.py index 4848171..429353b 100644 --- a/modules/plugins/builtins/loader.py +++ b/modules/plugins/builtins/loader.py @@ -15,9 +15,9 @@ class EZPluginLoader(IPluginLoader): - info = PluginLoaderInfo( - "ez.plugins.loader", - "EZ Plugin Loader", + info = PluginLoaderInfo.model_construct( + id="ez.plugins.loader", + name="EZ Plugin Loader", ) EZ_PLUGIN_ENTRY_POINT_FILENAME = "plugin.py" @@ -59,7 +59,7 @@ def _load_plugin(self, plugin_id: PluginId) -> EZPlugin: metadata["version"] = Version.parse(metadata["version"]) if "description" not in metadata: metadata["description"] = module.__doc__ - info = PluginInfo( + info = PluginInfo.model_construct( **metadata ) diff --git a/modules/plugins/machinery/installer.py b/modules/plugins/machinery/installer.py index 031cd12..382adc4 100644 --- a/modules/plugins/machinery/installer.py +++ b/modules/plugins/machinery/installer.py @@ -1,23 +1,26 @@ -from dataclasses import dataclass +from pydantic import BaseModel +from pydantic.dataclasses import dataclass from pathlib import Path -from typing import TypeAlias, ClassVar +from typing import TypeAlias, ClassVar, TYPE_CHECKING from utilities.version import Version -from ..plugin_info import PluginId +if TYPE_CHECKING: + from ..plugin_info import PluginId PluginInstallerId: TypeAlias = str -@dataclass -class PluginInstallerInfo: +@dataclass() +class PluginInstallerInfo(BaseModel): id: PluginInstallerId name: str +# This is not yet pydantic_dataclass because the `Version` type is not compatible with pydantic @dataclass -class PluginInstallationResult: +class PluginInstallationResult(BaseModel): installer_id: PluginInstallerId package_name: str version: Version @@ -41,5 +44,5 @@ def plugin_dir(self) -> Path: def install(self, path: str) -> PluginInstallationResult: raise NotImplementedError - def uninstall(self, plugin_id: str) -> None: + def uninstall(self, plugin_id: "PluginId") -> None: raise NotImplementedError diff --git a/modules/plugins/machinery/loader.py b/modules/plugins/machinery/loader.py index 58d00b0..c107285 100644 --- a/modules/plugins/machinery/loader.py +++ b/modules/plugins/machinery/loader.py @@ -1,4 +1,5 @@ -from dataclasses import dataclass +from pydantic import BaseModel +from pydantic.dataclasses import dataclass from pathlib import Path from typing import TypeAlias, ClassVar @@ -10,7 +11,7 @@ @dataclass -class PluginLoaderInfo: +class PluginLoaderInfo(BaseModel): id: PluginLoaderId name: str diff --git a/modules/plugins/plugin_info.py b/modules/plugins/plugin_info.py index 64341f7..963d04c 100644 --- a/modules/plugins/plugin_info.py +++ b/modules/plugins/plugin_info.py @@ -1,10 +1,12 @@ -from dataclasses import dataclass +from pydantic import BaseModel +from pydantic.dataclasses import dataclass from typing import TypeAlias, TYPE_CHECKING from utilities.version import Version -if TYPE_CHECKING: - from .machinery.installer import PluginInstallerId +# if TYPE_CHECKING: +# +from .machinery.installer import PluginInstallerId PluginId: TypeAlias = str @@ -12,11 +14,11 @@ @dataclass -class PluginInfo: +class PluginInfo(BaseModel): name: str version: Version - description: str - installer_id: "PluginInstallerId" + description: str | None + installer_id: PluginInstallerId package_name: str @property diff --git a/modules/utilities/semver.py b/modules/utilities/semver.py index 15f538a..d5eb3e8 100644 --- a/modules/utilities/semver.py +++ b/modules/utilities/semver.py @@ -1,16 +1,12 @@ -class SemanticVersion: +from pydantic import BaseModel + + +class SemanticVersion(BaseModel): major: int minor: int patch: int - pre_release: str - build: str - - def __init__(self, major: int, minor: int, patch: int, pre_release: str = "", build: str = ""): - self.major = major - self.minor = minor - self.patch = patch - self.pre_release = pre_release - self.build = build + pre_release: str = "" + build: str = "" @classmethod def parse(cls, version: str): @@ -24,7 +20,13 @@ def parse(cls, version: str): if "+" in pre_release: pre_release, build = pre_release.split("+") - return cls(int(major), int(minor), int(patch), pre_release, build) + return cls.model_construct( + major=int(major), + minor=int(minor), + patch=int(patch), + pre_release=pre_release, + build=build + ) def __str__(self): return f"{self.major}.{self.minor}.{self.patch}{f'-{self.pre_release}' if self.pre_release else ''}{f'+{self.build}' if self.build else ''}" From 5fc12a73575d7aa460b3f47b90c78b14d4243ca9 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Tue, 20 Feb 2024 13:16:51 +0200 Subject: [PATCH 16/45] Add base components --- modules/ezjsx/__main__.py | 3 +++ modules/ezjsx/components/__init__.py | 2 ++ modules/ezjsx/components/card.py | 14 +++++++++++++ modules/ezjsx/components/page.py | 30 ++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 modules/ezjsx/components/__init__.py create mode 100644 modules/ezjsx/components/card.py create mode 100644 modules/ezjsx/components/page.py diff --git a/modules/ezjsx/__main__.py b/modules/ezjsx/__main__.py index e92886d..853c10a 100644 --- a/modules/ezjsx/__main__.py +++ b/modules/ezjsx/__main__.py @@ -6,6 +6,7 @@ from jsx.html import Element from ez.events import HTTP from .events import TreeRenderer +from . import components @ez.on(HTTP.Out) @@ -21,6 +22,8 @@ def render_tree(_): mount(ez._app) +ez.extend_ez(components, "jsx") + __title__ = "EZ JSX Integration" __version__ = "1.0.0" __description__ = \ diff --git a/modules/ezjsx/components/__init__.py b/modules/ezjsx/components/__init__.py new file mode 100644 index 0000000..716568d --- /dev/null +++ b/modules/ezjsx/components/__init__.py @@ -0,0 +1,2 @@ +from .page import Page +from .card import Card \ No newline at end of file diff --git a/modules/ezjsx/components/card.py b/modules/ezjsx/components/card.py new file mode 100644 index 0000000..9142103 --- /dev/null +++ b/modules/ezjsx/components/card.py @@ -0,0 +1,14 @@ +from jsx.components import Component +from jsx.html import Div + +class Card(Component): + def __init__(self, *children, **props): + self.children = children + props["class_name"] = [props.get("class_name", ""), "card"] + self.props = props + + def render(self): + return Div( + *self.children, + **self.props, + ) \ No newline at end of file diff --git a/modules/ezjsx/components/page.py b/modules/ezjsx/components/page.py new file mode 100644 index 0000000..22f541a --- /dev/null +++ b/modules/ezjsx/components/page.py @@ -0,0 +1,30 @@ +from jsx.components import Component +from jsx.html import Fragment, Html, Head, Body, Title, Link, Script + + +class Page(Component): + def __init__(self, title="Ez Web", *children, **props): + self.title = title + self.children = children + self.props = props + + def render(self): + return Fragment( + "", + Html( + Head( + Title(self.title), + Link( + rel="stylesheet", + href="https://unpkg.com/browse/bootstrap@5.3.2/dist/css/bootstrap.min.css", + ), + Script(src="/_jsx/main.js"), + ), + Body( + self.body(), + ), + ), + ) + + def body(self): + return Fragment(*self.children) From 103b2692613999fe4d143c5bb5745e9dade1ba57 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Tue, 20 Feb 2024 13:18:00 +0200 Subject: [PATCH 17/45] Change request context middleware to make sense --- core/ez/__init__.py | 4 +++- modules/web/app/app.py | 21 ++++++--------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/core/ez/__init__.py b/core/ez/__init__.py index e0ab01d..a2b3357 100644 --- a/core/ez/__init__.py +++ b/core/ez/__init__.py @@ -25,7 +25,7 @@ PLUGINS_DIR: Path = SITE_DIR / "plugins" MODULE_DIR: Path = EZ_FRAMEWORK_DIR / "modules" PLUGIN_API_DIR: Path = SITE_DIR / "lib" / "public-api" / "plugins" - +EZ_ROUTE_ATTRIBUTE = "ez_web_route" #endregion @@ -230,6 +230,7 @@ async def wrapper(*args, **kwargs): result = await handler(*args, **kwargs) return response._auto_body(result) + setattr(wrapper, EZ_ROUTE_ATTRIBUTE, True) __ez.app.add_api_route(route, endpoint=wrapper, methods=methods) else: @@ -238,6 +239,7 @@ def wrapper(*args, **kwargs): result = handler(*args, **kwargs) return response._auto_body(result) + setattr(wrapper, EZ_ROUTE_ATTRIBUTE, True) __ez.app.add_api_route(route, endpoint=wrapper, methods=methods) log.debug(f"{methods} {route} -> {handler.__name__}") diff --git a/modules/web/app/app.py b/modules/web/app/app.py index 8b3887a..8d33dd6 100644 --- a/modules/web/app/app.py +++ b/modules/web/app/app.py @@ -21,26 +21,17 @@ async def dispatch( ): import ez - if request.url.path in docs_urls: - return await call_next(request) - - if request.url.path.startswith("/socket.io"): - return await call_next(request) - - if request.url.path == STATIC_PATH or request.url.path.startswith( - f"{STATIC_PATH}/" - ): - result = await call_next(request) - if result.status_code < 400: - return result - ez.request = request ez.response = _EzResponse() + ez.emit(HTTP.In, request) result = await call_next(request) - if 300 <= result.status_code < 400: + + route = request.scope.get("endpoint") + if not route or not hasattr(route, ez.EZ_ROUTE_ATTRIBUTE) or not getattr(route, ez.EZ_ROUTE_ATTRIBUTE): return result + ez.emit(HTTP.Out, ez.response) @@ -67,4 +58,4 @@ def _exception_handler(self, request: Request, exc: Exception): """ import ez - return ez.response.text(str(exc)) + return str(exc) From a46dd6b4b9613665947f5efb4fb22d0edcffed1d Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Tue, 20 Feb 2024 13:18:44 +0200 Subject: [PATCH 18/45] Add simple api for plugins --- modules/plugins/__main__.py | 1 + modules/plugins/plugin_info.py | 5 +++-- modules/plugins/router.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 modules/plugins/router.py diff --git a/modules/plugins/__main__.py b/modules/plugins/__main__.py index e69de29..8cd13e5 100644 --- a/modules/plugins/__main__.py +++ b/modules/plugins/__main__.py @@ -0,0 +1 @@ +from . import router \ No newline at end of file diff --git a/modules/plugins/plugin_info.py b/modules/plugins/plugin_info.py index 963d04c..60555bf 100644 --- a/modules/plugins/plugin_info.py +++ b/modules/plugins/plugin_info.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.dataclasses import dataclass from typing import TypeAlias, TYPE_CHECKING from utilities.version import Version @@ -18,8 +18,9 @@ class PluginInfo(BaseModel): name: str version: Version description: str | None - installer_id: PluginInstallerId + installer_id: PluginInstallerId = Field(exclude=True) package_name: str + author: str @property def id(self) -> PluginId: diff --git a/modules/plugins/router.py b/modules/plugins/router.py new file mode 100644 index 0000000..b09f9ff --- /dev/null +++ b/modules/plugins/router.py @@ -0,0 +1,21 @@ +import ez +import ez.plugins + + +@ez.get("/api/plugins") +def get_plugins(): + return [plugin.info.model_dump() for plugin in ez.plugins.get_plugins()] + + +@ez.post("/api/plugins/{plugin_id}/enable") +def enable_plugin(plugin_id: str): + print(f"Enabling plugin {plugin_id}...") + ez.plugins.enable_plugin(plugin_id) + return "Plugin enabled!" + + +@ez.post("/api/plugins/{plugin_id}/disable") +def disable_plugin(plugin_id: str): + print(f"Disabling plugin {plugin_id}...") + ez.plugins.disable_plugin(plugin_id) + return "Plugin disabled!" From 114739bf4351cf20e6044cc5db191aab2e97d96e Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen <42520501+binyamin555@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:22:41 +0200 Subject: [PATCH 19/45] Update app.py --- modules/web/app/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/web/app/app.py b/modules/web/app/app.py index 8d33dd6..7c07731 100644 --- a/modules/web/app/app.py +++ b/modules/web/app/app.py @@ -29,7 +29,7 @@ async def dispatch( result = await call_next(request) route = request.scope.get("endpoint") - if not route or not hasattr(route, ez.EZ_ROUTE_ATTRIBUTE) or not getattr(route, ez.EZ_ROUTE_ATTRIBUTE): + if not route or not getattr(route, ez.EZ_ROUTE_ATTRIBUTE, False): return result From 272774711e0fe15f6bdafbce64a99c4e2674f243 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen <42520501+binyamin555@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:35:53 +0200 Subject: [PATCH 20/45] Update __main__.py --- modules/ezjsx/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ezjsx/__main__.py b/modules/ezjsx/__main__.py index 853c10a..e4dcdef 100644 --- a/modules/ezjsx/__main__.py +++ b/modules/ezjsx/__main__.py @@ -24,9 +24,9 @@ def render_tree(_): ez.extend_ez(components, "jsx") -__title__ = "EZ JSX Integration" +__module_name__ = "EZ JSX Integration" __version__ = "1.0.0" -__description__ = \ +__doc__ = \ """ This module enables the jsx library for use in EZ Web Framework. """ From ffd97550e8d384057d869ffcd870a5a939a2c14c Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Tue, 20 Feb 2024 13:16:51 +0200 Subject: [PATCH 21/45] Add base components --- modules/ezjsx/__main__.py | 3 +++ modules/ezjsx/components/__init__.py | 2 ++ modules/ezjsx/components/card.py | 14 +++++++++++++ modules/ezjsx/components/page.py | 30 ++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 modules/ezjsx/components/__init__.py create mode 100644 modules/ezjsx/components/card.py create mode 100644 modules/ezjsx/components/page.py diff --git a/modules/ezjsx/__main__.py b/modules/ezjsx/__main__.py index e92886d..853c10a 100644 --- a/modules/ezjsx/__main__.py +++ b/modules/ezjsx/__main__.py @@ -6,6 +6,7 @@ from jsx.html import Element from ez.events import HTTP from .events import TreeRenderer +from . import components @ez.on(HTTP.Out) @@ -21,6 +22,8 @@ def render_tree(_): mount(ez._app) +ez.extend_ez(components, "jsx") + __title__ = "EZ JSX Integration" __version__ = "1.0.0" __description__ = \ diff --git a/modules/ezjsx/components/__init__.py b/modules/ezjsx/components/__init__.py new file mode 100644 index 0000000..716568d --- /dev/null +++ b/modules/ezjsx/components/__init__.py @@ -0,0 +1,2 @@ +from .page import Page +from .card import Card \ No newline at end of file diff --git a/modules/ezjsx/components/card.py b/modules/ezjsx/components/card.py new file mode 100644 index 0000000..9142103 --- /dev/null +++ b/modules/ezjsx/components/card.py @@ -0,0 +1,14 @@ +from jsx.components import Component +from jsx.html import Div + +class Card(Component): + def __init__(self, *children, **props): + self.children = children + props["class_name"] = [props.get("class_name", ""), "card"] + self.props = props + + def render(self): + return Div( + *self.children, + **self.props, + ) \ No newline at end of file diff --git a/modules/ezjsx/components/page.py b/modules/ezjsx/components/page.py new file mode 100644 index 0000000..22f541a --- /dev/null +++ b/modules/ezjsx/components/page.py @@ -0,0 +1,30 @@ +from jsx.components import Component +from jsx.html import Fragment, Html, Head, Body, Title, Link, Script + + +class Page(Component): + def __init__(self, title="Ez Web", *children, **props): + self.title = title + self.children = children + self.props = props + + def render(self): + return Fragment( + "", + Html( + Head( + Title(self.title), + Link( + rel="stylesheet", + href="https://unpkg.com/browse/bootstrap@5.3.2/dist/css/bootstrap.min.css", + ), + Script(src="/_jsx/main.js"), + ), + Body( + self.body(), + ), + ), + ) + + def body(self): + return Fragment(*self.children) From 423d63e46d508da13f9c0f98fc28a8c06d27f2f4 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Tue, 20 Feb 2024 15:13:07 +0200 Subject: [PATCH 22/45] Add page wrapping for non-page renders --- modules/ezjsx/__main__.py | 16 ++++++++++------ modules/ezjsx/components/page.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/ezjsx/__main__.py b/modules/ezjsx/__main__.py index 853c10a..5577d6a 100644 --- a/modules/ezjsx/__main__.py +++ b/modules/ezjsx/__main__.py @@ -14,19 +14,23 @@ def render_tree(_): if ez.request.method != "GET": return - if isinstance(ez.response.body, (Component, Element)): - ez.emit(TreeRenderer.WillRender, ez.response.body) - result = render(ez.response.body) - ez.emit(TreeRenderer.DidRender, ez.response.body, result) + body = ez.response.body + if isinstance(body, (Component, Element)): + if not isinstance(body, components.Page): + body = components.Page(body) + + ez.emit(TreeRenderer.WillRender, body) + result = render(body) + ez.emit(TreeRenderer.DidRender, body, result) ez.response.html(result) + mount(ez._app) ez.extend_ez(components, "jsx") __title__ = "EZ JSX Integration" __version__ = "1.0.0" -__description__ = \ -""" +__description__ = """ This module enables the jsx library for use in EZ Web Framework. """ diff --git a/modules/ezjsx/components/page.py b/modules/ezjsx/components/page.py index 22f541a..806d63a 100644 --- a/modules/ezjsx/components/page.py +++ b/modules/ezjsx/components/page.py @@ -3,7 +3,7 @@ class Page(Component): - def __init__(self, title="Ez Web", *children, **props): + def __init__(self, *children, title="Ez Web", **props): self.title = title self.children = children self.props = props From a06bb10855e092b9d7a1ff536bb5ed32e4f36ba2 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 17:30:59 +0200 Subject: [PATCH 23/45] Fix `Version` model validation and serialization --- modules/utilities/semver.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/utilities/semver.py b/modules/utilities/semver.py index d5eb3e8..d264cd1 100644 --- a/modules/utilities/semver.py +++ b/modules/utilities/semver.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, model_validator, model_serializer class SemanticVersion(BaseModel): @@ -28,6 +28,17 @@ def parse(cls, version: str): build=build ) + @model_validator(mode="before") + @classmethod + def validate(cls, data, _): + if isinstance(data, str): + return cls.parse(data) + return data + + @model_serializer + def serialize(self): + return str(self) + def __str__(self): return f"{self.major}.{self.minor}.{self.patch}{f'-{self.pre_release}' if self.pre_release else ''}{f'+{self.build}' if self.build else ''}" From 79c3d1d658290571d3528aaf38128860696de40c Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 17:31:21 +0200 Subject: [PATCH 24/45] Move some constants into config.py --- modules/plugins/config.py | 5 +++++ modules/plugins/machinery/manager.py | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/plugins/config.py b/modules/plugins/config.py index 1329a53..07967cc 100644 --- a/modules/plugins/config.py +++ b/modules/plugins/config.py @@ -4,4 +4,9 @@ PLUGINS_DIRECTORY_NAME = "plugins" PLUGINS_DIRECTORY = ez.SITE_DIR / PLUGINS_DIRECTORY_NAME +PLUGINS_PUBLIC_API_DIRECTORY_NAME = "lib/public-api" +PLUGINS_PUBLIC_API_MODULE_NAME = "ez_plugins" + +PLUGINS_PUBLIC_API_DIR = ez.SITE_DIR / PLUGINS_PUBLIC_API_DIRECTORY_NAME / PLUGINS_PUBLIC_API_MODULE_NAME + METADATA_FILENAME = "plugin-metadata" diff --git a/modules/plugins/machinery/manager.py b/modules/plugins/machinery/manager.py index 29d26bd..337c5e9 100644 --- a/modules/plugins/machinery/manager.py +++ b/modules/plugins/machinery/manager.py @@ -21,10 +21,11 @@ from ..machinery.installer import IPluginInstaller from ..machinery.loader import IPluginLoader +from ..config import PLUGINS_PUBLIC_API_MODULE_NAME + class PluginManager: EZ_PLUGIN_PREFIX = "ez.current-site.plugins" - EZ_PUBLIC_API_MODULE = "ez_plugins" def __init__( self, @@ -41,7 +42,7 @@ def __init__( self._default_plugin_installer = self.add_installer(default_installer) self._default_plugin_loader = self.add_loader(default_loader) - self._public_api = public_api or ModuleType(self.EZ_PUBLIC_API_MODULE) + self._public_api = public_api or ModuleType(PLUGINS_PUBLIC_API_MODULE_NAME) #region Plugin Public API @@ -50,7 +51,7 @@ def public_api(self): return self._public_api def enable_public_api(self, alias: str = None): - alias = alias or self.EZ_PUBLIC_API_MODULE + alias = alias or PLUGINS_PUBLIC_API_MODULE_NAME import sys From 58922140f72e66d36ef9e3cd4fd418ec2a1762c4 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 17:31:44 +0200 Subject: [PATCH 25/45] Implement (untested) default plugin installer --- modules/plugins/builtins/installer.py | 49 +++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/modules/plugins/builtins/installer.py b/modules/plugins/builtins/installer.py index 55df3e9..2aa6464 100644 --- a/modules/plugins/builtins/installer.py +++ b/modules/plugins/builtins/installer.py @@ -1,4 +1,13 @@ -from ..errors import EZPluginError +import yaml + +from pathlib import Path +from zipfile import ZipFile, BadZipFile +from pydantic import BaseModel, Field, ValidationError + +from utilities.version import Version + +from ..errors import EZPluginError, PluginAlreadyInstalledError +from ..plugin_info import PackageName from ..machinery.installer import IPluginInstaller, PluginInstallationResult, PluginInstallerInfo @@ -14,6 +23,13 @@ class InvalidPluginManifest(EZPluginInstallerError): ... +class PluginManifest(BaseModel): + name: str + package_name: PackageName = Field(alias="package-name") + version: Version = Field(alias="version") + typing_file: str = Field(alias="typing-file") + + class EZPluginInstaller(IPluginInstaller): info = PluginInstallerInfo.model_construct( id="ez.plugins.installer", @@ -21,7 +37,36 @@ class EZPluginInstaller(IPluginInstaller): ) def install(self, path: str) -> PluginInstallationResult: - ... + path: Path = Path(path) + + if not path.exists(): + raise FileNotFoundError(path) + + if not path.is_file(): + raise IsADirectoryError(path) + + try: + zip_file = ZipFile(path) + except BadZipFile as e: + raise InvalidPluginArchive(path) from e + + with zip_file: + manifest_file = zip_file.open("manifest.yaml") + manifest_data = yaml.safe_load(manifest_file) + try: + manifest = PluginManifest.model_validate(manifest_data) + except ValidationError: + raise InvalidPluginManifest(path) + + plugin_dir = self.plugin_dir / manifest.package_name + if plugin_dir.exists(): + raise PluginAlreadyInstalledError(manifest.package_name) + plugin_dir.mkdir() + + zip_file.extractall(str(plugin_dir)) + + if manifest.typing_file: + (plugin_dir / manifest.typing_file).rename() def uninstall(self, plugin_id: str) -> None: ... From 4a43ac116c2a490097091f81a63a4747a22e12d1 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Tue, 20 Feb 2024 17:44:41 +0200 Subject: [PATCH 26/45] Update components to jsx v0.2.0 --- modules/ezjsx/components/card.py | 18 +++++++++++++----- modules/ezjsx/components/page.py | 11 +++++------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/modules/ezjsx/components/card.py b/modules/ezjsx/components/card.py index 9142103..9439b99 100644 --- a/modules/ezjsx/components/card.py +++ b/modules/ezjsx/components/card.py @@ -1,14 +1,22 @@ -from jsx.components import Component +from jsx.components import ContainerComponent from jsx.html import Div -class Card(Component): + +class Card(ContainerComponent): def __init__(self, *children, **props): - self.children = children - props["class_name"] = [props.get("class_name", ""), "card"] + super().__init__(*children) + + class_name = props.get("class_name", []) + if isinstance(class_name, list): + class_name.append("card") + else: + class_name = [class_name, "card"] + + props["class_name"] = class_name self.props = props def render(self): return Div( *self.children, **self.props, - ) \ No newline at end of file + ) diff --git a/modules/ezjsx/components/page.py b/modules/ezjsx/components/page.py index 806d63a..bbe7988 100644 --- a/modules/ezjsx/components/page.py +++ b/modules/ezjsx/components/page.py @@ -1,12 +1,11 @@ -from jsx.components import Component +from jsx.components import ContainerComponent from jsx.html import Fragment, Html, Head, Body, Title, Link, Script -class Page(Component): - def __init__(self, *children, title="Ez Web", **props): +class Page(ContainerComponent): + def __init__(self, *children, title="Ez Web"): + super().__init__(*children) self.title = title - self.children = children - self.props = props def render(self): return Fragment( @@ -16,7 +15,7 @@ def render(self): Title(self.title), Link( rel="stylesheet", - href="https://unpkg.com/browse/bootstrap@5.3.2/dist/css/bootstrap.min.css", + href="https://unpkg.com/bootstrap@5.3.2/dist/css/bootstrap.min.css", ), Script(src="/_jsx/main.js"), ), From 064a943d3554ae2f723dab5f43515abe065d0247 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 17:55:06 +0200 Subject: [PATCH 27/45] Change the EZ plugin manifest filename to a constant --- modules/plugins/builtins/installer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/plugins/builtins/installer.py b/modules/plugins/builtins/installer.py index 2aa6464..d59ffc3 100644 --- a/modules/plugins/builtins/installer.py +++ b/modules/plugins/builtins/installer.py @@ -36,6 +36,8 @@ class EZPluginInstaller(IPluginInstaller): name="EZ Plugin Installer", ) + EZ_PLUGIN_MANIFEST_FILENAME = "manifest.yaml" + def install(self, path: str) -> PluginInstallationResult: path: Path = Path(path) @@ -51,7 +53,7 @@ def install(self, path: str) -> PluginInstallationResult: raise InvalidPluginArchive(path) from e with zip_file: - manifest_file = zip_file.open("manifest.yaml") + manifest_file = zip_file.open(self.EZ_PLUGIN_MANIFEST_FILENAME) manifest_data = yaml.safe_load(manifest_file) try: manifest = PluginManifest.model_validate(manifest_data) From dc6302b1d200b08f3b1e10047f7104677ace7d32 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 17:56:57 +0200 Subject: [PATCH 28/45] Remove old installer --- modules/plugins/installer.py | 128 ----------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 modules/plugins/installer.py diff --git a/modules/plugins/installer.py b/modules/plugins/installer.py deleted file mode 100644 index cd36367..0000000 --- a/modules/plugins/installer.py +++ /dev/null @@ -1,128 +0,0 @@ -import ez - -from pathlib import Path -from zipfile import ZipFile -from yaml import load, Loader -from shutil import rmtree - -from pydantic import BaseModel - -from utilities.semver import SemanticVersion - -from .manager import PluginManager - - -def read_manifest(path: Path | str): - if isinstance(path, str): - path = Path(path) - manifest_file = path / PluginInstaller.MANIFEST_FILENAME - - if not manifest_file.exists(): - raise FileNotFoundError(f"Manifest file not found in {path}") - - with manifest_file.open("r") as file: - data = load(file, Loader=Loader) - for key in data: - if '-' in key: - new_key = key.replace('-', '_') - data[new_key] = data.pop(key) - manifest = PluginManifest.model_validate(data) - - return manifest - - -def move_file(src: Path, dest: Path, name: str = None): - dest.mkdir(parents=True, exist_ok=True) - if name is None: - name = src.name - src.rename(dest / name) - - -class PluginRepository(BaseModel): - type: str - url: str - - -class PluginManifest(BaseModel): - name: str - version: str - description: str - author: str - license: str - - homepage: str - repository: list[PluginRepository] - - typing_file: str | None - package_name: str - - @property - def semantic_version(self): - if not hasattr(self, "_semantic_version"): - self._semantic_version = SemanticVersion.parse(self.version) - return self._semantic_version - - -class PluginInstaller: - MANIFEST_FILENAME = "manifest.yaml" - - def __init__(self, manager: PluginManager) -> None: - self.manager = manager - - def install_from_path(self, path: str): - manifest = read_manifest(path) - - self.install_plugin(manifest, path) - - def install_plugin(self, manifest: PluginManifest, path: Path): - plugin_dir = ez.PLUGINS_DIR / manifest.package_name - if plugin_dir.exists(): - raise FileExistsError(f"Plugin {manifest.package_name} is already installed") - - plugin_content = path / manifest.package_name - - # TODO: make manifest more flexible in terms of file transfer - - if manifest.typing_file: - ez.PLUGIN_API_DIR.mkdir(parents=True, exist_ok=True) - - target_typing_file_name = manifest.package_name.replace("-", "_") + ".pyi" - move_file(plugin_content / manifest.typing_file, ez.PLUGIN_API_DIR, target_typing_file_name) - - plugin_dir.mkdir(parents=False, exist_ok=False) - for file in plugin_content.iterdir(): - move_file(file, plugin_dir) - - def install_from_zip(self, path: str): - path: Path = Path(path) - - with ZipFile(path) as zip_file: - for info in zip_file.infolist(): - if Path(info.filename).name == self.MANIFEST_FILENAME: - break - else: - raise FileNotFoundError(f"Manifest file not found in {path}") - - tmpdir = ez.EZ_FRAMEWORK_DIR / "temp" / "plugins" - tmpdir.mkdir(parents=True, exist_ok=True) - - tmpdir /= path.stem - if tmpdir.exists(): - rmtree(str(tmpdir)) - tmpdir.mkdir(parents=False, exist_ok=False) - - zip_file.extractall(tmpdir) - - self.install_from_path(tmpdir) - - rmtree(str(tmpdir)) - - def uninstall_plugin(self, name: str): - raise NotImplementedError("Uninstalling plugins is not yet supported") - # plugin_dir = ez.PLUGINS_DIR / name - # if not plugin_dir.exists(): - # raise FileNotFoundError(f"Plugin {name} is not installed") - - # for file in plugin_dir.iterdir(): - # file.unlink() - # plugin_dir.rmdir() From 95f09b8b3a76675f68ce8f11197a07b3561f77e9 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 17:57:19 +0200 Subject: [PATCH 29/45] Remove old placeholder model for plugin --- modules/plugins/model.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 modules/plugins/model.py diff --git a/modules/plugins/model.py b/modules/plugins/model.py deleted file mode 100644 index 5c77c49..0000000 --- a/modules/plugins/model.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic import BaseModel - - -class PluginWebModel(BaseModel): - name: str - id: str - version: str - description: str - enabled: bool From 1ed29bf7d63d5df25d5093a6160e0d479d71afd9 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 17:57:31 +0200 Subject: [PATCH 30/45] Remove unused imports --- modules/plugins/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/plugins/plugin.py b/modules/plugins/plugin.py index 5d8ba24..a9c195f 100644 --- a/modules/plugins/plugin.py +++ b/modules/plugins/plugin.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from types import ModuleType from typing import TypeAlias, TYPE_CHECKING -from .plugin_info import PluginInfo, PluginId, PackageName +from .plugin_info import PluginInfo if TYPE_CHECKING: From 5ee49ebfe06700f24a1bc43e8be1b2845e80b5f9 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:10:26 +0200 Subject: [PATCH 31/45] Fix Plugins.WillLoad and Plugins.DidLoad event args --- core/ez/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/ez/__init__.py b/core/ez/__init__.py index e0ab01d..b1374e9 100644 --- a/core/ez/__init__.py +++ b/core/ez/__init__.py @@ -363,13 +363,14 @@ def _setup(__ez=_EZ.ez): from ez.plugins import PluginEvent, __pm - emit(PluginEvent.WillLoad) - # TODO: WTF???? - __pm.load_plugins( + plugins = [ "test-plugin", "title-changer" - ) - emit(PluginEvent.DidLoad) + ] + emit(PluginEvent.WillLoad, plugins) + # TODO: WTF???? + __pm.load_plugins(*plugins) + emit(PluginEvent.DidLoad, plugins) del PluginEvent From 3d22fb477cbf7db24d79ab2d2cb4cf5ae3f59eff Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:22:35 +0200 Subject: [PATCH 32/45] Simplify info object instantiation --- modules/plugins/builtins/installer.py | 2 +- modules/plugins/builtins/loader.py | 5 ++--- modules/plugins/machinery/installer.py | 4 ---- modules/plugins/machinery/loader.py | 2 -- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/modules/plugins/builtins/installer.py b/modules/plugins/builtins/installer.py index d59ffc3..f7f5a72 100644 --- a/modules/plugins/builtins/installer.py +++ b/modules/plugins/builtins/installer.py @@ -31,7 +31,7 @@ class PluginManifest(BaseModel): class EZPluginInstaller(IPluginInstaller): - info = PluginInstallerInfo.model_construct( + info = PluginInstallerInfo( id="ez.plugins.installer", name="EZ Plugin Installer", ) diff --git a/modules/plugins/builtins/loader.py b/modules/plugins/builtins/loader.py index 429353b..61e38e7 100644 --- a/modules/plugins/builtins/loader.py +++ b/modules/plugins/builtins/loader.py @@ -5,8 +5,7 @@ from utilities.version import Version -from ..plugin import Plugin -from ..plugin_info import PluginId, PluginInfo +from ..plugin import Plugin, PluginInfo, PluginId from ..machinery.loader import IPluginLoader, PluginLoaderInfo from .plugin import EZPlugin @@ -15,7 +14,7 @@ class EZPluginLoader(IPluginLoader): - info = PluginLoaderInfo.model_construct( + info = PluginLoaderInfo( id="ez.plugins.loader", name="EZ Plugin Loader", ) diff --git a/modules/plugins/machinery/installer.py b/modules/plugins/machinery/installer.py index 382adc4..88a4197 100644 --- a/modules/plugins/machinery/installer.py +++ b/modules/plugins/machinery/installer.py @@ -1,5 +1,4 @@ from pydantic import BaseModel -from pydantic.dataclasses import dataclass from pathlib import Path from typing import TypeAlias, ClassVar, TYPE_CHECKING @@ -12,14 +11,11 @@ PluginInstallerId: TypeAlias = str -@dataclass() class PluginInstallerInfo(BaseModel): id: PluginInstallerId name: str -# This is not yet pydantic_dataclass because the `Version` type is not compatible with pydantic -@dataclass class PluginInstallationResult(BaseModel): installer_id: PluginInstallerId package_name: str diff --git a/modules/plugins/machinery/loader.py b/modules/plugins/machinery/loader.py index c107285..30dc1fb 100644 --- a/modules/plugins/machinery/loader.py +++ b/modules/plugins/machinery/loader.py @@ -1,5 +1,4 @@ from pydantic import BaseModel -from pydantic.dataclasses import dataclass from pathlib import Path from typing import TypeAlias, ClassVar @@ -10,7 +9,6 @@ PluginLoaderId: TypeAlias = str -@dataclass class PluginLoaderInfo(BaseModel): id: PluginLoaderId name: str From 1d060c11520554a07e31e35ac25a6c852323d9dc Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:23:17 +0200 Subject: [PATCH 33/45] Fix import --- modules/plugins/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/plugins/errors.py b/modules/plugins/errors.py index 9d48ef1..1ca988b 100644 --- a/modules/plugins/errors.py +++ b/modules/plugins/errors.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from plugins.machinery.installer import PluginInstallerId - from plugins.machinery.loader import PluginLoaderId + from .machinery.installer import PluginInstallerId + from .machinery.loader import PluginLoaderId class EZPluginError(EZError): From 371e9563ef68baf1e6d24696623879fe07e5e950 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:24:03 +0200 Subject: [PATCH 34/45] Simplify imports --- modules/plugins/builtins/plugin_module.py | 2 +- modules/plugins/ez_plugins/__init__.py | 3 +-- modules/plugins/ez_plugins/machinery.py | 4 +++- modules/plugins/machinery/installer.py | 2 +- modules/plugins/machinery/loader.py | 3 +-- modules/plugins/plugin.py | 3 ++- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/modules/plugins/builtins/plugin_module.py b/modules/plugins/builtins/plugin_module.py index 18d1efc..881a33e 100644 --- a/modules/plugins/builtins/plugin_module.py +++ b/modules/plugins/builtins/plugin_module.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from ez.plugins import Plugin, PluginInfo + from ..plugin import Plugin, PluginInfo class PluginModule(ModuleType): diff --git a/modules/plugins/ez_plugins/__init__.py b/modules/plugins/ez_plugins/__init__.py index bf1696a..c56c819 100644 --- a/modules/plugins/ez_plugins/__init__.py +++ b/modules/plugins/ez_plugins/__init__.py @@ -1,7 +1,6 @@ from typing import Callable -from ..plugin import Plugin, PluginId, PluginAPI -from ..plugin_info import PluginInfo +from ..plugin import Plugin, PluginInfo, PluginId, PluginAPI from ..machinery.installer import IPluginInstaller, PluginInstallerInfo, PluginInstallerId from ..machinery.loader import IPluginLoader, PluginLoaderInfo diff --git a/modules/plugins/ez_plugins/machinery.py b/modules/plugins/ez_plugins/machinery.py index ca69536..013bb80 100644 --- a/modules/plugins/ez_plugins/machinery.py +++ b/modules/plugins/ez_plugins/machinery.py @@ -1,6 +1,6 @@ from ..machinery.installer import IPluginInstaller, PluginInstallerInfo, PluginInstallationResult from ..machinery.loader import IPluginLoader, PluginLoaderInfo -from ..plugin_info import PluginInfo +from ..plugin import Plugin, PluginInfo, PluginId __all__ = [ @@ -9,5 +9,7 @@ "PluginInstallationResult", "IPluginLoader", "PluginLoaderInfo", + "Plugin", "PluginInfo", + "PluginId", ] diff --git a/modules/plugins/machinery/installer.py b/modules/plugins/machinery/installer.py index 88a4197..bf9f292 100644 --- a/modules/plugins/machinery/installer.py +++ b/modules/plugins/machinery/installer.py @@ -5,7 +5,7 @@ from utilities.version import Version if TYPE_CHECKING: - from ..plugin_info import PluginId + from ..plugin import PluginId PluginInstallerId: TypeAlias = str diff --git a/modules/plugins/machinery/loader.py b/modules/plugins/machinery/loader.py index 30dc1fb..cf2b4c3 100644 --- a/modules/plugins/machinery/loader.py +++ b/modules/plugins/machinery/loader.py @@ -2,8 +2,7 @@ from pathlib import Path from typing import TypeAlias, ClassVar -from ..plugin import Plugin -from ..plugin_info import PluginId +from ..plugin import Plugin, PluginId PluginLoaderId: TypeAlias = str diff --git a/modules/plugins/plugin.py b/modules/plugins/plugin.py index a9c195f..efcc695 100644 --- a/modules/plugins/plugin.py +++ b/modules/plugins/plugin.py @@ -1,7 +1,8 @@ from dataclasses import dataclass from types import ModuleType from typing import TypeAlias, TYPE_CHECKING -from .plugin_info import PluginInfo + +from .plugin_info import PluginInfo, PluginId if TYPE_CHECKING: From a78e116910188aa6fa60f796795bb2aa2fdf59ef Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:24:28 +0200 Subject: [PATCH 35/45] Fix `ez.plugins.is_enabled()` --- modules/plugins/ez_plugins/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/plugins/ez_plugins/__init__.py b/modules/plugins/ez_plugins/__init__.py index c56c819..4170f33 100644 --- a/modules/plugins/ez_plugins/__init__.py +++ b/modules/plugins/ez_plugins/__init__.py @@ -40,7 +40,7 @@ def disable(plugin_id: PluginId) -> bool: def is_enabled(plugin_id: PluginId) -> bool: - plugin = __pm.get_plugin(plugin) + plugin = __pm.get_plugin(plugin_id) return plugin.enabled From 49539708df2e8202e1d4430250edf8be943575c3 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:24:44 +0200 Subject: [PATCH 36/45] Remove comment --- modules/plugins/plugin_info.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/plugins/plugin_info.py b/modules/plugins/plugin_info.py index 963d04c..b97b904 100644 --- a/modules/plugins/plugin_info.py +++ b/modules/plugins/plugin_info.py @@ -3,9 +3,7 @@ from typing import TypeAlias, TYPE_CHECKING from utilities.version import Version - -# if TYPE_CHECKING: -# + from .machinery.installer import PluginInstallerId From d4a6767b0db5696e8efbdf3d64f71a512f1ca4e0 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:25:07 +0200 Subject: [PATCH 37/45] Remove unused type --- modules/plugins/plugin.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/modules/plugins/plugin.py b/modules/plugins/plugin.py index efcc695..4d1fa57 100644 --- a/modules/plugins/plugin.py +++ b/modules/plugins/plugin.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from types import ModuleType from typing import TypeAlias, TYPE_CHECKING from .plugin_info import PluginInfo, PluginId @@ -15,43 +14,6 @@ PluginAPI: TypeAlias = object -class EZPlugin: - info: PluginInfo - module: ModuleType | None - enabled: bool - - def __init__(self, info: PluginInfo, module: ModuleType | None): - self.info = info - self.module = module - self.enabled = False - - @property - def is_loaded(self) -> bool: - return self.module is not None - - @property - def name(self) -> str: - return self.info.name - - @property - def version(self) -> str: - return str(self.info.version) - - @property - def has_api(self) -> bool: - return hasattr(self.module, API_ATTRIBUTE_NAME) - - @property - def api(self): - return getattr(self.module, API_ATTRIBUTE_NAME, None) - - def enable(self): - self.enabled = True - - def disable(self): - self.enabled = False - - @dataclass class Plugin: info: PluginInfo From 0bca68981d3903e1ca41226f0a5142253654b4d9 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:25:22 +0200 Subject: [PATCH 38/45] Fix * exports --- modules/plugins/plugin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/plugins/plugin.py b/modules/plugins/plugin.py index 4d1fa57..b4f3ad3 100644 --- a/modules/plugins/plugin.py +++ b/modules/plugins/plugin.py @@ -20,3 +20,10 @@ class Plugin: loader: "PluginLoaderInfo" enabled: bool api: PluginAPI | None + + +__all__ = [ + "Plugin", + "PluginId", + "PluginInfo", +] From 7bb7e1e5b497283041b2e93e5fafbbb4451b0239 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:25:40 +0200 Subject: [PATCH 39/45] Fix `ez.plugins.get_plugin_public_api_module_name() ` --- modules/plugins/ez_plugins/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/plugins/ez_plugins/__init__.py b/modules/plugins/ez_plugins/__init__.py index 4170f33..a66f2d2 100644 --- a/modules/plugins/ez_plugins/__init__.py +++ b/modules/plugins/ez_plugins/__init__.py @@ -8,7 +8,7 @@ from .events import Plugins as PluginEvent from ..manager import PLUGIN_MANAGER as __pm -from ..config import METADATA_FILENAME +from ..config import METADATA_FILENAME, PLUGINS_PUBLIC_API_MODULE_NAME def get_plugins() -> list[Plugin]: @@ -66,7 +66,7 @@ def get_metadata_filename() -> str: def get_plugin_public_api_module_name() -> str: - ... + return PLUGINS_PUBLIC_API_MODULE_NAME def expose(plugin: Plugin, api: PluginAPI): From 938b8c3970021a18edec880cc5365fa2039c03b5 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:27:52 +0200 Subject: [PATCH 40/45] Implemenet basic default plugin installer `uninstall()` --- modules/plugins/builtins/installer.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/plugins/builtins/installer.py b/modules/plugins/builtins/installer.py index f7f5a72..af1df4d 100644 --- a/modules/plugins/builtins/installer.py +++ b/modules/plugins/builtins/installer.py @@ -1,4 +1,5 @@ import yaml +import shutil from pathlib import Path from zipfile import ZipFile, BadZipFile @@ -6,6 +7,7 @@ from utilities.version import Version +from ..config import PLUGINS_PUBLIC_API_DIR from ..errors import EZPluginError, PluginAlreadyInstalledError from ..plugin_info import PackageName from ..machinery.installer import IPluginInstaller, PluginInstallationResult, PluginInstallerInfo @@ -71,4 +73,11 @@ def install(self, path: str) -> PluginInstallationResult: (plugin_dir / manifest.typing_file).rename() def uninstall(self, plugin_id: str) -> None: - ... + plugin_dir = self.plugin_dir / plugin_id + if not plugin_dir.exists(): + raise FileNotFoundError(plugin_dir) + + shutil.rmtree(str(plugin_dir)) + + if (PLUGINS_PUBLIC_API_DIR / f"{plugin_id}.py").exists(): + (PLUGINS_PUBLIC_API_DIR / f"{plugin_id}.py").unlink() From 2c2b3404663727aa5c22bf750fa8ca0405eb4b35 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Tue, 20 Feb 2024 18:30:21 +0200 Subject: [PATCH 41/45] Fix `uninstall()` --- modules/plugins/builtins/installer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/plugins/builtins/installer.py b/modules/plugins/builtins/installer.py index af1df4d..e4445f2 100644 --- a/modules/plugins/builtins/installer.py +++ b/modules/plugins/builtins/installer.py @@ -8,7 +8,7 @@ from utilities.version import Version from ..config import PLUGINS_PUBLIC_API_DIR -from ..errors import EZPluginError, PluginAlreadyInstalledError +from ..errors import EZPluginError, PluginAlreadyInstalledError, UnknownPluginError from ..plugin_info import PackageName from ..machinery.installer import IPluginInstaller, PluginInstallationResult, PluginInstallerInfo @@ -75,9 +75,9 @@ def install(self, path: str) -> PluginInstallationResult: def uninstall(self, plugin_id: str) -> None: plugin_dir = self.plugin_dir / plugin_id if not plugin_dir.exists(): - raise FileNotFoundError(plugin_dir) + raise UnknownPluginError(plugin_id) shutil.rmtree(str(plugin_dir)) - if (PLUGINS_PUBLIC_API_DIR / f"{plugin_id}.py").exists(): - (PLUGINS_PUBLIC_API_DIR / f"{plugin_id}.py").unlink() + typing_file = PLUGINS_PUBLIC_API_DIR / f"{plugin_id}.pyi" + typing_file.unlink(missing_ok=True) From 2f7bcf66f33c493be097f85ee41926adaa33fe86 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen <42520501+binyamin555@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:45:46 +0200 Subject: [PATCH 42/45] Update __init__.py Remove comments --- core/ez/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/ez/__init__.py b/core/ez/__init__.py index b1374e9..02bdd50 100644 --- a/core/ez/__init__.py +++ b/core/ez/__init__.py @@ -368,7 +368,6 @@ def _setup(__ez=_EZ.ez): "title-changer" ] emit(PluginEvent.WillLoad, plugins) - # TODO: WTF???? __pm.load_plugins(*plugins) emit(PluginEvent.DidLoad, plugins) From f64afd9065f02a2ad78f64089bc4a6feb3c3d069 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 23:46:02 +0200 Subject: [PATCH 43/45] Add URI class and tools --- modules/utilities/uri.py | 147 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 modules/utilities/uri.py diff --git a/modules/utilities/uri.py b/modules/utilities/uri.py new file mode 100644 index 0000000..f6925a1 --- /dev/null +++ b/modules/utilities/uri.py @@ -0,0 +1,147 @@ +from io import StringIO, SEEK_CUR + + +class URIParser: + def __init__(self, source: str) -> None: + self.source = source + self.stream = StringIO(source) + + def parse(self) -> "UnifiedResourceIdentifier": + scheme = self._parse_scheme() + if self.stream.read(2) == "//": + authority = self._parse_authority() + else: + authority = None + self.stream.seek(-1, SEEK_CUR) + path = self._parse_path() + query = self._parse_query() + fragment = self._parse_fragment() + + return UnifiedResourceIdentifier(scheme, authority, path, query, fragment, self.source) + + def _parse_scheme(self) -> str: + scheme = "" + while (char := self.stream.read(1)) != ":": + scheme += char + return scheme + + def _parse_authority(self) -> "URIAuthority": + userinfo = self._parse_userinfo() + host = self._parse_host() + port = self._parse_port() + return URIAuthority(userinfo, host, port) + + def _parse_userinfo(self) -> "URIUserInfo": + username = "" + while (char := self.stream.read(1)) != "@": + username += char + password = "" + while (char := self.stream.read(1)) != ":": + password += char + return URIUserInfo(username, password) + + def _parse_host(self) -> str: + host = "" + while (char := self.stream.read(1)) != ":" and char != "/": + host += char + return host + + def _parse_port(self) -> int: + port = "" + while (char := self.stream.read(1)) != "/": + port += char + return int(port) + + def _parse_path(self) -> str: + path = "" + while (char := self.stream.read(1)) != "?": + path += char + return path + + def _parse_query(self) -> str: + query = "" + while (char := self.stream.read(1)) != "#": + query += char + return query + + def _parse_fragment(self) -> str: + return self.stream.read() + + +class URIUserInfo: + username: str + password: str + + def __init__(self, username: str, password: str) -> None: + self.username = username + self.password = password + + +class URIAuthority: + userinfo: URIUserInfo + host: str + port: int + + def __init__(self, userinfo: URIUserInfo, host: str, port: int) -> None: + self.userinfo = userinfo + self.host = host + self.port = port + + +class URIPath: + SEPARATOR: str = "/" + + raw: str + segments: list[str] + + def __init__(self, raw: str) -> None: + self.raw = raw + self.segments = raw.split(self.SEPARATOR) + + @property + def name(self) -> str: + return self.segments[-1] + + @property + def parent(self) -> "URIPath": + return URIPath(self.SEPARATOR.join(self.segments[:-1])) + + def __str__(self) -> str: + return self.raw + + def __truediv__(self, other: str) -> "URIPath": + return URIPath(self.raw + self.SEPARATOR + other) + + def __rtruediv__(self, other: str) -> "URIPath": + return URIPath(other + self.SEPARATOR + self.raw) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, URIPath): + return NotImplemented + return self.raw == other.raw + + def __hash__(self) -> int: + return hash(self.raw) + + +class UnifiedResourceIdentifier: + scheme: str + authority: URIAuthority | None + path: str + query: str + fragment: str + + raw: str + + def __init__(self, scheme: str, authority: URIAuthority, path: str, query: str, fragment: str, raw: str) -> None: + self.scheme = scheme + self.authority = authority + self.path = path + self.query = query + self.fragment = fragment + self.raw = raw + + @classmethod + def parse(cls, uri: str) -> "UnifiedResourceIdentifier": + parser = URIParser(uri) + return parser.parse() From c52dabfbc001562908ae3ac3b331d3c6174cbd38 Mon Sep 17 00:00:00 2001 From: Binyamin Y Cohen Date: Sun, 18 Feb 2024 23:46:26 +0200 Subject: [PATCH 44/45] Add core resource types --- modules/resources/__init__.py | 5 +++ modules/resources/machinery/loader.py | 17 +++++++++ modules/resources/manager.py | 23 ++++++++++++ modules/resources/resource.py | 50 +++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 modules/resources/__init__.py create mode 100644 modules/resources/machinery/loader.py create mode 100644 modules/resources/manager.py create mode 100644 modules/resources/resource.py diff --git a/modules/resources/__init__.py b/modules/resources/__init__.py new file mode 100644 index 0000000..4cf0262 --- /dev/null +++ b/modules/resources/__init__.py @@ -0,0 +1,5 @@ +from .resource import Resource, URI + + +def load_resource(uri: URI) -> Resource: + ... diff --git a/modules/resources/machinery/loader.py b/modules/resources/machinery/loader.py new file mode 100644 index 0000000..cdc0ff9 --- /dev/null +++ b/modules/resources/machinery/loader.py @@ -0,0 +1,17 @@ +from typing import TypeAlias + +from ..resource import Resource, ResourceType, URI + + +ResourceLoaderId: TypeAlias = str + + +class ResourceLoaderInfo: + id: ResourceLoaderId + name: str + + + +class IResourceLoader: + def load_resource(self, uri: URI, resource_type: ResourceType | None = None) -> Resource: + raise NotImplementedError diff --git a/modules/resources/manager.py b/modules/resources/manager.py new file mode 100644 index 0000000..11fc715 --- /dev/null +++ b/modules/resources/manager.py @@ -0,0 +1,23 @@ +from .machinery.loader import IResourceLoader, ResourceLoaderId +from .resource import Resource, ResourceType, URI + + +class ResourceManager: + _loaders: dict[ResourceLoaderId, IResourceLoader] + _resources: dict[URI, Resource] + + def __init__(self) -> None: + self._loaders = {} + self._resources = {} + + def load_resource(self, uri: URI, resource_type: ResourceType | None = None, *, enable_caching: bool = True): + try: + return self._resources[uri] + except KeyError: + resource = self._load_resource(uri, resource_type) + if enable_caching: + self._resources[uri] = resource + return resource + + def _load_resource(self, uri: URI, resource_type: ResourceType | None = None): + raise NotImplementedError diff --git a/modules/resources/resource.py b/modules/resources/resource.py new file mode 100644 index 0000000..3c4a077 --- /dev/null +++ b/modules/resources/resource.py @@ -0,0 +1,50 @@ +from typing import TypeAlias, TypeVar, Type + +from utilities.uri import UnifiedResourceIdentifier + + +ResourceTypeId: TypeAlias = str +URI: TypeAlias = UnifiedResourceIdentifier + +ResourceT = TypeVar("ResourceT", bound="Resource") + + +class ResourceType: + _id: ResourceTypeId + _name: str + + def __init__(self, name: str, id: ResourceTypeId) -> None: + self._name = name + self._id = id + + @property + def name(self): + return self._name + + @property + def id(self): + return self._id + + +class Resource: + _type: ResourceType + _uri: URI + + def __init__(self, resource_type: ResourceType, uri: str) -> None: + self._type = resource_type + self._uri = uri + + @property + def type(self): + return self._type + + @property + def uri(self): + return self._uri + + def cast(self, cls: Type[ResourceT], *, strict: bool = True) -> ResourceT: + if not strict: + return self + if not isinstance(self, cls): + raise TypeError(f"Resource.cast(strict=True) could not convert resource of type {type(self).__name__} to {cls.__name__}") + return self From 48af38ecfdfb47814da373ca23692f11c1f1ad90 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Wed, 21 Feb 2024 16:38:00 +0200 Subject: [PATCH 45/45] Format code --- modules/resources/manager.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/resources/manager.py b/modules/resources/manager.py index 11fc715..e9ef2b3 100644 --- a/modules/resources/manager.py +++ b/modules/resources/manager.py @@ -10,14 +10,24 @@ def __init__(self) -> None: self._loaders = {} self._resources = {} - def load_resource(self, uri: URI, resource_type: ResourceType | None = None, *, enable_caching: bool = True): + def load_resource( + self, + uri: URI, + resource_type: ResourceType | None = None, + *, + enable_caching: bool = True + ): try: - return self._resources[uri] + return ( + self._resources[uri] + if enable_caching + else self._load_resource(uri, resource_type) + ) except KeyError: resource = self._load_resource(uri, resource_type) if enable_caching: self._resources[uri] = resource return resource - + def _load_resource(self, uri: URI, resource_type: ResourceType | None = None): raise NotImplementedError