Commit 1069a199 authored by Robin Obůrka's avatar Robin Obůrka

Provide first version of documentation

parent 0f85d30d
......@@ -11,6 +11,9 @@ SN - Sentinel networking library
:caption: Contents:
snbox
non_abstract_boxes
Indices and tables
==================
......
Non-abstract boxes
==================
**Terminology** for boxes:
* **in-out** box - box receives message, processes message and sends (typically modified/enriched) message.
* **out-only** box - box doesn't have sentinel input, but generates messages by another mechanism - from redis/DB/etc.
* **in-only** box - box receives message and doesn't propagate it - it stores it to DB or something else.
SNPipelineBox
-------------
.. autoclass:: sn.msgloop.SNPipelineBox
:members:
.. automethod:: __init__()
SNGeneratorBox
--------------
.. autoclass:: sn.msgloop.SNGeneratorBox
:members:
.. automethod:: __init__()
SNTerminationBox
----------------
.. autoclass:: sn.msgloop.SNTerminationBox
:members:
.. automethod:: __init__()
SNMultipleOutputPipelineBox
---------------------------
.. autoclass:: sn.msgloop.SNMultipleOutputPipelineBox
:members:
SNBox class
===========
.. toctree::
:maxdepth: 2
:caption: Contents:
.. autoclass:: sn.msgloop.SNBox
There are 4 types of programmes interested in ``SNBox`` class:
#. Programmer making changes in ``SNBox`` itself
#. Programmer of non-abstract box
#. Programmer of final-box for particular usage
#. Final code user
SNBox programmer
----------------
Please, refer source code directly.
Non-abstract box programmer
---------------------------
.. automethod:: sn.msgloop.SNBox.__init__
**Do not forget** to call ancesor of the method if you need to overload it.
.. automethod:: sn.msgloop.SNBox.check_configuration
.. automethod:: sn.msgloop.SNBox.get_processed_message
.. automethod:: sn.msgloop.SNBox.process_result
.. automethod:: sn.msgloop.SNBox.teardown_box
**Do not forget** to call ancesor of the method if you need to overload it.
Final-box programmer
--------------------
.. automethod:: sn.msgloop.SNBox.setup
.. automethod:: sn.msgloop.SNBox.teardown
.. automethod:: sn.msgloop.SNBox.before_first_request
.. automethod:: sn.msgloop.SNBox.process
Final code user
---------------
.. automethod:: sn.msgloop.SNBox.run
......@@ -22,7 +22,29 @@ class LoopFail(Exception):
class SNBox():
"""SNBox is abstract box providing basic message loop functionality.
This box provides interface for non-abstract and final-box classes. It
holds whole loop mechanism, protects process from unexpected errors and
contains some basic magic with signals.
It is missing some important parts that are necessary for full
functionality like receiving and sending messages itself.
:param box_name: Unique identification of the box in Sentinel network.
:param argparser: Enriched argparser - see :func:`sn.get_arg_parser`
"""
def __init__(self, box_name, argparser=None):
"""Initialize common Box resources.
The most important resources:
* ZMQ context
* SN context
* Logger
* Parsed arguments
"""
# Local contexts for dependencies
self.zmq_ctx = zmq.Context.instance()
self.sn_ctx = SN(self.zmq_ctx, argparser or get_arg_parser())
......@@ -40,29 +62,95 @@ class SNBox():
# Core methods - Will be implemented in non-abstract boxes
def check_configuration(self):
"""Check configuration of the box.
Check if box is able to start according to non-abstract box
requirements.
Method is called at least at the beginning of :meth:`run` method but
You can call it additionally at every place you want to (e.g. last step
of :meth:`__init__`).
"""
raise NotImplementedError("check_configuration")
def get_processed_message(self):
"""Return message that will be processed in loop.
There is expected that the message is already processed and not only
received. It provides variability in obtaining messages.
"""
raise NotImplementedError("get_processed_message")
def process_result(self, result):
"""Process generated message.
It usually means send to the Sentinel network. This method provides
variability in result format.
"""
raise NotImplementedError("process_result")
# Public API for boxes - will be optionally implemented in final boxes
def setup(self):
"""Setup all user's data.
This method is considered as a "constructor" for final-box. You can
allocate resources here or initialize important variables.
Final-box is not allowed to store its data as member variables (self.*).
It is considered as security mechanism. Every final-box must return a
dictionary. Variables will be available as ``self.ctx.dict_key`` in
another callbacks trough ``types.SimpleNamespace()``.
Setup is called at the beginning of :meth:`run` method, so there should be
all the box resources available.
Default implementation is pass.
"""
return {}
def teardown(self):
"""Destroy allocated resources.
This is place to release allocated resources if needed. Namesace
``self.ctx`` is still available at this point.
Default implementation is pass.
"""
pass
def before_first_request(self):
"""Generate message or do some other pre-run thing.
At this point is box fully set up but there is no message received.
With return value is treated in the same manner as :meth:`process` does.
It is useful for some initialisation message.
Default implementation is pass.
"""
pass
def process(self, msg_type, payload):
"""Process message callback.
This method is called as a part of :meth:`process_result`.
"""
raise NotImplementedError("process")
# Provided functionality - should be final implementation
def run(self):
"""Main method for all boxes.
Every existing box should be used in 2 steps:
* Initialization
* Call :meth:`run`
Nothing more or less. I recommend to run boxes as one-liner:
``FinalBox("box_name").run()``
"""
# This is the only way to be sure that check will be called.
# Constructors will be overwritten in non-abstract boxes
self.check_configuration()
......@@ -111,6 +199,11 @@ class SNBox():
self.process_result(result)
def teardown_box(self):
"""Destroy box specific resources allocated in :meth:`__init__`.
E.g. destroy ZMQ contex.
"""
self.zmq_ctx.destroy()
self.logger.info("SNBox shutting down box %s", self.name)
......@@ -151,29 +244,44 @@ class SNBox():
class SNPipelineBox(SNBox):
"""SNPipelineBox is implementation of **in-out** box.
It expects 2 specified resources called *in* and *out*. Then receives
messages from *in* resource, process message and sends the result to *out*
resource.
"""
def __init__(self, box_name, argparser=None):
"""Initializes *in* and *out* resources."""
super().__init__(box_name, argparser)
self.socket_recv = self.get_socket("in")
self.socket_send = self.get_socket("out")
def check_configuration(self):
"""Check if *in* and *out* are provided by arguments."""
if not self.socket_recv:
raise SetupError("Input socket wasn't provided")
if not self.socket_send:
raise SetupError("Output socket wasn't provided")
def teardown_box(self):
"""Explicitly closes *in* and *out* sockets and calls method of ancestor."""
self.socket_recv.close()
self.socket_send.close()
super().teardown_box()
def get_processed_message(self):
"""Receive message from *in* socket, parse it by :func:`parse_msg` and
call :meth:`process` method.
"""
msg = self.socket_recv.recv_multipart()
msg_type, payload = parse_msg(msg)
return self.process(msg_type, payload)
def process_result(self, result):
"""Check if :meth:`process` provided any answer and sent it to *out*
socket.
"""
if not result:
# The box hasn't any reasonable answer
return
......@@ -188,7 +296,20 @@ class SNPipelineBox(SNBox):
class SNGeneratorBox(SNBox):
"""SNGeneratorBox is implementation of **out-only** box.
It expects only *out* resource.
There is one big difference: :meth:`process` doesn't take ``msg_type`` and
``payload`` arguments and it must be a Python *generator*. (So it
yields it's results and doesn't return them.
**Warning**: ``SNBox`` is not able to provide it's standard protection
because Python generators automatically raises ``StopIteration`` after
uncatched exceptions.
"""
def __init__(self, box_name, argparser=None):
"""Initializes *out* resource."""
super().__init__(box_name, argparser)
self.socket_send = self.get_socket("out")
......@@ -198,19 +319,25 @@ class SNGeneratorBox(SNBox):
self.process_iterator = self.process()
def check_configuration(self):
"""Check *out* resource and check if :meth:`process` is a generator."""
if not self.socket_send:
raise SetupError("Output socket wasn't provided")
if not inspect.isgeneratorfunction(self.process):
raise SetupError("Generator is expected for output-only box")
def teardown_box(self):
"""Explicitly closes *out* socket and calls method of ancestor."""
self.socket_send.close()
super().teardown_box()
def get_processed_message(self):
"""Calls ``next()`` on :meth:`process` generator."""
return next(self.process_iterator)
def process_result(self, result):
"""Check if :meth:`process` provided any answer and sent it to *out*
socket.
"""
if not result:
# The box hasn't any reasonable answer
return
......@@ -225,31 +352,51 @@ class SNGeneratorBox(SNBox):
class SNTerminationBox(SNBox):
"""SNTerminationBox is implementation of **in-only** box.
It expects only *in* resource and raises :exc:`sn.SetupError` if
:meth:`process` has any result.
"""
def __init__(self, box_name, argparser=None):
"""Initializes *in* resource."""
super().__init__(box_name, argparser)
self.socket_recv = self.get_socket("in")
def check_configuration(self):
"""Check *in* resource."""
if not self.socket_recv:
raise SetupError("Input socket wasn't provided")
def teardown_box(self):
"""Explicitly closes *in* socket and calls method of ancestor."""
self.socket_recv.close()
super().teardown_box()
def get_processed_message(self):
"""Receive message from *in* socket, parse it by :func:`parse_msg` and
call :meth:`process` method.
"""
msg = self.socket_recv.recv_multipart()
msg_type, payload = parse_msg(msg)
return self.process(msg_type, payload)
def process_result(self, result):
"""Raises :exc:`sn.SetupError` because there is no resutl expected."""
if result:
raise SetupError("Input-only box generated output message. Possibly bug in box.")
class SNMultipleOutputPipelineBox(SNPipelineBox):
"""SNMultipleOutputPipelineBox is implementation of **in-out** box.
Its based on :class:`SNPipelineBox`. The only difference that it expects
not only one result but a ``list`` of results.
"""
def process_result(self, result):
"""Checks if :meth:`process` returned result, iterates over result and
call :meth:`process_result` of ancestor for each sub-result.
"""
if result:
for single_result in result:
super().process_result(single_result)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment