code: # coding=utf-8
  """
  Implementation of exporting process to CSV functionality, as proposed in article "Spreadsheet-Based Business
  Process Modeling" by Kluza k. and Wisniewski P.
  """
  from __future__ import print_function
  
  import copy
  
  import pandas as pd
  import re
  import six
  
  from . import bpmn_python_consts as consts
  from . import bpmn_diagram_exception as bpmn_exception
  
  regex_pa_trailing_number = r'^(.*[a-z|A-Z]|[^0-9]?)([0-9]+)$'
  regex_pa_trailing_letter = r'(.+)([a-z|A-Z])'
  regex_pa_merge_node_finder = r'(.*?)([0-9]+[a-z|A-Z])(.*?)'
  regex_pa_num_let = r'([0-9]+)([a-z,A-Z])'
  regex_prefix_split_succ = r'^'
  regex_suffix_split_succ = r'([a-z|A-Z]|[a-z|A-Z][1]+)$'
  
  default_process_id = 'process_1'
  default_plane_id = 'plane_1'
  
  
  def get_node_type(order, csv_line_dict):
      """
  
      :param order:
      :param csv_line_dict:
      :return:
      """
      if order == str(0):
          return consts.Consts.start_event
      if csv_line_dict[consts.Consts.csv_terminated] == 'yes':
          return consts.Consts.end_event
      if csv_line_dict[consts.Consts.csv_subprocess] == 'yes':
          return consts.Consts.subprocess
      else:
          return consts.Consts.task
  
  
  def add_node_info_to_diagram_graph(order, node_type, activity, process_id, bpmn_diagram):
      """
  
      :param order:
      :param node_type:
      :param activity:
      :param process_id:
      :param bpmn_diagram:
      """
      if node_type == consts.Consts.start_event:
          bpmn_diagram.add_start_event_to_diagram(process_id, start_event_name=activity, node_id=order)
      elif node_type == consts.Consts.subprocess:
          bpmn_diagram.add_subprocess_to_diagram(process_id, subprocess_name=activity, node_id=order)
      elif node_type == consts.Consts.end_event:
          bpmn_diagram.add_end_event_to_diagram(process_id, node_id=order)
      elif node_type == consts.Consts.inclusive_gateway:
          bpmn_diagram.add_inclusive_gateway_to_diagram(process_id, node_id=order)
      elif node_type == consts.Consts.exclusive_gateway:
          bpmn_diagram.add_exclusive_gateway_to_diagram(process_id, node_id=order)
      elif node_type == consts.Consts.parallel_gateway:
          bpmn_diagram.add_parallel_gateway_to_diagram(process_id, node_id=order)
      else:
          bpmn_diagram.add_task_to_diagram(process_id, task_name=activity, node_id=order)
  
  
  def import_nodes_info(process_dict, bpmn_diagram):
      """
  
      :param process_dict:
      :param bpmn_diagram:
      """
      for order, csv_line_dict in process_dict.items():
          node_type = get_node_type(order, csv_line_dict)
          activity = process_dict[order][consts.Consts.csv_activity]
          process_id = default_process_id
          add_node_info_to_diagram_graph(order, node_type, activity, process_id, bpmn_diagram)
  
  
  def remove_white_spaces_in_orders(process_dict):
      """
  
      :param process_dict:
      """
      tmp_process_dict = copy.deepcopy(process_dict)
      for order, csv_line_dict in tmp_process_dict.items():
          del process_dict[order]
          if isinstance(order, six.string_types) and order.strip() != order:
              process_dict[order.strip()] = csv_line_dict
          else:
              process_dict[str(order)] = csv_line_dict
  
  
  def get_possible_sequence_continuation_successor(node_id):
      """
  
      :param node_id:
      :return:
      """
      result = re.match(regex_pa_trailing_number, node_id)
      if result:
          last_number_in_order = result.group(2)
          next_number = str(int(last_number_in_order) + 1)
          prefix = result.group(1)
          return [prefix + next_number]
      else:
          # possible if e.g. 4a
          return []
  
  
  def get_possible_split_continuation_successor(node_id):
      """
  
      :param node_id:
      :return:
      """
      result = re.match(regex_pa_trailing_number, node_id)
      if result:
          trailing_number = result.group(2)
          prefix = result.group(1)
          new_trailing_number = str(int(trailing_number) + 1)
          new_node_id = prefix + new_trailing_number
          return [new_node_id + 'a', new_node_id + 'a1']
      else:
          return []
  
  
  def get_possible_merge_continuation_successors(node_id_arg):
      """
  
      :param node_id_arg:
      :return:
      """
      node_id = copy.deepcopy(node_id_arg)
      result_trailing_number = re.match(regex_pa_trailing_number, node_id)
      if result_trailing_number:
          node_id = result_trailing_number.group(1)
  
      result_trailing_letter = re.match(regex_pa_trailing_letter, node_id)
      if result_trailing_letter:
          possible_successors = []
          for result in re.finditer(regex_pa_merge_node_finder, node_id):
              num_let_pair = result.group(2)
              prefix = result.group(1)
              num_let_result = re.match(regex_pa_num_let, num_let_pair)
              num = num_let_result.group(1)
              inc_num = str(int(num) + 1)
              possible_successors.append(prefix + inc_num)
          return possible_successors
      else:
          return []
  
  
  def is_any_possible_successor_present_in_node_ids(possible_successors, nodes_ids):
      """
  
      :param possible_successors:
      :param nodes_ids:
      :return:
      """
      return bool(get_possible_successors_set_present_in_node_ids(possible_successors, nodes_ids))
  
  
  def get_possible_successors_set_present_in_node_ids(possible_successors, nodes_ids):
      """
  
      :param possible_successors:
      :param nodes_ids:
      :return:
      """
      return set(possible_successors).intersection(set(nodes_ids))
  
  
  def get_possible_successor_present_in_node_ids_or_raise_excp(poissible_successors_node_id, nodes_ids):
      """
  
      :param poissible_successors_node_id:
      :param nodes_ids:
      :return:
      """
      possible_successor_set = get_possible_successors_set_present_in_node_ids(poissible_successors_node_id, nodes_ids)
      if len(possible_successor_set) != 1:
          raise bpmn_exception.BpmnPythonError("Some error in program - there should be exactly one found successor.")
      else:
          return possible_successor_set.pop()
  
  
  def get_all_split_successors(node_id, nodes_ids):
      """
  
      :param node_id:
      :param nodes_ids:
      :return:
      """
      result = re.match(regex_pa_trailing_number, node_id)
      if not result:
          raise bpmn_exception.BpmnPythonError("Something wrong in program - look for " + node_id)
      trailing_number = result.group(2)
      prefix = result.group(1)
      new_trailing_number = str(int(trailing_number) + 1)
      next_node_id = prefix + new_trailing_number
  
      pattern = regex_prefix_split_succ + next_node_id + regex_suffix_split_succ
      split_successors = []
      for elem in nodes_ids:
          if re.match(pattern, elem):
              split_successors.append(elem)
      return split_successors
  
  
  def is_there_sequence_continuation(node_id, nodes_ids):
      """
  
      :param node_id:
      :param nodes_ids:
      :return:
      """
      possible_seq_succ = get_possible_sequence_continuation_successor(node_id)
      return is_any_possible_successor_present_in_node_ids(possible_seq_succ, nodes_ids)
  
  
  def is_there_split_continuation(node_id, nodes_ids):
      """
  
      :param node_id:
      :param nodes_ids:
      :return:
      """
      possible_split_succ = get_possible_split_continuation_successor(node_id)
      return is_any_possible_successor_present_in_node_ids(possible_split_succ, nodes_ids)
  
  
  def is_there_merge_continuation(node_id, nodes_ids):
      """
  
      :param node_id:
      :param nodes_ids:
      :return:
      """
      possible_merge_succ = get_possible_merge_continuation_successors(node_id)
      return is_any_possible_successor_present_in_node_ids(possible_merge_succ, nodes_ids)
  
  
  def is_node_the_end_event(node_id, process_dict):
      """
  
      :param node_id:
      :param process_dict:
      :return:
      """
      return process_dict[node_id][consts.Consts.csv_terminated] == 'yes'
  
  
  def add_outgoing_flow(node_id, successor_node_id, bpmn_diagram):
      """
  
      :param node_id:
      :param successor_node_id:
      :param bpmn_diagram:
      """
      if bpmn_diagram.diagram_graph.node[node_id].get(consts.Consts.outgoing_flow) is None:
          bpmn_diagram.diagram_graph.node[node_id][consts.Consts.outgoing_flow] = []
      bpmn_diagram.diagram_graph.node[node_id][consts.Consts.outgoing_flow].append(get_flow_id(node_id, successor_node_id))
  
  
  def add_incoming_flow(node_id, from_node_id, bpmn_diagram):
      """
  
      :param node_id:
      :param from_node_id:
      :param bpmn_diagram:
      """
      if bpmn_diagram.diagram_graph.node[node_id].get(consts.Consts.incoming_flow) is None:
          bpmn_diagram.diagram_graph.node[node_id][consts.Consts.incoming_flow] = []
      bpmn_diagram.diagram_graph.node[node_id][consts.Consts.incoming_flow].append(get_flow_id(from_node_id, node_id))
  
  
  def get_connection_condition_if_present(to_node_id, process_dict):
      """
  
      :param to_node_id:
      :param process_dict:
      :return:
      """
      if to_node_id in process_dict:
          return process_dict[to_node_id].get(consts.Consts.csv_condition)
  
  
  def get_flow_id(from_node_id, to_node_id):
      """
  
      :param from_node_id:
      :param to_node_id:
      :return:
      """
      return from_node_id + "__" + to_node_id
  
  
  def add_edge(from_node_id, to_node_id, process_dict, bpmn_diagram, sequence_flows):
      """
  
      :param from_node_id:
      :param to_node_id:
      :param process_dict:
      :param bpmn_diagram:
      :param sequence_flows:
      """
      condition = get_connection_condition_if_present(to_node_id, process_dict)
      bpmn_diagram.diagram_graph.add_edge(from_node_id, to_node_id)
      flow_id = get_flow_id(from_node_id, to_node_id)
      bpmn_diagram.diagram_graph[from_node_id][to_node_id][consts.Consts.id] = flow_id
      bpmn_diagram.diagram_graph[from_node_id][to_node_id][consts.Consts.process] = default_process_id
      bpmn_diagram.diagram_graph[from_node_id][to_node_id][consts.Consts.name] = ""
      bpmn_diagram.diagram_graph[from_node_id][to_node_id][consts.Consts.source_ref] = from_node_id
      bpmn_diagram.diagram_graph[from_node_id][to_node_id][consts.Consts.target_ref] = to_node_id
      if bool(condition):
          bpmn_diagram.diagram_graph[from_node_id][to_node_id][consts.Consts.condition_expression] = {
              consts.Consts.id: flow_id + "_cond",
              consts.Consts.condition_expression: condition
          }
      sequence_flows[flow_id] = {consts.Consts.name: flow_id, consts.Consts.source_ref: from_node_id,
                                 consts.Consts.target_ref: to_node_id}
  
  
  def add_connection(from_node_id, to_node_id, process_dict, diagram_graph, sequence_flows):
      """
  
      :param from_node_id:
      :param to_node_id:
      :param process_dict:
      :param diagram_graph:
      :param sequence_flows:
      """
      add_outgoing_flow(from_node_id, to_node_id, diagram_graph)
      add_incoming_flow(to_node_id, from_node_id, diagram_graph)
      add_edge(from_node_id, to_node_id, process_dict, diagram_graph, sequence_flows)
  
  
  def get_node_conditions(split_successors, process_dict):
      """
  
      :param split_successors:
      :param process_dict:
      :return:
      """
      conditions = []
      for succ in split_successors:
          conditions.append(process_dict[succ][consts.Consts.csv_condition].strip())
      return conditions
  
  
  def yes_no_conditions(node_conditions):
      """
  
      :param node_conditions:
      :return:
      """
      return set(node_conditions) == {"yes", "no"}
  
  
  def sth_else_conditions(node_conditions):
      """
  
      :param node_conditions:
      :return:
      """
      return "else" in node_conditions
  
  
  def no_conditions(node_conditions):
      """
  
      :param node_conditions:
      :return:
      """
      for node in node_conditions:
          if bool(node):
              return False
      return True
  
  
  def get_gateway_type(node_id_to_add_after, nodes_ids, process_dict):
      """
  
      :param node_id_to_add_after:
      :param nodes_ids:
      :param process_dict:
      :return:
      """
      split_successors = get_all_split_successors(node_id_to_add_after, nodes_ids)
      successors_conditions = get_node_conditions(split_successors, process_dict)
      if len(split_successors) == 2:
          if yes_no_conditions(successors_conditions) or sth_else_conditions(successors_conditions):
              return consts.Consts.exclusive_gateway
      if no_conditions(successors_conditions):
          return consts.Consts.parallel_gateway
      return consts.Consts.inclusive_gateway
  
  
  def add_split_gateway(node_id_to_add_after, nodes_ids, process_dict, diagram_graph):
      """
  
      :param node_id_to_add_after:
      :param nodes_ids:
      :param process_dict:
      :param diagram_graph:
      :return:
      """
      split_gateway_id = node_id_to_add_after + "_split"
      process_id = default_process_id
      gateway_type = get_gateway_type(node_id_to_add_after, nodes_ids, process_dict)
      activity = ""
      add_node_info_to_diagram_graph(split_gateway_id, gateway_type, activity, process_id, diagram_graph)
      return split_gateway_id
  
  
  def get_merge_node_type(merge_successor_id, bpmn_diagram):
      """
  
      :param merge_successor_id:
      :param bpmn_diagram:
      :return:
      """
      result = re.match(regex_pa_trailing_number, merge_successor_id)
      if result:
          trailing_number = result.group(2)
          prev_prev_number = int(trailing_number) - 2
          if prev_prev_number < 0:
              raise bpmn_exception.BpmnPythonError("Something wrong in csv file syntax - look for " + merge_successor_id)
          prefix = result.group(1)
          split_node_id = prefix + str(prev_prev_number) + "_split"
          if bool(bpmn_diagram.diagram_graph.has_node(split_node_id)):
              node_type = bpmn_diagram.diagram_graph.node[split_node_id][consts.Consts.type]
              if bool(node_type):
                  return node_type
          return consts.Consts.inclusive_gateway
  
  
  def add_merge_gateway_if_not_exists(merge_successor_id, bpmn_diagram):
      """
  
      :param merge_successor_id:
      :param bpmn_diagram:
      :return:
      """
      merge_gateway_id = merge_successor_id + "_join"
      if bpmn_diagram.diagram_graph.has_node(merge_gateway_id):
          just_created = False
          return merge_gateway_id, just_created
      else:
          just_created = True
          process_id = default_process_id
          gateway_type = get_merge_node_type(merge_successor_id, bpmn_diagram)
          activity = ""
          add_node_info_to_diagram_graph(merge_gateway_id, gateway_type, activity, process_id, bpmn_diagram)
          return merge_gateway_id, just_created
  
  
  def fill_graph_connections(process_dict, bpmn_diagram, sequence_flows):
      """
  
      :param process_dict:
      :param bpmn_diagram:
      :param sequence_flows:
      """
      nodes_ids = list(bpmn_diagram.diagram_graph.node.keys())
      nodes_ids_to_process = copy.deepcopy(nodes_ids)
      while bool(nodes_ids_to_process):
          node_id = str(nodes_ids_to_process.pop(0))
          if is_node_the_end_event(node_id, process_dict):
              pass
          elif is_there_sequence_continuation(node_id, nodes_ids):
              possible_sequence_successors = get_possible_sequence_continuation_successor(node_id)
              successor_node_id = get_possible_successor_present_in_node_ids_or_raise_excp(possible_sequence_successors,
                                                                                           nodes_ids)
              add_connection(node_id, successor_node_id, process_dict, bpmn_diagram, sequence_flows)
          elif is_there_split_continuation(node_id, nodes_ids):
              split_gateway_id = add_split_gateway(node_id, nodes_ids, process_dict, bpmn_diagram)
              add_connection(node_id, split_gateway_id, process_dict, bpmn_diagram, sequence_flows)
              for successor_node_id in get_all_split_successors(node_id, nodes_ids):
                  add_connection(split_gateway_id, successor_node_id, process_dict, bpmn_diagram, sequence_flows)
              pass
          elif is_there_merge_continuation(node_id, nodes_ids):
              possible_merge_successors = get_possible_merge_continuation_successors(node_id)
              merge_successor_id = get_possible_successor_present_in_node_ids_or_raise_excp(possible_merge_successors,
                                                                                            nodes_ids)
              merge_gateway_id, just_created = add_merge_gateway_if_not_exists(merge_successor_id, bpmn_diagram)
              if just_created:
                  add_connection(merge_gateway_id, merge_successor_id, process_dict, bpmn_diagram, sequence_flows)
              add_connection(node_id, merge_gateway_id, process_dict, bpmn_diagram, sequence_flows)
          else:
              raise bpmn_exception.BpmnPythonError("Something wrong in csv file syntax - look for " + node_id)
  
  
  def remove_outgoing_connection(base_node, bpmn_diagram, sequence_flows):
      """
  
      :param base_node:
      :param bpmn_diagram:
      :param sequence_flows:
      :return:
      """
      outgoing_flow_id = bpmn_diagram.diagram_graph.node[base_node][consts.Consts.outgoing_flow][0]
      neighbour_node = sequence_flows[outgoing_flow_id][consts.Consts.target_ref]
      bpmn_diagram.diagram_graph.node[neighbour_node][consts.Consts.incoming_flow].remove(outgoing_flow_id)
      del sequence_flows[outgoing_flow_id]
      bpmn_diagram.diagram_graph.remove_edge(base_node, neighbour_node)
      return neighbour_node
  
  
  def remove_incoming_connection(base_node, bpmn_diagram, sequence_flows):
      """
  
      :param base_node:
      :param bpmn_diagram:
      :param sequence_flows:
      :return:
      """
      incoming_flow_id = bpmn_diagram.diagram_graph.node[base_node][consts.Consts.incoming_flow][0]
      neighbour_node = sequence_flows[incoming_flow_id][consts.Consts.source_ref]
      bpmn_diagram.diagram_graph.node[neighbour_node][consts.Consts.outgoing_flow].remove(incoming_flow_id)
      del sequence_flows[incoming_flow_id]
      bpmn_diagram.diagram_graph.remove_edge(neighbour_node, base_node)
      return neighbour_node
  
  
  def remove_node(node_id_to_remove, process_dict, bpmn_diagram, sequence_flows):
      """
  
      :param node_id_to_remove:
      :param process_dict:
      :param bpmn_diagram:
      :param sequence_flows:
      :return:
      """
      new_source_node = remove_incoming_connection(node_id_to_remove, bpmn_diagram, sequence_flows)
      new_target_node = remove_outgoing_connection(node_id_to_remove, bpmn_diagram, sequence_flows)
      bpmn_diagram.diagram_graph.remove_node(node_id_to_remove)
      process_dict.pop(node_id_to_remove, None)
      # add new connection
      return new_source_node, new_target_node
  
  
  def remove_unnecessary_merge_gateways(process_dict, bpmn_diagram, sequence_flows):
      """
  
      :param process_dict:
      :param bpmn_diagram:
      :param sequence_flows:
      """
      for node in list(bpmn_diagram.get_nodes()):
          gateway_type = node[1].get(consts.Consts.type)
          if gateway_type in [consts.Consts.inclusive_gateway, consts.Consts.exclusive_gateway,
                              consts.Consts.parallel_gateway]:
              if len(node[1].get(consts.Consts.incoming_flow)) < 2 \
                      and len(node[1].get(consts.Consts.outgoing_flow)) < 2:
                  new_source_node, new_target_node = remove_node(node[0], process_dict, bpmn_diagram, sequence_flows)
                  add_connection(new_source_node, new_target_node, process_dict, bpmn_diagram, sequence_flows)
  
  
  def remove_goto_nodes(process_dict, diagram_graph, sequence_flows):
      """
  
      :param process_dict:
      :param diagram_graph:
      :param sequence_flows:
      """
      for order, csv_line_dict in copy.deepcopy(process_dict).items():
          if csv_line_dict[consts.Consts.csv_activity].lower().startswith("goto"):
              source_node, _ = remove_node(order, process_dict, diagram_graph, sequence_flows)
              target_node = csv_line_dict[consts.Consts.csv_activity].strip().split()[1]
              add_connection(source_node, target_node, process_dict, diagram_graph, sequence_flows)
  
  
  class BpmnDiagramGraphCSVImport(object):
      """
      Template
      """
      @staticmethod
      def load_diagram_from_csv(filepath, bpmn_diagram):
          """
          Reads an CSV file from given filepath and maps it into inner representation of BPMN diagram.
          Returns an instance of BPMNDiagramGraph class.
  
          :param filepath: string with output filepath,
          :param bpmn_diagram: an instance of BpmnDiagramGraph class.
          """
          sequence_flows = bpmn_diagram.sequence_flows
          process_elements_dict = bpmn_diagram.process_elements
          diagram_attributes = bpmn_diagram.diagram_attributes
          plane_attributes = bpmn_diagram.plane_attributes
  
          process_dict = BpmnDiagramGraphCSVImport.import_csv_file_as_dict(filepath)
  
          BpmnDiagramGraphCSVImport.populate_diagram_elements_dict(diagram_attributes)
          BpmnDiagramGraphCSVImport.populate_process_elements_dict(process_elements_dict, process_dict)
          BpmnDiagramGraphCSVImport.populate_plane_elements_dict(plane_attributes)
  
          BpmnDiagramGraphCSVImport.import_nodes(process_dict, bpmn_diagram, sequence_flows)
          BpmnDiagramGraphCSVImport.representation_adjustment(process_dict, bpmn_diagram, sequence_flows)
  
      @staticmethod
      def import_csv_file_as_dict(filepath):
          """
  
          :param filepath:
          :return:
          """
          process_dict = pd.read_csv(filepath, index_col=0).fillna("").to_dict('index')
          remove_white_spaces_in_orders(process_dict)
          return process_dict
  
      @staticmethod
      def get_given_task_as_dict(csv_df, order_val):
          """
  
          :param csv_df:
          :param order_val:
          :return:
          """
          return csv_df.loc[csv_df[consts.Consts.csv_order] == order_val].iloc[0].to_dict()
  
      @staticmethod
      def import_nodes(process_dict, bpmn_diagram, sequence_flows):
          """
  
          :param process_dict:
          :param bpmn_diagram:
          :param sequence_flows:
          """
          import_nodes_info(process_dict, bpmn_diagram)
          fill_graph_connections(process_dict, bpmn_diagram, sequence_flows)
  
      @staticmethod
      def populate_diagram_elements_dict(diagram_elements_dict):
          """
  
          :param diagram_elements_dict:
          """
          diagram_elements_dict[consts.Consts.id] = "diagram1"
          diagram_elements_dict[consts.Consts.name] = "diagram_name"
  
      @staticmethod
      def populate_process_elements_dict(process_elements_dict, process_dict):
          """
  
          :param process_elements_dict:
          :param process_dict:
          """
          process_id = default_process_id
          process_element_attributes = {consts.Consts.id: default_process_id,
                                        consts.Consts.name: "",
                                        consts.Consts.is_closed: "false",
                                        consts.Consts.is_executable: "false",
                                        consts.Consts.process_type: "None",
                                        consts.Consts.node_ids: list(process_dict.keys())}
          process_elements_dict[process_id] = process_element_attributes
  
      @staticmethod
      def populate_plane_elements_dict(plane_elements_dict):
          """
  
          :param plane_elements_dict:
          """
          plane_elements_dict[consts.Consts.id] = default_plane_id
          plane_elements_dict[consts.Consts.bpmn_element] = default_process_id
  
      @staticmethod
      def legacy_adjustment(bpmn_diagram):
          """
  
          :param bpmn_diagram:
          """
          for node in bpmn_diagram.get_nodes():
              if node[1].get(consts.Consts.incoming_flow) is None:
                  node[1][consts.Consts.incoming_flow] = []
              if node[1].get(consts.Consts.outgoing_flow) is None:
                  node[1][consts.Consts.outgoing_flow] = []
              if node[1].get(consts.Consts.event_definitions) is None:
                  node[1][consts.Consts.event_definitions] = []
  
      @staticmethod
      def representation_adjustment(process_dict, diagram_graph, sequence_flows):
          """
  
          :param process_dict:
          :param diagram_graph:
          :param sequence_flows:
          """
          BpmnDiagramGraphCSVImport.legacy_adjustment(diagram_graph)
          remove_goto_nodes(process_dict, diagram_graph, sequence_flows)
          remove_unnecessary_merge_gateways(process_dict, diagram_graph, sequence_flows)
  
apis: pandas.read_csv