Skip to content

Commit

Permalink
Merge pull request #257 from andrewwhitehead/feature/plugin-registry
Browse files Browse the repository at this point in the history
Add plugin registry used for protocol and admin route registration
  • Loading branch information
nrempel authored Nov 8, 2019
2 parents 9d6ec5c + 73f578d commit 5ee3ebc
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 105 deletions.
33 changes: 0 additions & 33 deletions aries_cloudagent/admin/routes.py

This file was deleted.

18 changes: 4 additions & 14 deletions aries_cloudagent/admin/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@

from marshmallow import fields, Schema

from ..classloader import ClassLoader
from ..config.base import ConfigError
from ..config.injection_context import InjectionContext
from ..messaging.outbound_message import OutboundMessage
from ..messaging.plugin_registry import PluginRegistry
from ..messaging.responder import BaseResponder
from ..stats import Collector
from ..task_processor import TaskProcessor
Expand All @@ -23,7 +22,6 @@

from .base_server import BaseAdminServer
from .error import AdminSetupError
from .routes import register_module_routes

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -193,18 +191,10 @@ async def collect_stats(request, handler):
web.get("/ws", self.websocket_handler),
]
)
await register_module_routes(app)

for protocol_module_path in self.context.settings.get("external_protocols", []):
try:
routes_module = ClassLoader.load_module(
f"{protocol_module_path}.routes"
)
await routes_module.register(app)
except Exception as e:
raise ConfigError(
f"Failed to load external protocol module '{protocol_module_path}'."
) from e
plugin_registry = await self.context.inject(PluginRegistry, required=False)
if plugin_registry:
await plugin_registry.register_admin_routes(app)

cors = aiohttp_cors.setup(
app,
Expand Down
18 changes: 16 additions & 2 deletions aries_cloudagent/admin/tests/test_admin_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from asynctest import TestCase as AsyncTestCase
from asynctest.mock import patch

from ...config.default_context import DefaultContextBuilder
from ...config.injection_context import InjectionContext
from ...config.provider import ClassProvider
from ...messaging.outbound_message import OutboundMessage
from ...messaging.protocol_registry import ProtocolRegistry
from ...transport.outbound.queue.base import BaseOutboundMessageQueue
from ...transport.outbound.queue.basic import BasicOutboundMessageQueue

Expand All @@ -18,8 +20,11 @@ class TestAdminServerBasic(AsyncTestCase):
async def setUp(self):
self.message_results = []

def get_admin_server(self, settings: dict = None) -> AdminServer:
context = InjectionContext()
def get_admin_server(
self, settings: dict = None, context: InjectionContext = None
) -> AdminServer:
if not context:
context = InjectionContext()
context.injector.bind_provider(
BaseOutboundMessageQueue, ClassProvider(BasicOutboundMessageQueue)
)
Expand Down Expand Up @@ -70,6 +75,15 @@ async def test_responder_webhook(self):
await admin_server.responder.send_webhook(test_topic, test_payload)
sender.assert_awaited_once_with(admin_server, test_topic, test_payload)

async def test_import_routes(self):
# this test just imports all default admin routes
# for routes with associated tests, this shouldn't make a difference in coverage
context = InjectionContext()
context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry())
await DefaultContextBuilder().load_plugins(context)
server = self.get_admin_server({"admin.admin_insecure_mode": True}, context)
app = await server.make_application()


class TestAdminServerClient(AioHTTPTestCase):
async def setUpAsync(self):
Expand Down
24 changes: 12 additions & 12 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@ class GeneralGroup(ArgumentGroup):

def add_arguments(self, parser: ArgumentParser):
"""Add general command line arguments to the parser."""
parser.add_argument(
"--plugin",
dest="external_plugins",
type=str,
action="append",
required=False,
metavar="<module>",
help="Load <module> as external plugin module. Multiple\
instances of this parameter can be specified.",
)
parser.add_argument(
"--storage-type",
type=str,
Expand All @@ -370,6 +380,8 @@ def add_arguments(self, parser: ArgumentParser):
def get_settings(self, args: Namespace) -> dict:
"""Extract general settings."""
settings = {}
if args.external_plugins:
settings["external_plugins"] = args.external_plugins
if args.storage_type:
settings["storage.type"] = args.storage_type
return settings
Expand Down Expand Up @@ -486,16 +498,6 @@ class ProtocolGroup(ArgumentGroup):

def add_arguments(self, parser: ArgumentParser):
"""Add protocol-specific command line arguments to the parser."""
parser.add_argument(
"--protocol",
dest="external_protocols",
type=str,
action="append",
required=False,
metavar="<module>",
help="Load <module> as external protocol module. Multiple\
instances of this parameter can be specified.",
)
parser.add_argument(
"--auto-ping-connection",
action="store_true",
Expand Down Expand Up @@ -524,8 +526,6 @@ def add_arguments(self, parser: ArgumentParser):
def get_settings(self, args: Namespace) -> dict:
"""Get protocol settings."""
settings = {}
if args.external_protocols:
settings["external_protocols"] = args.external_protocols
if args.auto_ping_connection:
settings["auto_ping_connection"] = True
if args.invite_base_url:
Expand Down
49 changes: 30 additions & 19 deletions aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
"""Classes for configuring the default injection context."""

from .base import ConfigError
from .base_context import ContextBuilder
from .injection_context import InjectionContext
from .provider import CachedProvider, ClassProvider, StatsProvider

from ..cache.base import BaseCache
from ..cache.basic import BasicCache
from ..classloader import ClassLoader
from ..defaults import default_protocol_registry
from ..ledger.base import BaseLedger
from ..ledger.provider import LedgerProvider
from ..issuer.base import BaseIssuer
from ..holder.base import BaseHolder
from ..verifier.base import BaseVerifier
from ..messaging.plugin_registry import PluginRegistry
from ..messaging.protocol_registry import ProtocolRegistry
from ..messaging.serializer import MessageSerializer
from ..protocols.actionmenu.base_service import BaseMenuService
Expand All @@ -40,9 +38,14 @@ async def build(self) -> InjectionContext:
collector = Collector()
context.injector.bind_instance(Collector, collector)

# Shared in-memory cache
context.injector.bind_instance(BaseCache, BasicCache())

# Global protocol registry
context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry())

await self.bind_providers(context)
await self.load_plugins(context)

return context

Expand Down Expand Up @@ -119,22 +122,6 @@ async def bind_providers(self, context: InjectionContext):
),
)

# Set up protocol registry
protocol_registry: ProtocolRegistry = default_protocol_registry()
# Dynamically register externally loaded protocol message types
for protocol_module_path in self.settings.get("external_protocols", []):
try:
external_module = ClassLoader.load_module(
f"{protocol_module_path}.message_types"
)
protocol_registry.register_message_types(external_module.MESSAGE_TYPES)
except Exception as e:
raise ConfigError(
"Failed to load external protocol module "
+ f"'{protocol_module_path}'"
) from e
context.injector.bind_instance(ProtocolRegistry, protocol_registry)

# Register message serializer
context.injector.bind_provider(
MessageSerializer,
Expand All @@ -160,3 +147,27 @@ async def bind_providers(self, context: InjectionContext):
context.injector.bind_instance(
BaseIntroductionService, DemoIntroductionService(context)
)

async def load_plugins(self, context: InjectionContext):
"""Set up plugin registry and load plugins."""

plugin_registry = PluginRegistry()
context.injector.bind_instance(PluginRegistry, plugin_registry)

# Register standard protocol plugins
plugin_registry.register_package("aries_cloudagent.protocols")

# Currently providing admin routes only
plugin_registry.register_plugin("aries_cloudagent.ledger")
plugin_registry.register_plugin(
"aries_cloudagent.messaging.credential_definitions"
)
plugin_registry.register_plugin("aries_cloudagent.messaging.schemas")
plugin_registry.register_plugin("aries_cloudagent.wallet")

# Register external plugins
for plugin_path in self.settings.get("external_plugins", []):
plugin_registry.register_plugin(plugin_path)

# Register message protocols
await plugin_registry.init_context(context)
22 changes: 0 additions & 22 deletions aries_cloudagent/defaults.py

This file was deleted.

78 changes: 78 additions & 0 deletions aries_cloudagent/messaging/plugin_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Handle registration of plugin modules for extending functionality."""

import logging
from collections import OrderedDict
from types import ModuleType
from typing import Sequence

from ..classloader import ClassLoader, ModuleLoadError
from ..config.injection_context import InjectionContext
from ..messaging.protocol_registry import ProtocolRegistry

LOGGER = logging.getLogger(__name__)


class PluginRegistry:
"""Plugin registry for indexing application plugins."""

def __init__(self):
"""Initialize a `PluginRegistry` instance."""
self._plugins = OrderedDict()

@property
def plugin_names(self) -> Sequence[str]:
"""Accessor for a list of all plugin modules."""
return list(self._plugins.keys())

@property
def plugins(self) -> Sequence[ModuleType]:
"""Accessor for a list of all plugin modules."""
return list(self._plugins.values())

def register_plugin(self, module_name) -> ModuleType:
"""Register a plugin module."""
if module_name not in self._plugins:
self._plugins[module_name] = ClassLoader.load_module(module_name)
return self._plugins[module_name]

def register_package(self, package_name: str) -> Sequence[ModuleType]:
"""Register all modules (sub-packages) under a given package name."""
module_names = ClassLoader.scan_subpackages(package_name)
return [self.register_plugin(module_name) for module_name in module_names]

async def init_context(self, context: InjectionContext):
"""Call plugin setup methods on the current context."""
for plugin in self._plugins.values():
if hasattr(plugin, "setup"):
await plugin.setup(context)
else:
await self.load_message_types(context, plugin)

async def load_message_types(self, context: InjectionContext, plugin: ModuleType):
"""For modules that don't implement setup, register protocols manually."""
registry = await context.inject(ProtocolRegistry)
try:
mod = ClassLoader.load_module(plugin.__name__ + ".message_types")
except ModuleLoadError:
LOGGER.info(
"No message types defined for plugin module: %s", plugin.__name__
)
return
if hasattr(mod, "MESSAGE_TYPES"):
registry.register_message_types(mod.MESSAGE_TYPES)
if hasattr(mod, "CONTROLLERS"):
registry.register_controllers(mod.CONTROLLERS)

async def register_admin_routes(self, app):
"""Call route registration methods on the current context."""
for plugin in self._plugins.values():
try:
mod = ClassLoader.load_module(plugin.__name__ + ".routes")
except ModuleLoadError:
continue
if hasattr(mod, "register"):
await mod.register(app)

def __repr__(self) -> str:
"""Return a string representation for this class."""
return "<{}>".format(self.__class__.__name__)
2 changes: 1 addition & 1 deletion aries_cloudagent/messaging/protocol_registry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Handle registration and publication of supported message families."""
"""Handle registration and publication of supported protocols."""

from typing import Mapping, Sequence

Expand Down
Loading

0 comments on commit 5ee3ebc

Please sign in to comment.