Skip to content

Commit

Permalink
feat: Support document worker plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
MarekSuchanek committed Jan 28, 2025
1 parent 40b940e commit 9cd8b88
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 5 deletions.
2 changes: 1 addition & 1 deletion packages/dsw-document-worker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ RUN python -m pip install --break-system-packages --user --no-cache --no-index /
&& rm -rf /home/user/wheels

# Run
CMD ["dsw-document-worker"]
CMD ["dsw-document-worker", "run"]
18 changes: 15 additions & 3 deletions packages/dsw-document-worker/dsw/document_worker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ def validate_config(ctx, param, value: typing.IO | None):
value.close()
return load_config_str(content)


@click.command(name='docworker')
@click.group(name='dsw-document-worker')
@click.version_option(version=VERSION)
def main():
pass


@main.command()
@click.argument('config', envvar=VAR_APP_CONFIG_PATH,
required=False, callback=validate_config,
type=click.File('r', encoding=DEFAULT_ENCODING))
@click.argument('workdir', envvar=VAR_WORKDIR_PATH,)
def main(config: DocumentWorkerConfig, workdir: str):
def run(config: DocumentWorkerConfig, workdir: str):
config.log.apply()
workdir_path = pathlib.Path(workdir)
workdir_path.mkdir(parents=True, exist_ok=True)
Expand All @@ -62,3 +66,11 @@ def main(config: DocumentWorkerConfig, workdir: str):
SentryReporter.capture_exception(e)
click.echo(f'Error: {e}', err=True)
sys.exit(2)


@main.command()
def list_plugins():
from .plugins.manager import create_manager
pm = create_manager()
for plugin in pm.list_name_plugin():
click.echo(f'{plugin[0]}: {plugin[1].__name__}')
2 changes: 2 additions & 0 deletions packages/dsw-document-worker/dsw/document_worker/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
DEFAULT_ENCODING = 'utf-8'
EXIT_SUCCESS = 0
NULL_UUID = '00000000-0000-0000-0000-000000000000'
PACKAGE_NAME = 'dsw-document-worker'
PLUGINS_ENTRYPOINT = 'dsw_document_worker_plugins'
PROG_NAME = 'docworker'
VERSION = '4.14.0'

Expand Down
6 changes: 5 additions & 1 deletion packages/dsw-document-worker/dsw/document_worker/context.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import dataclasses
import pathlib

import pluggy

from dsw.database import Database
from dsw.storage import S3Storage

from .config import DocumentWorkerConfig


Expand All @@ -15,6 +16,7 @@ def __init__(self):

@dataclasses.dataclass
class AppContext:
pm: pluggy.PluginManager
db: Database
s3: S3Storage
cfg: DocumentWorkerConfig
Expand Down Expand Up @@ -56,8 +58,10 @@ def get(cls) -> _Context:

@classmethod
def initialize(cls, db, s3, config, workdir):
from .plugins.manager import create_manager
cls._instance = _Context(
app=AppContext(
pm=create_manager(),
db=db,
s3=s3,
cfg=config,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .specs import hookspec, hookimpl

__all__ = ['hookspec', 'hookimpl']
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import pluggy


def create_manager():
import dsw.document_worker.plugins.specs as hookspecs
from ..consts import PACKAGE_NAME, PLUGINS_ENTRYPOINT

pm = pluggy.PluginManager(PACKAGE_NAME)
pm.load_setuptools_entrypoints(PLUGINS_ENTRYPOINT)
pm.add_hookspecs(hookspecs)
return pm
56 changes: 56 additions & 0 deletions packages/dsw-document-worker/dsw/document_worker/plugins/specs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pluggy
import jinja2

from ..consts import PACKAGE_NAME
from ..templates.steps import Step

hookspec = pluggy.HookspecMarker(PACKAGE_NAME)
hookimpl = pluggy.HookimplMarker(PACKAGE_NAME)


@hookspec
def provide_steps() -> dict[str, type[Step]]:
"""
Provide a list of steps that the plugin can execute.
Steps are used to process the document generation in a specific way. Each step
must comply with the interface of the provided `Step` class. The plugin must make
sure to return a list of steps that it can execute and in compliance with the
`Step` class in the current implementation (use correct dsw-document-worker as
a dependency).
:return: a list of steps
"""
pass


@hookspec
def enrich_document_context(context: dict) -> None:
"""
Enrich the document context with custom data.
The plugin can enrich the provided document context with custom data that can be
used in the document generation process. The context is a dictionary that is passed
to each step during the document generation process. The plugin can manipulate the
context in any way it sees fit.
:param context: the document context to enrich
"""


@hookspec
def enrich_jinja_env(jinja_env: jinja2.Environment, options: dict[str, str]) -> None:
"""
Enrich the Jinja environment with custom filters, tests, policies, etc.
The plugin can enrich the provided Jinja environment with custom features like
filters, tests, policies, global variables and other. It can manipulate the Jinja
environment in any way it sees fit but should ensure that the environment is safe
to use in the document generation process. This Jinja environment is used within
all steps that are subclass of the `JinjaPoweredStep` (mainly the `jinja` step
implemented in class `Jinja2Step`).
:param jinja_env: the Jinja environment to enrich
:param options: the options provided to the step
"""
pass
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ def __init__(self, template, options):
self.j2_env.add_extension('jinja2.ext.debug')
self._apply_policies(options)
self._add_j2_enhancements()

Context.get().app.pm.hook.enrich_jinja_env(
jinja_env=self.j2_env,
options=options,
)
except jinja2.exceptions.TemplateSyntaxError as e:
self.raise_exc(self._jinja_exception_msg(e))
except Exception as e:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ def __getitem__(self, format_uuid: str) -> Format:
return self.formats[format_uuid]

def render(self, format_uuid: str, context: dict) -> DocumentFile:
Context.get().app.pm.hook.enrich_document_context(context=context)

self.last_used = datetime.datetime.now(tz=datetime.UTC)
return self[format_uuid].execute(context)

Expand All @@ -233,6 +235,15 @@ def get(cls) -> 'TemplateRegistry':

def __init__(self):
self._templates: dict[str, dict[str, Template]] = {}
self._load_plugin_steps()

def _load_plugin_steps(self):
from .steps.base import register_step, Step
for steps_dict in Context.get().app.pm.hook.provide_steps():
for name, step_class in steps_dict.items():
if not issubclass(step_class, Step):
raise RuntimeError(f'Provided class "{step_class}" is not a subclass of Step')
register_step(name, step_class)

def has_template(self, tenant_uuid: str, template_id: str) -> bool:
return tenant_uuid in self._templates and \
Expand Down
1 change: 1 addition & 0 deletions packages/dsw-document-worker/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
'Markdown',
'MarkupSafe',
'pathvalidate',
'pluggy',
'python-dateutil',
'python-slugify',
'rdflib',
Expand Down
1 change: 1 addition & 0 deletions packages/dsw-mailer/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jmespath==1.0.1
Markdown==3.7
MarkupSafe==3.0.2
pathvalidate==3.2.1
pluggy==1.5.0
psycopg==3.2.3
psycopg-binary==3.2.3
python-dateutil==2.9.0.post0
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ panflute==2.3.1
pathspec==0.12.1
pathvalidate==3.2.1
pillow==11.0.0
pluggy==1.5.0
propcache==0.2.1
psycopg==3.2.3
psycopg-binary==3.2.3
Expand Down

0 comments on commit 9cd8b88

Please sign in to comment.