text: import json
  import logging
  import mimetypes
  import os
  import socket
  import threading
  import urllib.parse as urlparse
  from http.server import BaseHTTPRequestHandler, HTTPServer
  
  
  class Response:
      def code(self):
          return 200
  
      def headers(self) -> dict:
          return {}
  
      def response_body_bytes(self):
          return "".encode("utf-8")
  
  
  class ErrorResponse(Response):
      def __init__(self, code, message):
          self._code = code
          self._message = message
  
      def code(self):
          return self._code
  
      def headers(self) -> dict:
          return {'Content-type': 'application/json'}
  
      def response_body_bytes(self):
          return json.dumps({'error': self._message}, indent=4, default=lambda o: o.__dict__).encode('utf-8')
  
  
  class RedirectResponse(Response):
      def __init__(self, location):
          self._location = location
  
      def code(self):
          return 301
  
      def headers(self) -> dict:
          return {'Location': self._location}
  
  
  class JsonOkResponse(Response):
      def __init__(self, payload):
          self._payload = payload
  
      def headers(self) -> dict:
          return {'Content-type': 'application/json'}
  
      def response_body_bytes(self):
          return json.dumps(self._payload, indent=4, default=lambda o: o.__dict__).encode('utf-8')
  
  
  class StaticResourceResponse(Response):
      def __init__(self, mime, data):
          self._mime = mime
          self._data = data
  
      def headers(self) -> dict:
          return {'Content-type': self._mime}
  
      def response_body_bytes(self):
          return self._data
  
  
  class Action:
      def can_handle(self, method, path, params, payload):
          return False
  
      def handle(self, method, path, params, payload) -> Response:
          return None
  
  
  class JsonGet(Action):
      def __init__(self, endpoint, callable):
          self.path = endpoint
          self.callable = callable
  
      def can_handle(self, method, path, params, payload):
          return method == 'GET' and path == self.path
  
      def handle(self, method, path, params, payload) -> Response:
          output = self.callable(params)
          return JsonOkResponse(output)
  
  
  class JsonPost(Action):
      def __init__(self, endpoint, callable):
          self.path = endpoint
          self.callable = callable
  
      def can_handle(self, method, path, params, payload):
          return method == 'POST' and path == self.path
  
      def handle(self, method, path, params, payload) -> Response:
          if len(payload) > 0:
              try:
                  payload = json.loads(payload)
              except Exception as e:
                  raise ValidationException(e)
          output = self.callable(params, payload)
          return JsonOkResponse(output)
  
  
  class Redirect(Action):
      def __init__(self, path_from, path_to):
          self.path_from = path_from
          self.path_to = path_to
  
      def can_handle(self, method, path, params, payload):
          return path == self.path_from
  
      def handle(self, method, path, params, payload) -> Response:
          return RedirectResponse(self.path_to)
  
  
  class StaticResources(Action):
      def __init__(self, url_path_prefix, dir):
          self._url_prefix = url_path_prefix
          self._dir = dir
          if not self._url_prefix.endswith('/'):
              self._url_prefix += '/'
  
      def can_handle(self, method, path, params, payload):
          return method == 'GET' and path.startswith(self._url_prefix) and '/' not in path[len(self._url_prefix):]
  
      def handle(self, method, path, params, payload) -> Response:
          path = path[len(self._url_prefix):]
          optional_slash = '/' if not self._dir.endswith('/') and not path.startswith('/') else ''
          file = self._dir + optional_slash + path
          if os.path.isfile(file):
              mime, encoding = mimetypes.guess_type(file)
              with open(file, 'rb') as f:
                  data = f.read()
              return StaticResourceResponse(mime, data)
          else:
              logging.getLogger('web').info("Static resource %s not found" % file)
              return ErrorResponse(404, "File not found: %s" % path)
  
  
  class ValidationException(Exception):
      def __init__(self, *args, **kwargs):
          Exception.__init__(self, *args, **kwargs)
  
  
  class ServerController:
      def __init__(self, threads: list, sock):
          self.threads = threads
          self.sock = sock
          self.semaphore = threading.Semaphore(0)
  
      def start(self, block_caller_thread: bool = False):
          for thread in self.threads:
              thread.start()
          if block_caller_thread:
              self.semaphore.acquire()
  
      def stop(self):
          for thread in self.threads:
              thread.prepare_shutdown()
          self.sock.close()
          for thread in self.threads:
              thread.shutdown()
          self.semaphore.release()
  
  
  def http_server(port: int, actions: list, thread_count: int = 10) -> ServerController:
      logger = logging.getLogger('web')
  
      logger.info('Starting httpserver on port %s in %s threads' % (port, thread_count))
  
      socket_address = ('', port)
      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
      sock.bind(socket_address)
      sock.listen(5)
  
      class ServerThread(threading.Thread):
          def __init__(self, i):
              threading.Thread.__init__(self)
              self.i = i
              self.daemon = False
              self.httpd = None
              self.shutting_down = False
  
          def run(self):
              self.httpd = HTTPServer(socket_address, RequestHandler, False)
  
              # Prevent the HTTP server from re-binding every handler.
              # https://stackoverflow.com/questions/46210672/
              self.httpd.socket = sock
              self.httpd.server_bind = self.server_close = lambda self: None
  
              try:
                  self.httpd.serve_forever()
              except Exception as e:
                  if not self.shutting_down:
                      raise e
  
          def prepare_shutdown(self):
              self.shutting_down = True
  
          def shutdown(self):
              if self.httpd is not None:
                  self.httpd.shutdown()
  
      class RequestHandler(BaseHTTPRequestHandler):
          def do_GET(self):
              logger.debug("Accepted HTTP request %s on thread %s" % (self.path, threading.current_thread().name))
              try:
                  path, params = self.__parse(self.path)
                  for action in actions:
                      if action.can_handle('GET', path, params, {}):
                          output = action.handle('GET', path, params, {})
                          return self.__respond(output)
                  return self.__respond(ErrorResponse(404, "File not found"))
              except ValidationException as e:
                  self.__respond(ErrorResponse(400, "URL parse exception: %s" % str(e)))
                  logger.error("ValidationException", e)
              except Exception as e:
                  self.__respond(ErrorResponse(500, str(e)))
                  logger.error("Exception", e)
  
          def do_POST(self):
              logger.debug("Accepted HTTP request %s on thread %s" % (self.path, threading.current_thread().name))
              try:
                  path, params = self.__parse(self.path)
                  content_length = int(self.headers['Content-Length']) if 'Content-Length' in self.headers else 0
                  payload = self.rfile.read(content_length) if content_length > 0 else ""
                  for action in actions:
                      if action.can_handle('POST', path, params, payload):
                          output = action.handle('POST', path, params, payload)
                          return self.__respond(output)
                  return self.__respond(ErrorResponse(404, "File not found"))
              except ValidationException as e:
                  self.__respond(ErrorResponse(400, "URL parse exception: %s" % str(e)))
                  logger.error("ValidationException", e)
              except Exception as e:
                  self.__respond(ErrorResponse(500, str(e)))
                  logger.error("Exception", e)
  
          def __respond(self, output):
              code = output.code()
              headers = output.headers()
              payload = output.response_body_bytes()
              logger.debug("Returning HTTP %s %s with payload of %s bytes" % (code, headers, len(payload)))
              self.send_response(code)
              for header in headers:
                  self.send_header(header, headers[header])
              self.end_headers()
              self.wfile.write(payload)
  
          @staticmethod
          def __parse(url):
              try:
                  url_parsed = urlparse.urlparse(url)
                  url_params = urlparse.parse_qs(url_parsed.query)
                  url_path = url_parsed.path
                  return url_path, url_params
              except Exception as e:
                  raise ValidationException(e)
  
      threads = [ServerThread(i) for i in range(thread_count)]
  
      return ServerController(threads, sock)
  