Djapian-2.3.1/0000755000076500000240000000000011313243720012217 5ustar daevdialoutDjapian-2.3.1/LICENSE0000644000076500000240000000277111306551607013243 0ustar daevdialoutCopyright (c) 2007-2009, Rafael Sierra and Alex Koshelev All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY Rafael Sierra and Alex Koshelev ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Rafael Sierra and Alex Koshelev BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Djapian-2.3.1/PKG-INFO0000644000076500000240000000066411313243720013322 0ustar daevdialoutMetadata-Version: 1.0 Name: Djapian Version: 2.3.1 Summary: High-level Xapian full-text indexer integration for Django Home-page: http://code.google.com/p/djapian/ Author: Alex Koshelev, Rafael "SDM" Sierra Author-email: daevaorn@gmail.com License: New BSD License Description: UNKNOWN Platform: UNKNOWN Classifier: Framework :: Django Classifier: License :: OSI Approved :: BSD License Classifier: Topic :: Text Processing :: Indexing Djapian-2.3.1/setup.py0000644000076500000240000000132411313234717013737 0ustar daevdialout#!/usr/bin/env python from distutils.core import setup setup( name="Djapian", version="2.3.1", license="New BSD License", author='Alex Koshelev, Rafael "SDM" Sierra', author_email="daevaorn@gmail.com", url="http://code.google.com/p/djapian/", packages=[ "djapian", "djapian.utils", "djapian.tests", "djapian.management", "djapian.management.commands" ], package_dir={ "djapian": "src/djapian" }, description="High-level Xapian full-text indexer integration for Django", classifiers=[ "Framework :: Django", "License :: OSI Approved :: BSD License", "Topic :: Text Processing :: Indexing" ] ) Djapian-2.3.1/src/0000755000076500000240000000000011313243720013006 5ustar daevdialoutDjapian-2.3.1/src/djapian/0000755000076500000240000000000011313243720014414 5ustar daevdialoutDjapian-2.3.1/src/djapian/__init__.py0000644000076500000240000000051211306547451016535 0ustar daevdialoutfrom django.conf import settings from djapian.indexer import Field, Indexer, CompositeIndexer from djapian.database import Database from djapian.space import IndexSpace from djapian.utils import load_indexes from djapian.decider import X space = IndexSpace(settings.DJAPIAN_DATABASE_PATH, "global") add_index = space.add_index Djapian-2.3.1/src/djapian/admin.py0000644000076500000240000000045711306547451016076 0ustar daevdialoutfrom django.contrib import admin from djapian.models import Change class ChangeAdmin(admin.ModelAdmin): """Set what's displayed in admin""" list_display = ('content_type', 'object_id', 'action', "date", ) list_filter = ('content_type', 'action', ) admin.site.register(Change, ChangeAdmin) Djapian-2.3.1/src/djapian/database.py0000644000076500000240000000322211306547451016543 0ustar daevdialoutimport os import xapian from django.conf import settings class Database(object): def __init__(self, path): self._path = path def open(self, write=False): """ Opens database for manipulations """ if not os.path.exists(self._path): os.makedirs(self._path) if write: database = xapian.WritableDatabase( self._path, xapian.DB_CREATE_OR_OPEN, ) else: try: database = xapian.Database(self._path) except xapian.DatabaseOpeningError: self.create_database() database = xapian.Database(self._path) return database def create_database(self): database = xapian.WritableDatabase( self._path, xapian.DB_CREATE_OR_OPEN, ) del database def document_count(self): return self.open().get_doccount() def clear(self): try: for file_path in os.listdir(self._path): os.remove(os.path.join(self._path, file_path)) os.rmdir(self._path) except OSError: pass class CompositeDatabase(Database): def __init__(self, dbs): self._dbs = dbs def open(self, write=False): if write: raise ValueError("Composite database cannot be opened for writing") base = self._dbs[0] raw = base.open() for db in self._dbs[1:]: raw.add_database(db.open()) return raw def create_database(self): raise NonImplementedError def clear(self): raise NotImplementedError Djapian-2.3.1/src/djapian/decider.py0000644000076500000240000000553511306547451016407 0ustar daevdialoutimport operator import re import xapian from django.db import models from django.utils.functional import curry class X(models.Q): pass i = lambda f: lambda a, b: f(a.lower(), b.lower()) startswith = lambda a, b: a.startswith(b) endswith = lambda a, b: a.endswith(b) regex = lambda a, b: re.match(b, a) is not None iregex = lambda a, b: re.match(b, a, re.I) is not None class CompositeDecider(xapian.MatchDecider): # operators map op_map = { 'exact': operator.eq, 'iexact': i(operator.eq), 'startswith': startswith, 'istartswith': i(startswith), 'endswith': endswith, 'iendswith': i(endswith), 'contains': operator.contains, 'icontains': i(operator.contains), 'regex': regex, 'iregex': iregex, 'in': lambda a, b: operator.contains(b, a), 'gt': operator.gt, 'gte': operator.ge, 'lt': operator.lt, 'lte': operator.le, } def __init__(self, model, tags, filter, exclude): xapian.MatchDecider.__init__(self) self._model = model self._tags = tags self._values_map = dict([(t.prefix, t.number) for t in tags]) self._filter = filter self._exclude = exclude def __call__(self, document): if self._filter and not self._do_x(self._filter, document): return False if self._exclude and self._do_x(self._exclude, document): return False return True def get_tag(self, index): for tag in self._tags: if tag.number == index: return tag raise ValueError("No tag with number '%s'" % index) def _do_x(self, field, document): for child in field.children: if isinstance(child, X): result = self._do_x(child, document) else: result = self._do_field(child[0], child[1], document) if (result and field.connector == 'OR')\ or (not result and field.connector == 'AND'): break if field.negated: return not result else: return result def _do_field(self, lookup, value, document): if '__' in lookup: field, op = lookup.split('__', 1) else: field, op = lookup, 'exact' if op not in self.op_map: raise ValueError("Unknown lookup operator '%s'" % op) op = self.op_map[op] doc_value = document.get_value(self._values_map[field]) convert = curry( self.get_tag(self._values_map[field]).convert, model=self._model ) if isinstance(value, (list, tuple)): value = map(convert, value) else: value = convert(value) operands = [ doc_value, value, ] return reduce(op, operands) Djapian-2.3.1/src/djapian/indexer.py0000644000076500000240000003251211313234300016421 0ustar daevdialoutimport datetime import os from django.db import models from django.utils.itercompat import is_iterable from djapian.signals import post_save, pre_delete from django.conf import settings from django.utils.encoding import smart_unicode, force_unicode from djapian.resultset import ResultSet from djapian import utils, decider from djapian.utils.paging import paginate from djapian.utils.commiter import Commiter import xapian class Field(object): raw_types = (int, long, float, basestring, bool, models.Model, datetime.time, datetime.date, datetime.datetime) def __init__(self, path, weight=utils.DEFAULT_WEIGHT, prefix="", number=None): self.path = path self.weight = weight self.prefix = prefix self.number = number def get_tag(self): return self.prefix.upper() def convert(self, field_value, model): """ Generates index values (for sorting) for given field value and its content type """ # If it is a model field make some postprocessing of its value try: content_type = model._meta.get_field(self.path.split('.', 1)[0]) except models.FieldDoesNotExist: content_type = field_value value = field_value if isinstance(content_type, (models.IntegerField, int, long)): # Integer fields are stored with 12 leading zeros value = '%012d' % field_value elif isinstance(content_type, (models.BooleanField, bool)): # Boolean fields are stored as 't' or 'f' if field_value: value = 't' else: value = 'f' elif isinstance(content_type, (models.DateTimeField, datetime.datetime)): # DateTime fields are stored as %Y%m%d%H%M%S (better sorting) value = field_value.strftime('%Y%m%d%H%M%S') elif isinstance(content_type, (float, models.FloatField)): value = '%.10f' % value return value def resolve_one(self, value, name): value = getattr(value, name) if isinstance(value, models.Manager): value = value.all() elif callable(value): value = value() return value def resolve(self, value): bits = self.path.split(".") for bit in bits: if is_iterable(value): value = u', '.join( map(lambda v: force_unicode(self.resolve_one(v, bit)), value) ) else: value = self.resolve_one(value, bit) if isinstance(value, self.raw_types): return value if is_iterable(value): return u', '.join(map(force_unicode, value)) return value and force_unicode(value) or None def extract(self, document): if self.number: return document.get_value(self.number) return None class Indexer(object): field_class = Field decider = decider.CompositeDecider free_values_start_number = 11 fields = [] tags = [] aliases = {} trigger = lambda indexer, obj: True stemming_lang_accessor = None def __init__(self, db, model): """ Initialize an Indexer whose index data to `db`. `model` is the Model whose instances will be used as documents. Note that fields from other models can still be used in the index, but this model will be the one returned from search results. """ self._prepare(db, model) # Parse fields # For each field checks if it is a tuple or a list and add it's weight for field in self.__class__.fields: if isinstance(field, (tuple, list)): self.fields.append(self.field_class(field[0], field[1])) else: self.fields.append(self.field_class(field)) # Parse prefixed fields valueno = self.free_values_start_number for field in self.__class__.tags: tag, path = field[:2] if len(field) == 3: weight = field[2] else: weight = utils.DEFAULT_WEIGHT self.tags.append(self.field_class(path, weight, prefix=tag, number=valueno)) valueno += 1 for tag, aliases in self.__class__.aliases.iteritems(): if self.has_tag(tag): if not isinstance(aliases, (list, tuple)): aliases = (aliases,) self.aliases[tag] = aliases else: raise ValueError("Cannot create alias for tag `%s` that doesn't exist" % tag) models.signals.post_save.connect(post_save, sender=self._model) models.signals.pre_delete.connect(pre_delete, sender=self._model) def __unicode__(self): return self.__class__.get_descriptor() __str__ = __unicode__ def has_tag(self, name): return self.tag_index(name) is not None def tag_index(self, name): for field in self.tags: if field.prefix == name: return field.number return None @classmethod def get_descriptor(cls): return ".".join([cls.__module__, cls.__name__]).lower() # Public Indexer interface def update(self, documents=None, after_index=None, per_page=10000, commit_each=False): """ Update the database with the documents. There are some default value and terms in a document: * Values: 1. Used to store the ID of the document 2. Store the model of the object (in the string format, like "project.app.model") 3. Store the indexer descriptor (module path) 4..10. Free * Terms UID: Used to store the ID of the document, so we can replace the document by the ID """ # Open Xapian Database database = self._db.open(write=True) # If doesnt have any document at all if documents is None: update_queue = self._model.objects.all() else: update_queue = documents commiter = Commiter.create(commit_each)( lambda: database.begin_transaction(flush=True), database.commit_transaction, database.cancel_transaction ) # Get each document received for page in paginate(update_queue, per_page): try: commiter.begin_page() for obj in page.object_list: commiter.begin_object() try: if not self.trigger(obj): self.delete(obj.pk, database) continue doc = xapian.Document() # Add default terms and values uid = self._create_uid(obj) doc.add_term(self._create_uid(obj)) self._insert_meta_values(doc, obj) generator = xapian.TermGenerator() generator.set_database(database) generator.set_document(doc) generator.set_flags(xapian.TermGenerator.FLAG_SPELLING) stem_lang = self._get_stem_language(obj) if stem_lang: generator.set_stemmer(xapian.Stem(stem_lang)) for field in self.fields + self.tags: # Trying to resolve field value or skip it try: value = field.resolve(obj) except AttributeError: continue if field.prefix: index_value = field.convert(value, self._model) if index_value is not None: doc.add_value(field.number, smart_unicode(index_value)) prefix = smart_unicode(field.get_tag()) generator.index_text(smart_unicode(value), field.weight, prefix) if prefix: # if prefixed then also index without prefix generator.index_text(smart_unicode(value), field.weight) database.replace_document(uid, doc) if after_index: after_index(obj) commiter.commit_object() except Exception: commiter.cancel_object() raise commiter.commit_page() except Exception: commiter.cancel_page() raise database.flush() def search(self, query): return ResultSet(self, query) def delete(self, obj, database=None): """ Delete a document from index """ try: if database is None: database = self._db.open(write=True) database.delete_document(self._create_uid(obj)) except (IOError, RuntimeError, xapian.DocNotFoundError), e: pass def document_count(self): return self._db.document_count() __len__ = document_count def clear(self): self._db.clear() # Private Indexer interface def _prepare(self, db, model=None): """Initialize attributes""" self._db = db self._model = model self._model_name = model and utils.model_name(model) self.fields = [] # Simple text fields self.tags = [] # Prefixed fields self.aliases = {} def _get_meta_values(self, obj): if isinstance(obj, models.Model): pk = obj.pk else: pk = obj return [pk, self._model_name, self.__class__.get_descriptor()] def _insert_meta_values(self, doc, obj, start=1): for value in self._get_meta_values(obj): doc.add_value(start, smart_unicode(value)) start += 1 return start def _create_uid(self, obj): """ Generates document UID for given object """ return "UID-" + "-".join(map(smart_unicode, self._get_meta_values(obj))) def _do_search(self, query, offset, limit, order_by, flags, stemming_lang, filter, exclude): """ flags are as defined in the Xapian API : http://www.xapian.org/docs/apidoc/html/classXapian_1_1QueryParser.html Combine multiple values with bitwise-or (|). """ database = self._db.open() enquire = xapian.Enquire(database) if order_by in (None, 'RELEVANCE'): enquire.set_sort_by_relevance() else: ascending = True if order_by.startswith('-'): ascending = False if order_by[0] in '+-': order_by = order_by[1:] try: valueno = self.tag_index(order_by) except (ValueError, TypeError): raise ValueError("Field %s cannot be used in order_by clause" " because it doen't exist in index" % order_by) enquire.set_sort_by_relevance_then_value(valueno, ascending) query, query_parser = self._parse_query(query, database, flags, stemming_lang) enquire.set_query( query ) decider = self.decider(self._model, self.tags, filter, exclude) return enquire.get_mset( offset, limit, None, decider ), query, query_parser def _get_stem_language(self, obj=None): """ Returns stemmig language for given object if acceptable or model wise """ # Use the language defined in DJAPIAN_STEMMING_LANG language = getattr(settings, 'DJAPIAN_STEMMING_LANG', 'none') if language == 'multi': language = 'none' if obj: try: language = self.field_class( self.stemming_lang_accessor, self._model ).resolve(obj) except AttributeError: pass return language def _parse_query(self, term, db, flags, stemming_lang): """ Parses search queries """ # Instance Xapian Query Parser query_parser = xapian.QueryParser() for field in self.tags: query_parser.add_prefix(field.prefix.lower(), field.get_tag()) if field.prefix in self.aliases: for alias in self.aliases[field.prefix]: query_parser.add_prefix(alias, field.get_tag()) query_parser.set_database(db) query_parser.set_default_op(xapian.Query.OP_AND) if stemming_lang in (None, "none"): stemming_lang = self._get_stem_language() if stemming_lang: query_parser.set_stemmer(xapian.Stem(stemming_lang)) query_parser.set_stemming_strategy(xapian.QueryParser.STEM_SOME) parsed_query = query_parser.parse_query(term, flags) return parsed_query, query_parser class CompositeIndexer(Indexer): def __init__(self, *indexers): from djapian.database import CompositeDatabase self._prepare( db=CompositeDatabase([indexer._db for indexer in indexers]) ) def clear(self): raise NotImplementedError def update(self, *args): raise NotImplementedError Djapian-2.3.1/src/djapian/management/0000755000076500000240000000000011313243720016530 5ustar daevdialoutDjapian-2.3.1/src/djapian/management/__init__.py0000644000076500000240000000000011306547451020641 0ustar daevdialoutDjapian-2.3.1/src/djapian/management/commands/0000755000076500000240000000000011313243720020331 5ustar daevdialoutDjapian-2.3.1/src/djapian/management/commands/__init__.py0000644000076500000240000000000011306547451022442 0ustar daevdialoutDjapian-2.3.1/src/djapian/management/commands/index.py0000644000076500000240000001362311306547677022043 0ustar daevdialoutfrom django.core.management.base import BaseCommand from django.db import transaction from django.utils.daemonize import become_daemon from django.contrib.contenttypes.models import ContentType import os import sys import operator from datetime import datetime from optparse import make_option from djapian.models import Change from djapian import utils from djapian.utils.paging import paginate from djapian.utils.commiter import Commiter from djapian import IndexSpace def get_content_types(*actions): types = Change.objects.filter(action__in=actions)\ .values_list('content_type', flat=True)\ .distinct() return ContentType.objects.filter(pk__in=types) def get_indexers(content_type): return reduce( operator.add, [space.get_indexers_for_model(content_type.model_class()) for space in IndexSpace.instances] ) @transaction.commit_manually def update_changes(verbose, timeout, once, per_page, commit_each): counter = [0] def reset_counter(): counter[0] = 0 def after_index(obj): counter[0] += 1 if verbose: sys.stdout.write('.') sys.stdout.flush() commiter = Commiter.create(commit_each)( lambda: None, transaction.commit, transaction.rollback ) while True: count = Change.objects.count() if count > 0 and verbose: print 'There are %d objects to update' % count for ct in get_content_types('add', 'edit'): indexers = get_indexers(ct) for page in paginate( Change.objects.filter(content_type=ct, action__in=('add', 'edit'))\ .select_related('content_type')\ .order_by('object_id'), per_page ):# The objects must be sorted by date commiter.begin_page() try: for indexer in indexers: indexer.update( ct.model_class()._default_manager.filter( pk__in=[c.object_id for c in page.object_list] ).order_by('pk'), after_index, per_page, commit_each ) for change in page.object_list: change.delete() commiter.commit_page() except Exception: if commit_each: for change in page.object_list[:counter[0]]: change.delete() commiter.commit_object() else: commiter.cancel_page() raise reset_counter() for ct in get_content_types('delete'): indexers = get_indexers(ct) for change in Change.objects.filter(content_type=ct, action='delete'): for indexer in indexers: indexer.delete(change.object_id) change.delete() # If using transactions and running Djapian as a daemon, transactions # need to be committed on each iteration, otherwise Djapian will not # catch changes. We also need to use the commit_manually decorator. # # Background information: # # Autocommit is turned off by default according to PEP 249. # PEP 249 states "Database modules that do not support transactions # should implement this method with void functionality". # Consistent Nonlocking Reads (InnoDB): # http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read-example.html transaction.commit() if once: break time.sleep(timeout) def rebuild(verbose, per_page, commit_each): def after_index(obj): if verbose: sys.stdout.write('.') sys.stdout.flush() for space in IndexSpace.instances: for model, indexers in space.get_indexers().iteritems(): for indexer in indexers: indexer.clear() indexer.update(None, after_index, per_page, commit_each) class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--verbose', action='store_true', default=False, help='Verbosity output'), make_option('--daemonize', dest='make_daemon', default=False, action='store_true', help='Do not fork the process'), make_option('--time-out', dest='timeout', default=10, type='int', help='Time to sleep between each query to the' ' database (default: %default)'), make_option('--rebuild', dest='rebuild_index', default=False, action='store_true', help='Rebuild index database'), make_option('--per_page', dest='per_page', default=1000, action='store', type=int, help='Working page size'), make_option('--commit_each', dest='commit_each', default=False, action='store_true', help='Commit/flush changes on every document update'), ) help = 'This is the Djapian daemon used to update the index based on djapian_change table.' requires_model_validation = True def handle(self, verbose=False, make_daemon=False, timeout=10, rebuild_index=False, per_page=1000, commit_each=False, *args, **options): utils.load_indexes() if make_daemon: become_daemon() if rebuild_index: rebuild(verbose, per_page, commit_each) else: update_changes(verbose, timeout, not make_daemon, per_page, commit_each) if verbose: print '\n' Djapian-2.3.1/src/djapian/management/commands/indexshell.py0000644000076500000240000001447711306547451023071 0ustar daevdialoutimport sys import cmd from django.core.management.base import BaseCommand from django.utils.text import smart_split from djapian import utils from djapian import IndexSpace def with_index(func): def _decorator(cmd, arg): if cmd._current_index is None: print "No index selected" return return func(cmd, arg) _decorator.__doc__ = func.__doc__ return _decorator def split_arg(func): def _decorator(cmd, arg): bits = list(smart_split(arg)) return func(cmd, *bits) _decorator.__doc__ = func.__doc__ return _decorator class Interpreter(cmd.Cmd): prompt = ">>> " def __init__(self, *args): self._current_index = None self._current_index_path = [None, None, None] if len(args): self.do_use(args[0]) cmd.Cmd.__init__(self) def do_list(self, arg): """ Lists all available indexes with their ids """ print "Installed spaces/models/indexers:" def _is_selected(space, model=None, index=None): selected = [b for b in (space, model, index) if b is not None] return len( [1 for c, s in zip(self._current_index_path, selected) if c == s] ) == len(selected) for space_i, space in enumerate(IndexSpace.instances): print (_is_selected(space_i) and '* ' or '- ') + '%s: `%s`' % (space_i, space) for model_indexer_i, pair in enumerate(space.get_indexers().items()): model, indexers = pair print (_is_selected(space_i, model_indexer_i) and ' * ' or ' - ') +\ "%s.%s: `%s`" % (space_i, model_indexer_i, utils.model_name(model)) for indexer_i, indexer in enumerate(indexers): print (_is_selected(space_i, model_indexer_i, indexer_i) and ' * ' or ' - ') +\ "%s.%s.%s: `%s`" % (space_i, model_indexer_i, indexer_i, indexer) def do_exit(self, arg): """ Exit shell """ return True def do_use(self, index): """ Changes current index """ space, model, indexer, path = self._get_indexer(index) if indexer is not None: self._current_index = indexer self._current_index_path = path print "Using `%s:%s:%s` index." % (space, utils.model_name(model), indexer) def do_usecomposite(self, indexes): """ Changes current index to composition of given indexers """ from djapian.indexer import CompositeIndexer indexers = [] for index in indexes.split(' '): indexers.append(self._get_indexer(index.strip())) self._current_index = CompositeIndexer(*[i[2] for i in indexers]) print "Using composition of:" for indexer in indexers: space, model, indexer = indexer print " `%s:%s:%s`" % (space, utils.model_name(model), indexer) print "indexes." @with_index @split_arg def do_query(self, query, _slice=''): """ Returns objects fetched by given query """ _slice = slice(*self._parse_slice(_slice)) print list(self._current_index.search(query)[_slice]) @with_index def do_count(self, query): """ Returns count of objects fetched by given query """ print self._current_index.search(query).count() @with_index def do_total(self, arg): """ Returns count of objects in index """ print self._current_index.document_count() def do_stats(self, arg): """ Print index status information """ import operator print "Number of spaces: %s" % len(IndexSpace.instances) print "Number of indexes: %s" % reduce( operator.add, [len(space.get_indexers()) for space in IndexSpace.instances] ) @with_index def do_docslist(self, slice=""): """ Returns count of objects in index """ db = self._current_index._db.open() start, end = self._parse_slice(slice, default=(1, db.get_lastdocid())) for i in range(start, end + 1): doc = db.get_document(i) print "doc #%s:\n\tValues (%s):" % (i, doc.values_count()) val = doc.values_begin() for i in range(doc.values_count()): print "\t\t%s: %s" % (val.get_valueno(), val.get_value()) val.next() print "\tTerms (%s):" % doc.termlist_count() termlist = doc.termlist_begin() for i in range(doc.termlist_count()): print termlist.get_term(), termlist.next() print "\n" @with_index def do_delete(self, id): """ Removes document by id """ id = int(id) db = self._current_index._db.open(write=True) db.delete_document(id) print "Document #%s deleted." % id def _get_indexer(self, index): try: _space, _model, _indexer = self._parse_slice(index, '.') space = IndexSpace.instances[_space] model = space.get_indexers().keys()[_model] indexer = space.get_indexers()[model][_indexer] except (TypeError, IndexError, KeyError, ValueError): print 'Illegal index alias `%s`. See `list` command for available aliases' % index return None, None, None, None return space, model, indexer, [_space, _model, _indexer] def _parse_slice(self, slice='', delimeter=':', default=tuple()): if slice: def _select(b, d): try: return int(b) except ValueError: return d bits = [_select(b, d) for b, d in zip(slice.split(delimeter), default or ([None] * 3))] elif default: return default else: raise ValueError("Empty slice") return bits class Command(BaseCommand): help = "Djapian shell that provides capabilities to monitoring indexes." args = '[index_id]' requires_model_validation = True def handle(self, *args, **options): utils.load_indexes() try: Interpreter(*args).cmdloop("Interactive Djapian shell.") except KeyboardInterrupt: print "\n" Djapian-2.3.1/src/djapian/models.py0000644000076500000240000000347111306547451016270 0ustar daevdialoutfrom django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic from django.utils.encoding import smart_str from datetime import datetime from djapian import utils class ChangeManager(models.Manager): def create(self, object, action, **kwargs): ct = ContentType.objects.get_for_model(object.__class__) pk = smart_str(object.pk) try: old_change = self.get( content_type=ct, object_id=pk ) if old_change.action=="add": if action=="edit": old_change.save() return old_change elif action=="delete": old_change.delete() return None old_change.delete() except self.model.DoesNotExist: old_change = self.model(content_type=ct, object_id=pk) old_change.action = action old_change.save() return old_change class Change(models.Model): ACTIONS = ( ("add", "object added"), ("edit", "object edited"), ("delete", "object deleted"), ) content_type = models.ForeignKey(ContentType, db_index=True) object_id = models.CharField(max_length=150) date = models.DateTimeField(default=datetime.now) action = models.CharField(max_length=6, choices=ACTIONS) object = generic.GenericForeignKey() objects = ChangeManager() def __unicode__(self): return u'%s %s#%s on %s' % ( self.action, self.content_type, self.object_id, self.date ) def save(self): self.date = datetime.now() super(Change, self).save() class Meta: unique_together = [("content_type", "object_id")] Djapian-2.3.1/src/djapian/resultset.py0000644000076500000240000001754111306547451017042 0ustar daevdialoutimport xapian import operator from copy import deepcopy from django.db.models import get_model from django.utils.encoding import force_unicode from djapian import utils, decider class ResultSet(object): def __init__(self, indexer, query_str, offset=0, limit=utils.DEFAULT_MAX_RESULTS, order_by=None, prefetch=False, flags=None, stemming_lang=None, filter=None, exclude=None, prefetch_select_related=False): self._indexer = indexer self._query_str = query_str self._offset = offset self._limit = limit self._order_by = order_by self._prefetch = prefetch self._prefetch_select_related = prefetch_select_related self._filter = filter or decider.X() self._exclude = exclude or decider.X() if flags is None: flags = xapian.QueryParser.FLAG_PHRASE\ | xapian.QueryParser.FLAG_BOOLEAN\ | xapian.QueryParser.FLAG_LOVEHATE self._flags = flags self._stemming_lang = stemming_lang self._resultset_cache = None self._mset = None self._query = None self._query_parser = None # Public methods that produce another ResultSet def all(self): return self._clone() def spell_correction(self): return self._clone( flags=self._flags | xapian.QueryParser.FLAG_SPELLING_CORRECTION\ | xapian.QueryParser.FLAG_WILDCARD ) def prefetch(self, select_related=False): return self._clone( prefetch=True, prefetch_select_related=select_related ) def order_by(self, field): return self._clone(order_by=field) def flags(self, flags): return self._clone(flags=flags) def stemming(self, lang): return self._clone(stemming_lang=lang) def count(self): return self._clone()._do_count() def get_corrected_query_string(self): self._get_mset() return self._query_parser.get_corrected_query_string() def filter(self, *fields, **raw_fields): clone = self._clone() clone._add_filter_fields(fields, raw_fields) return clone def exclude(self, *fields, **raw_fields): clone = self._clone() clone._add_exclude_fields(fields, raw_fields) return clone # Private methods def _prepare_fields(self, fields=None, raw_fields=None): fields = fields and reduce(operator.and_, fields) or decider.X() if raw_fields: fields = fields & reduce( operator.and_, map( lambda value: decider.X(**{value[0]: value[1]}), raw_fields.iteritems() ) ) self._check_fields(fields) return fields def _add_filter_fields(self, fields=None, raw_fields=None): self._filter &= self._prepare_fields(fields, raw_fields) def _add_exclude_fields(self, fields=None, raw_fields=None): self._exclude &= self._prepare_fields(fields, raw_fields) def _check_fields(self, fields): known_fields = set([f.prefix for f in self._indexer.tags]) for field in fields.children: if isinstance(field, decider.X): self._check_fields(field) else: if field[0].split('__', 1)[0] not in known_fields: raise ValueError("Unknown field '%s'" % field[0]) def _clone(self, **kwargs): data = { "indexer": self._indexer, "query_str": self._query_str, "offset": self._offset, "limit": self._limit, "order_by": self._order_by, "prefetch": self._prefetch, "prefetch_select_related": self._prefetch_select_related, "flags": self._flags, "stemming_lang": self._stemming_lang, "filter": deepcopy(self._filter), "exclude": deepcopy(self._exclude), } data.update(kwargs) return ResultSet(**data) def _do_count(self): self._get_mset() return self._mset.size() def _do_prefetch(self): model_map = {} for hit in self._resultset_cache: model_map.setdefault(hit.model, []).append(hit) for model, hits in model_map.iteritems(): pks = [hit.pk for hit in hits] instances = model._default_manager.all() if self._prefetch_select_related: instances = instances.select_related() instances = instances.in_bulk(pks) for hit in hits: hit.instance = instances[hit.pk] def _get_mset(self): if self._mset is None: self._mset, self._query, self._query_parser = self._indexer._do_search( self._query_str, self._offset, self._limit, self._order_by, self._flags, self._stemming_lang, self._filter, self._exclude, ) def _fetch_results(self): if self._resultset_cache is None: self._get_mset() self._parse_results() return self._resultset_cache def _parse_results(self): self._resultset_cache = [] for match in self._mset: doc = match.get_document() model = doc.get_value(2) model = get_model(*model.split('.')) pk = model._meta.pk.to_python(doc.get_value(1)) percent = match.get_percent() rank = match.get_rank() weight = match.get_weight() tags = dict([(tag.prefix, tag.extract(doc))\ for tag in self._indexer.tags]) self._resultset_cache.append( Hit(pk, model, percent, rank, weight, tags) ) if self._prefetch: self._do_prefetch() def __iter__(self): self._fetch_results() return iter(self._resultset_cache) def __len__(self): self._fetch_results() return len(self._resultset_cache) def __getitem__(self, k): if not isinstance(k, (slice, int, long)): raise TypeError assert ((not isinstance(k, slice) and (k >= 0)) or (isinstance(k, slice) and (k.start is None or k.start >= 0) and (k.stop is None or k.stop >= 0))), \ "Negative indexing is not supported." if self._resultset_cache is not None: return self._fetch_results()[k] else: if isinstance(k, slice): start, stop = k.start, k.stop if start is None: start = 0 if stop is None: kstop = utils.DEFAULT_MAX_RESULTS return self._clone( offset=start, limit=stop - start ) else: return list(self._clone( offset=k, limit=1 ))[k] def __unicode__(self): return u"" % force_unicode(self._query_str) class Hit(object): def __init__(self, pk, model, percent, rank, weight, tags): self.pk = pk self.model = model self.percent = percent self.rank = rank self.weight = weight self.tags = tags self._instance = None def get_instance(self): if self._instance is None: self._instance = self.model._default_manager.get(pk=self.pk) return self._instance def set_instance(self, instance): self._instance = instance instance = property(get_instance, set_instance) def __repr__(self): return "" % ( utils.model_name(self.model), self.pk, self.percent, self.rank, self.weight ) Djapian-2.3.1/src/djapian/signals.py0000644000076500000240000000070611306547451016443 0ustar daevdialout""" Here are the post_save and the pre_delete signals """ from djapian.models import Change def post_save(sender, instance, created, *args, **kwargs): '''Create the Change object to update the index''' Change.objects.create(object=instance, action= created and "add" or "edit") def pre_delete(sender, instance, *args, **kwargs): '''Create the Change object to update the index''' Change.objects.create(object=instance, action="delete") Djapian-2.3.1/src/djapian/space.py0000644000076500000240000000426211306547451016077 0ustar daevdialoutimport os import new from django.db import models from django.conf import settings from django.utils.datastructures import SortedDict from djapian import utils from djapian.database import Database from djapian.indexer import Indexer class IndexSpace(object): instances = [] def __init__(self, base_dir, name): self._base_dir = os.path.abspath(base_dir) self._indexers = SortedDict() self._name = name self.__class__.instances.append(self) def __unicode__(self): return self._name def __str__(self): from django.utils.encoding import smart_str return smart_str(self.__unicode__()) def add_index(self, model, indexer=None, attach_as=None): if indexer is None: indexer = self.create_default_indexer(model) db = Database( os.path.join( self._base_dir, model._meta.app_label, model._meta.object_name.lower(), indexer.get_descriptor() ) ) indexer = indexer(db, model) if attach_as is not None: if hasattr(model, attach_as): raise ValueError("Attribute with name `%s` is already exist" % attach_as) else: model.add_to_class(attach_as, indexer) if model in self._indexers: self._indexers[model].append(indexer) else: self._indexers[model] = [indexer] return indexer def get_indexers(self): return self._indexers def get_indexers_for_model(self, model): try: return self._indexers[model] except KeyError: return [] def create_default_indexer(self, model): tags = [] fields = [] for field in model._meta.fields: if isinstance(field, models.TextField): fields.append(field.attname) else: tags.append((field.name, field.attname)) return new.classobj( "Default%sIndexer" % utils.model_name(model).replace('.', ''), (Indexer,), { "tags": tags, "fields": fields } ) Djapian-2.3.1/src/djapian/tests/0000755000076500000240000000000011313243720015556 5ustar daevdialoutDjapian-2.3.1/src/djapian/tests/__init__.py0000644000076500000240000000032711306547451017703 0ustar daevdialoutfrom djapian.tests.query import * from djapian.tests.index import * from djapian.tests.search import * from djapian.tests.common import * from djapian.tests.pagination import * from djapian.tests.filtering import * Djapian-2.3.1/src/djapian/tests/common.py0000644000076500000240000000501411306547451017432 0ustar daevdialoutimport os from djapian import Field from djapian.tests.utils import BaseTestCase, BaseIndexerTest, Entry, Person from djapian.models import Change from django.utils.encoding import force_unicode class IndexerTest(BaseTestCase): def test_fields_count(self): self.assertEqual(len(Entry.indexer.fields), 1) def test_tags_count(self): self.assertEqual(len(Entry.indexer.tags), 8) class FieldResolverTest(BaseTestCase): def setUp(self): person = Person.objects.create(name="Alex", age=22) another_person = Person.objects.create(name="Sam", age=25) self.entry = Entry.objects.create(author=person, title="Test entry") self.entry.editors.add(person, another_person) def test_simple_attribute(self): self.assertEqual(Field("title").resolve(self.entry), "Test entry") def test_related_attribute(self): self.assertEqual(Field("author.name").resolve(self.entry), "Alex") def test_fk_attribute(self): self.assertEqual(force_unicode(Field("author").resolve(self.entry)), "Alex") def test_m2m_attribute(self): self.assertEqual(force_unicode(Field("editors").resolve(self.entry)), "Alex, Sam") def test_m2m_field_attribute(self): self.assertEqual(force_unicode(Field("editors.age").resolve(self.entry)), "22, 25") def test_method(self): self.assertEqual( Field("headline").resolve(self.entry), "Alex - Test entry" ) class ChangeTrackingTest(BaseTestCase): def setUp(self): p = Person.objects.create(name="Alex") Entry.objects.create(author=p, title="Test entry") Entry.objects.create( author=p, title="Another test entry", is_active=False ) def test_change_count(self): self.assertEqual(Change.objects.count(), 2) class ChangeTrackingUpdateTest(BaseTestCase): def setUp(self): p = Person.objects.create(name="Alex") entry = Entry.objects.create(author=p, title="Test entry") entry.text = "Foobar text" entry.save() def test_change_count(self): self.assertEqual(Change.objects.count(), 1) def test_change_action(self): self.assertEqual(Change.objects.get().action, "add") class ChangeTrackingDeleteTest(BaseTestCase): def setUp(self): p = Person.objects.create(name="Alex") entry = Entry.objects.create(author=p, title="Test entry") entry.delete() def test_change_count(self): self.assertEqual(Change.objects.count(), 0) Djapian-2.3.1/src/djapian/tests/filtering.py0000644000076500000240000000452511306547451020133 0ustar daevdialoutfrom django.test import TestCase from djapian.tests.utils import BaseTestCase, BaseIndexerTest, Entry, Person from djapian import X class FilteringTest(BaseIndexerTest, BaseTestCase): def setUp(self): super(FilteringTest, self).setUp() self.result = Entry.indexer.search("text") def test_filter(self): self.assertEqual(self.result.filter(count=5).count(), 1) self.assertEqual(self.result.filter(count__lt=6).count(), 2) self.assertEqual(self.result.filter(count__gte=5).count(), 2) self.assertEqual(self.result.filter(count__in=[5, 7]).count(), 2) self.assertEqual(self.result.filter(rating__lte=4.5).count(), 2) def test_exclude(self): self.assertEqual(self.result.exclude(count=5).count(), 2) self.assertEqual(self.result.exclude(count__lt=6).count(), 1) self.assertEqual(self.result.exclude(count__gte=5).count(), 1) def test_filter_exclude(self): self.assertEqual(self.result.filter(count__lt=6).exclude(count=5).count(), 1) def test_complex(self): self.assertEqual(self.result.filter(X(count__lt=6) & ~X(count=5)).count(), 1) self.assertEqual(self.result.filter(X(count=7) | X(count=5)).count(), 2) class LookupTest(BaseIndexerTest, BaseTestCase): def setUp(self): super(LookupTest, self).setUp() self.result = Entry.indexer.search("text") def test_exact(self): self.assertEqual(self.result.filter(title__startswith='Test entry').count(), 1) self.assertEqual(self.result.filter(title__istartswith='test entry').count(), 1) def test_startswith(self): self.assertEqual(self.result.filter(title__startswith='Third').count(), 1) self.assertEqual(self.result.filter(title__istartswith='third').count(), 1) def test_endswith(self): self.assertEqual(self.result.filter(title__endswith='- second').count(), 1) self.assertEqual(self.result.filter(title__iendswith='- Second').count(), 1) def test_contains(self): self.assertEqual(self.result.filter(title__contains='for').count(), 1) self.assertEqual(self.result.filter(title__icontains='For').count(), 1) def test_regex(self): self.assertEqual(self.result.filter(title__regex=r'^Test[ \w]+$').count(), 1) self.assertEqual(self.result.filter(title__iregex=r'^test[ \w]+$').count(), 1) Djapian-2.3.1/src/djapian/tests/index.py0000644000076500000240000000177211306547451017260 0ustar daevdialoutimport os from datetime import datetime from django.db import models from djapian import Indexer, Field from djapian.tests.utils import BaseTestCase, BaseIndexerTest, Entry, Person class IndexerUpdateTest(BaseIndexerTest, BaseTestCase): def test_database_exists(self): self.assert_(os.path.exists(Entry.indexer._db._path)) def test_document_count(self): self.assertEqual(Entry.indexer.document_count(), 3) class IndexCommandTest(BaseTestCase): def setUp(self): p = Person.objects.create(name="Alex") entry1 = Entry.objects.create( author=p, title="Test entry", text="Not large text field" ) entry2 = Entry.objects.create( author=p, title="Another test entry", is_active=False ) from django.core.management import call_command call_command("index", daemonize=False) def test_database(self): self.assertEqual(Entry.indexer.document_count(), 1) Djapian-2.3.1/src/djapian/tests/pagination.py0000644000076500000240000000141311306547451020272 0ustar daevdialoutfrom django.core.paginator import Paginator from djapian.tests.utils import BaseTestCase, Entry, Person class ResultSetPaginationTest(BaseTestCase): num_entries = 100 per_page = 10 num_pages = num_entries / per_page def setUp(self): p = Person.objects.create(name="Alex") for i in range(self.num_entries): Entry.objects.create( author=p, title="Entry with number %s" % i, text="foobar " * i ) Entry.indexer.update() self.result = Entry.indexer.search("title:number") def test_pagintion(self): paginator = Paginator(self.result, self.per_page) self.assertEqual(paginator.num_pages, self.num_pages) page = paginator.page(5) Djapian-2.3.1/src/djapian/tests/query.py0000644000076500000240000000131611306547451017310 0ustar daevdialoutfrom djapian.tests.utils import BaseIndexerTest, BaseTestCase, Entry def query_test(query, count): class _QueryTest(BaseIndexerTest, BaseTestCase): def setUp(self): super(_QueryTest, self).setUp() self.result = Entry.indexer.search(query) def test_result_count(self): self.assertEqual(len(self.result), count) _QueryTest.__name__ = _QueryTest.__name__ + '_' + query.replace(" ", "_") return _QueryTest IndexerSearchCharFieldTest = query_test("title:test", 2) IndexerSearchAliasFieldTest = query_test("subject:test", 2) IndexerSearchBoolFieldTest = query_test("active:True", 3) IndexerSearchAndQueryTest = query_test("title:test AND title:another", 1) Djapian-2.3.1/src/djapian/tests/search.py0000644000076500000240000000443611306547451017416 0ustar daevdialoutfrom django.test import TestCase from djapian.tests.utils import BaseTestCase, BaseIndexerTest, Entry, Person, Comment from djapian.indexer import CompositeIndexer class IndexerSearchTextTest(BaseIndexerTest, BaseTestCase): def setUp(self): super(IndexerSearchTextTest, self).setUp() self.result = Entry.indexer.search("text") def test_result_count(self): self.assertEqual(len(self.result), 3) def test_result_row(self): self.assertEqual(self.result[0].instance, self.entries[0]) def test_result_list(self): result = [r.instance for r in self.result] result.sort(key=lambda i: i.pk) expected = self.entries[0:3] expected.sort(key=lambda i: i.pk) self.assertEqual(result, expected) def test_score(self): self.assert_(self.result[0].percent in (99, 100)) def test_hit_fields(self): hit = self.result[0] self.assertEqual(hit.tags['title'], 'Test entry') def test_prefetch(self): result = self.result.prefetch() self.assertEqual(result[0].instance.author.name, 'Alex') result = self.result.prefetch(select_related=True) self.assert_(hasattr(result[0].instance, '_author_cache')) self.assertEqual(result[0].instance.author.name, 'Alex') class AliasesTest(BaseTestCase): num_entries = 5 def setUp(self): p = Person.objects.create(name="Alex") for i in range(self.num_entries): Entry.objects.create(author=p, title="Entry with number %s" % i, text="foobar " * i) Entry.indexer.update() self.result = Entry.indexer.search("subject:number") def test_result(self): self.assertEqual(len(self.result), self.num_entries) class CorrectedQueryStringTest(BaseIndexerTest, BaseTestCase): def test_correction(self): results = Entry.indexer.search("texte").spell_correction() self.assertEqual(results.get_corrected_query_string(), "text") class CompositeIndexerTest(BaseIndexerTest, BaseTestCase): def setUp(self): super(CompositeIndexerTest, self).setUp() self.indexer = CompositeIndexer(Entry.indexer, Comment.indexer) def test_search(self): results = self.indexer.search('entry') self.assertEqual(len(results), 4) # 3 entries + 1 comment Djapian-2.3.1/src/djapian/tests/utils.py0000644000076500000240000000672311306547451017312 0ustar daevdialoutimport os from datetime import datetime, timedelta from django.db import models from django.test import TestCase import djapian class Person(models.Model): name = models.CharField(max_length=150) age = models.PositiveIntegerField(default=0) def __unicode__(self): return self.name class Meta: app_label = "djapian" class Entry(models.Model): title = models.CharField(max_length=250, primary_key=True) author = models.ForeignKey(Person, related_name="entries") tags = models.CharField(max_length=250, null=True) created_on = models.DateTimeField(default=datetime.now) rating = models.FloatField(default=0) asset_count = models.PositiveIntegerField(default=0) is_active = models.BooleanField(default=True) text = models.TextField() editors = models.ManyToManyField(Person, related_name="edited_entries") def headline(self): return "%s - %s" % (self.author, self.title) def __unicode__(self): return self.title class Meta: app_label = "djapian" class Comment(models.Model): entry = models.ForeignKey(Entry) author = models.ForeignKey(Person) text = models.TextField() class Meta: app_label = "djapian" class EntryIndexer(djapian.Indexer): fields = ["text"] tags = [ ("author", "author.name"), ("title", "title", 3), ("tag", "tags", 2), ("date", "created_on"), ("active", "is_active"), ("count", "asset_count"), ("editors", "editors"), ('rating', 'rating'), ] aliases = { "title": "subject", "author": "user", } trigger = lambda indexer, obj: obj.is_active class CommentIndexer(djapian.Indexer): fields = ['text'] tags = [ ('author', 'author.name') ] djapian.add_index(Entry, EntryIndexer, attach_as='indexer') djapian.add_index(Comment, CommentIndexer, attach_as='indexer') class BaseTestCase(TestCase): def tearDown(self): Entry.indexer.clear() Comment.indexer.clear() class BaseIndexerTest(object): def setUp(self): self.person = Person.objects.create(name="Alex") self.entries= [ Entry.objects.create( author=self.person, title="Test entry", rating=4.5, text="Not large text field wich helps us to test Djapian" ), Entry.objects.create( author=self.person, title="Another test entry - second", rating=3.6, text="Another not useful text message for tests", asset_count=5, created_on=datetime.now()-timedelta(hours=4) ), Entry.objects.create( author=self.person, title="Third entry for testing", rating=4.65, text="Third message text", asset_count=7, created_on=datetime.now()-timedelta(hours=2) ), Entry.objects.create( author=self.person, title="Innactive entry", is_active=False, text="Text wich will not be indexed" ) ] Entry.indexer.update() self.comments =[ Comment.objects.create( entry=self.entries[0], author=self.person, text='Hey, I comment my own entry!' ) ] Comment.indexer.update() Djapian-2.3.1/src/djapian/utils/0000755000076500000240000000000011313243720015554 5ustar daevdialoutDjapian-2.3.1/src/djapian/utils/__init__.py0000644000076500000240000000060411306547451017677 0ustar daevdialoutfrom django.conf import settings DEFAULT_MAX_RESULTS = 100000 DEFAULT_WEIGHT = 1 def model_name(model): return "%s.%s" % (model._meta.app_label, model._meta.object_name) def load_indexes(): from djapian.utils import loading for app in settings.INSTALLED_APPS: try: loading.get_module(app, "index") except loading.NoModuleError: pass Djapian-2.3.1/src/djapian/utils/commiter.py0000644000076500000240000000162411306547451017762 0ustar daevdialoutclass Commiter(object): def __init__(self, begin, commit, cancel): self._begin = begin self._commit = commit self._cancel = cancel def begin_page(self): pass def begin_object(self): pass def commit_page(self): pass def commit_object(self): pass def cancel_page(self): pass def cancel_object(self): pass @classmethod def create(cls, commit_each): class _ConcreteCommiter(cls): pass prefix = commit_each and 'object' or 'page' for name in ('begin', 'commit', 'cancel'): def make_method(name): return lambda self: getattr(self, '_%s' % name)() setattr( _ConcreteCommiter, '%s_%s' % (name, prefix), lambda self: make_method(name) ) return _ConcreteCommiter Djapian-2.3.1/src/djapian/utils/loading.py0000644000076500000240000000125011306547451017553 0ustar daevdialout# Module taken from Turbion blog engine import os import imp from django.utils.importlib import import_module class NoModuleError(Exception): """ Custom exception class indicates that given module does not exit at all """ pass def get_module(base, module_name): try: base_path = __import__(base, {}, {}, [base.split('.')[-1]]).__path__ except AttributeError: raise NoModuleError("Cannot load base `%s`" % base) try: imp.find_module(module_name, base_path) except ImportError: raise NoModuleError("Cannot find module `%s` in base `%s`" % (module_name, base)) return import_module('.%s' % module_name, base) Djapian-2.3.1/src/djapian/utils/paging.py0000644000076500000240000000027711306547451017413 0ustar daevdialoutfrom django.core.paginator import Paginator def paginate(queue, per_page): paginator = Paginator(queue, per_page) for num in paginator.page_range: yield paginator.page(num)