text: import os
  import logging
  import traceback as tb
  from flask import request, jsonify, make_response
  import json  # it MUST be included after flask!
  import pkg_resources
  import hashlib
  import calendar
  import pytz
  import flask
  import socket
  import requests
  import label_studio
  
  from pkg_resources import parse_version
  
  from collections import defaultdict, OrderedDict
  from lxml import etree, objectify
  from datetime import datetime
  from dateutil.tz import tzlocal
  
  from .io import find_dir
  
  logger = logging.getLogger(__name__)
  
  
  # settings from django analogue
  class Settings:
      TASKS_MAX_NUMBER = 250000
      TASKS_MAX_FILE_SIZE = 200 * 1024 * 1024
      UPLOAD_DATA_UNDEFINED_NAME = '$undefined$'
  
  
  # make an answer to client
  def answer(status=0, msg='', result=None):
      if status == 0 and not msg and result is None:
          status = -1000
          msg = "nothing happened"
  
      if status == 200 and not msg:
          msg = 'ok'
  
      a = {"status": status, "detail": msg}
      a.update({'request': request.args})
  
      if result is not None:
          a.update({"result": result})
  
      return make_response(jsonify(a), status)
  
  
  # make an answer as exception
  class AnswerException(Exception):
      def __init__(self, status, msg='', result=None):
          self.status, self.msg, self.result = status, msg, result
          self.answer = answer(status, msg, result)
          Exception.__init__(self, self.answer)
  
  
  # standard exception treatment for any api function
  def exception_handler(f):
      def exception_f(*args, **kwargs):
          try:
              return f(*args, **kwargs)
  
          except AnswerException as e:
              traceback = tb.format_exc()
  
              if 'traceback' not in e.result:
                  e.result['traceback'] = traceback
              if hasattr(exception_f, 'request_id') and not e.result['request_id']:
                  e.result['request_id'] = exception_f.request_id
              return answer(e.status, e.msg, e.result)
  
          except Exception as e:
              traceback = tb.format_exc()
              logger.debug(traceback)
              body = {'traceback': traceback}
              if hasattr(exception_f, 'request_id'):
                  body['request_id'] = exception_f.request_id
              return answer(500, str(e), body)
  
      exception_f.__name__ = f.__name__
      return exception_f
  
  
  # standard exception treatment for any page function
  def exception_handler_page(f):
      def exception_f(*args, **kwargs):
          try:
              return f(*args, **kwargs)
          except Exception as e:
              error = str(e)
              traceback = tb.format_exc()
              logger.debug(traceback)
              print(traceback)
              return flask.render_template(
                  'includes/error.html',
                  error=error, header="Project loading error", traceback=traceback)
  
      exception_f.__name__ = f.__name__
      return exception_f
  
  
  def config_line_stripped(xml_config):
      """ Remove comments
  
      :param xml_config: xml config string
      :return: xml config string
      """
      xml_config = config_comments_free(xml_config)
      return xml_config.replace('\n', '').replace('\r', '')
  
  
  def config_comments_free(xml_config):
      """ Remove \n and \r from xml, flat xml to string
  
      :param xml_config: xml config string
      :return: xml config string
      """
      tree = etree.fromstring(xml_config)
      comments = tree.xpath('//comment()')
  
      for xml_config in comments:
          p = xml_config.getparent()
          if p:
              p.remove(xml_config)
          xml_config = etree.tostring(tree, encoding='utf8', method='html').decode("utf-8")
  
      return xml_config
  
  
  def get_app_version():
      return pkg_resources.get_distribution('label-studio').version
  
  
  _LABEL_TAGS = {'Label', 'Choice'}
  _NOT_CONTROL_TAGS = {'Filter',}
  
  
  def parse_config(config_string):
      """
      :param config_string: Label config string
      :return: structured config of the form:
      {
          "<ControlTag>.name": {
              "type": "ControlTag",
              "to_name": ["<ObjectTag1>.name", "<ObjectTag2>.name"],
              "inputs: [
                  {"type": "ObjectTag1", "value": "<ObjectTag1>.value"},
                  {"type": "ObjectTag2", "value": "<ObjectTag2>.value"}
              ],
              "labels": ["Label1", "Label2", "Label3"] // taken from "alias" if exists or "value"
      }
      """
      if not config_string:
          return {}
  
      def _is_input_tag(tag):
          return tag.attrib.get('name') and tag.attrib.get('value')
  
      def _is_output_tag(tag):
          return tag.attrib.get('name') and tag.attrib.get('toName') and tag.tag not in _NOT_CONTROL_TAGS
  
      def _get_parent_output_tag_name(tag, outputs):
          # Find parental <Choices> tag for nested tags like <Choices><View><View><Choice>...
          parent = tag
          while True:
              parent = parent.getparent()
              if parent is None:
                  return
              name = parent.attrib.get('name')
              if name in outputs:
                  return name
  
      xml_tree = etree.fromstring(config_string)
  
      inputs, outputs, labels = {}, {}, defaultdict(dict)
      for tag in xml_tree.iter():
          if _is_output_tag(tag):
              outputs[tag.attrib['name']] = {'type': tag.tag, 'to_name': tag.attrib['toName'].split(',')}
          elif _is_input_tag(tag):
              inputs[tag.attrib['name']] = {'type': tag.tag, 'value': tag.attrib['value'].lstrip('$')}
          if tag.tag not in _LABEL_TAGS:
              continue
          parent_name = _get_parent_output_tag_name(tag, outputs)
          if parent_name is not None:
              actual_value = tag.attrib.get('alias') or tag.attrib.get('value')
              if not actual_value:
                  logger.debug(
                      'Inspecting tag {tag_name}... found no "value" or "alias" attributes.'.format(
                          tag_name=etree.tostring(tag, encoding='unicode').strip()[:50]))
              else:
                  labels[parent_name][actual_value] = dict(tag.attrib)
      for output_tag, tag_info in outputs.items():
          tag_info['inputs'] = []
          for input_tag_name in tag_info['to_name']:
              if input_tag_name not in inputs:
                  raise KeyError('to_name={input_tag_name} is specified for output tag name={output_tag}, '
                                 'but we can\'t find it among input tags'
                                 .format(input_tag_name=input_tag_name, output_tag=output_tag))
              tag_info['inputs'].append(inputs[input_tag_name])
          tag_info['labels'] = list(labels[output_tag])
          tag_info['labels_attrs'] = labels[output_tag]
      logger.debug('Parsed config:\n' + json.dumps(outputs, indent=2))
      return outputs
  
  
  def parse_label_attrs(config_string):
      xml_tree = etree.fromstring(config_string)
  
  
  def iter_config_templates(templates_dir=None):
      try:
          templates_dir = find_dir('examples') if templates_dir is None else find_dir(templates_dir)
      except IOError:
          pass  # use templates_dir as is
  
      for d in os.listdir(templates_dir):
          # check xml config file exists
          path = os.path.join(templates_dir, d, 'config.xml')
          if not os.path.exists(path):
              continue
          yield path
  
  
  def get_config_templates(config):
      """ Get label config templates from directory (as usual 'examples' directory)
      """
      from collections import defaultdict, OrderedDict
      templates = defaultdict(lambda: defaultdict(list))
  
      template_dir = config.get('templates_dir', 'examples')
      for i, path in enumerate(iter_config_templates(template_dir)):
          # open and check xml
          code = open(path).read()
          try:
              objectify.fromstring(code)
          except Exception as e:
              logger.error("Can't parse XML for label config template from " + path + ':' + str(e))
              continue
  
          # extract fields from xml and pass them to template
          try:
              json_string = code.split('<!--')[1].split('-->')[0]
              meta = json.loads(json_string)
          except Exception as e:
              logger.error("Can't parse meta info from label config " + path + ': ' + str(e))
              continue
  
          meta['pk'] = i
          meta['label_config'] = '-->\n'.join(code.split('-->\n')[1:])  # remove all comments at the beginning of code
  
          meta['category'] = meta['category'] if 'category' in meta else 'no category'
          meta['complexity'] = meta['complexity'] if 'complexity' in meta else 'no complexity'
          templates[meta['complexity']][meta['category']].append(meta)
  
      # sort by title
      ordering = {
          'basic': ['audio', 'image', 'text', 'html', 'time-series'],
          'advanced': ['layouts', 'nested', 'per-region', 'other', 'time-series']
      }
      ordered_templates = OrderedDict()
      for complexity in ['basic', 'advanced']:
          ordered_templates[complexity] = OrderedDict()
          # add the rest from categories not presented in manual ordering
          x, y = ordering[complexity], templates[complexity].keys()
          ordering[complexity] = x + list((set(x) | set(y)) - set(x))
          for category in ordering[complexity]:
              sort = sorted(templates[complexity][category], key=lambda z: z.get('order', None) or z['title'])
              ordered_templates[complexity][category] = sort
  
      return ordered_templates
  
  
  def convert_string_to_hash(string):
      return hashlib.md5(string.encode()).hexdigest()
  
  
  def datetime_to_timestamp(dt):
      if dt.tzinfo:
          dt = dt.astimezone(pytz.UTC)
      return calendar.timegm(dt.timetuple())
  
  
  def timestamp_to_datetime(timestamp, tz=pytz.UTC):
      return datetime.fromtimestamp(timestamp, tz)
  
  
  def timestamp_to_local_datetime(timestamp):
      return timestamp_to_datetime(timestamp, tzlocal())
  
  
  def timestamp_now():
      return datetime_to_timestamp(datetime.utcnow())
  
  
  def serialize_class(class_instance, keys=None):
      """ Serialize class instance
  
      param keys: list of fields to serialize
      """
      keys = [d for d in dir(class_instance) if not d.startswith('_')] \
          if keys is None else keys
  
      # execute fields
      dictionary = {key: getattr(class_instance, key) for key in keys}
  
      # convert fields
      output = OrderedDict()
      for key in keys:
          value = dictionary[key]
          if isinstance(value, str) or isinstance(value, bool) \
                  or isinstance(value, int) or isinstance(value, float) \
                  or value is None:
              output[key] = dictionary[key]
  
      return output
  
  
  class DirectionSwitch:
      def __init__(self, obj, inverted):
          self.obj = obj
          self.inverted = inverted
  
      def __eq__(self, other):
          if self.obj is None and other.obj is None:
              return True
          if self.obj is None and other.obj is not None:
              return False
          if self.obj is not None and other.obj is None:
              return False
  
          return other.obj == self.obj
  
      def __lt__(self, other):
          if self.obj is None and other.obj is None:
              return True
          if self.obj is None and other.obj is not None:
              return False
          if self.obj is not None and other.obj is None:
              return True
  
          result = other.obj < self.obj
          return not result if self.inverted else result
  
  
  def compare_with_none(field, inverted):
      def compare_with_none_func(a, b):
          if a[field] is None and b[field] is None:
              return True
          if a[field] is None and b[field] is not None:
              return False
          if a[field] is not None and b[field] is None:
              return True
  
          result = b[field] < a[field]
          return not result if inverted else result
      return compare_with_none_func
  
  
  def check_port_in_use(host, port):
      logger.info('Checking if host & port is available :: ' + str(host) + ':' + str(port))
      host = host.replace('https://', '').replace('http://', '')
      with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
          return s.connect_ex((host, port)) == 0
  
  
  def start_browser(ls_url, no_browser):
      import threading
      import webbrowser
      if no_browser:
          return
  
      browser_url = ls_url + '/welcome'
      threading.Timer(2.5, lambda: webbrowser.open(browser_url)).start()
      print('Start browser at URL: ' + browser_url)
  
  
  def get_latest_version():
      pypi_url = 'https://pypi.org/pypi/%s/json' % label_studio.package_name
      try:
          response = requests.get(pypi_url).text
          latest_version = json.loads(response)['info']['version']
      except Exception as exc:
          logger.error("Can't get latest version.", exc_info=True)
      else:
          return latest_version
  
  
  def current_version_is_outdated(latest_version):
      latest_version = parse_version(latest_version)
      current_version = parse_version(label_studio.__version__)
      return current_version < latest_version
  
  
  def str2datetime(timestamp_str):
      try:
          ts = int(timestamp_str)
      except:
          return timestamp_str
      # return datetime.utcfromtimestamp(ts).strftime('%Y%m%d.%H%M%S')
      return datetime.utcfromtimestamp(ts).strftime('%c')
  