text: """TcEx Framework Playbook module"""
  # standard library
  import re
  
  from .playbooks_base import PlaybooksBase
  
  
  class Playbooks(PlaybooksBase):
      """Playbook methods for accessing key value store.
  
      Args:
          tcex (object): Instance of TcEx.
          context (str): The KV Store context/session_id.
          output_variables (list): The requested output variables.
      """
  
      def __init__(self, tcex, context, output_variables):
          """Initialize the Class properties."""
          super().__init__(tcex, context, output_variables)
  
          # properties
          self.output_data = {}
  
      def add_output(self, key, value, variable_type, append_array=True):
          """Dynamically add output to output_data dictionary to be written to DB later.
  
          This method provides an alternative and more dynamic way to create output variables in an
          App. Instead of storing the output data manually and writing all at once the data can be
          stored inline, when it is generated and then written before the App completes.
  
          .. code-block:: python
              :linenos:
              :lineno-start: 1
  
              colors = ['blue', 'red', 'yellow']:
              tcex.playbook.add_output('app.colors', colors, 'StringArray')
  
              tcex.playbook.write_output()  #  writes the output stored in output_data
  
          .. code-block:: json
              :linenos:
              :lineno-start: 1
  
              {
                  "my_color-String": {
                      "key": "my_color",
                      "type": "String",
                      "value": "blue"
                  },
                  "my_numbers-String": {
                      "key": "my_numbers",
                      "type": "String",
                      "value": "seven"
                  },
                  "my_numbers-StringArray": {
                      "key": "my_numbers",
                      "type": "StringArray",
                      "value": ["seven", "five"]
                  }
              }
  
          Args:
              key (str): The variable name to write to storage.
              value (any): The value to write to storage.
              variable_type (str): The variable type being written.
              append_array (bool): If True arrays will be appended instead of being overwritten.
  
          """
          index = f'{key}-{variable_type}'
          self.output_data.setdefault(index, {})
          if value is None:
              return
  
          if variable_type in self._variable_array_types and append_array:
              self.output_data[index].setdefault('key', key)
              self.output_data[index].setdefault('type', variable_type)
              if isinstance(value, list):
                  self.output_data[index].setdefault('value', []).extend(value)
              else:
                  self.output_data[index].setdefault('value', []).append(value)
          else:
              self.output_data[index] = {'key': key, 'type': variable_type, 'value': value}
  
      def check_output_variable(self, variable):
          """Check to see if output variable was requested by downstream app.
  
          Using the auto generated dictionary of output variables check to see if provided
          variable was requested by downstream app.
  
          Args:
              variable (str): The variable name, not the full variable.
  
          Returns:
              (boolean): Boolean value indicator whether a match was found.
          """
          return variable in self.output_variables_by_name
  
      def create(self, key, value):
          """Create method of CRUD operation for working with KeyValue DB.
  
          This method will automatically determine the variable type and
          call the appropriate method to write the data.  If a non standard
          type is provided the data will be written as RAW data.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
  
          Returns:
              (str): Result string of DB write.
          """
          data = None
          if key is not None:
              key = key.strip()
              parsed_key = self.parse_variable(key.strip())
              variable_type = parsed_key['type']
  
              # log/debug
              self.log.debug(f'create variable {key}')
              if variable_type not in ['Binary', 'BinaryArray']:
                  self.log.trace(f'variable value: {value}')
  
              if variable_type in self._variable_single_types:
                  data = self._create(key, value)
              elif variable_type in self._variable_array_types:
                  data = self._create_array(key, value)
              else:
                  data = self.create_raw(key, value)
          return data
  
      def create_binary(self, key, value):
          """Create method of CRUD operation for binary data.
  
          Args:
              key (str): The variable to write to the Key Value Store.
              value (any): The data to write to the Key Value Store.
  
          Returns:
              (str): Result of Key Value Store write.
          """
          supported_variable_type = 'Binary'
          if self.variable_type(key) != supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create(key, value)
  
      def create_binary_array(self, key, value):
          """Create method of CRUD operation for binary array data.
  
          Args:
              key (str): The variable to write to the Key Value Store.
              value (any): The data to write to the Key Value Store.
  
          Returns:
              (str): Result of Key Value Store write.
          """
          supported_variable_type = 'BinaryArray'
          if self.variable_type(key) != supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create_array(key, value)
  
      def create_key_value(self, key, value):
          """Create method of CRUD operation for key/value data.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
  
          Returns:
              (str): Result of DB write
          """
          supported_variable_type = 'KeyValue'
          if self.variable_type(key) != supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create(key, value)
  
      def create_key_value_array(self, key, value):
          """Create method of CRUD operation for key/value array data.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
  
          Returns:
              (str): Result of DB write.
          """
          supported_variable_type = 'KeyValueArray'
          if self.variable_type(key) != supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create_array(key, value)
  
      def create_string(self, key, value):
          """Create method of CRUD operation for string data.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
  
          Returns:
              (str): Result of DB write.
          """
          supported_variable_type = 'String'
          if self.variable_type(key) != supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create(key, value)
  
      def create_string_array(self, key, value):
          """Create method of CRUD operation for string array data.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
  
          Returns:
              (str): Result of DB write.
          """
          supported_variable_type = 'StringArray'
          if self.variable_type(key) != supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create_array(key, value)
  
      def create_tc_entity(self, key, value):
          """Create method of CRUD operation for TC entity data.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
  
          Returns:
              (str): Result of DB write.
          """
          supported_variable_type = 'TCEntity'
          if self.variable_type(key) != supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create(key, value)
  
      def create_tc_entity_array(self, key, value):
          """Create method of CRUD operation for TC entity array data.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
  
          Returns:
              (str): Result of DB write.
          """
          supported_variable_type = ['TCEntityArray', 'TCEnhancedEntity', 'TCEnhancedEntityArray']
          if self.variable_type(key) not in supported_variable_type:
              raise RuntimeError(f'The key provided ({key}) is not a {supported_variable_type} key.')
          return self._create_array(key, value)
  
      def create_output(self, key, value, variable_type=None):
          """Alias for Create method of CRUD operation for working with KeyValue DB.
  
          This method will automatically check to see if provided variable was requested by
          a downstream app and if so create the data in the KeyValue DB.
  
          Args:
              key (str): The variable to write to the DB.
              value (any): The data to write to the DB.
              variable_type (str): The variable type being written.
  
          Returns:
              (str): Result string of DB write.
          """
          #  This is if no downstream variables are requested then nothing should be returned.
          if not self.output_variables_by_type:  # pragma: no cover
              self.log.debug(f'Variable {key} was NOT requested by downstream app.')
              return None
  
          if key is None:
              self.log.info('Key has a none value and will not be written.')
              return None
  
          if value is None:
              self.log.info(f'Variable {key} has a none value and will not be written.')
              return None
  
          key = key.strip()
          key_type = f'{key}-{variable_type}'
          results = None
          if self.output_variables_by_type.get(key_type) is not None:
              # variable key-type has been requested
              v = self.output_variables_by_type.get(key_type)
              self.log.info(f"Variable {v.get('variable')} was requested by downstream App.")
              results = self.create(v.get('variable'), value)
          elif self.output_variables_by_name.get(key) is not None and variable_type is None:
              # variable key has been requested
              v = self.output_variables_by_name.get(key)
              self.log.info(f"Variable {v.get('variable')} was requested by downstream App.")
              results = self.create(v.get('variable'), value)
          else:
              self.log.trace(f'requested output variables: {self.output_variables_by_name}')
              self.log.debug(f'Variable {key} was NOT requested by downstream app.')
  
          return results
  
      def delete(self, key):
          """Delete method of CRUD operation for all data types.
  
          This method is only supported when using key_value_redis.
  
          Args:
              key (str): The variable to write to the DB.
  
          Returns:
              (str): Result of DB write.
          """
          data = None
          if key is not None:
              data = self.tcex.key_value_store.delete(self._context, key.strip())
          else:  # pragma: no cover
              self.log.warning('The key field was None.')
          return data
  
      def exit(self, code=None, msg=None):
          """Exit the App
  
          Playbooks do not support partial failures so we change the exit method from 3 to 1 and call
          it a partial success instead.
  
          Args:
              code (int): The exit code value for the app.
              msg (str): A message to log and add to message tc output.
          """
          if code is None:
              code = self.tcex.exit_code
              if code == 3:
                  self.log.info('Changing exit code from 3 to 0.')
                  code = 0  # playbooks doesn't support partial failure
          elif code not in [0, 1]:
              code = 1
  
          # write outputs before exiting
          self.write_output()
  
          # required only for tcex testing framework
          if self.tcex.args.tcex_testing_context is not None:  # pragma: no cover
              self.tcex.redis_client.hset(self.tcex.args.tcex_testing_context, '_exit_message', msg)
  
          self.tcex.exit(code, msg)
  
      def is_variable(self, key):
          """Return True if provided key is a properly formatted variable."""
          if not isinstance(key, str):
              return False
          if re.match(self._variable_match, key):
              return True
          return False
  
      @property
      def output_variables_by_name(self):
          """Return output variables stored as name dict."""
          if self._output_variables_by_name is None:
              self._parse_output_variables()
          return self._output_variables_by_name
  
      @property
      def output_variables_by_type(self):
          """Return output variables stored as name-type dict."""
          if self._output_variables_by_type is None:
              self._parse_output_variables()
          return self._output_variables_by_type
  
      def parse_variable(self, variable):
          """Parse an input or output variable.
  
          **Example Variable**::
  
          #App:1234:output!String
  
          Args:
              variable (str): The variable name to parse.
  
          Returns:
              (dictionary): Result of parsed string.
          """
          data = None
          if variable is not None:
              variable = variable.strip()
              if re.match(self._variable_match, variable):
                  var = re.search(self._variable_parse, variable)
                  data = {
                      'root': var.group(0),
                      'job_id': var.group(2),
                      'name': var.group(3),
                      'type': var.group(4),
                  }
          return data
  
      def read(self, key, array=False, embedded=True):
          """Read method of CRUD operation for working with KeyValue DB.
  
          This method will automatically check to see if a single variable is passed
          or if "mixed" data is passed and return the results from the DB. It will also
          automatically determine the variable type to read.
  
          Args:
              key (str): The variable to read from the DB.
              array (boolean): Convert string/dict to Array/List before returning.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (any): Results retrieved from DB
          """
          # if a non-variable value is passed it should be the default
          value = key
          if isinstance(key, str):
              key = key.strip()
              variable_type = self.variable_type(key)
              if re.match(self._variable_match, key):
                  # only log key if it's a variable
                  self.log.debug(f'read variable {key}')
                  if variable_type in self._variable_single_types:
                      value = self._read(key=key, embedded=embedded)
                  elif variable_type in self._variable_array_types:
                      value = self._read_array(key=key, embedded=embedded)
                  else:
                      value = self.read_raw(key)
              else:
                  if variable_type == 'String':
                      # replace "\s" with a space only for user input.
                      # using '\\s' will prevent replacement.
                      value = re.sub(r'(?<!\\)\\s', ' ', value)
                      value = re.sub(r'\\\\s', r'\\s', value)
  
                  if embedded:
                      # check for any embedded variables
                      value = self._read_embedded(value)
  
          # return data as a list
          if array and not isinstance(value, list):
              if value is not None:
                  value = [value]
              else:
                  # Adding none value to list breaks App logic. It's better to not request
                  # Array and build array externally if None values are required.
                  value = []
  
          return value
  
      def _entity_field(self, key, field, entity_type=None, default=None):
          """Read the value of the given key and return the data at the given field of the value.
  
          This method is used by functions designed to make it easier to get
          data from a particular field from TC(Enhanced)Entity(Array).
  
          Args:
              key (str): The variable to read from the DB.
              field (str): The field to find in the data.
              entity_type (Any): Default is None. The type of data being collected (valid values:
                  ['groups', 'indicators']).
              default (Any): Default is None. The value to use for malformed TCEntities or
                  TCEnhancedEntities.
  
          Returns:
              (list): A list of strings containing the desired values.
          """
          read_results = self.read(key, array=True)
          if not read_results:
              return []
  
          variable_type = self.variable_type(key).lower()
          enhanced_entity_field = None
          VALID_ENTITY_TYPES = ['group', 'indicator']
  
          # handle the odd format of tcEnhancedEntityArrays which follow the format detailed here:
          # https://docs.threatconnect.com/en/latest/rest_api/indicators/indicators.html#batch-indicator-input-file-format-v2
          if read_results and variable_type == 'tcenhancedentityarray':
              # type specific values
              if entity_type == 'indicator' and field == 'value':
                  enhanced_entity_field = 'summary'
              elif entity_type == 'group' and field == 'value':
                  enhanced_entity_field = 'name'
              elif entity_type not in VALID_ENTITY_TYPES:  # pragma: no cover
                  message = (
                      f'Invalid entity_type ({entity_type}). Valid options: {VALID_ENTITY_TYPES}.'
                  )
                  raise RuntimeError(message)
  
              read_results = read_results[0].get(entity_type, [])
  
          is_tc_enhanced = enhanced_entity_field and variable_type in [
              'tcenhancedentity',
              'tcenhancedentityarray',
          ]
          is_tc_entity = variable_type in ['tcentity', 'tcentityarray']
  
          if is_tc_enhanced:
              values = [i.get(enhanced_entity_field, default) for i in read_results]
          elif is_tc_entity:
              values = [i.get(field, default) for i in read_results]
          else:
              values = read_results
  
          return values
  
      def read_indicator_values(self, key, default=None):
          """Read the value of the given key and return indicators from the value.
  
          This method will call the `read` method and then will process the data so as to return a
          list of strings where each string is an indicator (the summary of an indicator - e.g.
          ["foo.example.com", "bar.example.com"]).
  
          Args:
              key (str): The variable to read from the DB.
              default (Any): Default is None. The value to use for malformed TCEntities or
                  TCEnhancedEntities.
  
          Returns:
              (list): A list of strings containing the indicators
          """
          return self._entity_field(key, 'value', entity_type='indicator', default=default)
  
      def read_group_values(self, key, default=None):
          """Read the value of the given key and return group names from the value.
  
          This method will call the `read` method and then will process the data
          so as to return a list of strings where each string is a group name.
  
          Args:
              key (str): The variable to read from the DB.
              default (Any): Default is None. The value to use for malformed TCEntities or
                  TCEnhancedEntities.
  
          Returns:
              (list): A list of strings containing the group names
          """
          return self._entity_field(key, 'value', entity_type='group', default=default)
  
      def read_group_ids(self, key, default=None):
          """Read the value of the given key and return group ids from the value.
  
          This method will call the `read` method and then will process the data
          so as to return a list of strings where each string is a group id.
  
          Args:
              key (str): The variable to read from the DB.
              default (Any): Default is None. The value to use for malformed TCEntities or
                  TCEnhancedEntities.
  
          Returns:
              (list): A list of strings containing the group ids
          """
          return self._entity_field(key, 'id', entity_type='group', default=default)
  
      def read_choice(self, key, alt_key):
          """Read method for choice inputs.
  
          Behavior:
          * If key is "-- Select --" return None.
          * If key is "-- Variable Input --" return resolved alt_key.
          * Else return resolved value for key.
  
          Args:
              key (str): The variable to read from KeyValue Store.
              alt_key (str): The alternate variable to read from the KeyValue Store.
  
          Returns:
              (None|str): Results retrieved from KeyValue Store
          """
          if key is None:
              return None
  
          if key.strip() == '-- Select --':
              return None
  
          if key.strip() == '-- Variable Input --':
              return self.read(alt_key)
  
          return self.read(key)
  
      def read_array(self, key, embedded=True):
          """Read playbook variable and return array for any variable type.
  
          Args:
              key (str): The variable to read from the DB.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (any): Results retrieved from DB
          """
          return self.read(key, True, embedded)
  
      def read_binary(self, key, b64decode=True, decode=False):
          """Read method of CRUD operation for binary data.
  
          Args:
              key (str): The variable to read from the DB.
              b64decode (bool): If true the data will be base64 decoded.
              decode (bool): If true the data will be decoded to a String.
  
          Returns:
              (bytes|string): Results retrieved from DB.
          """
          return self._read(key, b64decode=b64decode, decode=decode)
  
      def read_binary_array(self, key, b64decode=True, decode=False):
          """Read method of CRUD operation for binary array data.
  
          Args:
              key (str): The variable to read from the DB.
              b64decode (bool): If true the data will be base64 decoded.
              decode (bool): If true the data will be decoded to a String.
  
          Returns:
              (list): Results retrieved from DB.
          """
          return self._read_array(key, b64decode=b64decode, decode=decode)
  
      def read_key_value(self, key, embedded=True):
          """Read method of CRUD operation for key/value data.
  
          Args:
              key (str): The variable to read from the DB.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (dictionary): Results retrieved from DB.
          """
          return self._read(key, embedded=embedded)
  
      def read_key_value_array(self, key, embedded=True):
          """Read method of CRUD operation for key/value array data.
  
          Args:
              key (str): The variable to read from the DB.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (list): Results retrieved from DB.
          """
          return self._read_array(key, embedded=embedded)
  
      def read_string(self, key, embedded=True):
          """Read method of CRUD operation for string data.
  
          Args:
              key (str): The variable to read from the DB.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (str): Results retrieved from DB.
          """
          return self._read(key, embedded=embedded)
  
      def read_string_array(self, key, embedded=True):
          """Read method of CRUD operation for string array data.
  
          Args:
              key (str): The variable to read from the DB.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (list): Results retrieved from DB.
          """
          return self._read_array(key, embedded=embedded)
  
      def read_tc_entity(self, key, embedded=True):
          """Read method of CRUD operation for TC entity data.
  
          Args:
              key (str): The variable to read from the DB.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (dictionary): Results retrieved from DB.
          """
          return self._read(key, embedded=embedded)
  
      def read_tc_entity_array(self, key, embedded=True):
          """Read method of CRUD operation for TC entity array data.
  
          Args:
              key (str): The variable to read from the DB.
              embedded (boolean): Resolve embedded variables.
  
          Returns:
              (list): Results retrieved from DB.
          """
          return self._read_array(key, embedded=embedded)
  
      def variable_type(self, variable):
          """Get the Type from the variable string or default to String type.
  
          The default type is "String" for those cases when the input variable is
          contains not "DB variable" and is just a String.
  
          **Example Variable**::
  
              #App:1234:output!StringArray returns **StringArray**
  
          **Example String**::
  
              "My Data" returns **String**
  
          Args:
              variable (str): The variable to be parsed
  
          Returns:
              (str): The variable type.
          """
          var_type = 'String'
          if isinstance(variable, str):
              variable = variable.strip()
              if re.match(self._variable_match, variable):
                  var_type = re.search(self._variable_parse, variable).group(4)
          return var_type
  
      def write_output(self):
          """Write all stored output data to storage."""
          for data in self.output_data.values():
              self.create_output(data.get('key'), data.get('value'), data.get('type'))
  