diff --git a/SpiffWorkflow/bpmn/serializer/config.py b/SpiffWorkflow/bpmn/serializer/config.py index ad2d868d..84ce01cd 100644 --- a/SpiffWorkflow/bpmn/serializer/config.py +++ b/SpiffWorkflow/bpmn/serializer/config.py @@ -85,7 +85,6 @@ BpmnEventConverter, ) from .helpers.spec import BpmnDataSpecificationConverter, EventDefinitionConverter -from .default.data_spec import IOSpecificationConverter from .default.process_spec import BpmnProcessSpecConverter from .default.task_spec import ( BpmnTaskSpecConverter, @@ -99,6 +98,7 @@ ParallelGatewayConverter, EventConverter, BoundaryEventConverter, + IOSpecificationConverter, ) from .default.event_definition import ( TimerEventDefinitionConverter, diff --git a/SpiffWorkflow/bpmn/serializer/default/data_spec.py b/SpiffWorkflow/bpmn/serializer/default/data_spec.py deleted file mode 100644 index 5701e2f3..00000000 --- a/SpiffWorkflow/bpmn/serializer/default/data_spec.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2023 Sartography -# -# This file is part of SpiffWorkflow. -# -# SpiffWorkflow is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# SpiffWorkflow is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301 USA - -from ..helpers.registry import BpmnConverter - -class IOSpecificationConverter(BpmnConverter): - - def to_dict(self, spec): - return { - 'data_inputs': [self.registry.convert(item) for item in spec.data_inputs], - 'data_outputs': [self.registry.convert(item) for item in spec.data_outputs], - } - - def from_dict(self, dct): - return self.target_class( - data_inputs=[self.registry.restore(item) for item in dct['data_inputs']], - data_outputs=[self.registry.restore(item) for item in dct['data_outputs']], - ) diff --git a/SpiffWorkflow/bpmn/serializer/default/process_spec.py b/SpiffWorkflow/bpmn/serializer/default/process_spec.py index bc8fd6b2..62c2bb3c 100644 --- a/SpiffWorkflow/bpmn/serializer/default/process_spec.py +++ b/SpiffWorkflow/bpmn/serializer/default/process_spec.py @@ -17,10 +17,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -from ..helpers.spec import WorkflowSpecConverter +from ..helpers.registry import BpmnConverter -class BpmnProcessSpecConverter(WorkflowSpecConverter): +class BpmnProcessSpecConverter(BpmnConverter): def convert_task_spec_extensions(self, task_spec, dct): # Extensions will be moved out of the base parser, but since we currently add them to some diff --git a/SpiffWorkflow/bpmn/serializer/default/task_spec.py b/SpiffWorkflow/bpmn/serializer/default/task_spec.py index d599d25b..e6b95eaa 100644 --- a/SpiffWorkflow/bpmn/serializer/default/task_spec.py +++ b/SpiffWorkflow/bpmn/serializer/default/task_spec.py @@ -19,20 +19,77 @@ from SpiffWorkflow.bpmn.specs.bpmn_task_spec import _BpmnCondition +from ..helpers.spec import BpmnConverter from ..helpers.spec import TaskSpecConverter +class IOSpecificationConverter(BpmnConverter): + """The converter for an IOSpecification""" + + def to_dict(self, spec): + """Converts an IO spec to a dictionary representation + + Arguments: + spec (BpmnIOSpecification): the `BpmnIOSpecification` of a `BpmnTaskSpec` + + Returns: + dict: a dictionary representation of the IO spec + """ + return { + 'data_inputs': [self.registry.convert(item) for item in spec.data_inputs], + 'data_outputs': [self.registry.convert(item) for item in spec.data_outputs], + } + + def from_dict(self, dct): + """Restore a `BpmnIOSpecification` from a dictionary representation + + Arguments: + dct (dict): the dictionary representation + + Returns: + `BpmnIOSpecification`: a `BpmnTaskSpec` IO spec + """ + return self.target_class( + data_inputs=[self.registry.restore(item) for item in dct['data_inputs']], + data_outputs=[self.registry.restore(item) for item in dct['data_outputs']], + ) + class BpmnTaskSpecConverter(TaskSpecConverter): + """The base converter for a `BpmnTaskSpec` + + This converter can be extended for customized task specs with additional attributes (e.g. the + ones defined in this module, which can serve as examples for anyone who has created a custom + BPMN task spec. + """ def to_dict(self, spec): - dct = self.get_default_attributes(spec) - return dct + """Create a dictionary representation of the shared `BpmnTaskSpec` attributes + + Arguments: + spec: the spec to be converter to a dictionary + + Returns: + dict: a dictionary representation of shared attributes + """ + return self.get_default_attributes(spec) def from_dict(self, dct): + """Restore a `BpmnTaskSpec` from a dictionary of attributes + + If you have added only custom attributes that can be passed to `__init__`, you won't need + to extend this. + + Arguments: + dct (dict): the task spec's dictionary representation + + Returns: + an instance of the target class + """ return self.task_spec_from_dict(dct) class ScriptTaskConverter(BpmnTaskSpecConverter): + """The default converter for `ScriptTask`""" def to_dict(self, spec): dct = self.get_default_attributes(spec) @@ -41,6 +98,7 @@ def to_dict(self, spec): class StandardLoopTaskConverter(BpmnTaskSpecConverter): + """The default converter for `StandardLoopTask`""" def to_dict(self, spec): dct = self.get_default_attributes(spec) @@ -49,6 +107,7 @@ def to_dict(self, spec): class MultiInstanceTaskConverter(BpmnTaskSpecConverter): + """The default converter for Parallel and Sequential MultiInstance Tasks""" def to_dict(self, spec): dct = self.get_default_attributes(spec) @@ -70,6 +129,7 @@ def from_dict(self, dct): class BoundaryEventJoinConverter(BpmnTaskSpecConverter): + """The default converter for `BoundaryEventJoin`""" def to_dict(self, spec): dct = super().to_dict(spec) @@ -77,6 +137,7 @@ def to_dict(self, spec): return dct class SubWorkflowConverter(BpmnTaskSpecConverter): + """The default converter for subworkflows (`SubWOrkflowTask`, `CallActivity`, `TransactionSubprocess`)""" def to_dict(self, spec): dct = super().to_dict(spec) @@ -89,6 +150,7 @@ def from_dict(self, dct): class ConditionalGatewayConverter(BpmnTaskSpecConverter): + """A converter class that adds attributes for a `TaskSpec` with conditional outputs""" def to_dict(self, spec): dct = super().to_dict(spec) @@ -114,6 +176,7 @@ def bpmn_condition_to_dict(self, condition): class ExclusiveGatewayConverter(ConditionalGatewayConverter): + """THe default converterfor `ExclusiveGateway`task specs""" def to_dict(self, spec): dct = super().to_dict(spec) @@ -128,6 +191,7 @@ def from_dict(self, dct): class ParallelGatewayConverter(BpmnTaskSpecConverter): + """The default converter for `ParallelGateway` task specs """ def to_dict(self, spec): dct = super().to_dict(spec) @@ -139,6 +203,7 @@ def from_dict(self, dct): class EventConverter(BpmnTaskSpecConverter): + """The default converter for BPMN events""" def to_dict(self, spec): dct = super().to_dict(spec) @@ -151,6 +216,7 @@ def from_dict(self, dct): class BoundaryEventConverter(EventConverter): + """The default converter for `BoundaryEvent` task specs""" def to_dict(self, spec): dct = super().to_dict(spec) diff --git a/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py b/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py index d2af5b28..7481abdf 100644 --- a/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py +++ b/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py @@ -41,8 +41,12 @@ class DictionaryConverter: If a registered `typename` is found, the supplied `from_dict` function will be called. Unrecognized objects will be returned as-is. - For a simple example of how to use this class, see the `BpmnDataConverter` in - `registry`. + For a simple example of how to use this class, see `registry.DefaultRegistry`. + + Attributes: + typenames (dict): a mapping class to typename + convert_to_dict (dict): a mapping of typename to function + convert_from_dct (dict): a mapping of typename to function """ def __init__(self): @@ -53,41 +57,46 @@ def __init__(self): def register(self, cls, to_dict, from_dict, typename=None): """Register a conversion/restoration. - The `to_dict` function must return a dictionary; if no `typename` is given, - the unquallified class name will be used. + Arguments: + cls: the class that will be converted/restored + to_dict (function): a function that will be called with the object as an argument + from_dict (function): a function that restores the object from the dict + typename (str): an optional typename for identifying the converted object - :param cls: the class that will be converted/restored - :param to_dict: a function that will be called with the object as an argument - :param from_dict: a function that restores the object from the dict - :param typename: an optional typename for identifying the converted object + Notes: + The `to_dict` function must return a dictionary; if no `typename` is given, + the unquallified class name will be used. """ typename = cls.__name__ if typename is None else typename self.typenames[cls] = typename - self.convert_to_dict[typename] = partial(self.obj_to_dict, typename, to_dict) - self.convert_from_dict[typename] = partial(self.obj_from_dict, from_dict) + self.convert_to_dict[typename] = partial(self._obj_to_dict, typename, to_dict) + self.convert_from_dict[typename] = partial(self._obj_from_dict, from_dict) @staticmethod - def obj_to_dict(typename, func, obj, **kwargs): + def _obj_to_dict(typename, func, obj, **kwargs): + """A method for automatically inserting the typename in the dictionary returned by to_dict.""" dct = func(obj, **kwargs) dct.update({'typename': typename}) return dct @staticmethod - def obj_from_dict(func, dct, **kwargs): + def _obj_from_dict(func, dct, **kwargs): + """A method for calling the from_dict function on recognized objects.""" return func(dct, **kwargs) def convert(self, obj, **kwargs): - """ + """Convert a known object to a dictionary. + This is the public conversion method. It will be applied to dictionary values, list items, and the object itself, applying the to_dict functions of any registered type to the objects, or return the object unchanged if it is not recognized. - :param obj: the object to be converter + Arguments: + obj: the object to be converter Returns: - the dictionary representation for registered objects or the original - for unregistered objects + dict: the dictionary representation for registered objects or the original for unregistered objects """ typename = self.typenames.get(obj.__class__) if typename in self.convert_to_dict: @@ -101,17 +110,18 @@ def convert(self, obj, **kwargs): return obj def restore(self, val, **kwargs): - """ + """Restore a known object from a dictionary. + This is the public restoration method. It will be applied to dictionary values, list items, and the value itself, checking for a `typename` key and applying the from_dict function of any registered type, or return the value unchanged if it is not recognized. - :param val: the value to be converted + Arguments: + val: the value to be converted Returns: - the restored object for registered objects or the original for - unregistered values + dict: the restored object for registered objects or the original for unregistered values """ if isinstance(val, dict) and 'typename' in val: from_dict = self.convert_from_dict.get(val.pop('typename')) diff --git a/SpiffWorkflow/bpmn/serializer/helpers/registry.py b/SpiffWorkflow/bpmn/serializer/helpers/registry.py index 57890a89..9cb441b6 100644 --- a/SpiffWorkflow/bpmn/serializer/helpers/registry.py +++ b/SpiffWorkflow/bpmn/serializer/helpers/registry.py @@ -23,13 +23,11 @@ from .dictionary import DictionaryConverter class DefaultRegistry(DictionaryConverter): - """ - The default converter for task and workflow data. It allows some commonly used python objects - to be converted to a form that can be serialized with JSOM - - It also serves as a simple example for anyone who needs custom data serialization. If you have - custom objects or python objects not included here in your workflow/task data, then you should - replace or extend this with one that can handle the contents of your workflow. + """This class forms the basis of serialization for BPMN workflows. + + It contains serialization rules for a few python data types that are not JSON serializable by default which + are used internally by Spiff. It can be instantiated and customized to handle arbitrary task or workflow + data as well (see `dictionary.DictionaryConverter`). """ def __init__(self): @@ -39,11 +37,28 @@ def __init__(self): self.register(timedelta, lambda v: { 'days': v.days, 'seconds': v.seconds }, lambda v: timedelta(**v)) def convert(self, obj): + """Convert an object to a dictionary, with preprocessing. + + Arguments: + obj: the object to preprocess and convert + + Returns: + the result of `convert` conversion after preprocessing + """ cleaned = self.clean(obj) return super().convert(cleaned) def clean(self, obj): - # This can be used to remove functions and other callables; by default we remove these from task data + """A method that can be used to preprocess an object before conversion to a dict. + + It is used internally by Spiff to remove callables from task data before serialization. + + Arguments: + obj: the object to preprocess + + Returns: + the preprocessed object + """ if isinstance(obj, dict): return dict((k, v) for k, v in obj.items() if not callable(v)) else: @@ -53,17 +68,16 @@ def clean(self, obj): class BpmnConverter: """The base class for conversion of BPMN classes. - In general, most classes that extend this would simply take an existing registry as an - argument and automatically supply the class along with the implementations of the - conversion functions `to_dict` and `from_dict`. + In general, most classes that extend this would simply take an existing registry as an argument + nd supply the class along with the implementations of the conversion functions `to_dict` and + `from_dict`. The operation of the converter is a little opaque, but hopefully makes sense with a little explanation. The registry is a `DictionaryConverter` that registers conversion methods by class. It can be pre-populated with methods for custom data (though this is not required) and is passed into - each of these sublclasses. When a subclass of this one gets instantiated, it adds the spec it - is intended to operate on to this registry. + subclasses of this one, which will consolidate conversions as classes are instantiated. There is a lot of interdependence across the classes in spiff -- most of them need to know about many of the other classes. Subclassing this is intended to consolidate the boiler plate required @@ -74,15 +88,15 @@ class BpmnConverter: So for example, it is not necessary to re-implemnent any of the event-based task spec conversions because, eg, the `MessageEventDefintion` was modified; the existing `MessageEventDefinitionConverter` - can be replaced with a customized one and it will automatically be used when the event specs are - transformed. + can be replaced with a customized one and it will automatically be used with any event-based task. """ def __init__(self, target_class, registry, typename=None): - """Constructor for a BPMN class. + """Constructor for a dictionary converter for a BPMN class. - :param spec_class: the class of the spec the subclass provides conversions for - :param registry: a registry of conversions to which this one should be added - :param typename: the name of the class as it will appear in the serialization + Arguemnts: + target_class: the type the subclass provides conversions for + registry (`DictionaryConverter`): a registry of conversions to which this one should be added + typename (str): the name of the class as it will appear in the serialization """ self.target_class = target_class self.registry = registry @@ -90,9 +104,11 @@ def __init__(self, target_class, registry, typename=None): self.registry.register(target_class, self.to_dict, self.from_dict, self.typename) def to_dict(self, spec): + """This method should take an object and convert it to a dictionary that is JSON-serializable""" raise NotImplementedError def from_dict(self, dct): + """This method take a dictionary and restore the original object""" raise NotImplementedError def mapping_to_dict(self, mapping, **kwargs): diff --git a/SpiffWorkflow/bpmn/serializer/helpers/spec.py b/SpiffWorkflow/bpmn/serializer/helpers/spec.py index 54f9a628..e08288cd 100644 --- a/SpiffWorkflow/bpmn/serializer/helpers/spec.py +++ b/SpiffWorkflow/bpmn/serializer/helpers/spec.py @@ -29,13 +29,30 @@ class BpmnDataSpecificationConverter(BpmnConverter): """This is the base Data Spec converter. - Currently the only use is Data Objects. + This is used for `DataObject` and `TaskDataReference`; it can be extended for other + types of data specs. """ def to_dict(self, data_spec): + """Convert a data specification into a dictionary. + + Arguments: + data_spec (`BpmnDataSpecification): a BPMN data specification + + Returns: + dict: a dictionary representation of the data spec + """ return { 'bpmn_id': data_spec.bpmn_id, 'bpmn_name': data_spec.bpmn_name } def from_dict(self, dct): + """Restores a data specification. + + Arguments: + dct (dict): the dictionary representation + + Returns: + an instance of the target class + """ return self.target_class(**dct) @@ -48,6 +65,14 @@ class EventDefinitionConverter(BpmnConverter): """ def to_dict(self, event_definition): + """Convert an event definition into a dictionary. + + Arguments: + event_definition: the event definition + + Returns: + dict: a dictionary representation of the event definition + """ dct = { 'description': event_definition.description, 'name': event_definition.name @@ -55,19 +80,42 @@ def to_dict(self, event_definition): return dct def from_dict(self, dct): + """Restores an event definition. + + Arguments: + dct: the dictionary representation + + Returns; + an instance of the target event definition + """ event_definition = self.target_class(**dct) return event_definition def correlation_properties_to_dict(self, props): + """Convert correlation properties to a dictionary representation. + + Arguments: + list(`CorrelationProperty`): the correlation properties associated with a message + + Returns: + list(dict): a list of dictionary representations of the correlation properties + """ return [prop.__dict__ for prop in props] def correlation_properties_from_dict(self, props): + """Restore correlation properties from a dictionary representation + + Arguments: + props (list(dict)): a list if dictionary representations of correlation properties + + Returns: + a list of `CorrelationProperty` of a message + """ return [CorrelationProperty(**prop) for prop in props] class TaskSpecConverter(BpmnConverter): - """ - This the base Task Spec Converter. + """Base Task Spec Converter. It contains methods for parsing generic and BPMN task spec attributes. @@ -75,17 +123,17 @@ class TaskSpecConverter(BpmnConverter): implement a converter for those task spec types. You'll need to implement the `to_dict` and `from_dict` methods on any inheriting classes. - The default task spec converters are in the `task`, 'process_spec`, and 'event_definitions` - modules of this package; the `camunda`,`dmn`, and `spiff` serialization packages contain other - examples. + The default task spec converters are in the `default.task_spec` modules of this package; the + `camunda`,`dmn`, and `spiff` serialization packages contain other examples. """ def get_default_attributes(self, spec): - """Extracts the default Spiff attributes from a task spec. + """Extracts the default BPMN attributes from a task spec. - :param spec: the task spec to be converted + Arguments: + spec: the task spec to be converted Returns: - a dictionary of standard task spec attributes + dict: a dictionary of standard task spec attributes """ return { 'name': spec.name, @@ -98,18 +146,19 @@ def get_default_attributes(self, spec): 'bpmn_name': spec.bpmn_name, 'lane': spec.lane, 'documentation': spec.documentation, - 'data_input_associations': [ self.registry.convert(obj) for obj in spec.data_input_associations ], - 'data_output_associations': [ self.registry.convert(obj) for obj in spec.data_output_associations ], + 'data_input_associations': self.registry.convert(spec.data_input_associations), + 'data_output_associations': self.registry.convert(spec.data_output_associations), 'io_specification': self.registry.convert(spec.io_specification), } def get_join_attributes(self, spec): """Extracts attributes for task specs that inherit from `Join`. - :param spec: the task spec to be converted + Arguments: + spec: the task spec to be converted Returns: - a dictionary of `Join` task spec attributes + dict: a dictionary of `Join` task spec attributes """ return { 'split_task': spec.split_task, @@ -120,20 +169,22 @@ def get_join_attributes(self, spec): def get_subworkflow_attributes(self, spec): """Extracts attributes for task specs that inherit from `SubWorkflowTask`. - :param spec: the task spec to be converted + Arguments: + spec: the task spec to be converted Returns: - a dictionary of subworkflow task spec attributes + dict: a dictionary of subworkflow task spec attributes """ return {'spec': spec.spec} def get_standard_loop_attributes(self, spec): """Extracts attributes for standard loop tasks. - - :param spec: the task spec to be converted + + Arguments: + spec: the task spec to be converted Returns: - a dictionary of standard loop task spec attributes + dict: a dictionary of standard loop task spec attributes """ return { 'task_spec': spec.task_spec, @@ -143,72 +194,29 @@ def get_standard_loop_attributes(self, spec): } def task_spec_from_dict(self, dct): - """ - Creates a task spec based on the supplied dictionary. It handles setting the default - task spec attributes as well as attributes added by `BpmnSpecMixin`. + """Creates a task spec based on the supplied dictionary. + + It handles setting the default task spec attributes as well as attributes added by `BpmnSpecMixin`. - :param dct: the dictionary to create the task spec from + Arguments: + dct (dict): the dictionary to create the task spec from Returns: a restored task spec """ dct['data_input_associations'] = self.registry.restore(dct.pop('data_input_associations', [])) dct['data_output_associations'] = self.registry.restore(dct.pop('data_output_associations', [])) - - inputs = dct.pop('inputs') - outputs = dct.pop('outputs') + dct['io_specification'] = self.registry.restore(dct.pop('io_specification', None)) wf_spec = dct.pop('wf_spec') name = dct.pop('name') bpmn_id = dct.pop('bpmn_id') spec = self.target_class(wf_spec, name, **dct) - spec._inputs = inputs - spec._outputs = outputs if issubclass(self.target_class, BpmnSpecMixin) and bpmn_id != name: # This is a hack for multiinstance tasks :( At least it is simple. # Ideally I'd fix it in the parser, but I'm afraid of quickly running into a wall there spec.bpmn_id = bpmn_id - if isinstance(spec, BpmnSpecMixin): - spec.io_specification = self.registry.restore(dct.pop('io_specification', None)) - - return spec - - -class WorkflowSpecConverter(BpmnConverter): - """ - This is the base converter for a BPMN workflow spec. - - It will register converters for the task spec types contained in the workflow, as well as - the workflow spec class itself. - - This class can be extended if you implement a custom workflow spec type. See the converter - in `workflow_spec_converter` for an example. - """ - - def __init__(self, spec_class, registry): - """ - Converter for a BPMN workflow spec class. - - The `to_dict` and `from_dict` methods of the given task spec converter classes will - be registered, so that they can be restored automatically. - - The data_converter applied to task *spec* data, not task data, and may be `None`. See - `BpmnTaskSpecConverter` for more discussion. - - :param spec_class: the workflow spec class - :param task_spec_converters: a list of `BpmnTaskSpecConverter` classes - """ - super().__init__(spec_class, registry) - - # Leaving these as-as, as I can't imagine anyone would need or want to extend - self.registry.register(Attrib, self.attrib_to_dict, partial(self.attrib_from_dict, Attrib)) - self.registry.register(PathAttrib, self.attrib_to_dict, partial(self.attrib_from_dict, PathAttrib)) - - def attrib_to_dict(self, attrib): - return { 'name': attrib.name } - - def attrib_from_dict(self, attrib_class, dct): - return attrib_class(dct['name']) \ No newline at end of file + return spec \ No newline at end of file diff --git a/SpiffWorkflow/bpmn/serializer/workflow.py b/SpiffWorkflow/bpmn/serializer/workflow.py index 18426718..84149254 100644 --- a/SpiffWorkflow/bpmn/serializer/workflow.py +++ b/SpiffWorkflow/bpmn/serializer/workflow.py @@ -24,58 +24,57 @@ from .config import DEFAULT_CONFIG -# This is the default version set on the workflow, it can be overwritten in init +# This is the default version set on the workflow, it can be overridden in init VERSION = "1.3" class BpmnWorkflowSerializer: - """ - This class implements a customizable BPMN Workflow serializer, based on a Workflow Spec Converter - and a Data Converter. - - The goal is to provide modular serialization capabilities. - - You'll need to configure a Workflow Spec Converter with converters for any task, data, or event types - present in your workflows. + """This class implements a customizable BPMN Workflow serializer, based on the `DefaultRegistry`. - If you have implemented any custom specs, you'll need to write a converter to handle them and - replace the converter from the default confiuration with your own. + Workflows contain two types of objects: workflows/tasks/standard specs (objects that Spiff provides + serialization for automatically) and arbitrary data (associated with tasks and workflows). The goal + of this serializer is to provide a mechanism that allows for handling both, as well as the ability + to replace one of the default internal conversion mechanisms with your own if you've extended any of + the classes. - If your workflow contains non-JSON-serializable objects, you'll need to extend or replace the - default data converter with one that will handle them. This converter needs to implement - `convert` and `restore` methods. + See `configure` for more details on customization. Serialization occurs in two phases: the first is to convert everything in the workflow to a - dictionary containing only JSON-serializable objects and the second is dumping to JSON. - - This means that you can call the `workflow_to_dict` or `workflow_from_dict` methods separately from - conversion to JSON for further manipulation of the state, or selective serialization of only certain - parts of the workflow more conveniently. You can of course call methods from the Workflow Spec and - Data Converters via the `spec_converter` and `data_converter` attributes as well to bypass the - overhead of converting or restoring the entire thing. + dictionary containing only JSON-serializable objects and the second is dumping to JSON, which happens + only at the very end. + + Attributes: + registry (`DictionaryConverter`): a registry that keeps track of all objects the serializer knows + json_encoder_cls: passed into `convert` to provides additional json encding capabilities (optional) + json_decoder_cls: passed into `restore` to provide additional json decoding capabilities (optional) + version (str): the serializer version """ VERSION_KEY = "serializer_version" # Why is this customizable? @staticmethod def configure(config=None, registry=None): - """ - This method can be used to create a spec converter that uses custom specs. + """Can be used to create a with custom Spiff classes. + + If you have replaced any of the default classes that Spiff uses with your own, Spiff will not know + how to serialize them and you'll have to provide conversion mechanisms. - The task specs may contain arbitrary data, though none of the default task specs use it. We don't - recommend that you do this, as we may disallow it in the future. However, if you have task spec data, - then you'll also need to make sure it can be serialized. + The `config` is a dictionary with keys for each (Spiff) class that needs to be handled that map to a + converter for that class. There are some basic converters which provide from methods for handling + essential Spiff attributes in the `helpers` package of this module; the default converters, found in + the `defaults` package of this module extend these. The default configuration is found in `config`. - The workflow spec serializer is based on the `DictionaryConverter` in the `helpers` package. You can - create one of your own, add custom data serializtion to that and pass that in as the `registry`. The - conversion classes in the spec_config will be added this "registry" and any classes with entries there - will be serialized/deserialized. + The `registry` contains optional custom data conversions and the items in `config` will be added to + it, to create one repository of information about serialization. See `DictionaryConverter` for more + information about customized data. This parameter is optional and if not provided, `DefaultRegistry` + will be used. - See the documentation for `helpers.spec.BpmnSpecConverter` for more information about what's going - on here. + Objects that are unknown to the `registry` will be passed on as-is and serialization can be handled + through custom JSON encoding/decoding as an alternative. - :param spec_config: a dictionary specifying how to save and restore any classes used by the spec - :param registry: a `DictionaryConverter` with conversions for custom data (if applicable) + Arguments: + spec_config (dict): a mapping of class -> objects containing `BpmnConverter` + registry (`DictionaryConverter`): with conversions for custom data (if applicable) """ config = config or DEFAULT_CONFIG if registry is None: @@ -85,11 +84,13 @@ def configure(config=None, registry=None): return registry def __init__(self, registry=None, version=VERSION, json_encoder_cls=None, json_decoder_cls=None): - """Intializes a Workflow Serializer with the given Workflow, Task and Data Converters. + """Intializes a Workflow Serializer. - :param registry: a registry of conversions to dictionaries - :param json_encoder_cls: JSON encoder class to be used for dumps/dump operations - :param json_decoder_cls: JSON decoder class to be used for loads/load operations + Arguments: + registry (`DictionaryConverter`): a registry that keeps track of all objects the serializer knows + version (str): the serializer version + json_encoder_cls: passed into `convert` to provides additional json encding capabilities (optional) + json_decoder_cls: passed into `restore` to provide additional json decoding capabilities (optional) """ super().__init__() self.registry = registry or self.configure() @@ -100,10 +101,12 @@ def __init__(self, registry=None, version=VERSION, json_encoder_cls=None, json_d def serialize_json(self, workflow, use_gzip=False): """Serialize the dictionary representation of the workflow to JSON. - :param workflow: the workflow to serialize + Arguments: + workflow: the workflow to serialize + use_gzip (bool): optionally gzip the resulting string Returns: - a JSON dump of the dictionary representation + a JSON dump of the dictionary representation or a gzipped version of it """ dct = self.to_dict(workflow) dct[self.VERSION_KEY] = self.VERSION @@ -111,12 +114,29 @@ def serialize_json(self, workflow, use_gzip=False): return gzip.compress(json_str.encode('utf-8')) if use_gzip else json_str def deserialize_json(self, serialization, use_gzip=False): + """Deserialize a workflow from an optionally zipped JSON-dumped workflow. + + Arguments: + serialization: the serialization to restore + use_gzip (bool): optionally gunzip the input + + Returns: + the restored workflow + """ json_str = gzip.decompress(serialization) if use_gzip else serialization dct = json.loads(json_str, cls=self.json_decoder_cls) self.migrate(dct) return self.from_dict(dct) def get_version(self, serialization): + """Get the version specified in the serialization + + Arguments: + serialization: a string or dictionary representation of a workflow + + Returns: + the version of the serializer the serilization we done with, if present + """ if isinstance(serialization, dict): return serialization.get(self.VERsiON_KEY) elif isinstance(serialization, str): @@ -124,13 +144,35 @@ def get_version(self, serialization): return dct.get(self.VERSION_KEY) def migrate(self, dct): - # Upgrade serialized version if necessary + """Update the serialization format, if necessaary.""" version = dct.pop(self.VERSION_KEY) if version in MIGRATIONS: MIGRATIONS[version](dct) def to_dict(self, obj, **kwargs): + """Apply any know conversions to an object. + + Arguments: + obj: the object + + Keyword arguments: + optional keyword args that will be passed to `self.registry.convert` + + Returns: + a dictionary representation of the object + """ return self.registry.convert(obj, **kwargs) def from_dict(self, dct, **kwargs): + """Restore an known object from a dict. + + Arguments: + dct: the dictionary representation of the object + + Keyword arguments: + optional keyword args that will be passed to `self.registry.restore` + + Returns: + a restored object + """ return self.registry.restore(dct, **kwargs) \ No newline at end of file diff --git a/SpiffWorkflow/bpmn/specs/bpmn_task_spec.py b/SpiffWorkflow/bpmn/specs/bpmn_task_spec.py index 22e805ef..a3002113 100644 --- a/SpiffWorkflow/bpmn/specs/bpmn_task_spec.py +++ b/SpiffWorkflow/bpmn/specs/bpmn_task_spec.py @@ -28,7 +28,8 @@ class BpmnTaskSpec(TaskSpec): easy way of knowing whether a task appearson the diagram. """ def __init__(self, wf_spec, name, lane=None, documentation=None, - data_input_associations=None, data_output_associations=None, **kwargs): + data_input_associations=None, data_output_associations=None, + io_specification=None, **kwargs): """ :param lane: Indicates the name of the lane that this task belongs to :param documentation: the contents of the documentation element @@ -42,7 +43,7 @@ def __init__(self, wf_spec, name, lane=None, documentation=None, self.documentation = documentation self.data_input_associations = data_input_associations or [] self.data_output_associations = data_output_associations or [] - self.io_specification = None + self.io_specification = io_specification if self.description is None: self.description = 'BPMN Task' diff --git a/SpiffWorkflow/specs/base.py b/SpiffWorkflow/specs/base.py index 8b92ce77..7e9ef2f9 100644 --- a/SpiffWorkflow/specs/base.py +++ b/SpiffWorkflow/specs/base.py @@ -92,8 +92,8 @@ def __init__(self, wf_spec, name, **kwargs): self._wf_spec = wf_spec self.name = str(name) self.description = kwargs.get('description', None) - self._inputs = [] - self._outputs = [] + self._inputs = kwargs.get('inputs', []) + self._outputs = kwargs.get('outputs', []) self.manual = kwargs.get('manual', False) self.data = kwargs.get('data', {}) self.defines = kwargs.get('defines', {})