text: # -*- coding: utf-8 -*-
  from BTrees.IOBTree import IOBTree
  from BTrees.Length import Length
  from node.ext.zodb import OOBTNode
  from persistent import Persistent
  from plumber import Behavior
  from plumber import default
  from plumber import plumbing
  from souper.interfaces import ICatalogFactory
  from souper.interfaces import INodeAttributeIndexer
  from souper.interfaces import INodeTextIndexer
  from souper.interfaces import IRecord
  from souper.interfaces import ISoup
  from souper.interfaces import IStorageLocator
  from zope.component import getUtility
  from zope.component import queryAdapter
  from zope.interface import implementer
  import random
  import six
  
  
  def get_soup(soup_name, context):
      return Soup(soup_name, context)
  
  
  class SoupData(Persistent):
  
      def __init__(self):
          self.data = IOBTree()
          self.catalog = None
          self._length = Length()
  
      @property
      def length(self):
          return self._length
  
      def __len__(self):
          return self.length()
  
  
  @implementer(ISoup)
  class Soup(object):
  
      def __init__(self, soup_name, context):
          """Initialize soup with its name and some context used in
          conjunction with the IStorageLocator adapter lookup.
          """
          self.soup_name = soup_name
          self.context = context
  
      @property
      def storage(self):
          locator = queryAdapter(self.context, IStorageLocator)
          if not locator:
              raise ValueError(
                  "Can't find IStorageLocator adapter for context "
                  "%s in order to locate soup '%s'."
                  % (repr(self.context), self.soup_name)
              )
          return locator.storage(self.soup_name)
  
      @property
      def data(self):
          return self.storage.data
  
      @property
      def catalog(self):
          """Return the catalog of the soup.
  
          If the catalog does not exist it creates a new and empty catalog using
          the named utility ICatalogFactory.
          """
          storage = self.storage
          if not storage.catalog:
              factory = getUtility(ICatalogFactory, name=self.soup_name)
              storage.catalog = factory(self.context)
          return storage.catalog
  
      def get(self, intid):
          return self.data[intid]
  
      def add(self, record):
          """Add a new record to the soup, create soup unique id and index it.
          """
          record.intid = self._generateid()
          self.data[record.intid] = record
          self.storage.length.change(1)
          self.catalog.index_doc(record.intid, record)
          return record.intid
  
      def query(
          self,
          queryobject,
          sort_index=None,
          limit=None,
          sort_type=None,
          reverse=False,
          names=None,
          with_size=False,
      ):
          size, iids = self.catalog.query(
              queryobject,
              sort_index=sort_index,
              limit=limit,
              sort_type=sort_type,
              reverse=reverse,
              names=names,
          )
          if with_size:
              yield size
          for iid in iids:
              yield self.data[iid]
  
      def lazy(
          self,
          queryobject,
          sort_index=None,
          limit=None,
          sort_type=None,
          reverse=False,
          names=None,
          with_size=False,
      ):
          size, iids = self.catalog.query(
              queryobject,
              sort_index=sort_index,
              limit=limit,
              sort_type=sort_type,
              reverse=reverse,
              names=names,
          )
          if with_size:
              yield size
          for iid in iids:
              yield LazyRecord(iid, self)
  
      def clear(self):
          self.storage.__init__()
          self.rebuild()
  
      def rebuild(self):
          """Drop the existing catalog and create a new one using the
          named utility ICatalogFactory.
          """
          self.storage.catalog = getUtility(
              ICatalogFactory, name=self.soup_name
          )(self.context)
          self.reindex()
  
      def reindex(self, records=None):
          """Reindex one or more specific records or all records.
  
          If records is not given all records are indexed. Otherwise for records
          an iterable of records is expected.
          """
          if records is None:
              records = self.data.values()
          for record in records:
              self.catalog.index_doc(record.intid, record)
  
      def __delitem__(self, record):
          """Remove one specific record and unindex it.
          """
          try:
              del self.data[record.intid]
              self.storage.length.change(-1)
          except Exception as e:
              raise e
          self.catalog.unindex_doc(record.intid)
  
      _v_nextid = None
      _randrange = random.randrange
  
      def _generateid(self):
          # Taken from zope.app.intid.
          while True:
              if self._v_nextid is None:
                  self._v_nextid = self._randrange(0, 2 ** 31)
              intid = self._v_nextid
              self._v_nextid += 1
              if intid not in self.data:
                  return intid
              self._v_nextid = None
  
  
  class LazyRecord(object):
  
      def __init__(self, intid, soup):
          self.intid = intid
          self.soup = soup
  
      def __call__(self):
          return self.soup.data[self.intid]
  
  
  @implementer(IRecord)
  class RecordBehavior(Behavior):
      intid = default(None)
  
  
  @plumbing(RecordBehavior)
  class Record(OOBTNode):
      pass
  
  
  @implementer(INodeAttributeIndexer)
  class NodeAttributeIndexer(object):
  
      def __init__(self, attr):
          self.attr = attr
  
      def __call__(self, context, default):
          if self.attr in context.attrs:
              return context.attrs[self.attr]
          return default
  
  
  @implementer(INodeTextIndexer)
  class NodeTextIndexer(object):
  
      def __init__(self, attrs):
          self.attrs = attrs
  
      def __call__(self, context, default):
          values = list()
          for attr in self.attrs:
              value = context.attrs.get(attr, u"")
              if isinstance(value, six.binary_type):
                  value = value.decode("utf-8")
              if not isinstance(value, six.string_types):
                  value = str(value)
              value = value.strip()
              if not value:
                  continue
              values.append(value)
          if not values:
              return default
          return u" ".join(values)
  