text: # ┌────────────────────────────────────────────────────────────────────────────────────┐
  # │ GENERAL IMPORTS                                                                    │
  # └────────────────────────────────────────────────────────────────────────────────────┘
  
  import math
  
  from datetime import datetime
  
  # ┌────────────────────────────────────────────────────────────────────────────────────┐
  # │ SQLALCHEMY IMPORTS                                                                 │
  # └────────────────────────────────────────────────────────────────────────────────────┘
  
  from sqlalchemy import Column, func, Integer
  from sqlalchemy.ext.hybrid import hybrid_property
  
  # ┌────────────────────────────────────────────────────────────────────────────────────┐
  # │ PROJECT IMPORTS                                                                    │
  # └────────────────────────────────────────────────────────────────────────────────────┘
  
  import yank.constants as _c
  
  from yank.browser import Browser
  from yank.interface_database_mixin import InterfaceDatabaseMixin
  from yank.interface_display_mixin import InterfaceDisplayMixin
  
  
  # ┌────────────────────────────────────────────────────────────────────────────────────┐
  # │ INTERFACE                                                                          │
  # └────────────────────────────────────────────────────────────────────────────────────┘
  
  
  class Interface(InterfaceDatabaseMixin, InterfaceDisplayMixin):
      """ A utility class for managing the schema of yanked data """
  
      # ┌────────────────────────────────────────────────────────────────────────────────┐
      # │ CONSTANTS                                                                      │
      # └────────────────────────────────────────────────────────────────────────────────┘
  
      # Interface
      CAST = _c.CAST
      DISPLAY = _c.DISPLAY
      NULL = _c.NULL
      RANK = _c.RANK
      UNIQUE = _c.UNIQUE
      WEIGHT = _c.WEIGHT
  
      # Fields
      ID = _c.ID
      URL = _c.URL
      YANKED_AT = _c.YANKED_AT
  
      # ┌────────────────────────────────────────────────────────────────────────────────┐
      # │ CLASS ATTRIBUTES                                                               │
      # └────────────────────────────────────────────────────────────────────────────────┘
  
      # Initialize name to empty string
      name = ""
  
      # Initialize skip by URL to False
      skip_by_url = False
  
      # Initialize session limit to None
      session_limit = None
  
      # Initialize session count to 0
      session_count = 0
  
      # Initialize display list by to None
      display_list_by = None
  
      # Initialize list display map to None
      list_display_map = None
  
      # Initialize display detail by to None
      display_detail_by = None
  
      # Initialize detail display map to None
      detail_display_map = None
  
      # Initialize default browser
      default_browser = Browser.CHROME
  
      # Initialize cached browser
      _browser = None
  
      # ┌────────────────────────────────────────────────────────────────────────────────┐
      # │ INIT METHOD                                                                    │
      # └────────────────────────────────────────────────────────────────────────────────┘
  
      def __init__(self, DBBase, db_table_name, inflector, **kwargs):
          """ Init Method """
  
          # Get common constants
          CAST = self.CAST
          DISPLAY = self.DISPLAY
          RANK = self.RANK
          UNIQUE = self.UNIQUE
          WEIGHT = self.WEIGHT
  
          # Set table name
          self.db_table_name = db_table_name
  
          # Set inflector
          self.inflector = inflector
  
          # ┌────────────────────────────────────────────────────────────────────────────┐
          # │ CUSTOM ATTRIBUTES                                                          │
          # └────────────────────────────────────────────────────────────────────────────┘
  
          # Get attributes
          attributes = {k: v for k, v in kwargs.items() if k.startswith("__")}
  
          # Iterate over attributes
          for k, v in attributes.items():
  
              # Set attribute
              setattr(self, k.replace("__", "", 1).strip(), v)
  
          # ┌────────────────────────────────────────────────────────────────────────────┐
          # │ FIELD MAP                                                                  │
          # └────────────────────────────────────────────────────────────────────────────┘
  
          # Initialize field map
          field_map = {_c.ID: {_c.CAST: int, _c.DISPLAY: "ID"}}
  
          # Add kwargs to field map
          field_map = {
              **field_map,
              **{k: v for k, v in kwargs.items() if k not in attributes},
          }
  
          # Add yanked at and URL to field map
          field_map[self.URL] = {CAST: str, DISPLAY: "URL"}
          field_map[self.YANKED_AT] = datetime
  
          # Normalize the structure of the field map
          field_map = {
              k: (v if type(v) is dict else {CAST: v}) for k, v in field_map.items()
          }
  
          # Set field map
          self.field_map = field_map
  
          # ┌────────────────────────────────────────────────────────────────────────────┐
          # │ DEFAULTS                                                                   │
          # └────────────────────────────────────────────────────────────────────────────┘
  
          # Compute total weight
          weight_total = sum(
              [abs(info.get(WEIGHT, 0)) for info in self.field_map.values()]
          )
  
          # Iterate over field map
          for field, info in self.field_map.items():
  
              # Define display
              info[DISPLAY] = info.get(DISPLAY) or " ".join(
                  [w.title() for w in field.split("_")]
              )
  
              # Get weight
              weight = info.get(WEIGHT) or None
  
              # Determine if weight is reversed
              weight_is_reversed = weight < 0 if weight else False
  
              # Normalize weight according to total
              weight = abs(weight) / weight_total if weight else None
  
              # Reverse weight if necessary
              weight = weight * -1 if weight_is_reversed else weight
  
              # Set weight
              info[WEIGHT] = weight
  
          # Get weighted columns
          weighted_columns = {
              field: info[WEIGHT]
              for field, info in self.field_map.items()
              if info[WEIGHT] is not None
          }
  
          # Get positive weighted columns
          weighted_columns_positive = {k: v for k, v in weighted_columns.items() if v > 0}
  
          # Get negative weighted columns
          weighted_columns_negative = {k: v for k, v in weighted_columns.items() if v < 0}
  
          # ┌────────────────────────────────────────────────────────────────────────────┐
          # │ COLUMNS                                                                    │
          # └────────────────────────────────────────────────────────────────────────────┘
  
          # Define columns decorator
          def columns(cls):
              """ Dynamically sets SQLAlchemy columns on a target ORM class """
  
              # Iterate over field map
              for field, info in self.field_map.items():
  
                  # Continue if field is ID (created manually)
                  if field == _c.ID:
                      continue
  
                  # Get column type
                  ColType = self.TYPE_MAP[info[CAST]]
  
                  # Get unique
                  unique = info.get(UNIQUE, False)
  
                  # Set class attribute
                  setattr(cls, field, Column(ColType, unique=unique))
  
              # Define rank
              @hybrid_property
              def rank(self):
  
                  # Get rank
                  rank = math.prod(
                      [1]
                      + [
                          getattr(self, field) ** weight
                          for field, weight in weighted_columns_positive.items()
                      ]
                  ) / math.prod(
                      [1]
                      + [
                          getattr(self, field) ** abs(weight)
                          for field, weight in weighted_columns_negative.items()
                      ]
                  )
  
                  # Return rank
                  return rank
  
              # Define rank expression
              @rank.expression
              def rank(cls):
  
                  # Get rank
                  rank = math.prod(
                      [1]
                      + [
                          func.pow(getattr(cls, field), weight)
                          for field, weight in weighted_columns_positive.items()
                      ]
                  ) / math.prod(
                      [1]
                      + [
                          func.pow(getattr(cls, field), abs(weight))
                          for field, weight in weighted_columns_negative.items()
                      ]
                  )
                  return rank
  
              # Set rank property
              setattr(cls, RANK, rank)
  
              # Return the ORM class
              return cls
  
          # ┌────────────────────────────────────────────────────────────────────────────┐
          # │ ITEM                                                                       │
          # └────────────────────────────────────────────────────────────────────────────┘
  
          # Define Item ORM class
          @columns
          class Item(DBBase):
              """ The ORM class that will house the user-defined columns """
  
              # Set table name
              __tablename__ = self.db_table_name
  
              # Set ID as primary key
              id = Column(Integer, primary_key=True)
  
          # Set Item class on interface object
          self.Item = Item
  
          # ┌────────────────────────────────────────────────────────────────────────────┐
          # │ LIST DISPLAY SETTINGS                                                      │
          # └────────────────────────────────────────────────────────────────────────────┘
  
          # Initialize list display map
          self.list_display_map = {self.field_map[_c.ID][_c.DISPLAY]: _c.ID}
  
          # Check if display list by is defined
          if self.display_list_by:
  
              # Iterate over display list by
              for item in self.display_list_by:
  
                  # Initialize display
                  display = None
  
                  # Check if item is a list or tuple
                  if type(item) in (list, tuple):
  
                      # Unpack field and display
                      field, display = item
  
                  # Otherwise handle string
                  else:
  
                      # Set field
                      field = item
  
                  # Continue if field not in field map
                  if field not in self.field_map:
                      continue
  
                  # Set display
                  display = display or self.field_map[field][_c.DISPLAY]
  
                  # Add to display map
                  self.list_display_map[display] = field
  
          # Otherwise handle case of undefined display list by
          else:
  
              # Iterate over the first five items of the field map
              for field in list(self.field_map.keys())[:5]:
  
                  # Add to display map
                  self.list_display_map[self.field_map[field][_c.DISPLAY]] = field
  
          # Convert display list by into a dictionary
          self.display_list_by = {v: k for k, v in self.list_display_map.items()}
  
          # ┌────────────────────────────────────────────────────────────────────────────┐
          # │ DETAIL DISPLAY SETTINGS                                                    │
          # └────────────────────────────────────────────────────────────────────────────┘
  
          # Initialize detail display map
          self.detail_display_map = {}
  
          # Initialize new display detail by
          _display_detail_by = []
  
          # Check if display detail by is defined
          if self.display_detail_by:
  
              # Iterate over rows in display detail by
              for row in self.display_detail_by:
  
                  # Initialize columns
                  cols = {}
  
                  # Iterate over columns in row
                  for col in row:
  
                      # Initialize display
                      display = None
  
                      # Check if item is a list or tuple
                      if type(col) in (list, tuple):
  
                          # Unpack field and display
                          field, display = col
  
                      # Otherwise handle string
                      else:
  
                          # Set field
                          field = col
  
                      # Continue if field not in field map
                      if field not in self.field_map:
                          continue
  
                      # Set display
                      display = display or self.field_map[field][_c.DISPLAY]
  
                      # Add column to columns
                      cols[field] = display
  
                      # Add to display map
                      self.detail_display_map[display] = field
  
                  # Add columns to new display detail by
                  _display_detail_by.append(cols)
  
          # Otherwise handle case of undefined display detail by
          else:
  
              # Initialize columns
              cols = {}
  
              # Get detail display fields as first ten fields
              detail_display_fields = list(self.field_map.keys())[:10]
  
              # Remove URL and yanked at from fields
              detail_display_fields = [
                  f for f in detail_display_fields if f not in (_c.URL, _c.YANKED_AT)
              ]
  
              # Iterate over detail display fields
              for i, field in enumerate(detail_display_fields, 1):
  
                  # Get display
                  display = self.field_map[field][_c.DISPLAY]
  
                  # Add column to columns
                  cols[field] = display
  
                  # Add to display map
                  self.detail_display_map[display] = field
  
                  # Check if index is divisible by two or is last
                  if i % 2 == 0 or i == len(detail_display_fields):
  
                      # Add columns to new display detail by
                      _display_detail_by.append(cols)
  
                      # Reset columns
                      cols = {}
  
              # Get URL display
              url_display = self.field_map[_c.URL][_c.DISPLAY]
  
              # Get yanked at display
              yanked_at_display = self.field_map[_c.YANKED_AT][_c.DISPLAY]
  
              # Add to display map
              self.detail_display_map[url_display] = _c.URL
              self.detail_display_map[yanked_at_display] = _c.YANKED_AT
  
              # Add URL and yanked at as a pair at the end
              _display_detail_by.append(
                  {_c.URL: url_display, _c.YANKED_AT: yanked_at_display}
              )
  
          # Redefine display detail by
          self.display_detail_by = _display_detail_by
  
      # ┌────────────────────────────────────────────────────────────────────────────────┐
      # │ BROWSER                                                                        │
      # └────────────────────────────────────────────────────────────────────────────────┘
  
      @property
      def browser(self):
          """ Returns a cached browser instance """
  
          # Check if browser is cached
          if self._browser:
  
              # Return cached browser
              return self._browser
  
          # Initialize a new browser
          browser = Browser(
              self.default_browser,
              driver_mode=Browser.NORMAL,
              driver_headless=False,
              driver_requests=False,
          )
  
          # Cache browser
          self._browser = browser
  
          # Return browser
          return browser
  
      # ┌────────────────────────────────────────────────────────────────────────────────┐
      # │ CAST FIELDS                                                                    │
      # └────────────────────────────────────────────────────────────────────────────────┘
  
      def cast_fields(self, item_dict, display_map=None):
          """ Casts a dict of item fields according to a the interface's field map """
  
          # Initialize display map
          display_map = (
              {k.lower(): v for k, v in display_map.items()} if display_map else {}
          )
  
          # Convert fields from display
          item_dict = {
              field
              if field in self.field_map
              else display_map.get(field.lower(), field): value
              for field, value in item_dict.items()
          }
  
          # Cast item dict
          item_dict = {
              field: self.cast_field(field, value) for field, value in item_dict.items()
          }
  
          # Return the item dict
          return item_dict
  
      # ┌────────────────────────────────────────────────────────────────────────────────┐
      # │ CAST FIELD                                                                     │
      # └────────────────────────────────────────────────────────────────────────────────┘
  
      def cast_field(self, field, value):
          """ Casts a value according to a field in the interface's field map """
  
          # Get field map
          field_map = self.field_map
  
          # Return value if field not in field map
          if field not in field_map:
              return value
  
          # Check if value is None
          if value is None:
  
              # Get null boolean
              null = field_map[field].get(self.NULL, False)
  
              # Check if null is permitted
              if null:
  
                  # Return the value
                  return value
  
          # Get value type
          value_type = type(value)
  
          # Check if value is a list
          if value_type is list:
  
              # Return a comma separated list
              # Currently no support for array structures in database
              return "; ".join([str(i).replace(";", ",") for i in value])
  
          # Get to type
          to_type = field_map[field][self.CAST]
  
          # Check if to type is int
          if to_type is int:
  
              # Check if value is str
              if value_type is str:
  
                  # Remove common unwanted characters
                  value = value.replace(",", "")
  
          # Handle case of datetime
          elif to_type is datetime:
  
              # Assert value is datetime
              assert value_type is datetime, f"{field} is not a valid datetime"
  
          # Cast value to appropriate type
          value = to_type(value)
  
          # Return value
          return value
  