Source code for fairways.decorators.entrypoint

"""Entrypoint is a function which should handle some kind of external "requests" (different ways of invocation for your application). 
You can define different entrypoins in one module and associate some specific data with it.
Example (not functional here, for illustaration purposes only):

>>> # You can invoke this function running "python <yourappname> -c migrate":
>>> @entrypoint.cli(param="migrate")
>>> # Later in the "main" part of module we should call entrypoint.cmd.run() to run all registered entrypoins...
... def make_migration(): pass
>>> @amqp.consumer(queue="myqueue")
... def my_counsumer(): pass
>>> # Later in the "main" part of module we should call amqp.consumer.run() to run all registered entrypoins (or use its async counterpart: amqp.consumer.create_tasks_future())...

Generally, we can define multiple entrypoins per one module. """

from .entities import (Mark, RegistryItem, register_decorator)
from ..funcflow import FuncFlow as ff

import logging
log = logging.getLogger(__name__)

import functools

from abc import abstractmethod

import sys
import argparse

[docs]class EntrypointRegistryItem(RegistryItem): """Internal container to keep data related to single entrypoint """ @property def handler(self): """Wrapped function :return: Decorated function which becomes handler for associated entrypoint :rtype: Callable """ return self.subject def __str__(self): return f"Entrypoint: '{self.module}:{self.mark_name}' in function: '{self.handler.__name__}'"
[docs] @classmethod @abstractmethod def run(self, args=None): """Invoke all functions decorated by this type of entrypoint. :param args: Some data to init run, defaults to None :type args: Any, optional """ pass
[docs]class Channel(Mark): mark_name = "channel" registry_item_class = EntrypointRegistryItem
[docs]@register_decorator class Cron(Channel): mark_name = "cron" decorator_kwargs = ["seconds"] decorator_required_kwargs = []
[docs]@register_decorator class Cli(Channel): mark_name = "cli" decorator_kwargs = [] decorator_required_kwargs = []
[docs]@register_decorator class Cmd(Channel): """Invocation via terminal. Special "param" allows to run selected function only when you provide \-c / \--command argument while starting application. """ mark_name = "cmd" decorator_kwargs = ["param"] decorator_required_kwargs = [] description = "Run command by args" once_per_module = False @classmethod def run(cls, args=None): # TODO: add children parsers to parse context arguments (with displaying help also) def run_item(entrypoint_item): pass args = args or sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument('-c', '--command', help='Select entrypoint by command param') # parser.add_argument('-f', '--file', help='File with initial data') args = parser.parse_args(args) command = args.command print("COMMAND FOUND", command) item_to_run = cls.chain().find(lambda item: item.meta.get("param") == command).value if not item_to_run: log.debug("Cmd decorator: no entrypoints for -c param %s", command) return # raise ValueError(f"Cannot find entrypoint by param: {command}") return item_to_run.handler({})
[docs]class Listener(Channel): @classmethod def asgi_factory(cls): pass @classmethod def run(cls, args=None): raise NotImplementedError()
[docs]class Transmitter(Channel): @classmethod def run(cls, args=None): raise NotImplementedError()
[docs]@register_decorator class QA(Channel): mark_name = "qa" description = "Run test" once_per_module = False
[docs]@register_decorator class Http(Channel): mark_name = "http"
[docs] def as_routes(self): """Enum string routes with related handlers. Returns: [type] -- [description] """ return ff.map(self.items(), lambda rec: ( f'/{rec.mark_name}/{rec.module}.{rec.handler.__name__}', rec.handler ))
[docs]@register_decorator class ConfigHandler(Channel): mark_name = "conf" decorator_kwargs = ["config_key"] decorator_required_kwargs = ["config_key"] # Sometimes we need to use several config keys in one module: once_per_module = False