From c88dab29b2ad3e54b47b097881970023c7728255 Mon Sep 17 00:00:00 2001 From: cobycloud <25079070+cobycloud@users.noreply.github.com> Date: Wed, 27 Aug 2025 05:20:08 -0500 Subject: [PATCH] refactor: rename pluginarium package to jollof --- pkgs/base/swarmauri_base/__init__.py | 14 +- pkgs/pyproject.toml | 3 + pkgs/standards/jollof/LICENSE | 201 ++++++++++++++++++ pkgs/standards/jollof/README.md | 16 ++ pkgs/standards/jollof/jollof/__init__.py | 6 + .../standards/jollof/jollof/plugin_manager.py | 75 +++++++ pkgs/standards/jollof/jollof/registry.py | 46 ++++ pkgs/standards/jollof/pyproject.toml | 32 +++ pkgs/standards/jollof/tests/dummy_plugins.py | 6 + pkgs/standards/jollof/tests/test_formats.py | 24 +++ .../jollof/tests/test_performance.py | 12 ++ .../jollof/tests/test_plugin_manager.py | 39 ++++ pkgs/standards/jollof/tests/test_registry.py | 22 ++ 13 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 pkgs/standards/jollof/LICENSE create mode 100644 pkgs/standards/jollof/README.md create mode 100644 pkgs/standards/jollof/jollof/__init__.py create mode 100644 pkgs/standards/jollof/jollof/plugin_manager.py create mode 100644 pkgs/standards/jollof/jollof/registry.py create mode 100644 pkgs/standards/jollof/pyproject.toml create mode 100644 pkgs/standards/jollof/tests/dummy_plugins.py create mode 100644 pkgs/standards/jollof/tests/test_formats.py create mode 100644 pkgs/standards/jollof/tests/test_performance.py create mode 100644 pkgs/standards/jollof/tests/test_plugin_manager.py create mode 100644 pkgs/standards/jollof/tests/test_registry.py diff --git a/pkgs/base/swarmauri_base/__init__.py b/pkgs/base/swarmauri_base/__init__.py index 4f3b28b709..889dcbb399 100644 --- a/pkgs/base/swarmauri_base/__init__.py +++ b/pkgs/base/swarmauri_base/__init__.py @@ -1,10 +1,22 @@ """Expose common dynamic base utilities for convenience.""" +from pydantic import BaseModel + from swarmauri_base.DynamicBase import ( SubclassUnion, FullUnion, register_model, register_type, ) +from swarmauri_base.TomlMixin import TomlMixin +from swarmauri_base.YamlMixin import YamlMixin -__all__ = ["SubclassUnion", "FullUnion", "register_model", "register_type"] +__all__ = [ + "SubclassUnion", + "FullUnion", + "register_model", + "register_type", + "BaseModel", + "YamlMixin", + "TomlMixin", +] diff --git a/pkgs/pyproject.toml b/pkgs/pyproject.toml index 131ab91ca6..e30f669e1c 100644 --- a/pkgs/pyproject.toml +++ b/pkgs/pyproject.toml @@ -79,6 +79,7 @@ members = [ "standards/peagen", "standards/auto_authn", "standards/auto_kms", + "standards/jollof", "standards/swarmauri_certs_composite", "standards/swarmauri_keyprovider_local", "standards/swarmauri_keyprovider_pkcs11", @@ -218,6 +219,8 @@ cayaml = { workspace = true } catoml = { workspace = true } jaml = { workspace = true } +jollof = { workspace = true } + swarmauri_vectorstore_doc2vec = { workspace = true } swarmauri_embedding_doc2vec = { workspace = true } diff --git a/pkgs/standards/jollof/LICENSE b/pkgs/standards/jollof/LICENSE new file mode 100644 index 0000000000..b7b70230d1 --- /dev/null +++ b/pkgs/standards/jollof/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2025] [Jacob Stewart @ Swarmauri] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pkgs/standards/jollof/README.md b/pkgs/standards/jollof/README.md new file mode 100644 index 0000000000..1d2660eff5 --- /dev/null +++ b/pkgs/standards/jollof/README.md @@ -0,0 +1,16 @@ +# Jollof + +A universal plugin management system for managing plugin domains and groups across arbitrary projects. It provides discovery, registration, and instance loading from JSON, YAML, or TOML configurations. + +## Features +- Entry point discovery and registration +- Domain and group aware plugin registry +- Configuration based instantiation via Pydantic with JSON/YAML/TOML helpers +- Utilities for dumping plugin configuration to multiple formats + +## Usage +```python +from jollof import PluginManager + +pm = PluginManager(domain="example") +``` diff --git a/pkgs/standards/jollof/jollof/__init__.py b/pkgs/standards/jollof/jollof/__init__.py new file mode 100644 index 0000000000..97e8186979 --- /dev/null +++ b/pkgs/standards/jollof/jollof/__init__.py @@ -0,0 +1,6 @@ +"""Jollof: generic plugin management.""" + +from .plugin_manager import PluginManager +from .registry import PluginDomainRegistry + +__all__ = ["PluginManager", "PluginDomainRegistry"] diff --git a/pkgs/standards/jollof/jollof/plugin_manager.py b/pkgs/standards/jollof/jollof/plugin_manager.py new file mode 100644 index 0000000000..db684cdd2c --- /dev/null +++ b/pkgs/standards/jollof/jollof/plugin_manager.py @@ -0,0 +1,75 @@ +"""General purpose plugin manager supporting domains and groups.""" + +from importlib import import_module +from importlib.metadata import entry_points +from typing import Any, Dict, Optional, Type + +from .registry import PluginDomainRegistry + + +class PluginManager: + """Discover, register and load plugins.""" + + def __init__( + self, + domain: str = "default", + groups: Optional[Dict[str, tuple[str, type]]] = None, + ): + self.domain = domain + self.groups = groups or {} + + def discover( + self, mode: str = "fan-out", switch_map: Optional[Dict[str, str]] = None + ) -> None: + """Discover entry points and register them in the registry.""" + switch_map = switch_map or {} + for group_key, (ep_group, base_cls) in self.groups.items(): + eps = list(entry_points(group=ep_group)) + for ep in eps: + if mode == "switch": + target = switch_map.get(group_key) + if target and ep.name != target: + continue + obj = ep.load() + if base_cls and not isinstance(obj, type): + raise TypeError(f"Entry-point '{ep.name}' must resolve to a class.") + if base_cls and not issubclass(obj, base_cls): + raise TypeError( + f"Entry-point '{ep.name}' in group '{group_key}' must subclass {base_cls.__name__}." + ) + PluginDomainRegistry.add( + self.domain, group_key, ep.name, f"{ep.module}:{ep.attr}" + ) + + def register(self, group: str, name: str, obj: Type[Any]) -> None: + """Manually register a plugin class.""" + PluginDomainRegistry.add( + self.domain, group, name, f"{obj.__module__}:{obj.__name__}" + ) + + def load(self, group: str, name: str, config: str, fmt: str = "json") -> Any: + """Instantiate a registered plugin using configuration data.""" + path = PluginDomainRegistry.get(self.domain, group, name) + if not path: + raise KeyError(f"Plugin '{name}' not registered in group '{group}'.") + module_path, attr = path.split(":") + module = import_module(module_path) + plugin_cls = getattr(module, attr) + if fmt == "json": + return plugin_cls.model_validate_json(config) + if fmt == "yaml": + return plugin_cls.model_validate_yaml(config) + if fmt == "toml": + return plugin_cls.model_validate_toml(config) + raise ValueError("Unsupported format") + + @staticmethod + def dump(plugin: Any, fmt: str = "json") -> str: + """Serialize a plugin instance to JSON, YAML, or TOML.""" + if fmt == "json": + return plugin.model_dump_json() + if fmt == "yaml": + return plugin.model_dump_yaml() + if fmt == "toml": + return plugin.model_dump_toml() + raise ValueError("Unsupported format") diff --git a/pkgs/standards/jollof/jollof/registry.py b/pkgs/standards/jollof/jollof/registry.py new file mode 100644 index 0000000000..7ec6cf5ce4 --- /dev/null +++ b/pkgs/standards/jollof/jollof/registry.py @@ -0,0 +1,46 @@ +"""Generic registry for plugin domains and groups.""" + +from collections import defaultdict +from typing import Dict + + +class PluginDomainRegistry: + """Maintain plugin registrations keyed by domain and group.""" + + _registry: Dict[str, Dict[str, Dict[str, str]]] = defaultdict( + lambda: defaultdict(dict) + ) + + @classmethod + def add(cls, domain: str, group: str, name: str, object_path: str) -> None: + """Register a plugin under a domain and group.""" + cls._registry[domain][group][name] = object_path + + @classmethod + def get(cls, domain: str, group: str, name: str) -> str | None: + """Retrieve a registered plugin path.""" + return cls._registry.get(domain, {}).get(group, {}).get(name) + + @classmethod + def remove(cls, domain: str, group: str, name: str) -> None: + cls._registry.get(domain, {}).get(group, {}).pop(name, None) + + @classmethod + def update(cls, domain: str, group: str, name: str, object_path: str) -> None: + cls._registry[domain][group][name] = object_path + + @classmethod + def delete_group(cls, domain: str, group: str) -> None: + cls._registry.get(domain, {}).pop(group, None) + + @classmethod + def known_domains(cls) -> list[str]: + return list(cls._registry.keys()) + + @classmethod + def known_groups(cls, domain: str) -> list[str]: + return list(cls._registry.get(domain, {}).keys()) + + @classmethod + def total_registry(cls) -> Dict[str, Dict[str, Dict[str, str]]]: + return cls._registry diff --git a/pkgs/standards/jollof/pyproject.toml b/pkgs/standards/jollof/pyproject.toml new file mode 100644 index 0000000000..49db109bcb --- /dev/null +++ b/pkgs/standards/jollof/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "jollof" +version = "0.1.0" +description = "Generic plugin management utilities" +license = "Apache-2.0" +readme = "README.md" +requires-python = ">=3.10,<3.13" +authors = [{ name = "Swarmauri Team", email = "support@swarmauri.com" }] +dependencies = [ + "swarmauri_base", + "pydantic>=2.7", +] + +[tool.uv.sources] +swarmauri_base = { workspace = true } + +[tool.pytest.ini_options] +markers = [ + "unit: Unit tests", + "perf: Performance tests that measure execution time and resource usage", +] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[dependency-groups] +dev = [ + "pytest>=8.0", + "pytest-benchmark>=4.0.0", + "ruff>=0.9.9", +] diff --git a/pkgs/standards/jollof/tests/dummy_plugins.py b/pkgs/standards/jollof/tests/dummy_plugins.py new file mode 100644 index 0000000000..23b1d60931 --- /dev/null +++ b/pkgs/standards/jollof/tests/dummy_plugins.py @@ -0,0 +1,6 @@ +from swarmauri_base import BaseModel, TomlMixin, YamlMixin + + +class ExamplePlugin(YamlMixin, TomlMixin, BaseModel): + name: str + value: int diff --git a/pkgs/standards/jollof/tests/test_formats.py b/pkgs/standards/jollof/tests/test_formats.py new file mode 100644 index 0000000000..b7a6e67aa6 --- /dev/null +++ b/pkgs/standards/jollof/tests/test_formats.py @@ -0,0 +1,24 @@ +import json +import tomllib + +import pytest +import yaml + +from jollof.plugin_manager import PluginManager +from .dummy_plugins import ExamplePlugin + + +@pytest.mark.parametrize( + "fmt,config,parser", + [ + ("json", '{"name": "a", "value": 1}', json.loads), + ("yaml", "name: a\nvalue: 1\n", yaml.safe_load), + ("toml", 'name = "a"\nvalue = 1\n', tomllib.loads), + ], +) +def test_roundtrip_formats(fmt, config, parser): + pm = PluginManager() + pm.register("examples", "ExamplePlugin", ExamplePlugin) + inst = pm.load("examples", "ExamplePlugin", config, fmt) + dumped = pm.dump(inst, fmt) + assert parser(dumped) == {"name": "a", "value": 1} diff --git a/pkgs/standards/jollof/tests/test_performance.py b/pkgs/standards/jollof/tests/test_performance.py new file mode 100644 index 0000000000..d0d0af647b --- /dev/null +++ b/pkgs/standards/jollof/tests/test_performance.py @@ -0,0 +1,12 @@ +from jollof.plugin_manager import PluginManager +from .dummy_plugins import ExamplePlugin + + +def test_registration_performance(benchmark): + pm = PluginManager(domain="perf") + + def _register(): + for i in range(100): + pm.register("bench", f"Plugin{i}", ExamplePlugin) + + benchmark(_register) diff --git a/pkgs/standards/jollof/tests/test_plugin_manager.py b/pkgs/standards/jollof/tests/test_plugin_manager.py new file mode 100644 index 0000000000..a059da2c0b --- /dev/null +++ b/pkgs/standards/jollof/tests/test_plugin_manager.py @@ -0,0 +1,39 @@ +import json +from importlib.metadata import EntryPoint + +from jollof.plugin_manager import PluginManager +from jollof.registry import PluginDomainRegistry +from .dummy_plugins import ExamplePlugin + + +def _fake_entry_points(group): + if group == "example.group": + ep = EntryPoint( + name="ExamplePlugin", + value="tests.dummy_plugins:ExamplePlugin", + group="example.group", + ) + return [ep] + return [] + + +def test_discover_and_load(monkeypatch): + monkeypatch.setattr( + "jollof.plugin_manager.entry_points", + lambda group: _fake_entry_points(group), + ) + pm = PluginManager(groups={"examples": ("example.group", ExamplePlugin)}) + pm.discover() + inst = pm.load( + "examples", "ExamplePlugin", json.dumps({"name": "a", "value": 1}), "json" + ) + assert inst.value == 1 + dumped = pm.dump(inst, "yaml") + reloaded = pm.load("examples", "ExamplePlugin", dumped, "yaml") + assert reloaded.name == "a" + + +def test_manual_registration(): + pm = PluginManager() + pm.register("examples", "ExamplePlugin", ExamplePlugin) + assert PluginDomainRegistry.get("default", "examples", "ExamplePlugin") diff --git a/pkgs/standards/jollof/tests/test_registry.py b/pkgs/standards/jollof/tests/test_registry.py new file mode 100644 index 0000000000..c3e4dae6a9 --- /dev/null +++ b/pkgs/standards/jollof/tests/test_registry.py @@ -0,0 +1,22 @@ +from jollof.registry import PluginDomainRegistry + + +def test_registry_methods(): + PluginDomainRegistry._registry.clear() + PluginDomainRegistry.add("dom", "grp", "plug", "m:Cls") + assert PluginDomainRegistry.get("dom", "grp", "plug") == "m:Cls" + + PluginDomainRegistry.update("dom", "grp", "plug", "m:NewCls") + assert PluginDomainRegistry.get("dom", "grp", "plug") == "m:NewCls" + + assert "dom" in PluginDomainRegistry.known_domains() + assert "grp" in PluginDomainRegistry.known_groups("dom") + + PluginDomainRegistry.remove("dom", "grp", "plug") + assert PluginDomainRegistry.get("dom", "grp", "plug") is None + + PluginDomainRegistry.add("dom", "grp", "plug2", "m:C2") + PluginDomainRegistry.delete_group("dom", "grp") + assert "grp" not in PluginDomainRegistry.known_groups("dom") + + assert isinstance(PluginDomainRegistry.total_registry(), dict)