Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/artemis-development-group/artemis/llms.txt

Use this file to discover all available pages before exploring further.

The Artemis plugin system lets you extend the platform by packaging a Python class that inherits from Plugin and advertising it via a setup.py entry point. Plugins can contribute templates, static files, JavaScript modules, configuration keys, URL routes, and event-driven hooks without modifying the core codebase.

The Plugin base class

Every plugin is a Python class that inherits from r2.lib.plugin.Plugin. The base class is defined in r2/r2/lib/plugin.py and provides the structure the loader expects.
plugin.py (base class)
class Plugin(object):
    js = {}
    config = {}
    live_config = {}
    needs_static_build = False
    needs_translation = True
    errors = {}
    source_root_url = None

    def __init__(self, entry_point):
        self.entry_point = entry_point

    @property
    def name(self):
        return self.entry_point.name

    @property
    def path(self):
        module = sys.modules[type(self).__module__]
        return os.path.dirname(module.__file__)

    @property
    def template_dir(self):
        """Add module/templates/ as a template directory."""
        return os.path.join(self.path, 'templates')

    @property
    def static_dir(self):
        return os.path.join(self.path, 'public')

    def on_load(self, g):
        pass

    def add_routes(self, mc):
        pass

    def load_controllers(self):
        pass

    def declare_queues(self, queues):
        pass

Properties

PropertyTypeDescription
namestrThe entry point name as declared in setup.py. Used to identify the plugin in logs and config.
pathstrAbsolute filesystem path to the directory containing the plugin module.
template_dirstrResolves to <path>/templates/. Automatically prepended to Mako’s template search path on load.
static_dirstrResolves to <path>/public/. Used when needs_static_build is True to locate assets for the static build pipeline.

Class-level attributes

These are declared directly on your subclass, not as instance attributes.
AttributeTypeDefaultDescription
jsdict{}Maps JS bundle names to r2.lib.js.Module instances. Merged into the global module registry on load.
configdict{}Config spec entries added to g.config via add_spec. Use ini-file keys as keys.
live_configdict{}Like config, but for values that can change at runtime without a restart.
needs_static_buildboolFalseSet to True if the plugin has static assets that require a build step.
needs_translationboolTrueSet to False to opt out of the translation pipeline for this plugin.
errorsdict{}Maps error code names to r2.lib.errors.ErrorSet entries. Registered into the global error registry when controllers load.

Methods to override

on_load(g)

Called once during application startup after config is available. Use this to perform any initialization that depends on g (the Pylons app globals).
def on_load(self, g):
    # example: configure a third-party client
    self.api_client = MyClient(g.config["myplugin.api_key"])

add_routes(mc)

Called during route setup. mc is a Pylons mapper (Routes Mapper). Use it to add URL routes that your plugin’s controllers handle.
def add_routes(self, mc):
    mc.connect("/plugin/dashboard", controller="myplugin", action="dashboard")

load_controllers()

Called after all plugins are loaded, once i18n is available. Import and register your controllers here.
def load_controllers(self):
    from myplugin import controllers  # noqa: side-effects register routes

declare_queues(queues)

Called during queue initialization. Add any AMQP queues your plugin needs.

Writing a plugin

1

Create the package

Structure your plugin as a standard Python package:
myplugin/
├── setup.py
├── myplugin/
│   ├── __init__.py
│   ├── plugin.py        # your Plugin subclass
│   ├── templates/       # Mako templates (optional)
│   └── public/          # static assets (optional)
2

Define the Plugin subclass

myplugin/plugin.py
from r2.lib.plugin import Plugin

class MyPlugin(Plugin):
    # Declare config keys your plugin expects in the ini file
    config = {
        "myplugin.api_key": str,
    }

    # Set True if you have assets in public/ that need building
    needs_static_build = False

    def on_load(self, g):
        # Runs at startup; g.config is available here
        pass

    def add_routes(self, mc):
        mc.connect(
            "/myplugin/hello",
            controller="myplugin",
            action="hello",
        )

    def load_controllers(self):
        from myplugin import controllers  # noqa
3

Register the entry point in setup.py

setup.py
from setuptools import setup, find_packages

setup(
    name="myplugin",
    packages=find_packages(),
    entry_points="""
    [r2.plugin]
    myplugin = myplugin.plugin:MyPlugin
    """,
)
The key under [r2.plugin] becomes the plugin’s name.
4

Install the package

pip install -e .
The plugin will be discovered automatically the next time Artemis starts.

Default plugins

The default Artemis install enables two plugins, set via the ARTEMIS_PLUGINS variable in install.cfg:
PluginPurpose
aboutProvides /about pages (rules, contact, jobs, etc.)
goldArtemis Gold subscription features
To change which plugins load, set ARTEMIS_PLUGINS before running the installer:
ARTEMIS_PLUGINS="about gold myplugin" sudo ./install-artemis.sh

The HookRegistrar system

Hooks let plugins react to events fired by the core application without modifying core code. The system is defined in r2/r2/lib/hooks.py.

How hooks work

Core code fires a hook at a named point:
from r2.lib import hooks

# somewhere in core code
hooks.get_hook('frontpage.listing').call(listing=listing_obj)
A plugin registers a handler for that name using a HookRegistrar:
myplugin/hooks.py
from r2.lib.hooks import HookRegistrar

hooks = HookRegistrar()

@hooks.on('frontpage.listing')
def modify_listing(listing):
    # runs when core fires 'frontpage.listing'
    listing.things = sorted(listing.things, key=lambda t: t.score)

hooks.register_all()
register_all() must be called after all @hooks.on(...) decorators have been evaluated. Typically this means calling it at module level, at the bottom of the file that defines your handlers.

HookRegistrar API

HookRegistrar()

Create a new registrar instance. Each plugin should create its own instance to keep registrations isolated.
from r2.lib.hooks import HookRegistrar

hooks = HookRegistrar()

@hooks.on(name)

Decorator that queues the wrapped function as a handler for hook name. The function is not registered into the global hook until register_all() is called.
@hooks.on('some.hook.name')
def my_handler(**kwargs):
    pass

hooks.register_all()

Flushes all deferred registrations into the global hook table. Call this once, at module scope, after all @hooks.on decorators:
hooks.register_all()

Firing hooks (core side)

To fire a hook from within the application or from another plugin:
from r2.lib import hooks

# call all handlers, collect results as a list
results = hooks.get_hook('my.event').call(key=value)

# call handlers in order, return first non-None result
result = hooks.get_hook('my.event').call_until_return(key=value)

Hook.call(**kwargs)

Calls every registered handler in registration order. Returns a list of all handler return values.

Hook.call_until_return(**kwargs)

Calls handlers in registration order and returns the return value of the first handler that returns a non-None value. Useful for provider-style hooks where only one handler should handle the event.

Provider extension points

In addition to the [r2.plugin] group, setup.py defines several provider groups for replacing built-in services. Implement the corresponding interface and register under the appropriate group:

r2.provider.media

Storage backends for uploaded media. Built-in providers: s3, filesystem.

r2.provider.cdn

CDN integration for cache purging. Built-in providers: fastly, cloudflare, null.

r2.provider.auth

Authentication backends. Built-in providers: cookie, http.

r2.provider.support

Support ticket integration. Built-in provider: zendesk.

r2.provider.search

Search backends. Built-in providers: cloudsearch, solr.

r2.provider.image_resizing

On-the-fly image resizing. Built-in providers: imgix, no_op, unsplashit.

r2.provider.email

Outbound email. Built-in providers: null, mailgun.
Register a provider the same way as a plugin, using the provider group name instead of r2.plugin:
setup.py (provider entry point)
entry_points="""
[r2.provider.email]
myemail = myplugin.email:MyEmailProvider
"""

Build docs developers (and LLMs) love