Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial check-in

  • Loading branch information...
commit 90894be664fbfa0913b2134301445e2234ea0f19 0 parents
@coleifer authored
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2010 Charles Leifer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
3  MANIFEST.in
@@ -0,0 +1,3 @@
+include LICENSE
+include MANIFEST.in
+include README.rst
50 README.rst
@@ -0,0 +1,50 @@
+redis-completion
+================
+
+autocompletion with `redis <http://redis.io>`_ based on:
+
+* http://antirez.com/post/autocomplete-with-redis.html
+* http://stackoverflow.com/questions/1958005/redis-autocomplete/1966188
+
+
+usage
+-----
+
+If you just want to store really simple things, like strings:
+
+::
+
+ engine = RedisEngine()
+ titles = ['python programming', 'programming c', 'unit testing python',
+ 'testing software', 'software design']
+ map(engine.store, titles)
+
+ >>> engine.search('pyt')
+ ['python programming', 'unit testing python']
+
+ >>> engine.search('test')
+ ['testing software', 'unit testing python']
+
+
+If you want to store more complex data, like blog entries:
+
+::
+
+ Entry.create(title='an entry about python', published=True)
+ Entry.create(title='all about redis', published=True)
+ Entry.create(title='using redis with python', published=False)
+
+ for entry in Entry.select():
+ engine.store_json(entry.id, entry.title, {
+ 'published': entry.published,
+ 'title': entry.title,
+ 'url': entry.get_absolute_url(),
+ })
+
+ >>> engine.search_json('pytho')
+ [{'published': True, 'title': 'an entry about python', 'url': '/blog/1/'},
+ {'published': False, 'title': 'using redis with python', 'url': '/blog/3/'}]
+
+ # just published entries, please
+ >>> engine.search_json('redis', filters=[lambda i: i['published'] == True])
+ [{u'published': True, u'title': u'all about redis', u'url': u'/blog/2/'}]
1  redis_completion/__init__.py
@@ -0,0 +1 @@
+from redis_completion.engine import RedisEngine
241 redis_completion/engine.py
@@ -0,0 +1,241 @@
+try:
+ import simplejson as json
+except ImportError:
+ import json
+import re
+
+from redis import Redis
+
+from redis_completion.stop_words import STOP_WORDS as _STOP_WORDS
+from redis_completion.text import create_key as _ck, partial_complete as _pc
+
+
+# aggressive stop words will be better when the length of the document is longer
+AGGRESSIVE_STOP_WORDS = _STOP_WORDS
+
+# default stop words should work fine for titles and things like that
+DEFAULT_STOP_WORDS = set(['a', 'an', 'of', 'the'])
+
+DEFAULT_GRAM_LENGTHS = (2, 3)
+
+
+class RedisEngine(object):
+ """
+ References
+ ----------
+
+ http://antirez.com/post/autocomplete-with-redis.html
+ http://stackoverflow.com/questions/1958005/redis-autocomplete/1966188#1966188
+ """
+ def __init__(self, gram_lengths=None, min_length=3, prefix='ac', stop_words=None, terminator='^', **conn_kwargs):
+ self.conn_kwargs = conn_kwargs
+ self.client = self.get_client()
+
+ gl = (gram_lengths is None) and DEFAULT_GRAM_LENGTHS or gram_lengths
+ assert len(gl) == 2, 'gram_lengths must be a 2-tuple'
+ self.min_words, self.max_words = gl
+
+ self.min_length = min_length
+ self.prefix = prefix
+ self.stop_words = (stop_words is None) and DEFAULT_STOP_WORDS or stop_words
+ self.terminator = terminator
+
+ self.data_key = lambda k: '%s:d:%s' % (self.prefix, k)
+ self.members_key = lambda k: '%s:m:%s' % (self.prefix, k)
+ self.search_key = lambda k: '%s:s:%s' % (self.prefix, k)
+ self.title_key = lambda k: '%s:t:%s' % (self.prefix, k)
+
+ def get_client(self):
+ return Redis(**self.conn_kwargs)
+
+ def flush(self, everything=False, batch_size=1000):
+ if everything:
+ return self.client.flushdb()
+
+ # this could be expensive :-(
+ keys = self.client.keys('%s*' % self.prefix)
+
+ # batch keys
+ for i in range(0, len(keys), batch_size):
+ self.client.delete(*keys[i:i+batch_size])
+
+ def score_key(self, k, max_size=10):
+ k_len = len(k)
+ iters = min(max_size, k_len)
+ a = ord('a') - 1
+ score = 0
+
+ for i in range(iters):
+ c = (ord(k[i]) - a)
+ score += c*(26**(iters-i))
+ return score
+
+ def create_key(self, phrase):
+ return _ck(phrase, self.max_words, self.stop_words)
+
+ def partial_complete(self, phrase):
+ return _pc(phrase, self.min_words, self.max_words, self.stop_words)
+
+ def autocomplete_keys(self, phrase):
+ key = self.create_key(phrase)
+ ml = self.min_length
+
+ for i, char in enumerate(key[ml:]):
+ yield (key[:i+ml], char, ord(char))
+
+ yield (key, self.terminator, 0)
+
+ def store(self, obj_id, title=None, data=None):
+ pipe = self.client.pipeline()
+
+ if title is None:
+ title = obj_id
+ if data is None:
+ data = title
+
+ title_score = self.score_key(self.create_key(title))
+
+ pipe.set(self.data_key(obj_id), data)
+ pipe.set(self.title_key(obj_id), title)
+
+ # create tries using sorted sets and add obj_data to the lookup set
+ for partial_title in self.partial_complete(title):
+ # store a reference to our object in the lookup set
+ partial_key = self.create_key(partial_title)
+ pipe.zadd(self.members_key(partial_key), obj_id, title_score)
+
+ for (key, value, score) in self.autocomplete_keys(partial_title):
+ pipe.zadd(self.search_key(key), value, score)
+
+ pipe.execute()
+
+ def store_json(self, obj_id, title, data_dict):
+ return self.store(obj_id, title, json.dumps(data_dict))
+
+ def remove(self, obj_id):
+ obj_id = str(obj_id)
+ title = self.client.get(self.title_key(obj_id)) or ''
+ keys = []
+
+ #...how to figure out if its the final item...
+ for partial_title in self.partial_complete(title):
+ # get a list of all the keys that would have been set for the tries
+ autocomplete_keys = list(self.autocomplete_keys(partial_title))
+
+ # flag for whether ours is the last object at this lookup
+ is_last = False
+
+ # grab all the members of this lookup set
+ partial_key = self.create_key(partial_title)
+ set_key = self.members_key(partial_key)
+ objects_at_key = self.client.zrange(set_key, 0, -1)
+
+ # check the data at this lookup set to see if ours was the only obj
+ # referenced at this point
+ if obj_id not in objects_at_key:
+ # something weird happened and our data isn't even here
+ continue
+ elif len(objects_at_key) == 1:
+ # only one object stored here, remove the terminal flag
+ zset_key = self.search_key(partial_key)
+ self.client.zrem(zset_key, '^')
+
+ # see if there are any other references to keys here
+ is_last = self.client.zcard(zset_key) == 0
+
+ if is_last:
+ for (key, value, score) in reversed(autocomplete_keys):
+ key = self.search_key(key)
+
+ # another lookup ends here, so bail
+ if '^' in self.client.zrange(key, 0, 1):
+ self.client.zrem(key, value)
+ break
+ else:
+ self.client.delete(key)
+
+ # we can just blow away the lookup key
+ self.client.delete(set_key)
+ else:
+ # remove only our object's data
+ self.client.zrem(set_key, obj_id)
+
+ # finally, remove the data from the data key
+ self.client.delete(self.data_key(obj_id))
+ self.client.delete(self.title_key(obj_id))
+
+ def search(self, phrase, limit=None, filters=None, mappers=None):
+ """
+ Wrap our search & results with prefixing
+ """
+ phrase = self.create_key(phrase)
+
+ # perform the depth-first search over the sorted sets
+ results = self._search(self.search_key(phrase), limit)
+
+ # strip the prefix off the keys that indicated they matched a lookup
+ prefix_len = len(self.prefix) + 3 # 3 becuase ':x:'
+ cleaned_keys = map(lambda x: self.members_key(x[prefix_len:]), results)
+
+ # lookup the data references for each lookup set
+ obj_ids = []
+ for key in cleaned_keys:
+ obj_ids.extend(self.client.zrange(key, 0, -1, withscores=True))
+
+ obj_ids.sort(key=lambda i: i[1])
+
+ seen = set()
+ ct = 0
+ data = []
+
+ # grab the data for each object
+ for lookup, _ in obj_ids:
+ if lookup in seen:
+ continue
+
+ seen.add(lookup)
+
+ raw_data = self.client.get(self.data_key(lookup))
+ if not raw_data:
+ continue
+
+ if mappers:
+ for m in mappers:
+ raw_data = m(raw_data)
+
+ if filters:
+ passes = True
+ for f in filters:
+ if not f(raw_data):
+ passes = False
+ break
+
+ if not passes:
+ continue
+
+ data.append(raw_data)
+ ct += 1
+ if limit and ct == limit:
+ break
+
+ return data
+
+ def search_json(self, phrase, limit=None, filters=None, mappers=None):
+ if not mappers:
+ mappers = []
+ mappers.insert(0, json.loads)
+ return self.search(phrase, limit, filters, mappers)
+
+ def _search(self, text, limit):
+ w = []
+
+ for char in self.client.zrange(text, 0, -1):
+ if char == self.terminator:
+ w.append(text)
+ else:
+ w.extend(self._search(text + char, limit))
+
+ if limit and len(w) >= limit:
+ return w[:limit]
+
+ return w
594 redis_completion/stop_words.py
@@ -0,0 +1,594 @@
+words = """a
+a's
+able
+about
+above
+according
+accordingly
+across
+actually
+after
+afterwards
+again
+against
+ain't
+all
+allow
+allows
+almost
+alone
+along
+already
+also
+although
+always
+am
+among
+amongst
+amoungst
+amount
+an
+and
+another
+any
+anybody
+anyhow
+anyone
+anything
+anyway
+anyways
+anywhere
+apart
+appear
+appreciate
+appropriate
+are
+aren't
+around
+as
+aside
+ask
+asking
+associated
+at
+available
+away
+awfully
+back
+be
+became
+because
+become
+becomes
+becoming
+been
+before
+beforehand
+behind
+being
+believe
+below
+beside
+besides
+best
+better
+between
+beyond
+bill
+both
+bottom
+brief
+but
+by
+c'mon
+c's
+call
+came
+can
+can't
+cannot
+cant
+cause
+causes
+certain
+certainly
+changes
+clearly
+co
+com
+come
+comes
+computer
+con
+concerning
+consequently
+consider
+considering
+contain
+containing
+contains
+corresponding
+could
+couldn't
+couldnt
+course
+cry
+currently
+de
+definitely
+describe
+described
+despite
+detail
+did
+didn't
+different
+do
+does
+doesn't
+doing
+don't
+done
+down
+downwards
+due
+during
+each
+edu
+eg
+eight
+either
+eleven
+else
+elsewhere
+empty
+enough
+entirely
+especially
+et
+etc
+even
+ever
+every
+everybody
+everyone
+everything
+everywhere
+ex
+exactly
+example
+except
+far
+few
+fifteen
+fifth
+fify
+fill
+find
+fire
+first
+five
+followed
+following
+follows
+for
+former
+formerly
+forth
+forty
+found
+four
+from
+front
+full
+further
+furthermore
+get
+gets
+getting
+give
+given
+gives
+go
+goes
+going
+gone
+got
+gotten
+greetings
+had
+hadn't
+happens
+hardly
+has
+hasn't
+hasnt
+have
+haven't
+having
+he
+he's
+hello
+help
+hence
+her
+here
+here's
+hereafter
+hereby
+herein
+hereupon
+hers
+herself
+hi
+him
+himself
+his
+hither
+hopefully
+how
+howbeit
+however
+hundred
+i
+i'd
+i'll
+i'm
+i've
+ie
+if
+ignored
+immediate
+in
+inasmuch
+inc
+indeed
+indicate
+indicated
+indicates
+inner
+insofar
+instead
+interest
+into
+inward
+is
+isn't
+it
+it'd
+it'll
+it's
+its
+itself
+just
+keep
+keeps
+kept
+know
+known
+knows
+last
+lately
+later
+latter
+latterly
+least
+less
+lest
+let
+let's
+like
+liked
+likely
+little
+look
+looking
+looks
+ltd
+made
+mainly
+many
+may
+maybe
+me
+mean
+meanwhile
+merely
+might
+mill
+mine
+more
+moreover
+most
+mostly
+move
+much
+must
+my
+myself
+name
+namely
+nd
+near
+nearly
+necessary
+need
+needs
+neither
+never
+nevertheless
+new
+next
+nine
+no
+nobody
+non
+none
+noone
+nor
+normally
+not
+nothing
+novel
+now
+nowhere
+obviously
+of
+off
+often
+oh
+ok
+okay
+old
+on
+once
+one
+ones
+only
+onto
+or
+other
+others
+otherwise
+ought
+our
+ours
+ourselves
+out
+outside
+over
+overall
+own
+part
+particular
+particularly
+per
+perhaps
+placed
+please
+plus
+possible
+presumably
+probably
+provides
+put
+que
+quite
+qv
+rather
+rd
+re
+really
+reasonably
+regarding
+regardless
+regards
+relatively
+respectively
+right
+said
+same
+saw
+say
+saying
+says
+second
+secondly
+see
+seeing
+seem
+seemed
+seeming
+seems
+seen
+self
+selves
+sensible
+sent
+serious
+seriously
+seven
+several
+shall
+she
+should
+shouldn't
+show
+side
+since
+sincere
+six
+sixty
+so
+some
+somebody
+somehow
+someone
+something
+sometime
+sometimes
+somewhat
+somewhere
+soon
+sorry
+specified
+specify
+specifying
+still
+sub
+such
+sup
+sure
+system
+t's
+take
+taken
+tell
+ten
+tends
+th
+than
+thank
+thanks
+thanx
+that
+that's
+thats
+the
+their
+theirs
+them
+themselves
+then
+thence
+there
+there's
+thereafter
+thereby
+therefore
+therein
+theres
+thereupon
+these
+they
+they'd
+they'll
+they're
+they've
+thick
+thin
+think
+third
+this
+thorough
+thoroughly
+those
+though
+three
+through
+throughout
+thru
+thus
+to
+together
+too
+took
+top
+toward
+towards
+tried
+tries
+truly
+try
+trying
+twelve
+twenty
+twice
+two
+un
+under
+unfortunately
+unless
+unlikely
+until
+unto
+up
+upon
+us
+use
+used
+useful
+uses
+using
+usually
+value
+various
+very
+via
+viz
+vs
+want
+wants
+was
+wasn't
+way
+we
+we'd
+we'll
+we're
+we've
+welcome
+well
+went
+were
+weren't
+what
+what's
+whatever
+when
+whence
+whenever
+where
+where's
+whereafter
+whereas
+whereby
+wherein
+whereupon
+wherever
+whether
+which
+while
+whither
+who
+who's
+whoever
+whole
+whom
+whose
+why
+will
+willing
+wish
+with
+within
+without
+won't
+wonder
+would
+wouldn't
+yes
+yet
+you
+you'd
+you'll
+you're
+you've
+your
+yours
+yourself
+yourselves
+zero"""
+STOP_WORDS = set([
+ w.strip() for w in words.splitlines() if w
+])
202 redis_completion/tests.py
@@ -0,0 +1,202 @@
+import random
+from unittest import TestCase
+
+from redis_completion.engine import RedisEngine
+from redis_completion.text import clean_phrase, create_key, partial_complete
+
+
+stop_words = set(['a', 'an', 'the', 'of'])
+
+class RedisCompletionTestCase(TestCase):
+ def setUp(self):
+ self.engine = RedisEngine(prefix='testac', db=15)
+ self.engine.flush()
+
+ def store_data(self, id=None):
+ test_data = (
+ (1, 'testing python'),
+ (2, 'testing python code'),
+ (3, 'web testing python code'),
+ (4, 'unit tests with python'),
+ )
+ for obj_id, title in test_data:
+ if id is None or id == obj_id:
+ self.engine.store_json(obj_id, title, {
+ 'obj_id': obj_id,
+ 'title': title,
+ 'secret': obj_id % 2 == 0 and 'derp' or 'herp',
+ })
+
+ def sort_results(self, r):
+ return sorted(r, key=lambda i:i['obj_id'])
+
+ def test_search(self):
+ self.store_data()
+
+ results = self.engine.search_json('testing python')
+ self.assertEqual(self.sort_results(results), [
+ {'obj_id': 1, 'title': 'testing python', 'secret': 'herp'},
+ {'obj_id': 2, 'title': 'testing python code', 'secret': 'derp'},
+ {'obj_id': 3, 'title': 'web testing python code', 'secret': 'herp'},
+ ])
+
+ results = self.engine.search_json('test')
+ self.assertEqual(self.sort_results(results), [
+ {'obj_id': 1, 'title': 'testing python', 'secret': 'herp'},
+ {'obj_id': 2, 'title': 'testing python code', 'secret': 'derp'},
+ {'obj_id': 3, 'title': 'web testing python code', 'secret': 'herp'},
+ {'obj_id': 4, 'title': 'unit tests with python', 'secret': 'derp'},
+ ])
+
+ results = self.engine.search_json('unit')
+ self.assertEqual(results, [
+ {'obj_id': 4, 'title': 'unit tests with python', 'secret': 'derp'},
+ ])
+
+ results = self.engine.search_json('')
+ self.assertEqual(results, [])
+
+ results = self.engine.search_json('missing')
+ self.assertEqual(results, [])
+
+ def test_limit(self):
+ self.store_data()
+
+ results = self.engine.search_json('testing', limit=1)
+ self.assertEqual(results, [
+ {'obj_id': 1, 'title': 'testing python', 'secret': 'herp'},
+ ])
+
+ def test_filters(self):
+ self.store_data()
+
+ f = lambda i: i['secret'] == 'herp'
+ results = self.engine.search_json('testing python', filters=[f])
+
+ self.assertEqual(self.sort_results(results), [
+ {'obj_id': 1, 'title': 'testing python', 'secret': 'herp'},
+ {'obj_id': 3, 'title': 'web testing python code', 'secret': 'herp'},
+ ])
+
+ def test_simple(self):
+ self.engine.print_scores = True
+ self.engine.store('testing python')
+ self.engine.store('testing python code')
+ self.engine.store('web testing python code')
+ self.engine.store('unit tests with python')
+
+ results = self.engine.search('testing')
+ self.assertEqual(results, ['testing python', 'testing python code', 'web testing python code'])
+
+ results = self.engine.search('code')
+ self.assertEqual(results, ['testing python code', 'web testing python code'])
+
+ def test_correct_sorting(self):
+ strings = ['aaaa%s' % chr(i + ord('a')) for i in range(26)]
+ random.shuffle(strings)
+
+ for s in strings:
+ self.engine.store(s)
+
+ results = self.engine.search('aaa')
+ self.assertEqual(results, sorted(strings))
+
+ def test_removing_objects(self):
+ self.store_data()
+
+ self.engine.remove(1)
+
+ results = self.engine.search_json('testing')
+ self.assertEqual(self.sort_results(results), [
+ {'obj_id': 2, 'title': 'testing python code', 'secret': 'derp'},
+ {'obj_id': 3, 'title': 'web testing python code', 'secret': 'herp'},
+ ])
+
+ self.store_data(1)
+ self.engine.remove(2)
+
+ results = self.engine.search_json('testing')
+ self.assertEqual(self.sort_results(results), [
+ {'obj_id': 1, 'title': 'testing python', 'secret': 'herp'},
+ {'obj_id': 3, 'title': 'web testing python code', 'secret': 'herp'},
+ ])
+
+ def test_removing_objects_in_depth(self):
+ # want to ensure that redis is cleaned up and does not become polluted
+ # with spurious keys when objects are removed
+ redis_client = self.engine.client
+ prefix = self.engine.prefix
+
+ initial_key_count = len(redis_client.keys())
+
+ # store the blog "testing python"
+ self.store_data(1)
+
+ # see how many keys we have in the db - check again in a bit
+ key_len = len(redis_client.keys())
+
+ # make sure that the final item in our sorted set indicates such
+ values = redis_client.zrange(self.engine.search_key('testingpython'), 0, -1)
+ self.assertEqual(values, [self.engine.terminator])
+
+ self.store_data(2)
+ key_len2 = len(redis_client.keys())
+
+ self.assertTrue(key_len != key_len2)
+
+ # check to see that the final item in the sorted set from earlier now
+ # includes a reference to 'c'
+ values = redis_client.zrange(self.engine.search_key('testingpython'), 0, -1)
+ self.assertEqual(values, [self.engine.terminator, 'c'])
+
+ self.engine.remove(2)
+
+ # see that the reference to 'c' is removed so that we aren't following
+ # a path that no longer exists
+ values = redis_client.zrange(self.engine.search_key('testingpython'), 0, -1)
+ self.assertEqual(values, [self.engine.terminator])
+
+ # back to the original amount of keys
+ self.assertEqual(len(redis_client.keys()), key_len)
+
+ self.engine.remove(1)
+ self.assertEqual(len(redis_client.keys()), initial_key_count)
+
+ def test_clean_phrase(self):
+ stop_words = set(['a', 'an', 'the', 'of'])
+ self.assertEqual(clean_phrase('abc def ghi'), ['abc', 'def', 'ghi'])
+
+ self.assertEqual(clean_phrase('a A tHe an a', stop_words), [])
+ self.assertEqual(clean_phrase('', stop_words), [])
+
+ self.assertEqual(
+ clean_phrase('The Best of times, the blurst of times', stop_words),
+ ['best', 'times,', 'blurst', 'times'])
+
+ def test_partial_complete(self):
+ self.assertEqual(list(partial_complete('1')), ['1'])
+ self.assertEqual(list(partial_complete('1 2')), ['1 2', '2'])
+ self.assertEqual(list(partial_complete('1 2 3')), ['1 2', '1 2 3', '2 3', '3'])
+ self.assertEqual(list(partial_complete('1 2 3 4')), ['1 2', '1 2 3', '2 3', '2 3 4', '3 4', '4'])
+
+ self.assertEqual(
+ list(partial_complete('The Best of times, the blurst of times', stop_words=stop_words)),
+ ['best times,', 'best times, blurst', 'times, blurst', 'times, blurst times', 'blurst times', 'times']
+ )
+
+ self.assertEqual(list(partial_complete('a the An', stop_words=stop_words)), [])
+ self.assertEqual(list(partial_complete('a', stop_words=stop_words)), [])
+
+ def test_create_key(self):
+ self.assertEqual(
+ create_key('the best of times, the blurst of Times', stop_words=stop_words),
+ 'besttimesblurst'
+ )
+
+ self.assertEqual(create_key('<?php $bling; $bling; ?>'),
+ 'phpblingbling')
+
+ self.assertEqual(create_key(''), '')
+
+ self.assertEqual(create_key('the a an', stop_words=stop_words), '')
+ self.assertEqual(create_key('a', stop_words=stop_words), '')
26 redis_completion/text.py
@@ -0,0 +1,26 @@
+import re
+
+
+def clean_phrase(phrase, stop_words=None):
+ phrase = phrase.lower()
+ stop_words = stop_words or set()
+ return [w for w in phrase.split() if w not in stop_words]
+
+def partial_complete(phrase, min_words=2, max_words=3, stop_words=None):
+ words = clean_phrase(phrase, stop_words)
+
+ max_words = max(
+ min(len(words), max_words), min_words
+ )
+
+ wc = len(words)
+ for i in range(wc):
+ if max_words + i > wc:
+ yield ' '.join(words[i:i+min_words])
+ else:
+ for ct in range(min_words, max_words + 1):
+ yield ' '.join(words[i:i+ct])
+
+def create_key(phrase, max_words=3, stop_words=None):
+ key = ' '.join(clean_phrase(phrase, stop_words)[:max_words])
+ return re.sub('[^a-z0-9_-]', '', key)
17 runtests.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+import sys
+import unittest
+
+from redis_completion import tests
+
+def runtests(*test_args):
+ suite = unittest.TestLoader().loadTestsFromModule(tests)
+ result = unittest.TextTestRunner(verbosity=2).run(suite)
+ if result.failures:
+ sys.exit(1)
+ elif result.errors:
+ sys.exit(2)
+ sys.exit(0)
+
+if __name__ == '__main__':
+ runtests(*sys.argv[1:])
26 setup.py
@@ -0,0 +1,26 @@
+import os
+from setuptools import setup, find_packages
+
+
+setup(
+ name='redis-completion',
+ version="0.1.0",
+ description='autocomplete with redis',
+ author='Charles Leifer',
+ author_email='coleifer@gmail.com',
+ url='http://github.com/coleifer/redis-completion/tree/master',
+ packages=find_packages(),
+ package_data = {
+ 'redis_completion': [
+ ],
+ },
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ ],
+ test_suite='runtests.runtests',
+)
Please sign in to comment.
Something went wrong with that request. Please try again.