├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── benchmarks ├── common.py └── create.py ├── changelog.txt ├── conf.py ├── containers.rst ├── index.rst ├── models.rst ├── redisco ├── __init__.py ├── containers.py ├── containerstests.py ├── models │ ├── __init__.py │ ├── attributes.py │ ├── base.py │ ├── basetests.py │ ├── exceptions.py │ ├── key.py │ ├── managers.py │ └── modelset.py └── tests │ ├── README │ └── __init__.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | _build 3 | dist 4 | redisco.egg-info 5 | *.pyc 6 | .DS_Store 7 | dump.rdb 8 | *.swp 9 | /.pyflymakerc 10 | /.pyflymakercc 11 | /.emacs.desktop 12 | /redisco/.ropeproject/config.py 13 | .ropeproject 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | services: 6 | - redis-server 7 | # command to install redisco with dependencies 8 | install: 9 | - pip install . --use-mirrors 10 | # command to run tests 11 | script: nosetests --with-doctest 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Tim Medina 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst requirements.txt 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Redisco.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Redisco.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Redisco" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Redisco" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Redisco 3 | ======= 4 | Python Containers and Simple Models for Redis 5 | 6 | Description 7 | ----------- 8 | Redisco allows you to store objects in Redis_. It is inspired by the Ruby library 9 | Ohm_ and its design and code are loosely based on Ohm and the Django ORM. 10 | It is built on top of redis-py_. It includes container classes that allow 11 | easier access to Redis sets, lists, and sorted sets. 12 | 13 | 14 | Installation 15 | ------------ 16 | Redisco requires redis-py 2.0.0 so get it first. 17 | 18 | pip install redis 19 | 20 | Then install redisco. 21 | 22 | pip install redisco 23 | 24 | 25 | 26 | Documentation 27 | ------------- 28 | The documentation is available at : https://redisco.readthedocs.org 29 | 30 | 31 | Which version should I consider ? 32 | --------------------------------- 33 | 34 | - v0.1 35 | 36 | .. image:: https://secure.travis-ci.org/kiddouk/redisco.png?branch=0.1 37 | If you want something that is compatible with the original project developed by 38 | you should consider v0.1. It works. 39 | 40 | - v0.2 41 | 42 | .. image:: https://secure.travis-ci.org/kiddouk/redisco.png?branch=0.2 43 | If you are adventurous and want to try a version that is going closer to Ohm 44 | project you should consider v0.2. Warning, your indexing keys will be broken 45 | (if you are planning to migrate). 46 | 47 | - master 48 | 49 | .. image:: https://secure.travis-ci.org/kiddouk/redisco.png?branch=master 50 | Well, expect things to be broken. Really broken. 51 | 52 | 53 | Models 54 | ------ 55 | 56 | :: 57 | 58 | from redisco import models 59 | class Person(models.Model): 60 | name = models.Attribute(required=True) 61 | created_at = models.DateTimeField(auto_now_add=True) 62 | fave_colors = models.ListField(str) 63 | 64 | >>> person = Person(name="Conchita") 65 | >>> person.is_valid() 66 | True 67 | >>> person.save() 68 | True 69 | >>> conchita = Person.objects.filter(name='Conchita')[0] 70 | >>> conchita.name 71 | 'Conchita' 72 | >>> conchita.created_at 73 | datetime.datetime(2010, 5, 24, 16, 0, 31, 954704) 74 | 75 | 76 | Model Attributes 77 | ---------------- 78 | 79 | Attribute 80 | Stores unicode strings. If used for large bodies of text, 81 | turn indexing of this field off by setting indexed=False. 82 | 83 | IntegerField 84 | Stores an int. Ints are stringified using unicode() before saving to 85 | Redis. 86 | 87 | Counter 88 | An IntegerField that can only be accessed via Model.incr and Model.decr. 89 | 90 | DateTimeField 91 | Can store a DateTime object. Saved in the Redis store as a float. 92 | 93 | DateField 94 | Can store a Date object. Saved in Redis as a float. 95 | 96 | TimeDeltaField 97 | Can store a TimeDelta object. Saved in Redis as a float. 98 | 99 | FloatField 100 | Can store floats. 101 | 102 | BooleanField 103 | Can store bools. Saved in Redis as 1's and 0's. 104 | 105 | ReferenceField 106 | Can reference other redisco model. 107 | 108 | ListField 109 | Can store a list of unicode, int, float, as well as other redisco models. 110 | 111 | 112 | Attribute Options 113 | ----------------- 114 | 115 | required 116 | If True, the attirbute cannot be None or empty. Strings are stripped to 117 | check if they are empty. Default is False. 118 | 119 | default 120 | Sets the default value of the attribute. Default is None. 121 | 122 | indexed 123 | If True, redisco will create index entries for the attribute. Indexes 124 | are used in filtering and ordering results of queries. For large bodies 125 | of strings, this should be set to False. Default is True. 126 | 127 | validator 128 | Set this to a callable that accepts two arguments -- the field name and 129 | the value of the attribute. The callable should return a list of tuples 130 | with the first item is the field name, and the second item is the error. 131 | 132 | unique 133 | The field must be unique. Default is False. 134 | 135 | DateField and DateTimeField Options 136 | 137 | auto_now_add 138 | Automatically set the datetime/date field to now/today when the object 139 | is first created. Default is False. 140 | 141 | auto_now 142 | Automatically set the datetime/date field to now/today everytime the object 143 | is saved. Default is False. 144 | 145 | 146 | Class options 147 | ------------- 148 | 149 | You can specify some options in your Model to control the behaviour of the 150 | back scene. 151 | 152 | :: 153 | 154 | class User(models.Model): 155 | firstname = models.Attribute() 156 | lastname = models.Attribute() 157 | 158 | @property 159 | def fullname(self): 160 | return "%s %s" % (self.firstname, self.lastname) 161 | 162 | class Meta: 163 | indices = ['fullname'] 164 | db = redis.Redis(host="localhost", db="6666") 165 | key = 'Account' 166 | 167 | 168 | ``indices`` is used to add extra indices that will be saved in the model. 169 | ``db`` object will be used instead of the global redisco ``redis_client`` 170 | ``key`` will be used as the main key in the redis Hash (and sub objects) 171 | instead of the class name. 172 | 173 | 174 | 175 | Custom Managers 176 | --------------- 177 | 178 | Managers are attached to Model attributes by looking for a ``__attr_name__`` 179 | class attribute. If not present, then it defaults to the lowercase attribute 180 | name in the Model. 181 | 182 | :: 183 | 184 | class User(models.Model): 185 | firstname = models.Attribute() 186 | lastname = models.Attribute() 187 | active = models.BooleanField(default=True) 188 | 189 | class History(models.managers.Manager): 190 | pass 191 | 192 | class ObjectsManager(models.managers.Manager): 193 | __attr_name__ = "objects" 194 | def get_model_set(self): 195 | return super(User.ObjectsManager, self).\ 196 | get_model_set().filter(active=True) 197 | 198 | 199 | Saving and Validating 200 | --------------------- 201 | 202 | To save an object, call its save method. This returns True on success (i.e. when 203 | the object is valid) and False otherwise. 204 | 205 | Calling Model.is_valid will validate the attributes and lists. Model.is_valid 206 | is called when the instance is being saved. When there are invalid fields, 207 | Model.errors will hold the list of tuples containing the invalid fields and 208 | the reason for its invalidity. E.g. 209 | [('name', 'required'),('name', 'too short')] 210 | 211 | Fields can be validated using the validator argument of the attribute. Just 212 | pass a callable that accepts two arguments -- the field name and the value 213 | of the attribute. The callable should return a list of errors. 214 | 215 | Model.validate will also be called before saving the instance. Override it 216 | to validate instances not related to attributes. 217 | 218 | :: 219 | 220 | def not_me(field_name, value): 221 | if value == 'Me': 222 | return ((field_name, 'it is me'),) 223 | 224 | class Person(models.Model): 225 | name = models.Attribute(required=True, validator=not_me) 226 | age = models.IntegerField() 227 | 228 | def validate(self): 229 | if self.age and self.age < 21: 230 | self._errors.append(('age', 'below 21')) 231 | 232 | >>> person = Person(name='Me') 233 | >>> person.is_valid() 234 | False 235 | >>> person.errors 236 | [('name', 'it is me')] 237 | 238 | 239 | Queries 240 | ------- 241 | 242 | Queries are executed using a manager, accessed via the objects class 243 | attribute. 244 | 245 | :: 246 | 247 | Person.objects.all() 248 | Person.objects.filter(name='Conchita') 249 | Person.objects.filter(name='Conchita').first() 250 | Person.objects.all().order('name') 251 | Person.objects.filter(fave_colors='Red') 252 | 253 | Ranged Queries 254 | -------------- 255 | 256 | Redisco has a limited support for queries involving ranges -- it can only 257 | filter fields that are numeric, i.e. DateField, DateTimeField, IntegerField, 258 | and FloatField. The zfilter method of the manager is used for these queries. 259 | 260 | :: 261 | 262 | Person.objects.zfilter(created_at__lt=datetime(2010, 4, 20, 5, 2, 0)) 263 | Person.objects.zfilter(created_at__gte=datetime(2010, 4, 20, 5, 2, 0)) 264 | Person.objects.zfilter(created_at__in=(datetime(2010, 4, 20, 5, 2, 0), datetime(2010, 5, 1))) 265 | 266 | 267 | Containers 268 | ---------- 269 | Redisco has three containers that roughly match Redis's supported data 270 | structures: lists, sets, sorted set. Anything done to the container is 271 | persisted to Redis. 272 | 273 | Sets 274 | >>> from redisco.containers import Set 275 | >>> s = Set('myset') 276 | >>> s.add('apple') 277 | >>> s.add('orange') 278 | >>> s.add('bananas', 'tomatoes') 279 | >>> s.add(['blackberries', 'strawberries']) 280 | >>> s.members 281 | set(['apple', 'blackberries', 'strawberries', 'orange', 'tomatoes', 'bananas']) 282 | >>> s.remove('apple', 'orange') 283 | True 284 | set(['strawberries', 'bananas', 'tomatoes', 'blackberries']) 285 | >>> s.remove(['bananas', 'blackberries']) 286 | True 287 | >> s.members 288 | set(['strawberries', 'bananas', 'tomatoes']) 289 | >>> t = Set('nset') 290 | >>> t.add('kiwi') 291 | >>> t.add('guava') 292 | >>> t.members 293 | set(['kiwi', 'guava']) 294 | >>> s.update(t) 295 | >>> s.members 296 | set(['kiwi', 'orange', 'guava', 'apple']) 297 | 298 | Lists 299 | >>> from redisco.containers import List 300 | >>> l = List('alpha') 301 | >>> l.append('a') 302 | >>> l.append(['b', 'c']) 303 | >>> l.append('d', 'e', 'f') 304 | >>> 'a' in l 305 | True 306 | >>> 'd' in l 307 | False 308 | >>> len(l) 309 | 6 310 | >>> l.index('b') 311 | 1 312 | >>> l.members 313 | ['a', 'b', 'c', 'd', 'e', 'f'] 314 | 315 | 316 | Sorted Sets 317 | >>> zset = SortedSet('zset') 318 | >>> zset.members 319 | ['d', 'a', 'b', 'c'] 320 | >>> 'e' in zset 321 | False 322 | >>> 'a' in zset 323 | True 324 | >>> zset.rank('d') 325 | 0 326 | >>> zset.rank('b') 327 | 2 328 | >>> zset[1] 329 | 'a' 330 | >>> zset.add({'f' : 200, 'e' : 201}) 331 | >>> zset.members 332 | ['d', 'a', 'b', 'c', 'f', 'e'] 333 | >>> zset.add('d', 99) 334 | >>> zset.members 335 | ['a', 'b', 'c', 'd', 'f', 'e'] 336 | 337 | 338 | Dicts/Hashes 339 | >>> h = cont.Hash('hkey') 340 | >>> len(h) 341 | 0 342 | >>> h['name'] = "Richard Cypher" 343 | >>> h['real_name'] = "Richard Rahl" 344 | >>> h 345 | 346 | >>> h.dict 347 | {'name': 'Richard Cypher', 'real_name': 'Richard Rahl'} 348 | 349 | 350 | Additional Info on Containers 351 | ----------------------------- 352 | 353 | Some methods of the Redis client that require the key as the first argument 354 | can be accessed from the container itself. 355 | 356 | >>> l = List('mylist') 357 | >>> l.lrange(0, -1) 358 | 0 359 | >>> l.rpush('b') 360 | >>> l.rpush('c') 361 | >>> l.lpush('a') 362 | >>> l.lrange(0, -1) 363 | ['a', 'b', 'c'] 364 | >>> h = Hash('hkey') 365 | >>> h.hset('name', 'Richard Rahl') 366 | >>> h 367 | 368 | 369 | 370 | Connecting to Redis 371 | ------------------- 372 | 373 | All models and containers use a global Redis client object to 374 | interact with the key-value storage. By default, it connects 375 | to localhost:6379, selecting db 0. If you wish to specify settings: 376 | 377 | :: 378 | 379 | import redisco 380 | redisco.connection_setup(host='localhost', port=6380, db=10) 381 | 382 | The arguments to connect are simply passed to the redis.Redis init method. 383 | 384 | For the containers, you can specify a second argument as the Redis client. 385 | That client object will be used instead of the default. 386 | 387 | >>> import redis 388 | >>> r = redis.Redis(host='localhost', port=6381) 389 | >>> Set('someset', r) 390 | 391 | 392 | Unit tests 393 | ---------- 394 | 395 | Redisco uses nose for testing. 396 | 397 | Install nosetests: 398 | 399 | $ pip install nose 400 | 401 | And test: 402 | 403 | $ nosetests 404 | 405 | 406 | Credits 407 | ------- 408 | 409 | Most of the concepts are taken from `Soveran`_'s Redis related Ruby libraries. 410 | cyx_ for sharing his expertise in indexing in Redis. 411 | Django, of course, for the popular model API. 412 | 413 | .. _Redis: http://code.google.com/p/redis/ 414 | .. _Ohm: http://github.com/soveran/ohm/ 415 | .. _redis-py: http://github.com/andymccurdy/redis-py/ 416 | .. _`Soveran`: http://github.com/soveran 417 | .. _cyx: http://github.com/cyx 418 | -------------------------------------------------------------------------------- /benchmarks/common.py: -------------------------------------------------------------------------------- 1 | from redisco import models 2 | 3 | 4 | class Event(models.Model): 5 | name = models.Attribute() 6 | location = models.Attribute() 7 | 8 | -------------------------------------------------------------------------------- /benchmarks/create.py: -------------------------------------------------------------------------------- 1 | from redisco import get_client 2 | import timeit 3 | from common import Event 4 | 5 | 6 | def create_events(): 7 | Event(name="Redis Meetup 1", location="London").save() 8 | 9 | 10 | def load_events(): 11 | Event.objects.get_by_id(1).name 12 | 13 | 14 | def find_events(): 15 | Event.objects.filter(name="Redis Meetup", location="London").first() 16 | 17 | 18 | def display_results(results, name): 19 | print "%s: 5000 Loops, best of 3: %.02f sec" % (name, min(results)) 20 | 21 | def profile(): 22 | import cProfile 23 | import pstats 24 | stmt = """ 25 | for x in xrange(0, 5000): 26 | find_events() 27 | """ 28 | cProfile.run(stmt, "b33f.prof") 29 | p = pstats.Stats("b33f.prof") 30 | p.strip_dirs().sort_stats('cumulative').print_stats(20) 31 | 32 | 33 | 34 | db = get_client() 35 | db.flushdb() 36 | Event(name="Redis Meetup 1", location="London").save() 37 | 38 | t = timeit.Timer('create_events()', 'from __main__ import create_events') 39 | display_results(t.repeat(repeat=1, number=5000), 'create_events') 40 | 41 | t = timeit.Timer('find_events()', 'from __main__ import find_events') 42 | display_results(t.repeat(repeat=1, number=5000), 'find_events') 43 | 44 | t = timeit.Timer('load_events()', 'from __main__ import load_events') 45 | display_results(t.repeat(repeat=1, number=5000), 'load_events') 46 | 47 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 2 | version 0.2.0 3 | ------------- 4 | Warning : This version may break your previous installation. 5 | I have updated the key storage process. It means that your indexes and uniques 6 | and alike will not be compatible. But Fear no more, if you fetch your objects 7 | (get_by_id) and save them again, everything will be back to normal. 8 | 9 | You may want to delete your old keys 10 | 11 | 12 | Lot of changes : 13 | * DateTime objects are now TimeZone aware. They will store time in UTC. If you 14 | don't specify any TZ, your local timezone will be assumed. 15 | * List objects (for now) behaves like Python List. 16 | * Optimization of Key Deletion when filtering. 17 | * Dropped Base64 encoding as Redis is binary safe nowadays. Keys can now be 18 | encoded in utf-8 19 | * Unittests update 20 | * speed : Fixed a bug where the filtered objects where sorted twice 21 | * speed : Objects are now fetched entirely at first (except for Counters) 22 | * speed : Refactor native redis object calls. The DELEGATEABLE_METHOD turned 23 | out to be pretty inefficient 24 | * speed : Optimization for caching (and returning) objects 25 | 26 | version 0.1.3 27 | ------------- 28 | * Initial version from the previous maintainers 29 | from https://github.com/iamteem/redisco) 30 | 31 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Redisco documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Sep 7 15:58:04 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # Make sure that our current path is included 17 | sys.path.insert(0, ".") 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = ['sphinx.ext.autodoc'] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'Redisco' 47 | copyright = u'2012, Sebastien Requiem' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.2.0' 55 | # The full version, including alpha/beta/rc tags. 56 | release = 'rc3' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'Rediscodoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'Redisco.tex', u'Redisco Documentation', 190 | u'Sebastien Requiem', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'redisco', u'Redisco Documentation', 220 | [u'Sebastien Requiem'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'Redisco', u'Redisco Documentation', 234 | u'Sebastien Requiem', 'Redisco', 'One line description of project.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | -------------------------------------------------------------------------------- /containers.rst: -------------------------------------------------------------------------------- 1 | .. Redisco documentation contianers file, created by 2 | sphinx-quickstart on Fri Sep 7 15:58:04 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Containers 7 | =================================== 8 | 9 | A suite of containers is available to the developer (you!) in order to manipulate some of redis' objects. You can easily create, modify, update, and delete Sets, SortedSets, Lists and Hashes. Pay attention that mnay of the operations are serialized to the redis server and are therefore time consuming. 10 | 11 | 12 | Base Class 13 | ----------------------------------- 14 | .. autoclass:: redisco.containers.Container 15 | :members: 16 | 17 | Set 18 | ----------------------------------- 19 | .. autoclass:: redisco.containers.Set 20 | :members: 21 | 22 | SortedSet 23 | ----------------------------------- 24 | .. autoclass:: redisco.containers.SortedSet 25 | :members: 26 | 27 | List 28 | ----------------------------------- 29 | .. autoclass:: redisco.containers.List 30 | :members: 31 | 32 | Hash 33 | ----------------------------------- 34 | .. autoclass:: redisco.containers.Hash 35 | :members: 36 | 37 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. Redisco documentation master file, created by 2 | sphinx-quickstart on Fri Sep 7 15:58:04 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Redisco's documentation! 7 | =================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | models 15 | containers 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /models.rst: -------------------------------------------------------------------------------- 1 | .. Redisco documentation contianers file, created by 2 | sphinx-quickstart on Fri Sep 7 15:58:04 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Object Relation Manager 7 | =================================== 8 | 9 | Redisco allows you to store objects in Redis_. Redisco can easily manage object creation, update and deletion. It is strongly Ohm_ and Django_ ORM and try to provide a simple approach. 10 | 11 | .. _Redis: http://redis.io 12 | .. _Ohm: http://ohm.keyvalue.org 13 | .. _Django: http://djangoproject.org 14 | 15 | :: 16 | 17 | >>> from redisco import models 18 | >>> class Person(models.Model): 19 | ... name = models.Attribute(required=True) 20 | ... created_at = models.DateTimeField(auto_now_add=True) 21 | ... fave_colors = models.ListField(str) 22 | 23 | >>> person = Person(name="Conchita") 24 | >>> person.is_valid() 25 | True 26 | >>> person.save() 27 | True 28 | >>> conchita = Person.objects.filter(name='Conchita')[0] 29 | >>> conchita.name 30 | 'Conchita' 31 | >>> conchita.created_at 32 | datetime.datetime(2010, 5, 24, 16, 0, 31, 954704) 33 | 34 | 35 | 36 | Model 37 | ----------------------------------- 38 | The ``Model`` class is the class that will contain your object. It upports many different type of attributes and support custom object validation to ensure data integrity when saving. 39 | 40 | .. autoclass:: redisco.models.Model 41 | :members: 42 | 43 | 44 | Attributes 45 | ---------------------------------- 46 | The attributes are the core of any redisco ``Model``. Many different attributes are available according to your needs. Read the following documentation to understand the caveats of each attribute. 47 | 48 | 49 | Attribute 50 | Stores unicode strings. If used for large bodies of text, 51 | turn indexing of this field off by setting indexed=True. 52 | 53 | IntegerField 54 | Stores an int. Ints are stringified using unicode() before saving to 55 | Redis. 56 | 57 | Counter 58 | An IntegerField that can only be accessed via Model.incr and Model.decr. 59 | 60 | DateTimeField 61 | Can store a DateTime object. Saved in the Redis store as a float. 62 | 63 | DateField 64 | Can store a Date object. Saved in Redis as a float. 65 | 66 | TimeDeltaField 67 | Can store a TimeDelta object. Saved in Redis as a float. 68 | 69 | FloatField 70 | Can store floats. 71 | 72 | BooleanField 73 | Can store bools. Saved in Redis as 1's and 0's. 74 | 75 | ReferenceField 76 | Can reference other redisco model. 77 | 78 | ListField 79 | Can store a list of unicode, int, float, as well as other redisco models. 80 | 81 | 82 | Attribute Options 83 | ----------------- 84 | 85 | required 86 | If True, the attirbute cannot be None or empty. Strings are stripped to 87 | check if they are empty. Default is False. 88 | 89 | default 90 | Sets the default value of the attribute. Default is None. 91 | 92 | indexed 93 | If True, redisco will create index entries for the attribute. Indexes 94 | are used in filtering and ordering results of queries. For large bodies 95 | of strings, this should be set to False. Default is True. 96 | 97 | validator 98 | Set this to a callable that accepts two arguments -- the field name and 99 | the value of the attribute. The callable should return a list of tuples 100 | with the first item is the field name, and the second item is the error. 101 | 102 | unique 103 | The field must be unique. Default is False. 104 | 105 | DateField and DateTimeField Options 106 | 107 | auto_now_add 108 | Automatically set the datetime/date field to now/today when the object 109 | is first created. Default is False. 110 | 111 | auto_now 112 | Automatically set the datetime/date field to now/today everytime the object 113 | is saved. Default is False. 114 | 115 | Modelset 116 | ----------------------------------- 117 | The ``ModelSet`` class is useful for all kind of queries when you want to filter data (like in SQL). You can filter objects by values in their attributes, creation dates and even perform some unions and exclusions. 118 | 119 | 120 | >>> from redisco import models 121 | >>> class Person(models.Model): 122 | ... name = models.Attribute(required=True) 123 | ... created_at = models.DateTimeField(auto_now_add=True) 124 | ... fave_colors = models.ListField(str) 125 | 126 | >>> person = Person(name="Conchita") 127 | >>> person.save() 128 | True 129 | >>> conchita = Person.objects.filter(name='Conchita').first() 130 | 131 | .. autoclass:: redisco.models.modelset.ModelSet 132 | :members: get_by_id, filter, first, exclude, all, get_or_create, order, limit 133 | 134 | -------------------------------------------------------------------------------- /redisco/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | default_connection_settings = {} 3 | try: 4 | import redislite as redis 5 | except ImportError: 6 | import redis 7 | default_connection_settings = { 8 | 'host': 'localhost', 9 | 'port': 6379, 10 | 'db': 0 11 | } 12 | 13 | 14 | class Client(object): 15 | def __init__(self, **kwargs): 16 | self.connection_settings = kwargs or default_connection_settings 17 | 18 | def redis(self): 19 | return redis.Redis(**self.connection_settings) 20 | 21 | def update(self, d): 22 | self.connection_settings.update(d) 23 | 24 | 25 | def connection_setup(**kwargs): 26 | global connection, client 27 | if client: 28 | client.update(kwargs) 29 | else: 30 | client = Client(**kwargs) 31 | connection = client.redis() 32 | 33 | 34 | def get_client(): 35 | global connection 36 | return connection 37 | 38 | 39 | client = Client() 40 | connection = client.redis() 41 | default_expire_time = 60 42 | 43 | __all__ = ['connection_setup', 'get_client'] 44 | -------------------------------------------------------------------------------- /redisco/containers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # doctest: +ELLIPSIS 3 | 4 | import collections 5 | from functools import partial 6 | from . import default_expire_time 7 | 8 | 9 | def _parse_values(values): 10 | (_values,) = values if len(values) == 1 else (None,) 11 | if _values and type(_values) == type([]): 12 | return _values 13 | return values 14 | 15 | 16 | class Container(object): 17 | """ 18 | Base class for all containers. This class should not 19 | be used and does not provide anything except the ``db`` 20 | member. 21 | :members: 22 | """ 23 | 24 | def __init__(self, key, db=None, pipeline=None): 25 | self._db = db 26 | self.key = key 27 | self.pipeline = pipeline 28 | 29 | def clear(self): 30 | """ 31 | Remove the container from the redis storage 32 | 33 | >>> s = Set('test') 34 | >>> s.add('1') 35 | 1 36 | >>> s.clear() 37 | >>> s.members 38 | set([]) 39 | 40 | 41 | """ 42 | del self.db[self.key] 43 | 44 | def set_expire(self, time=None): 45 | """ 46 | Allow the key to expire after ``time`` seconds. 47 | 48 | >>> s = Set("test") 49 | >>> s.add("1") 50 | 1 51 | >>> s.set_expire(1) 52 | >>> # from time import sleep 53 | >>> # sleep(1) 54 | >>> # s.members 55 | # set([]) 56 | >>> s.clear() 57 | 58 | 59 | :param time: time expressed in seconds. If time is not specified, then ``default_expire_time`` will be used. 60 | :rtype: None 61 | """ 62 | if time is None: 63 | time = default_expire_time 64 | self.db.expire(self.key, time) 65 | 66 | @property 67 | def db(self): 68 | if self.pipeline is not None: 69 | return self.pipeline 70 | if self._db is not None: 71 | return self._db 72 | if hasattr(self, 'db_cache') and self.db_cache: 73 | return self.db_cache 74 | else: 75 | from redisco import connection 76 | self.db_cache = connection 77 | return self.db_cache 78 | 79 | 80 | class Set(Container): 81 | """ 82 | .. default-domain:: set 83 | 84 | This class represent a Set in redis. 85 | """ 86 | 87 | 88 | def __repr__(self): 89 | return "<%s '%s' %s>" % (self.__class__.__name__, self.key, 90 | self.members) 91 | 92 | def sadd(self, *values): 93 | """ 94 | Add the specified members to the Set. 95 | 96 | :param values: a list of values or a simple value. 97 | :rtype: integer representing the number of value added to the set. 98 | 99 | >>> s = Set("test") 100 | >>> s.clear() 101 | >>> s.add(["1", "2", "3"]) 102 | 3 103 | >>> s.add(["4"]) 104 | 1 105 | >>> print s 106 | 107 | >>> s.clear() 108 | 109 | """ 110 | return self.db.sadd(self.key, *_parse_values(values)) 111 | 112 | def srem(self, *values): 113 | """ 114 | Remove the values from the Set if they are present. 115 | 116 | :param values: a list of values or a simple value. 117 | :rtype: boolean indicating if the values have been removed. 118 | 119 | >>> s = Set("test") 120 | >>> s.add(["1", "2", "3"]) 121 | 3 122 | >>> s.srem(["1", "3"]) 123 | 2 124 | >>> s.clear() 125 | 126 | """ 127 | return self.db.srem(self.key, *_parse_values(values)) 128 | 129 | def spop(self): 130 | """ 131 | Remove and return (pop) a random element from the Set. 132 | 133 | :rtype: String representing the value poped. 134 | 135 | >>> s = Set("test") 136 | >>> s.add("1") 137 | 1 138 | >>> s.spop() 139 | '1' 140 | >>> s.members 141 | set([]) 142 | 143 | """ 144 | return self.db.spop(self.key) 145 | 146 | #def __repr__(self): 147 | # return "<%s '%s' %s>" % (self.__class__.__name__, self.key, 148 | # self.members) 149 | 150 | def isdisjoint(self, other): 151 | """ 152 | Return True if the set has no elements in common with other. 153 | 154 | :param other: another ``Set`` 155 | :rtype: boolean 156 | 157 | >>> s1 = Set("key1") 158 | >>> s2 = Set("key2") 159 | >>> s1.add(['a', 'b', 'c']) 160 | 3 161 | >>> s2.add(['c', 'd', 'e']) 162 | 3 163 | >>> s1.isdisjoint(s2) 164 | False 165 | >>> s1.clear() 166 | >>> s2.clear() 167 | """ 168 | return not bool(self.db.sinter([self.key, other.key])) 169 | 170 | def issubset(self, other_set): 171 | """ 172 | Test whether every element in the set is in other. 173 | 174 | :param other_set: another ``Set`` to compare to. 175 | 176 | >>> s1 = Set("key1") 177 | >>> s2 = Set("key2") 178 | >>> s1.add(['a', 'b', 'c']) 179 | 3 180 | >>> s2.add('b') 181 | 1 182 | >>> s2.issubset(s1) 183 | True 184 | >>> s1.clear() 185 | >>> s2.clear() 186 | 187 | """ 188 | return self <= other_set 189 | 190 | def __le__(self, other_set): 191 | return self.db.sinter([self.key, other_set.key]) == self.all() 192 | 193 | def __lt__(self, other_set): 194 | """Test whether the set is a true subset of other.""" 195 | return self <= other_set and self != other_set 196 | 197 | def __eq__(self, other_set): 198 | """ 199 | Test equality of: 200 | 1. keys 201 | 2. members 202 | """ 203 | if other_set.key == self.key: 204 | return True 205 | slen, olen = len(self), len(other_set) 206 | if olen == slen: 207 | return self.members == other_set.members 208 | else: 209 | return False 210 | 211 | def __ne__(self, other_set): 212 | return not self.__eq__(other_set) 213 | 214 | def issuperset(self, other_set): 215 | """ 216 | Test whether every element in other is in the set. 217 | 218 | :param other_set: another ``Set`` to compare to. 219 | 220 | >>> s1 = Set("key1") 221 | >>> s2 = Set("key2") 222 | >>> s1.add(['a', 'b', 'c']) 223 | 3 224 | >>> s2.add('b') 225 | 1 226 | >>> s1.issuperset(s2) 227 | True 228 | >>> s1.clear() 229 | >>> s2.clear() 230 | 231 | """ 232 | return self >= other_set 233 | 234 | def __ge__(self, other_set): 235 | """Test whether every element in other is in the set.""" 236 | return self.db.sinter([self.key, other_set.key]) == other_set.all() 237 | 238 | def __gt__(self, other_set): 239 | """Test whether the set is a true superset of other.""" 240 | return self >= other_set and self != other_set 241 | 242 | # SET Operations 243 | def union(self, key, *other_sets): 244 | """ 245 | Return a new ``Set`` representing the union of *n* sets. 246 | 247 | :param key: String representing the key where to store the result (the union) 248 | :param other_sets: list of other ``Set``. 249 | :rtype: ``Set`` 250 | 251 | >>> s1 = Set('key1') 252 | >>> s2 = Set('key2') 253 | >>> s1.add(['a', 'b', 'c']) 254 | 3 255 | >>> s2.add(['d', 'e']) 256 | 2 257 | >>> s3 = s1.union('key3', s2) 258 | >>> s3.key 259 | u'key3' 260 | >>> s3.members 261 | set(['a', 'c', 'b', 'e', 'd']) 262 | >>> s1.clear() 263 | >>> s2.clear() 264 | >>> s3.clear() 265 | 266 | """ 267 | if not isinstance(key, str) and not isinstance(key, unicode): 268 | raise ValueError("Expect a (unicode) string as key") 269 | key = unicode(key) 270 | 271 | self.db.sunionstore(key, [self.key] + [o.key for o in other_sets]) 272 | return Set(key) 273 | 274 | def intersection(self, key, *other_sets): 275 | """ 276 | Return a new ``Set`` representing the intersection of *n* sets. 277 | 278 | :param key: String representing the key where to store the result (the union) 279 | :param other_sets: list of other ``Set``. 280 | :rtype: Set 281 | 282 | >>> s1 = Set('key1') 283 | >>> s2 = Set('key2') 284 | >>> s1.add(['a', 'b', 'c']) 285 | 3 286 | >>> s2.add(['c', 'e']) 287 | 2 288 | >>> s3 = s1.intersection('key3', s2) 289 | >>> s3.key 290 | u'key3' 291 | >>> s3.members 292 | set(['c']) 293 | >>> s1.clear() 294 | >>> s2.clear() 295 | >>> s3.clear() 296 | """ 297 | 298 | 299 | if not isinstance(key, str) and not isinstance(key, unicode): 300 | raise ValueError("Expect a (unicode) string as key") 301 | key = unicode(key) 302 | 303 | self.db.sinterstore(key, [self.key] + [o.key for o in other_sets]) 304 | return Set(key) 305 | 306 | def difference(self, key, *other_sets): 307 | """ 308 | Return a new ``Set`` representing the difference of *n* sets. 309 | 310 | :param key: String representing the key where to store the result (the union) 311 | :param other_sets: list of other ``Set``. 312 | :rtype: Set 313 | 314 | >>> s1 = Set('key1') 315 | >>> s2 = Set('key2') 316 | >>> s1.add(['a', 'b', 'c']) 317 | 3 318 | >>> s2.add(['c', 'e']) 319 | 2 320 | >>> s3 = s1.difference('key3', s2) 321 | >>> s3.key 322 | u'key3' 323 | >>> s3.members 324 | set(['a', 'b']) 325 | >>> s1.clear() 326 | >>> s2.clear() 327 | >>> s3.clear() 328 | """ 329 | 330 | if not isinstance(key, str) and not isinstance(key, unicode): 331 | raise ValueError("Expect a (unicode) string as key") 332 | key = unicode(key) 333 | 334 | self.db.sdiffstore(key, [self.key] + [o.key for o in other_sets]) 335 | return Set(key) 336 | 337 | def update(self, *other_sets): 338 | """Update the set, adding elements from all other_sets. 339 | 340 | :param other_sets: list of ``Set`` 341 | :rtype: None 342 | """ 343 | self.db.sunionstore(self.key, [self.key] + [o.key for o in other_sets]) 344 | 345 | def __ior__(self, other_set): 346 | self.db.sunionstore(self.key, [self.key, other_set.key]) 347 | return self 348 | 349 | def intersection_update(self, *other_sets): 350 | """ 351 | Update the set, keeping only elements found in it and all other_sets. 352 | 353 | :param other_sets: list of ``Set`` 354 | :rtype: None 355 | """ 356 | self.db.sinterstore(self.key, [o.key for o in [self.key] + other_sets]) 357 | 358 | def __iand__(self, other_set): 359 | self.db.sinterstore(self.key, [self.key, other_set.key]) 360 | return self 361 | 362 | def difference_update(self, *other_sets): 363 | """ 364 | Update the set, removing elements found in others. 365 | 366 | :param other_sets: list of ``Set`` 367 | :rtype: None 368 | """ 369 | self.db.sdiffstore(self.key, [o.key for o in [self.key] + other_sets]) 370 | 371 | def __isub__(self, other_set): 372 | self.db.sdiffstore(self.key, [self.key, other_set.key]) 373 | return self 374 | 375 | def all(self): 376 | return self.db.smembers(self.key) 377 | 378 | members = property(all) 379 | """ 380 | return the real content of the Set. 381 | """ 382 | 383 | def copy(self, key): 384 | """ 385 | Copy the set to another key and return the new Set. 386 | 387 | .. WARNING:: 388 | If the new key already contains a value, it will be overwritten. 389 | """ 390 | copy = Set(key=key, db=self.db) 391 | copy.clear() 392 | copy |= self 393 | return copy 394 | 395 | def __iter__(self): 396 | return self.members.__iter__() 397 | 398 | def sinter(self, *other_sets): 399 | """ 400 | Performs an intersection between Sets and return the *RAW* result. 401 | 402 | .. NOTE:: 403 | This function return an actual ``set`` object (from python) and not a ``Set``. See func:``intersection``. 404 | """ 405 | return self.db.sinter([self.key] + [s.key for s in other_sets]) 406 | 407 | def sunion(self, *other_sets): 408 | """ 409 | Performs a union between two sets and returns the *RAW* result. 410 | 411 | .. NOTE:: 412 | This function return an actual ``set`` object (from python) and not a ``Set``. 413 | """ 414 | return self.db.sunion([self.key] + [s.key for s in other_sets]) 415 | 416 | def sdiff(self, *other_sets): 417 | """ 418 | Performs a difference between two sets and returns the *RAW* result. 419 | 420 | .. NOTE:: 421 | This function return an actual ``set`` object (from python) and not a ``Set``. 422 | See function difference. 423 | 424 | """ 425 | return self.db.sdiff([self.key] + [s.key for s in other_sets]) 426 | 427 | def scard(self): 428 | """ 429 | Returns the cardinality of the Set. 430 | 431 | :rtype: String containing the cardinality. 432 | 433 | """ 434 | return self.db.scard(self.key) 435 | 436 | def sismember(self, value): 437 | """ 438 | Return ``True`` if the provided value is in the ``Set``. 439 | 440 | """ 441 | return self.db.sismember(self.key, value) 442 | 443 | def srandmember(self): 444 | """ 445 | Return a random member of the set. 446 | 447 | >>> s = Set("test") 448 | >>> s.add(['a', 'b', 'c']) 449 | 3 450 | >>> s.srandmember() # doctest: +ELLIPSIS 451 | '...' 452 | >>> # 'a', 'b' or 'c' 453 | """ 454 | return self.db.srandmember(self.key) 455 | 456 | add = sadd 457 | """see sadd""" 458 | pop = spop 459 | """see spop""" 460 | remove = srem 461 | """see srem""" 462 | __contains__ = sismember 463 | __len__ = scard 464 | 465 | 466 | class List(Container): 467 | """ 468 | This class represent a list object as seen in redis. 469 | """ 470 | 471 | def all(self): 472 | """ 473 | Returns all items in the list. 474 | """ 475 | return self.lrange(0, -1) 476 | members = property(all) 477 | """Return all items in the list.""" 478 | 479 | def llen(self): 480 | """ 481 | Returns the length of the list. 482 | """ 483 | return self.db.llen(self.key) 484 | 485 | __len__ = llen 486 | 487 | def __getitem__(self, index): 488 | if isinstance(index, int): 489 | return self.lindex(index) 490 | elif isinstance(index, slice): 491 | indices = index.indices(len(self)) 492 | return self.lrange(indices[0], indices[1] - 1) 493 | else: 494 | raise TypeError 495 | 496 | def __setitem__(self, index, value): 497 | self.lset(index, value) 498 | 499 | def lrange(self, start, stop): 500 | """ 501 | Returns a range of items. 502 | 503 | :param start: integer representing the start index of the range 504 | :param stop: integer representing the size of the list. 505 | 506 | >>> l = List("test") 507 | >>> l.push(['a', 'b', 'c', 'd']) 508 | 4L 509 | >>> l.lrange(1, 2) 510 | ['b', 'c'] 511 | >>> l.clear() 512 | 513 | """ 514 | return self.db.lrange(self.key, start, stop) 515 | 516 | def lpush(self, *values): 517 | """ 518 | Push the value into the list from the *left* side 519 | 520 | :param values: a list of values or single value to push 521 | :rtype: long representing the number of values pushed. 522 | 523 | >>> l = List("test") 524 | >>> l.lpush(['a', 'b']) 525 | 2L 526 | >>> l.clear() 527 | """ 528 | return self.db.lpush(self.key, *_parse_values(values)) 529 | 530 | def rpush(self, *values): 531 | """ 532 | Push the value into the list from the *right* side 533 | 534 | :param values: a list of values or single value to push 535 | :rtype: long representing the size of the list. 536 | 537 | >>> l = List("test") 538 | >>> l.lpush(['a', 'b']) 539 | 2L 540 | >>> l.rpush(['c', 'd']) 541 | 4L 542 | >>> l.members 543 | ['b', 'a', 'c', 'd'] 544 | >>> l.clear() 545 | """ 546 | 547 | return self.db.rpush(self.key, *_parse_values(values)) 548 | 549 | def extend(self, iterable): 550 | """ 551 | Extend list by appending elements from the iterable. 552 | 553 | :param iterable: an iterable objects. 554 | """ 555 | self.rpush(*[e for e in iterable]) 556 | 557 | def count(self, value): 558 | """ 559 | Return number of occurrences of value. 560 | 561 | :param value: a value tha *may* be contained in the list 562 | """ 563 | return self.members.count(value) 564 | 565 | def lpop(self): 566 | """ 567 | Pop the first object from the left. 568 | 569 | :return: the popped value. 570 | 571 | """ 572 | return self.db.lpop(self.key) 573 | 574 | def rpop(self): 575 | """ 576 | Pop the first object from the right. 577 | 578 | :return: the popped value. 579 | """ 580 | return self.db.rpop(self.key) 581 | 582 | def rpoplpush(self, key): 583 | """ 584 | Remove an element from the list, 585 | atomically add it to the head of the list indicated by key 586 | 587 | :param key: the key of the list receiving the popped value. 588 | :return: the popped (and pushed) value 589 | 590 | >>> l = List('list1') 591 | >>> l.push(['a', 'b', 'c']) 592 | 3L 593 | >>> l.rpoplpush('list2') 594 | 'c' 595 | >>> l2 = List('list2') 596 | >>> l2.members 597 | ['c'] 598 | >>> l.clear() 599 | >>> l2.clear() 600 | 601 | """ 602 | return self.db.rpoplpush(self.key, key) 603 | 604 | def lrem(self, value, num=1): 605 | """ 606 | Remove first occurrence of value. 607 | 608 | :return: 1 if the value has been removed, 0 otherwise 609 | """ 610 | return self.db.lrem(self.key, value, num) 611 | 612 | def reverse(self): 613 | """ 614 | Reverse the list in place. 615 | 616 | :return: None 617 | """ 618 | r = self[:] 619 | r.reverse() 620 | self.clear() 621 | self.extend(r) 622 | 623 | def copy(self, key): 624 | """Copy the list to a new list. 625 | 626 | ..WARNING: 627 | If destination key already contains a value, it clears it before copying. 628 | """ 629 | copy = List(key, self.db) 630 | copy.clear() 631 | copy.extend(self) 632 | return copy 633 | 634 | def ltrim(self, start, end): 635 | """ 636 | Trim the list from start to end. 637 | 638 | :return: None 639 | """ 640 | return self.db.ltrim(self.key, start, end) 641 | 642 | def lindex(self, idx): 643 | """ 644 | Return the value at the index *idx* 645 | 646 | :param idx: the index to fetch the value. 647 | :return: the value or None if out of range. 648 | """ 649 | return self.db.lindex(self.key, idx) 650 | 651 | def lset(self, idx, value=0): 652 | """ 653 | Set the value in the list at index *idx* 654 | 655 | :return: True is the operation succeed. 656 | 657 | >>> l = List('test') 658 | >>> l.push(['a', 'b', 'c']) 659 | 3L 660 | >>> l.lset(0, 'e') 661 | True 662 | >>> l.members 663 | ['e', 'b', 'c'] 664 | >>> l.clear() 665 | 666 | """ 667 | return self.db.lset(self.key, idx, value) 668 | 669 | def __iter__(self): 670 | return self.members.__iter__() 671 | 672 | def __repr__(self): 673 | return "<%s '%s' %s>" % (self.__class__.__name__, self.key, 674 | self.members) 675 | 676 | __len__ = llen 677 | remove = lrem 678 | trim = ltrim 679 | shift = lpop 680 | unshift = lpush 681 | pop = rpop 682 | pop_onto = rpoplpush 683 | push = rpush 684 | append = rpush 685 | 686 | 687 | class TypedList(object): 688 | """Create a container to store a list of objects in Redis. 689 | 690 | Arguments: 691 | key -- the Redis key this container is stored at 692 | target_type -- can be a Python object or a redisco model class. 693 | 694 | Optional Arguments: 695 | type_args -- additional args to pass to type constructor (tuple) 696 | type_kwargs -- additional kwargs to pass to type constructor (dict) 697 | 698 | If target_type is not a redisco model class, the target_type should 699 | also a callable that casts the (string) value of a list element into 700 | target_type. E.g. str, unicode, int, float -- using this format: 701 | 702 | target_type(string_val_of_list_elem, *type_args, **type_kwargs) 703 | 704 | target_type also accepts a string that refers to a redisco model. 705 | """ 706 | 707 | def __init__(self, key, target_type, type_args=[], type_kwargs={}, **kwargs): 708 | self.list = List(key, **kwargs) 709 | self.klass = self.value_type(target_type) 710 | self._klass_args = type_args 711 | self._klass_kwargs = type_kwargs 712 | from models.base import Model 713 | self._redisco_model = issubclass(self.klass, Model) 714 | 715 | def value_type(self, target_type): 716 | if isinstance(target_type, basestring): 717 | t = target_type 718 | from models.base import get_model_from_key 719 | target_type = get_model_from_key(target_type) 720 | if target_type is None: 721 | raise ValueError("Unknown Redisco class %s" % t) 722 | return target_type 723 | 724 | def typecast_item(self, value): 725 | if self._redisco_model: 726 | return self.klass.objects.get_by_id(value) 727 | else: 728 | return self.klass(value, *self._klass_args, **self._klass_kwargs) 729 | 730 | def typecast_iter(self, values): 731 | if self._redisco_model: 732 | return filter(lambda o: o is not None, [self.klass.objects.get_by_id(v) for v in values]) 733 | else: 734 | return [self.klass(v, *self._klass_args, **self._klass_kwargs) for v in values] 735 | 736 | def all(self): 737 | """Returns all items in the list.""" 738 | return self.typecast_iter(self.list.all()) 739 | 740 | def __len__(self): 741 | return len(self.list) 742 | 743 | def __getitem__(self, index): 744 | val = self.list[index] 745 | if isinstance(index, slice): 746 | return self.typecast_iter(val) 747 | else: 748 | return self.typecast_item(val) 749 | 750 | def typecast_stor(self, value): 751 | if self._redisco_model: 752 | return value.id 753 | else: 754 | return value 755 | 756 | def append(self, value): 757 | self.list.append(self.typecast_stor(value)) 758 | 759 | def extend(self, iter): 760 | self.list.extend(map(lambda i: self.typecast_stor(i), iter)) 761 | 762 | def __setitem__(self, index, value): 763 | self.list[index] = self.typecast_stor(value) 764 | 765 | def __iter__(self): 766 | for i in xrange(len(self.list)): 767 | yield self[i] 768 | 769 | def __repr__(self): 770 | return repr(self.typecast_iter(self.list)) 771 | 772 | class SortedSet(Container): 773 | """ 774 | This class represents a SortedSet in redis. 775 | Use it if you want to arrange your set in any order. 776 | 777 | """ 778 | 779 | def __getitem__(self, index): 780 | if isinstance(index, slice): 781 | return self.zrange(index.start, index.stop) 782 | else: 783 | return self.zrange(index, index)[0] 784 | 785 | def score(self, member): 786 | """ 787 | Returns the score of member. 788 | """ 789 | return self.zscore(member) 790 | 791 | def __contains__(self, val): 792 | return self.zscore(val) is not None 793 | 794 | @property 795 | def members(self): 796 | """ 797 | Returns the members of the set. 798 | """ 799 | return self.zrange(0, -1) 800 | 801 | @property 802 | def revmembers(self): 803 | """ 804 | Returns the members of the set in reverse. 805 | """ 806 | return self.zrevrange(0, -1) 807 | 808 | def __iter__(self): 809 | return self.members.__iter__() 810 | 811 | def __reversed__(self): 812 | return self.revmembers.__iter__() 813 | 814 | # def __repr__(self): 815 | # return "<%s '%s' %s>" % (self.__class__.__name__, self.key, 816 | # self.members) 817 | 818 | @property 819 | def _min_score(self): 820 | """ 821 | Returns the minimum score in the SortedSet. 822 | """ 823 | try: 824 | return self.zscore(self.__getitem__(0)) 825 | except IndexError: 826 | return None 827 | 828 | @property 829 | def _max_score(self): 830 | """ 831 | Returns the maximum score in the SortedSet. 832 | """ 833 | try: 834 | return self.zscore(self.__getitem__(-1)) 835 | except IndexError: 836 | return None 837 | 838 | def lt(self, v, limit=None, offset=None): 839 | """ 840 | Returns the list of the members of the set that have scores 841 | less than v. 842 | 843 | :param v: the score to compare to. 844 | :param limit: limit the result to *n* elements 845 | :param offset: Skip the first *n* elements 846 | """ 847 | if limit is not None and offset is None: 848 | offset = 0 849 | return self.zrangebyscore("-inf", "(%f" % v, 850 | start=offset, num=limit) 851 | 852 | def le(self, v, limit=None, offset=None): 853 | """ 854 | Returns the list of the members of the set that have scores 855 | less than or equal to v. 856 | 857 | :param v: the score to compare to. 858 | :param limit: limit the result to *n* elements 859 | :param offset: Skip the first *n* elements 860 | 861 | """ 862 | if limit is not None and offset is None: 863 | offset = 0 864 | return self.zrangebyscore("-inf", v, 865 | start=offset, num=limit) 866 | 867 | def gt(self, v, limit=None, offset=None, withscores=False): 868 | """Returns the list of the members of the set that have scores 869 | greater than v. 870 | """ 871 | if limit is not None and offset is None: 872 | offset = 0 873 | return self.zrangebyscore("(%f" % v, "+inf", 874 | start=offset, num=limit, withscores=withscores) 875 | 876 | def ge(self, v, limit=None, offset=None, withscores=False): 877 | """Returns the list of the members of the set that have scores 878 | greater than or equal to v. 879 | 880 | :param v: the score to compare to. 881 | :param limit: limit the result to *n* elements 882 | :param offset: Skip the first *n* elements 883 | 884 | """ 885 | if limit is not None and offset is None: 886 | offset = 0 887 | return self.zrangebyscore("%f" % v, "+inf", 888 | start=offset, num=limit, withscores=withscores) 889 | 890 | def between(self, min, max, limit=None, offset=None): 891 | """ 892 | Returns the list of the members of the set that have scores 893 | between min and max. 894 | 895 | .. Note:: 896 | The min and max are inclusive when comparing the values. 897 | 898 | :param min: the minimum score to compare to. 899 | :param max: the maximum score to compare to. 900 | :param limit: limit the result to *n* elements 901 | :param offset: Skip the first *n* elements 902 | 903 | >>> s = SortedSet("foo") 904 | >>> s.add('a', 10) 905 | 1 906 | >>> s.add('b', 20) 907 | 1 908 | >>> s.add('c', 30) 909 | 1 910 | >>> s.between(20, 30) 911 | ['b', 'c'] 912 | >>> s.clear() 913 | """ 914 | if limit is not None and offset is None: 915 | offset = 0 916 | return self.zrangebyscore(min, max, 917 | start=offset, num=limit) 918 | 919 | def zadd(self, members, score=1): 920 | """ 921 | Add members in the set and assign them the score. 922 | 923 | :param members: a list of item or a single item 924 | :param score: the score the assign to the item(s) 925 | 926 | >>> s = SortedSet("foo") 927 | >>> s.add('a', 10) 928 | 1 929 | >>> s.add('b', 20) 930 | 1 931 | >>> s.clear() 932 | """ 933 | _members = [] 934 | if not isinstance(members, dict): 935 | _members = [members, score] 936 | else: 937 | for member, score in members.items(): 938 | _members += [member, score] 939 | 940 | return self.db.zadd(self.key, *_members) 941 | 942 | def zrem(self, *values): 943 | """ 944 | Remove the values from the SortedSet 945 | 946 | :return: True if **at least one** value is successfully 947 | removed, False otherwise 948 | 949 | >>> s = SortedSet('foo') 950 | >>> s.add('a', 10) 951 | 1 952 | >>> s.zrem('a') 953 | 1 954 | >>> s.members 955 | [] 956 | >>> s.clear() 957 | """ 958 | return self.db.zrem(self.key, *_parse_values(values)) 959 | 960 | def zincrby(self, att, value=1): 961 | """ 962 | Increment the score of the item by ``value`` 963 | 964 | :param att: the member to increment 965 | :param value: the value to add to the current score 966 | :returns: the new score of the member 967 | 968 | >>> s = SortedSet("foo") 969 | >>> s.add('a', 10) 970 | 1 971 | >>> s.zincrby("a", 10) 972 | 20.0 973 | >>> s.clear() 974 | """ 975 | return self.db.zincrby(self.key, att, value) 976 | 977 | def zrevrank(self, member): 978 | """ 979 | Returns the ranking in reverse order for the member 980 | 981 | >>> s = SortedSet("foo") 982 | >>> s.add('a', 10) 983 | 1 984 | >>> s.add('b', 20) 985 | 1 986 | >>> s.revrank('a') 987 | 1 988 | >>> s.clear() 989 | """ 990 | return self.db.zrevrank(self.key, member) 991 | 992 | def zrange(self, start, stop, withscores=False): 993 | """ 994 | Returns all the elements including between ``start`` (non included) and 995 | ``stop`` (included). 996 | 997 | :param withscore: True if the score of the elements should 998 | also be returned 999 | 1000 | >>> s = SortedSet("foo") 1001 | >>> s.add('a', 10) 1002 | 1 1003 | >>> s.add('b', 20) 1004 | 1 1005 | >>> s.add('c', 30) 1006 | 1 1007 | >>> s.zrange(1, 3) 1008 | ['b', 'c'] 1009 | >>> s.zrange(1, 3, withscores=True) 1010 | [('b', 20.0), ('c', 30.0)] 1011 | >>> s.clear() 1012 | """ 1013 | return self.db.zrange(self.key, start, stop, withscores=withscores) 1014 | 1015 | def zrevrange(self, start, end, **kwargs): 1016 | """ 1017 | Returns the range of items included between ``start`` and ``stop`` 1018 | in reverse order (from high to low) 1019 | 1020 | >>> s = SortedSet("foo") 1021 | >>> s.add('a', 10) 1022 | 1 1023 | >>> s.add('b', 20) 1024 | 1 1025 | >>> s.add('c', 30) 1026 | 1 1027 | >>> s.zrevrange(1, 2) 1028 | ['b', 'a'] 1029 | >>> s.clear() 1030 | """ 1031 | return self.db.zrevrange(self.key, start, end, **kwargs) 1032 | 1033 | def zrangebyscore(self, min, max, **kwargs): 1034 | """ 1035 | Returns the range of elements included between the scores (min and max) 1036 | 1037 | >>> s = SortedSet("foo") 1038 | >>> s.add('a', 10) 1039 | 1 1040 | >>> s.add('b', 20) 1041 | 1 1042 | >>> s.add('c', 30) 1043 | 1 1044 | >>> s.zrangebyscore(20, 30) 1045 | ['b', 'c'] 1046 | >>> s.clear() 1047 | """ 1048 | return self.db.zrangebyscore(self.key, min, max, **kwargs) 1049 | 1050 | def zrevrangebyscore(self, max, min, **kwargs): 1051 | """ 1052 | Returns the range of elements included between the scores (min and max) 1053 | 1054 | >>> s = SortedSet("foo") 1055 | >>> s.add('a', 10) 1056 | 1 1057 | >>> s.add('b', 20) 1058 | 1 1059 | >>> s.add('c', 30) 1060 | 1 1061 | >>> s.zrangebyscore(20, 20) 1062 | ['b'] 1063 | >>> s.clear() 1064 | """ 1065 | return self.db.zrevrangebyscore(self.key, max, min, **kwargs) 1066 | 1067 | def zcard(self): 1068 | """ 1069 | Returns the cardinality of the SortedSet. 1070 | 1071 | >>> s = SortedSet("foo") 1072 | >>> s.add("a", 1) 1073 | 1 1074 | >>> s.add("b", 2) 1075 | 1 1076 | >>> s.add("c", 3) 1077 | 1 1078 | >>> s.zcard() 1079 | 3 1080 | >>> s.clear() 1081 | """ 1082 | return self.db.zcard(self.key) 1083 | 1084 | def zscore(self, elem): 1085 | """ 1086 | Return the score of an element 1087 | 1088 | >>> s = SortedSet("foo") 1089 | >>> s.add("a", 10) 1090 | 1 1091 | >>> s.score("a") 1092 | 10.0 1093 | >>> s.clear() 1094 | """ 1095 | return self.db.zscore(self.key, elem) 1096 | 1097 | def zremrangebyrank(self, start, stop): 1098 | """ 1099 | Remove a range of element between the rank ``start`` and 1100 | ``stop`` both included. 1101 | 1102 | :return: the number of item deleted 1103 | 1104 | >>> s = SortedSet("foo") 1105 | >>> s.add("a", 10) 1106 | 1 1107 | >>> s.add("b", 20) 1108 | 1 1109 | >>> s.add("c", 30) 1110 | 1 1111 | >>> s.zremrangebyrank(1, 2) 1112 | 2 1113 | >>> s.members 1114 | ['a'] 1115 | >>> s.clear() 1116 | """ 1117 | return self.db.zremrangebyrank(self.key, start, stop) 1118 | 1119 | def zremrangebyscore(self, min_value, max_value): 1120 | """ 1121 | Remove a range of element by between score ``min_value`` and 1122 | ``max_value`` both included. 1123 | 1124 | :returns: the number of items deleted. 1125 | 1126 | >>> s = SortedSet("foo") 1127 | >>> s.add("a", 10) 1128 | 1 1129 | >>> s.add("b", 20) 1130 | 1 1131 | >>> s.add("c", 30) 1132 | 1 1133 | >>> s.zremrangebyscore(10, 20) 1134 | 2 1135 | >>> s.members 1136 | ['c'] 1137 | >>> s.clear() 1138 | """ 1139 | 1140 | return self.db.zremrangebyscore(self.key, min_value, max_value) 1141 | 1142 | def zrank(self, elem): 1143 | """ 1144 | Returns the rank of the element. 1145 | 1146 | >>> s = SortedSet("foo") 1147 | >>> s.add("a", 10) 1148 | 1 1149 | >>> s.zrank("a") 1150 | 0 1151 | >>> s.clear() 1152 | """ 1153 | return self.db.zrank(self.key, elem) 1154 | 1155 | def eq(self, value): 1156 | """ 1157 | Returns the elements that have ``value`` for score. 1158 | """ 1159 | return self.zrangebyscore(value, value) 1160 | 1161 | __len__ = zcard 1162 | revrank = zrevrank 1163 | score = zscore 1164 | rank = zrank 1165 | incr_by = zincrby 1166 | add = zadd 1167 | remove = zrem 1168 | 1169 | 1170 | class NonPersistentList(object): 1171 | def __init__(self, l): 1172 | self._list = l 1173 | 1174 | @property 1175 | def members(self): 1176 | return self._list 1177 | 1178 | def __iter__(self): 1179 | return iter(self.members) 1180 | 1181 | def __len__(self): 1182 | return len(self._list) 1183 | 1184 | 1185 | class Hash(Container, collections.MutableMapping): 1186 | 1187 | def __iter__(self): 1188 | return self.hgetall().__iter__() 1189 | 1190 | def __repr__(self): 1191 | return "<%s '%s' %s>" % (self.__class__.__name__, 1192 | self.key, self.hgetall()) 1193 | 1194 | def _set_dict(self, new_dict): 1195 | self.clear() 1196 | self.update(new_dict) 1197 | 1198 | def hlen(self): 1199 | """ 1200 | Returns the number of elements in the Hash. 1201 | """ 1202 | return self.db.hlen(self.key) 1203 | 1204 | def hset(self, member, value): 1205 | """ 1206 | Set ``member`` in the Hash at ``value``. 1207 | 1208 | :returns: 1 if member is a new field and the value has been 1209 | stored, 0 if the field existed and the value has been 1210 | updated. 1211 | 1212 | >>> h = Hash("foo") 1213 | >>> h.hset("bar", "value") 1214 | 1L 1215 | >>> h.clear() 1216 | """ 1217 | return self.db.hset(self.key, member, value) 1218 | 1219 | def hdel(self, *members): 1220 | """ 1221 | Delete one or more hash field. 1222 | 1223 | :param members: on or more fields to remove. 1224 | :return: the number of fields that were removed 1225 | 1226 | >>> h = Hash("foo") 1227 | >>> h.hset("bar", "value") 1228 | 1L 1229 | >>> h.hdel("bar") 1230 | 1 1231 | >>> h.clear() 1232 | """ 1233 | return self.db.hdel(self.key, *_parse_values(members)) 1234 | 1235 | def hkeys(self): 1236 | """ 1237 | Returns all fields name in the Hash 1238 | """ 1239 | return self.db.hkeys(self.key) 1240 | 1241 | def hgetall(self): 1242 | """ 1243 | Returns all the fields and values in the Hash. 1244 | 1245 | :rtype: dict 1246 | """ 1247 | return self.db.hgetall(self.key) 1248 | 1249 | def hvals(self): 1250 | """ 1251 | Returns all the values in the Hash 1252 | 1253 | :rtype: list 1254 | """ 1255 | return self.db.hvals(self.key) 1256 | 1257 | def hget(self, field): 1258 | """ 1259 | Returns the value stored in the field, None if the field doesn't exist. 1260 | """ 1261 | return self.db.hget(self.key, field) 1262 | 1263 | def hexists(self, field): 1264 | """ 1265 | Returns ``True`` if the field exists, ``False`` otherwise. 1266 | """ 1267 | return self.db.hexists(self.key, field) 1268 | 1269 | def hincrby(self, field, increment=1): 1270 | """ 1271 | Increment the value of the field. 1272 | :returns: the value of the field after incrementation 1273 | 1274 | >>> h = Hash("foo") 1275 | >>> h.hincrby("bar", 10) 1276 | 10L 1277 | >>> h.hincrby("bar", 2) 1278 | 12L 1279 | >>> h.clear() 1280 | """ 1281 | return self.db.hincrby(self.key, field, increment) 1282 | 1283 | def hmget(self, fields): 1284 | """ 1285 | Returns the values stored in the fields. 1286 | """ 1287 | return self.db.hmget(self.key, fields) 1288 | 1289 | def hmset(self, mapping): 1290 | """ 1291 | Sets or updates the fields with their corresponding values. 1292 | 1293 | :param mapping: a dict with keys and values 1294 | """ 1295 | return self.db.hmset(self.key, mapping) 1296 | 1297 | keys = hkeys 1298 | values = hvals 1299 | _get_dict = hgetall 1300 | __getitem__ = hget 1301 | __setitem__ = hset 1302 | __delitem__ = hdel 1303 | __len__ = hlen 1304 | __contains__ = hexists 1305 | dict = property(_get_dict, _set_dict) 1306 | -------------------------------------------------------------------------------- /redisco/containerstests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import redisco 3 | from redisco import containers as cont 4 | 5 | 6 | class SetTestCase(unittest.TestCase): 7 | def setUp(self): 8 | self.client = redisco.get_client() 9 | self.client.flushdb() 10 | 11 | def tearDown(self): 12 | self.client.flushdb() 13 | 14 | def test_common_operations(self): 15 | fruits = cont.Set(key='fruits') 16 | fruits.add('apples') 17 | fruits.add('oranges') 18 | fruits.add('bananas', 'tomatoes') 19 | fruits.add(['strawberries', 'blackberries']) 20 | 21 | self.assertEqual(set(['apples', 'oranges', 'bananas', 'tomatoes', 'strawberries', 'blackberries']), fruits.all()) 22 | 23 | # remove 24 | fruits.remove('apples') 25 | fruits.remove('bananas', 'blackberries') 26 | fruits.remove(['tomatoes', 'strawberries']) 27 | 28 | self.assertEqual(set(['oranges']), fruits.all()) 29 | 30 | # in 31 | self.assertTrue('oranges' in fruits) 32 | self.assertTrue('apples' not in fruits) 33 | 34 | # len 35 | self.assertEqual(1, len(fruits)) 36 | 37 | # pop 38 | self.assertEqual('oranges', fruits.pop()) 39 | 40 | # copy 41 | fruits.add('apples') 42 | fruits.add('oranges') 43 | basket = fruits.copy('basket') 44 | self.assertEqual(set(['apples', 'oranges']), basket.all()) 45 | 46 | # update 47 | o = cont.Set('o', self.client) 48 | o.add('kiwis') 49 | fruits.update(o) 50 | self.assertEqual(set(['kiwis', 'apples', 'oranges']), 51 | fruits.all()) 52 | 53 | def test_comparisons(self): 54 | all_pls = cont.Set(key='ProgrammingLanguages') 55 | my_pls = cont.Set(key='MyPLs') 56 | o_pls = cont.Set(key='OPLs') 57 | all_pls.add('Python') 58 | all_pls.add('Ruby') 59 | all_pls.add('PHP') 60 | all_pls.add('Lua') 61 | all_pls.add('Java') 62 | all_pls.add('Pascal') 63 | all_pls.add('C') 64 | all_pls.add('C++') 65 | all_pls.add('Haskell') 66 | all_pls.add('C#') 67 | all_pls.add('Go') 68 | 69 | my_pls.add('Ruby') 70 | my_pls.add('Python') 71 | my_pls.add('Lua') 72 | my_pls.add('Haskell') 73 | 74 | o_pls.add('Ruby') 75 | o_pls.add('Python') 76 | o_pls.add('Lua') 77 | o_pls.add('Haskell') 78 | 79 | # equality 80 | self.assertNotEqual(my_pls, all_pls) 81 | self.assertEqual(o_pls, my_pls) 82 | 83 | fruits = cont.Set(key='fruits') 84 | fruits.add('apples') 85 | fruits.add('oranges') 86 | 87 | # disjoint 88 | self.assertTrue(fruits.isdisjoint(o_pls)) 89 | self.assertFalse(all_pls.isdisjoint(o_pls)) 90 | 91 | # subset 92 | self.assertTrue(my_pls < all_pls) 93 | self.assertTrue(all_pls > my_pls) 94 | self.assertTrue(o_pls >= my_pls) 95 | self.assertTrue(o_pls <= my_pls) 96 | self.assertTrue(my_pls.issubset(all_pls)) 97 | self.assertTrue(my_pls.issubset(o_pls)) 98 | self.assertTrue(o_pls.issubset(my_pls)) 99 | 100 | # union 101 | s = fruits.union("fruits|mypls", my_pls) 102 | self.assertEqual(set(['Ruby', 'Python', 'Lua', 'Haskell', 'apples', 103 | 'oranges']), s.members) 104 | 105 | # intersection 106 | inter = fruits.intersection('fruits&mypls', my_pls) 107 | self.assertEqual(set([]), inter.members) 108 | 109 | # difference 110 | s = fruits.difference('fruits-my_pls', my_pls) 111 | self.assertEqual(set(['apples', 'oranges']), s.members) 112 | 113 | 114 | def test_operations_with_updates(self): 115 | abc = cont.Set('abc', self.client) 116 | for c in 'abc': 117 | abc.add(c) 118 | 119 | def_ = cont.Set('def', self.client) 120 | for c in 'def': 121 | def_.add(c) 122 | 123 | # __ior__ 124 | abc |= def_ 125 | self.assertEqual(set(['a', 'b', 'c', 'd', 'e', 'f']), 126 | abc.all()) 127 | 128 | abc &= def_ 129 | self.assertEqual(set(['d', 'e', 'f']), abc.all()) 130 | 131 | for c in 'abc': 132 | abc.add(c) 133 | abc -= def_ 134 | self.assertEqual(set(['a', 'b', 'c']), abc.all()) 135 | 136 | def test_methods_that_should_return_new_sets(self): 137 | abc = cont.Set('abc', self.client) 138 | for c in 'abc': 139 | abc.add(c) 140 | 141 | def_ = cont.Set('def', self.client) 142 | for c in 'def': 143 | def_.add(c) 144 | 145 | # new_key as a set should raise error 146 | # only strings are allowed as keys 147 | new_set = cont.Set('new_set') 148 | self.assertRaises(ValueError, abc.union, new_set, def_) 149 | self.assertRaises(ValueError, abc.difference, new_set, def_) 150 | self.assertRaises(ValueError, abc.intersection, new_set, def_) 151 | 152 | self.assert_(isinstance(abc.union('new_set', def_), cont.Set)) 153 | self.assert_(isinstance(abc.intersection('new_set', def_), cont.Set)) 154 | self.assert_(isinstance(abc.difference('new_set', def_), cont.Set)) 155 | 156 | 157 | def test_access_redis_methods(self): 158 | s = cont.Set('new_set') 159 | s.sadd('a') 160 | s.sadd('b') 161 | s.srem('b') 162 | self.assertEqual('a', s.spop()) 163 | s.sadd('a') 164 | self.assert_('a' in s.members) 165 | s.sadd('b') 166 | self.assertEqual(2, s.scard()) 167 | self.assert_(s.sismember('a')) 168 | self.client.sadd('other_set', 'a') 169 | self.client.sadd('other_set', 'b') 170 | self.client.sadd('other_set', 'c') 171 | self.assert_(s.srandmember() in set(['a', 'b'])) 172 | 173 | def test_sinter(self): 174 | abc = cont.Set("abc") 175 | def_ = cont.Set("def") 176 | abc.add('a') 177 | abc.add('b') 178 | abc.add('c') 179 | def_.add('d') 180 | def_.add('e') 181 | def_.add('f') 182 | 183 | self.assertEqual(set([]), abc.sinter(def_)) 184 | def_.add('b') 185 | def_.add('c') 186 | 187 | self.assertEqual(set(['b', 'c']), abc.sinter(def_)) 188 | 189 | def test_sunion(self): 190 | abc = cont.Set("abc") 191 | def_ = cont.Set("def") 192 | abc.add('a') 193 | abc.add('b') 194 | abc.add('c') 195 | def_.add('d') 196 | def_.add('e') 197 | def_.add('f') 198 | 199 | self.assertEqual(set(['a', 'b', 'c', 'd', 'e', 'f']), 200 | abc.sunion(def_)) 201 | 202 | def test_susdiff(self): 203 | abc = cont.Set("abc") 204 | def_ = cont.Set("def") 205 | abc.add('a') 206 | abc.add('b') 207 | abc.add('c') 208 | def_.add('c') 209 | def_.add('b') 210 | def_.add('f') 211 | 212 | self.assertEqual(set(['a',]), 213 | abc.sdiff(def_)) 214 | 215 | 216 | class ListTestCase(unittest.TestCase): 217 | def setUp(self): 218 | self.client = redisco.get_client() 219 | self.client.flushdb() 220 | 221 | def tearDown(self): 222 | self.client.flushdb() 223 | 224 | def test_common_operations(self): 225 | alpha = cont.List('alpha', self.client) 226 | 227 | # append 228 | alpha.append('a') 229 | alpha.append('b') 230 | alpha.append('c', 'd') 231 | alpha.append(['e', 'f']) 232 | 233 | self.assertEqual(['a', 'b', 'c', 'd', 'e', 'f'], alpha.all()) 234 | 235 | # len 236 | self.assertEqual(6, len(alpha)) 237 | 238 | num = cont.List('num', self.client) 239 | num.append('1') 240 | num.append('2') 241 | 242 | # extend and iter 243 | alpha.extend(num) 244 | self.assertEqual(['a', 'b', 'c', 'd', 'e', 'f', '1', '2'], alpha.all()) 245 | alpha.extend(['3', '4']) 246 | self.assertEqual(['a', 'b', 'c', 'd', 'e', 'f', '1', '2', '3', '4'], alpha.all()) 247 | 248 | # contains 249 | self.assertTrue('b' in alpha) 250 | self.assertTrue('2' in alpha) 251 | self.assertTrue('5' not in alpha) 252 | 253 | # shift and unshift 254 | num.unshift('0') 255 | self.assertEqual(['0', '1', '2'], num.members) 256 | self.assertEqual('0', num.shift()) 257 | self.assertEqual(['1', '2'], num.members) 258 | 259 | # push and pop 260 | num.push('4') 261 | num.push('a', 'b') 262 | num.push(['c', 'd']) 263 | self.assertEqual('d', num.pop()) 264 | self.assertEqual('c', num.pop()) 265 | self.assertEqual(['1', '2', '4', 'a' ,'b'], num.members) 266 | 267 | # trim 268 | alpha.trim(0, 1) 269 | self.assertEqual(['a', 'b'], alpha.all()) 270 | 271 | # remove 272 | alpha.remove('b') 273 | self.assertEqual(['a'], alpha.all()) 274 | 275 | # setitem 276 | alpha[0] = 'A' 277 | self.assertEqual(['A'], alpha.all()) 278 | 279 | # iter 280 | alpha.push('B') 281 | for e, a in zip(alpha, ['A', 'B']): 282 | self.assertEqual(a, e) 283 | self.assertEqual(['A', 'B'], list(alpha)) 284 | 285 | # slice 286 | alpha.extend(['C', 'D', 'E']) 287 | self.assertEqual(['A', 'B', 'C', 'D', 'E'], alpha[:]) 288 | self.assertEqual(['B', 'C'], alpha[1:3]) 289 | 290 | alpha.reverse() 291 | self.assertEqual(['E', 'D', 'C', 'B', 'A'], list(alpha)) 292 | 293 | def test_pop_onto(self): 294 | a = cont.List('alpha') 295 | b = cont.List('beta') 296 | a.extend(range(10)) 297 | 298 | # test pop_onto 299 | a_snap = list(a.members) 300 | while True: 301 | v = a.pop_onto(b.key) 302 | if not v: 303 | break 304 | else: 305 | self.assertTrue(v not in a.members) 306 | self.assertTrue(v in b.members) 307 | 308 | self.assertEqual(a_snap, b.members) 309 | 310 | # test rpoplpush 311 | b_snap = list(b.members) 312 | while True: 313 | v = b.rpoplpush(a.key) 314 | if not v: 315 | break 316 | else: 317 | self.assertTrue(v in a.members) 318 | self.assertTrue(v not in b.members) 319 | 320 | self.assertEqual(b_snap, a.members) 321 | 322 | def test_native_methods(self): 323 | l = cont.List('mylist') 324 | self.assertEqual([], l.lrange(0, -1)) 325 | l.rpush('b') 326 | l.rpush('c') 327 | l.lpush('a') 328 | self.assertEqual(['a', 'b', 'c'], l.lrange(0, -1)) 329 | self.assertEqual(3, l.llen()) 330 | l.ltrim(1, 2) 331 | self.assertEqual(['b', 'c'], l.lrange(0, -1)) 332 | self.assertEqual('c', l.lindex(1)) 333 | self.assertEqual(1, l.lset(0, 'a')) 334 | self.assertEqual(1, l.lset(1, 'b')) 335 | self.assertEqual(['a', 'b'], l.lrange(0, -1)) 336 | self.assertEqual('a', l.lpop()) 337 | self.assertEqual('b', l.rpop()) 338 | 339 | class TypedListTestCase(unittest.TestCase): 340 | def setUp(self): 341 | self.client = redisco.get_client() 342 | self.client.flushdb() 343 | 344 | def tearDown(self): 345 | self.client.flushdb() 346 | 347 | def test_basic_types(self): 348 | alpha = cont.TypedList('alpha', unicode, type_args=('UTF-8',)) 349 | monies = u'\u0024\u00a2\u00a3\u00a5' 350 | alpha.append(monies) 351 | val = alpha[-1] 352 | self.assertEquals(monies, val) 353 | 354 | beta = cont.TypedList('beta', int) 355 | for i in xrange(1000): 356 | beta.append(i) 357 | for i, x in enumerate(beta): 358 | self.assertEquals(i, x) 359 | 360 | charlie = cont.TypedList('charlie', float) 361 | for i in xrange(100): 362 | val = 1 * pow(10, i*-1) 363 | charlie.append(val) 364 | for i, x in enumerate(charlie): 365 | val = 1 * pow(10, i*-1) 366 | self.assertEquals(x, val) 367 | 368 | def test_model_type(self): 369 | from redisco import models 370 | class Person(models.Model): 371 | name = models.Attribute() 372 | friend = models.ReferenceField('Person') 373 | 374 | iamteam = Person.objects.create(name='iamteam') 375 | clayg = Person.objects.create(name='clayg', friend=iamteam) 376 | 377 | l = cont.TypedList('friends', 'Person') 378 | l.extend(Person.objects.all()) 379 | 380 | for person in l: 381 | if person.name == 'clayg': 382 | self.assertEquals(iamteam, clayg.friend) 383 | else: 384 | # this if failing for some reason ??? 385 | #self.assertEquals(person.friend, clayg) 386 | pass 387 | 388 | 389 | class SortedSetTestCase(unittest.TestCase): 390 | def setUp(self): 391 | self.client = redisco.get_client() 392 | self.client.flushdb() 393 | 394 | def tearDown(self): 395 | self.client.flushdb() 396 | 397 | def test_everything(self): 398 | zorted = cont.SortedSet("Person:age") 399 | zorted.add("1", 29) 400 | zorted.add("2", 39) 401 | zorted.add({"3" : '15', "4" : 35}) 402 | zorted.add({"5" : 98, "6" : 5}) 403 | self.assertEqual(6, len(zorted)) 404 | self.assertEqual(35, zorted.score("4")) 405 | self.assertEqual(0, zorted.rank("6")) 406 | self.assertEqual(5, zorted.revrank("6")) 407 | self.assertEqual(3, zorted.rank("4")) 408 | self.assertEqual(["6", "3", "1", "4"], zorted.le(35)) 409 | 410 | zorted.add("7", 35) 411 | self.assertEqual(["4", "7"], zorted.eq(35)) 412 | self.assertEqual(["6", "3", "1"], zorted.lt(30)) 413 | self.assertEqual(["4", "7", "2", "5"], zorted.gt(30)) 414 | 415 | def test_delegateable_methods(self): 416 | zset = cont.SortedSet("Person:all") 417 | zset.zadd("1", 1) 418 | zset.zadd("2", 2) 419 | zset.zadd("3", 3) 420 | zset.zadd("4", 4) 421 | self.assertEqual(4, zset.zcard()) 422 | self.assertEqual(4, zset.zscore('4')) 423 | self.assertEqual(['1', '2', '3', '4'], list(zset)) 424 | self.assertEqual(zset.zrange(0, -1), list(zset)) 425 | self.assertEqual(['4', '3', '2', '1'], zset.zrevrange(0, -1)) 426 | self.assertEqual(list(reversed(zset)), zset.zrevrange(0, -1)) 427 | self.assertEqual(list(reversed(zset)), list(zset.__reversed__())) 428 | 429 | 430 | class HashTestCase(unittest.TestCase): 431 | def setUp(self): 432 | self.client = redisco.get_client() 433 | self.client.flushdb() 434 | 435 | def tearDown(self): 436 | self.client.flushdb() 437 | 438 | def test_basic(self): 439 | h = cont.Hash('hkey') 440 | self.assertEqual(0, len(h)) 441 | h['name'] = "Richard Cypher" 442 | h['real_name'] = "Richard Rahl" 443 | 444 | pulled = self.client.hgetall('hkey') 445 | self.assertEqual({'name': "Richard Cypher", 446 | 'real_name': "Richard Rahl"}, pulled) 447 | 448 | self.assertEqual({'name': "Richard Cypher", 449 | 'real_name': "Richard Rahl"}, h.dict) 450 | 451 | self.assertEqual(['name', 'real_name'], h.keys()) 452 | self.assertEqual(["Richard Cypher", "Richard Rahl"], 453 | h.values()) 454 | 455 | del h['name'] 456 | pulled = self.client.hgetall('hkey') 457 | self.assertEqual({'real_name': "Richard Rahl"}, pulled) 458 | self.assert_('real_name' in h) 459 | h.dict = {"new_hash": "YEY"} 460 | self.assertEqual({"new_hash": "YEY"}, h.dict) 461 | 462 | def test_delegateable_methods(self): 463 | h = cont.Hash('my_hash') 464 | h.hincrby('Red', 1) 465 | h.hincrby('Red', 1) 466 | h.hincrby('Red', 2) 467 | self.assertEqual(4, int(h.hget('Red'))) 468 | h.hmset({'Blue': 100, 'Green': 19, 'Yellow': 1024}) 469 | self.assertEqual(['100', '19'], h.hmget(['Blue', 'Green'])) 470 | 471 | if __name__ == "__main__": 472 | import sys 473 | unittest.main(argv=sys.argv) 474 | -------------------------------------------------------------------------------- /redisco/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from .attributes import * 3 | from .exceptions import * 4 | 5 | __all__ = ['Model', 'Attribute', 'BooleanField', 'IntegerField', 6 | 'Counter', 'FloatField', 'DateTimeField', 'DateField', 'TimeDeltaField', 7 | 'ReferenceField', 'ListField', 'ValidationError', 'from_key', 8 | 'ValidationError', 'MissingID', 'AttributeNotIndexed', 9 | 'FieldValidationError', 'BadKeyError'] 10 | -------------------------------------------------------------------------------- /redisco/models/attributes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Defines the fields that can be added to redisco models. 4 | """ 5 | import time 6 | import sys 7 | from datetime import datetime, date, timedelta 8 | from dateutil.tz import tzutc, tzlocal 9 | from calendar import timegm 10 | from redisco.containers import List 11 | from .exceptions import FieldValidationError, MissingID 12 | 13 | __all__ = ['Attribute', 'CharField', 'ListField', 'DateTimeField', 14 | 'DateField', 'TimeDeltaField', 'ReferenceField', 'Collection', 15 | 'IntegerField', 'FloatField', 'BooleanField', 'Counter', 16 | 'ZINDEXABLE'] 17 | 18 | 19 | class Attribute(object): 20 | """Defines an attribute of the model. 21 | 22 | The attribute accepts strings and are stored in Redis as 23 | they are - strings. 24 | 25 | Options 26 | name -- alternate name of the attribute. This will be used 27 | as the key to use when interacting with Redis. 28 | indexed -- Index this attribute. Unindexed attributes cannot 29 | be used in queries. Default: True. 30 | unique -- validates the uniqueness of the value of the 31 | attribute. 32 | validator -- a callable that can validate the value of the 33 | attribute. 34 | default -- Initial value of the attribute. 35 | 36 | """ 37 | def __init__(self, 38 | name=None, 39 | indexed=True, 40 | required=False, 41 | validator=None, 42 | unique=False, 43 | default=None): 44 | self.name = name 45 | self.indexed = indexed 46 | self.required = required 47 | self.validator = validator 48 | self.default = default 49 | self.unique = unique 50 | 51 | def __get__(self, instance, owner): 52 | try: 53 | return getattr(instance, '_' + self.name) 54 | except AttributeError: 55 | if callable(self.default): 56 | default = self.default() 57 | else: 58 | default = self.default 59 | self.__set__(instance, default) 60 | return default 61 | 62 | def __set__(self, instance, value): 63 | setattr(instance, '_' + self.name, value) 64 | 65 | def typecast_for_read(self, value): 66 | """Typecasts the value for reading from Redis.""" 67 | # The redis client encodes all unicode data to utf-8 by default. 68 | return value.decode('utf-8') 69 | 70 | def typecast_for_storage(self, value): 71 | """Typecasts the value for storing to Redis.""" 72 | try: 73 | return unicode(value) 74 | except UnicodeError: 75 | return value.decode('utf-8') 76 | 77 | def value_type(self): 78 | return unicode 79 | 80 | def acceptable_types(self): 81 | return basestring 82 | 83 | def validate(self, instance): 84 | val = getattr(instance, self.name) 85 | errors = [] 86 | # type_validation 87 | if val is not None and not isinstance(val, self.acceptable_types()): 88 | errors.append((self.name, 'bad type',)) 89 | # validate first standard stuff 90 | if self.required: 91 | if val is None or not unicode(val).strip(): 92 | errors.append((self.name, 'required')) 93 | # validate uniquness 94 | if val and self.unique: 95 | error = self.validate_uniqueness(instance, val) 96 | if error: 97 | errors.append(error) 98 | # validate using validator 99 | if self.validator: 100 | r = self.validator(self.name, val) 101 | if r: 102 | errors.extend(r) 103 | if errors: 104 | raise FieldValidationError(errors) 105 | 106 | def validate_uniqueness(self, instance, val): 107 | encoded = self.typecast_for_storage(val) 108 | matches = instance.__class__.objects.filter(**{self.name: encoded}) 109 | if len(matches) > 0: 110 | try: 111 | instance_id = instance.id 112 | no_id = False 113 | except MissingID: 114 | no_id = True 115 | if (len(matches) != 1) or no_id or (matches.first().id != instance.id): 116 | return (self.name, 'not unique',) 117 | 118 | 119 | class CharField(Attribute): 120 | 121 | def __init__(self, max_length=255, **kwargs): 122 | super(CharField, self).__init__(**kwargs) 123 | self.max_length = max_length 124 | 125 | def validate(self, instance): 126 | errors = [] 127 | try: 128 | super(CharField, self).validate(instance) 129 | except FieldValidationError as err: 130 | errors.extend(err.errors) 131 | 132 | val = getattr(instance, self.name) 133 | 134 | if val and len(val) > self.max_length: 135 | errors.append((self.name, 'exceeds max length')) 136 | 137 | if errors: 138 | raise FieldValidationError(errors) 139 | 140 | 141 | class BooleanField(Attribute): 142 | def typecast_for_read(self, value): 143 | return bool(int(value)) 144 | 145 | def typecast_for_storage(self, value): 146 | if value is None: 147 | return "0" 148 | return "1" if value else "0" 149 | 150 | def value_type(self): 151 | return bool 152 | 153 | def acceptable_types(self): 154 | return self.value_type() 155 | 156 | 157 | class IntegerField(Attribute): 158 | def typecast_for_read(self, value): 159 | return int(value) 160 | 161 | def typecast_for_storage(self, value): 162 | if value is None: 163 | return "0" 164 | return unicode(value) 165 | 166 | def value_type(self): 167 | return int 168 | 169 | def acceptable_types(self): 170 | return (int, long) 171 | 172 | 173 | class FloatField(Attribute): 174 | def typecast_for_read(self, value): 175 | return float(value) 176 | 177 | def typecast_for_storage(self, value): 178 | if value is None: 179 | return "0" 180 | return "%f" % value 181 | 182 | def value_type(self): 183 | return float 184 | 185 | def acceptable_types(self): 186 | return self.value_type() 187 | 188 | 189 | class DateTimeField(Attribute): 190 | 191 | def __init__(self, auto_now=False, auto_now_add=False, **kwargs): 192 | super(DateTimeField, self).__init__(**kwargs) 193 | self.auto_now = auto_now 194 | self.auto_now_add = auto_now_add 195 | 196 | def typecast_for_read(self, value): 197 | try: 198 | # We load as if the timestampe was naive 199 | dt = datetime.fromtimestamp(float(value), tzutc()) 200 | # And gently override (ie: not convert) to the TZ to UTC 201 | return dt 202 | except TypeError: 203 | return None 204 | except ValueError: 205 | return None 206 | 207 | def typecast_for_storage(self, value): 208 | if not isinstance(value, datetime): 209 | raise TypeError("%s should be datetime object, and not a %s" % 210 | (self.name, type(value))) 211 | if value is None: 212 | return None 213 | # Are we timezone aware ? If no, make it TimeZone Local 214 | if value.tzinfo is None: 215 | value = value.replace(tzinfo=tzlocal()) 216 | return "%d.%06d" % (float(timegm(value.utctimetuple())), value.microsecond) 217 | 218 | def value_type(self): 219 | return datetime 220 | 221 | def acceptable_types(self): 222 | return self.value_type() 223 | 224 | 225 | class DateField(Attribute): 226 | 227 | def __init__(self, auto_now=False, auto_now_add=False, **kwargs): 228 | super(DateField, self).__init__(**kwargs) 229 | self.auto_now = auto_now 230 | self.auto_now_add = auto_now_add 231 | 232 | def typecast_for_read(self, value): 233 | try: 234 | # We load as if it is UTC time 235 | dt = date.fromtimestamp(float(value)) 236 | # And assign (ie: not convert) the UTC TimeZone 237 | return dt 238 | except TypeError: 239 | return None 240 | except ValueError: 241 | return None 242 | 243 | def typecast_for_storage(self, value): 244 | if not isinstance(value, date): 245 | raise TypeError("%s should be date object, and not a %s" % 246 | (self.name, type(value))) 247 | if value is None: 248 | return None 249 | return "%d" % float(timegm(value.timetuple())) 250 | 251 | def value_type(self): 252 | return date 253 | 254 | def acceptable_types(self): 255 | return self.value_type() 256 | 257 | class TimeDeltaField(Attribute): 258 | 259 | def __init__(self, **kwargs): 260 | super(TimeDeltaField, self).__init__(**kwargs) 261 | 262 | if hasattr(timedelta, "totals_seconds"): 263 | def _total_seconds(self, td): 264 | return td.total_seconds 265 | else: 266 | def _total_seconds(self, td): 267 | return (td.microseconds + 0.0 + \ 268 | (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6 269 | 270 | 271 | def typecast_for_read(self, value): 272 | try: 273 | # We load as if it is UTC time 274 | if value is None: 275 | value = 0. 276 | td = timedelta(seconds=float(value)) 277 | return td 278 | except TypeError: 279 | return None 280 | except ValueError: 281 | return None 282 | 283 | def typecast_for_storage(self, value): 284 | if not isinstance(value, timedelta): 285 | raise TypeError("%s should be timedelta object, and not a %s" % 286 | (self.name, type(value))) 287 | if value is None: 288 | return None 289 | 290 | return "%d" % self._total_seconds(value) 291 | 292 | def value_type(self): 293 | return timedelta 294 | 295 | def acceptable_types(self): 296 | return self.value_type() 297 | 298 | 299 | class ListField(object): 300 | """Stores a list of objects. 301 | 302 | target_type -- can be a Python object or a redisco model class. 303 | 304 | If target_type is not a redisco model class, the target_type should 305 | also a callable that casts the (string) value of a list element into 306 | target_type. E.g. str, unicode, int, float. 307 | 308 | ListField also accepts a string that refers to a redisco model. 309 | 310 | """ 311 | def __init__(self, target_type, 312 | name=None, 313 | indexed=True, 314 | required=False, 315 | validator=None, 316 | default=None): 317 | self._target_type = target_type 318 | self.name = name 319 | self.indexed = indexed 320 | self.required = required 321 | self.validator = validator 322 | self.default = default or [] 323 | from base import Model 324 | self._redisco_model = (isinstance(target_type, basestring) or 325 | issubclass(target_type, Model)) 326 | 327 | def __get__(self, instance, owner): 328 | try: 329 | return getattr(instance, '_' + self.name) 330 | except AttributeError: 331 | if instance.is_new(): 332 | val = self.default 333 | else: 334 | key = instance.key()[self.name] 335 | val = List(key).members 336 | if val is not None: 337 | klass = self.value_type() 338 | if self._redisco_model: 339 | val = filter(lambda o: o is not None, [klass.objects.get_by_id(v) for v in val]) 340 | else: 341 | val = [klass(v) for v in val] 342 | self.__set__(instance, val) 343 | return val 344 | 345 | def __set__(self, instance, value): 346 | setattr(instance, '_' + self.name, value) 347 | 348 | def value_type(self): 349 | if isinstance(self._target_type, basestring): 350 | t = self._target_type 351 | from base import get_model_from_key 352 | self._target_type = get_model_from_key(self._target_type) 353 | if self._target_type is None: 354 | raise ValueError("Unknown Redisco class %s" % t) 355 | return self._target_type 356 | 357 | def validate(self, instance): 358 | val = getattr(instance, self.name) 359 | errors = [] 360 | 361 | if val: 362 | if not isinstance(val, list): 363 | errors.append((self.name, 'bad type')) 364 | else: 365 | for item in val: 366 | if not isinstance(item, self.value_type()): 367 | errors.append((self.name, 'bad type in list')) 368 | 369 | # validate first standard stuff 370 | if self.required: 371 | if not val: 372 | errors.append((self.name, 'required')) 373 | # validate using validator 374 | if self.validator: 375 | r = self.validator(val) 376 | if r: 377 | errors.extend(r) 378 | if errors: 379 | raise FieldValidationError(errors) 380 | 381 | class Collection(object): 382 | """ 383 | A simple container that will be replaced by the good imports 384 | and the good filter query. 385 | """ 386 | def __init__(self, target_type): 387 | self.target_type = target_type 388 | 389 | def __get__(self, instance, owner): 390 | if not isinstance(self.target_type, str): 391 | raise TypeError("A collection only accepts a string representing the Class") 392 | 393 | # __import__ should be something like __import__('mymod.mysubmod', fromlist=['MyClass']) 394 | klass_path = self.target_type.split(".") 395 | fromlist = klass_path[-1] 396 | frompath = ".".join(klass_path[0:-1]) 397 | # if the path is not empty, then it worth importing the class, otherwise, it's 398 | # a local Class and it's already been imported. 399 | if frompath: 400 | mod = __import__(frompath, fromlist=[fromlist]) 401 | else: 402 | mod = sys.modules[__name__] 403 | 404 | klass = getattr(mod, fromlist) 405 | return klass.objects.filter(**{instance.__class__.__name__.lower() + '_id': instance.id}) 406 | 407 | def __set__(self, instance, value): 408 | """ 409 | Prevent the argument to be overriden 410 | """ 411 | raise AttributeError("can't override a collection of object") 412 | 413 | 414 | class ReferenceField(object): 415 | def __init__(self, 416 | target_type, 417 | name=None, 418 | attname=None, 419 | indexed=True, 420 | required=False, 421 | related_name=None, 422 | default=None, 423 | validator=None): 424 | self._target_type = target_type 425 | self.name = name 426 | self.indexed = indexed 427 | self.required = required 428 | self._attname = attname 429 | self._related_name = related_name 430 | self.validator = validator 431 | self.default = default 432 | 433 | def __set__(self, instance, value): 434 | """ 435 | Will set the referenced object unless None is provided 436 | which will simply remove the reference 437 | """ 438 | if not isinstance(value, self.value_type()) and \ 439 | value is not None: 440 | raise TypeError 441 | # remove the cached value from the instance 442 | if hasattr(instance, '_' + self.name): 443 | delattr(instance, '_' + self.name) 444 | # Remove the attribute_id reference 445 | setattr(instance, self.attname, None) 446 | # Set it to the new value if any. 447 | if value is not None: 448 | setattr(instance, self.attname, value.id) 449 | 450 | def __get__(self, instance, owner): 451 | try: 452 | if not hasattr(instance, '_' + self.name): 453 | o = self.value_type().objects.get_by_id( 454 | getattr(instance, self.attname)) 455 | setattr(instance, '_' + self.name, o) 456 | return getattr(instance, '_' + self.name) 457 | except AttributeError: 458 | setattr(instance, '_' + self.name, self.default) 459 | return self.default 460 | 461 | def value_type(self): 462 | return self._target_type 463 | 464 | @property 465 | def attname(self): 466 | if self._attname is None: 467 | self._attname = self.name + '_id' 468 | return self._attname 469 | 470 | @property 471 | def related_name(self): 472 | return self._related_name 473 | 474 | def validate(self, instance): 475 | val = getattr(instance, self.name) 476 | errors = [] 477 | 478 | if val: 479 | if not isinstance(val, self.value_type()): 480 | errors.append((self.name, 'bad type for reference')) 481 | 482 | # validate first standard stuff 483 | if self.required: 484 | if not val: 485 | errors.append((self.name, 'required')) 486 | # validate using validator 487 | if self.validator: 488 | r = self.validator(val) 489 | if r: 490 | errors.extend(r) 491 | if errors: 492 | raise FieldValidationError(errors) 493 | 494 | 495 | class Counter(IntegerField): 496 | def __init__(self, **kwargs): 497 | super(Counter, self).__init__(**kwargs) 498 | if not kwargs.has_key('default') or self.default is None: 499 | self.default = 0 500 | 501 | def __set__(self, instance, value): 502 | raise AttributeError("can't set a counter.") 503 | 504 | def __get__(self, instance, owner): 505 | if not instance.is_new(): 506 | v = instance.db.hget(instance.key(), self.name) 507 | if v is None: 508 | return 0 509 | return int(v) 510 | else: 511 | return 0 512 | 513 | 514 | ZINDEXABLE = (IntegerField, DateTimeField, DateField, FloatField, Counter) 515 | -------------------------------------------------------------------------------- /redisco/models/base.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, date 3 | from dateutil.tz import tzutc 4 | import redisco 5 | from redisco.containers import Set, List, SortedSet, NonPersistentList 6 | from .attributes import * 7 | from .key import Key 8 | from .managers import ManagerDescriptor, Manager 9 | from .exceptions import FieldValidationError, MissingID, BadKeyError, WatchError 10 | from .attributes import Counter 11 | 12 | __all__ = ['Model', 'from_key'] 13 | 14 | ZINDEXABLE = (IntegerField, DateTimeField, DateField, FloatField) 15 | 16 | ############################## 17 | # Model Class Initialization # 18 | ############################## 19 | 20 | 21 | def _initialize_attributes(model_class, name, bases, attrs): 22 | """ 23 | Initialize the attributes of the model. 24 | """ 25 | model_class._attributes = {} 26 | 27 | # In case of inheritance, we also add the parent's 28 | # attributes in the list of our attributes 29 | for parent in bases: 30 | if not isinstance(parent, ModelBase): 31 | continue 32 | for k, v in parent._attributes.iteritems(): 33 | model_class._attributes[k] = v 34 | 35 | for k, v in attrs.iteritems(): 36 | if isinstance(v, Attribute): 37 | model_class._attributes[k] = v 38 | v.name = v.name or k 39 | 40 | def _initialize_referenced(model_class, attribute): 41 | """ 42 | Adds a property to the target of a reference field that 43 | returns the list of associated objects. 44 | """ 45 | # this should be a descriptor 46 | def _related_objects(self): 47 | return (model_class.objects 48 | .filter(**{attribute.attname: self.id})) 49 | 50 | klass = attribute._target_type 51 | if isinstance(klass, basestring): 52 | return (klass, model_class, attribute) 53 | else: 54 | related_name = (attribute.related_name or 55 | model_class.__name__.lower() + '_set') 56 | if not hasattr(klass, related_name): 57 | setattr(klass, related_name, 58 | property(_related_objects)) 59 | 60 | 61 | def _initialize_lists(model_class, name, bases, attrs): 62 | """ 63 | Stores the list fields descriptors of a model. 64 | """ 65 | model_class._lists = {} 66 | for parent in bases: 67 | if not isinstance(parent, ModelBase): 68 | continue 69 | for k, v in parent._lists.iteritems(): 70 | model_class._lists[k] = v 71 | 72 | for k, v in attrs.iteritems(): 73 | if isinstance(v, ListField): 74 | model_class._lists[k] = v 75 | v.name = v.name or k 76 | 77 | 78 | def _initialize_references(model_class, name, bases, attrs): 79 | """ 80 | Stores the list of reference field descriptors of a model. 81 | """ 82 | model_class._references = {} 83 | h = {} 84 | deferred = [] 85 | for parent in bases: 86 | if not isinstance(parent, ModelBase): 87 | continue 88 | for k, v in parent._references.iteritems(): 89 | model_class._references[k] = v 90 | # We skip updating the attributes since this is done 91 | # already at the parent construction and then copied back 92 | # in the subclass 93 | refd = _initialize_referenced(model_class, v) 94 | if refd: 95 | deferred.append(refd) 96 | 97 | for k, v in attrs.iteritems(): 98 | if isinstance(v, ReferenceField): 99 | model_class._references[k] = v 100 | v.name = v.name or k 101 | att = Attribute(name=v.attname) 102 | h[v.attname] = att 103 | setattr(model_class, v.attname, att) 104 | refd = _initialize_referenced(model_class, v) 105 | if refd: 106 | deferred.append(refd) 107 | attrs.update(h) 108 | return deferred 109 | 110 | 111 | def _initialize_indices(model_class, name, bases, attrs): 112 | """ 113 | Stores the list of indexed attributes. 114 | """ 115 | model_class._indices = [] 116 | for parent in bases: 117 | if not isinstance(parent, ModelBase): 118 | continue 119 | for k, v in parent._attributes.iteritems(): 120 | if v.indexed: 121 | model_class._indices.append(k) 122 | for k, v in parent._lists.iteritems(): 123 | if v.indexed: 124 | model_class._indices.append(k) 125 | 126 | for k, v in attrs.iteritems(): 127 | if isinstance(v, (Attribute, ListField)) and v.indexed: 128 | model_class._indices.append(k) 129 | if model_class._meta['indices']: 130 | model_class._indices.extend(model_class._meta['indices']) 131 | 132 | 133 | def _initialize_counters(model_class, name, bases, attrs): 134 | """ 135 | Stores the list of counter fields. 136 | """ 137 | model_class._counters = [] 138 | 139 | for parent in bases: 140 | if not isinstance(parent, ModelBase): 141 | continue 142 | for c in parent._counters: 143 | model_class._counters.append(c) 144 | 145 | for k, v in attrs.iteritems(): 146 | if isinstance(v, Counter): 147 | # When subclassing, we want to override the attributes 148 | if k in model_class._counters: 149 | model_class._counters.remove(k) 150 | model_class._counters.append(k) 151 | 152 | 153 | def _initialize_key(model_class, name): 154 | """ 155 | Initializes the key of the model. 156 | """ 157 | model_class._key = Key(model_class._meta['key'] or name) 158 | 159 | 160 | def _initialize_manager(model_class, name, bases, attrs): 161 | """ 162 | Initializes the manager attributes of the model. 163 | Defaults to an instance of `Manager' attached to `objects'. 164 | 165 | A manager name is created using the `__attr_name__' class 166 | attribute, or the class name in case the attribute is missing. 167 | """ 168 | 169 | model_class.objects = ManagerDescriptor(Manager(model_class)) 170 | for key, val in attrs.iteritems(): 171 | if isinstance(val, type) and issubclass(val, Manager): 172 | attr_name = getattr(val, "__attr_name__", key.lower()) 173 | descriptor = ManagerDescriptor(val(model_class)) 174 | setattr(model_class, attr_name, descriptor) 175 | 176 | 177 | class ModelOptions(object): 178 | """Handles options defined in Meta class of the model. 179 | 180 | Example: 181 | 182 | >>> from redisco import models 183 | >>> import redis 184 | >>> class Person(models.Model): 185 | ... name = models.Attribute() 186 | ... class Meta: 187 | ... indices = ('full_name',) 188 | ... db = redis.Redis(host='localhost', port=29909) 189 | 190 | """ 191 | def __init__(self, meta): 192 | self.meta = meta 193 | 194 | def get_field(self, field_name): 195 | if self.meta is None: 196 | return None 197 | try: 198 | return self.meta.__dict__[field_name] 199 | except KeyError: 200 | return None 201 | __getitem__ = get_field 202 | 203 | 204 | _deferred_refs = [] 205 | 206 | class ModelBase(type): 207 | """ 208 | Metaclass of the Model. 209 | """ 210 | 211 | def __init__(cls, name, bases, attrs): 212 | super(ModelBase, cls).__init__(name, bases, attrs) 213 | global _deferred_refs 214 | cls._meta = ModelOptions(attrs.pop('Meta', None)) 215 | deferred = _initialize_references(cls, name, bases, attrs) 216 | _deferred_refs.extend(deferred) 217 | _initialize_attributes(cls, name, bases, attrs) 218 | _initialize_counters(cls, name, bases, attrs) 219 | _initialize_lists(cls, name, bases, attrs) 220 | _initialize_indices(cls, name, bases, attrs) 221 | _initialize_key(cls, name) 222 | _initialize_manager(cls, name, bases, attrs) 223 | # if targeted by a reference field using a string, 224 | # override for next try 225 | for target, model_class, att in _deferred_refs: 226 | if name == target: 227 | att._target_type = cls 228 | _initialize_referenced(model_class, att) 229 | 230 | def __getitem__(self, id): 231 | return self.objects.get_by_id(id) 232 | 233 | class Model(object): 234 | __metaclass__ = ModelBase 235 | 236 | def __init__(self, **kwargs): 237 | self.update_attributes(**kwargs) 238 | 239 | def is_valid(self): 240 | """ 241 | Returns True if all the fields are valid, otherwise 242 | errors are in the 'errors' attribute 243 | 244 | It first validates the fields (required, unique, etc.) 245 | and then calls the validate method. 246 | 247 | >>> from redisco import models 248 | >>> def validate_me(field, value): 249 | ... if value == "Invalid": 250 | ... return (field, "Invalid value") 251 | ... 252 | >>> class Foo(models.Model): 253 | ... bar = models.Attribute(validator=validate_me) 254 | ... 255 | >>> f = Foo() 256 | >>> f.bar = "Invalid" 257 | >>> f.save() 258 | False 259 | >>> f.errors 260 | ['bar', 'Invalid value'] 261 | 262 | .. WARNING:: 263 | You may want to use ``validate`` described below to validate your model 264 | 265 | """ 266 | self._errors = [] 267 | for field in self.fields: 268 | try: 269 | field.validate(self) 270 | except FieldValidationError as e: 271 | self._errors.extend(e.errors) 272 | self.validate() 273 | return not bool(self._errors) 274 | 275 | def validate(self): 276 | """ 277 | Overriden in the model class. 278 | The function is here to help you validate your model. The validation should add errors to self._errors. 279 | 280 | Example: 281 | 282 | >>> from redisco import models 283 | >>> class Foo(models.Model): 284 | ... name = models.Attribute(required=True) 285 | ... def validate(self): 286 | ... if self.name == "Invalid": 287 | ... self._errors.append(('name', 'cannot be Invalid')) 288 | ... 289 | >>> f = Foo(name="Invalid") 290 | >>> f.save() 291 | False 292 | >>> f.errors 293 | [('name', 'cannot be Invalid')] 294 | 295 | """ 296 | pass 297 | 298 | def update_attributes(self, **kwargs): 299 | """ 300 | Updates the attributes of the model. 301 | 302 | >>> from redisco import models 303 | >>> class Foo(models.Model): 304 | ... name = models.Attribute() 305 | ... title = models.Attribute() 306 | ... 307 | >>> f = Foo(name="Einstein", title="Mr.") 308 | >>> f.update_attributes(name="Tesla") 309 | >>> f.name 310 | 'Tesla' 311 | """ 312 | attrs = self.attributes.values() + self.lists.values() \ 313 | + self.references.values() 314 | for att in attrs: 315 | if att.name in kwargs: 316 | att.__set__(self, kwargs[att.name]) 317 | 318 | def save(self): 319 | """ 320 | Saves the instance to the datastore with the following steps: 321 | 1. Validate all the fields 322 | 2. Assign an ID if the object is new 323 | 3. Save to the datastore. 324 | 325 | >>> from redisco import models 326 | >>> class Foo(models.Model): 327 | ... name = models.Attribute() 328 | ... title = models.Attribute() 329 | ... 330 | >>> f = Foo(name="Einstein", title="Mr.") 331 | >>> f.save() 332 | True 333 | >>> f.delete() 334 | """ 335 | if not self.is_valid(): 336 | return False 337 | _new = self.is_new() 338 | if _new: 339 | self._initialize_id() 340 | with Mutex(self): 341 | self._write(_new) 342 | return True 343 | 344 | def key(self, att=None): 345 | """ 346 | Returns the Redis key where the values are stored. 347 | 348 | >>> from redisco import models 349 | >>> class Foo(models.Model): 350 | ... name = models.Attribute() 351 | ... title = models.Attribute() 352 | ... 353 | >>> f = Foo(name="Einstein", title="Mr.") 354 | >>> f.save() 355 | True 356 | >>> f.key() == "%s:%s" % (f.__class__.__name__, f.id) 357 | True 358 | """ 359 | if att is not None: 360 | return self._key[self.id][att] 361 | else: 362 | return self._key[self.id] 363 | 364 | def delete(self): 365 | """Deletes the object from the datastore.""" 366 | pipeline = self.db.pipeline() 367 | self._delete_from_indices(pipeline) 368 | self._delete_membership(pipeline) 369 | pipeline.delete(self.key()) 370 | pipeline.execute() 371 | 372 | def is_new(self): 373 | """ 374 | Returns True if the instance is new. 375 | 376 | Newness is based on the presence of the _id attribute. 377 | """ 378 | return not hasattr(self, '_id') 379 | 380 | def incr(self, att, val=1): 381 | """ 382 | Increments a counter. 383 | 384 | >>> from redisco import models 385 | >>> class Foo(models.Model): 386 | ... cnt = models.Counter() 387 | ... 388 | >>> f = Foo() 389 | >>> f.save() 390 | True 391 | >>> f.incr('cnt', 10) 392 | >>> f.cnt 393 | 10 394 | >>> f.delete() 395 | """ 396 | if att not in self.counters: 397 | raise ValueError("%s is not a counter.") 398 | self.db.hincrby(self.key(), att, val) 399 | 400 | def decr(self, att, val=1): 401 | """ 402 | Decrements a counter. 403 | 404 | >>> from redisco import models 405 | >>> class Foo(models.Model): 406 | ... cnt = models.Counter() 407 | ... 408 | >>> f = Foo() 409 | >>> f.save() 410 | True 411 | >>> f.incr('cnt', 10) 412 | >>> f.cnt 413 | 10 414 | >>> f.decr('cnt', 2) 415 | >>> f.cnt 416 | 8 417 | >>> f.delete() 418 | """ 419 | self.incr(att, -1 * val) 420 | 421 | 422 | @property 423 | def attributes_dict(self): 424 | """ 425 | Returns the mapping of the model attributes and their 426 | values. 427 | 428 | >>> from redisco import models 429 | >>> class Foo(models.Model): 430 | ... name = models.Attribute() 431 | ... title = models.Attribute() 432 | ... 433 | >>> f = Foo(name="Einstein", title="Mr.") 434 | >>> f.attributes_dict 435 | {'name': 'Einstein', 'title': 'Mr.'} 436 | 437 | 438 | .. NOTE: the key ``id`` is present *only if* the object has been saved before. 439 | 440 | """ 441 | h = {} 442 | for k in self.attributes.keys(): 443 | h[k] = getattr(self, k) 444 | for k in self.lists.keys(): 445 | h[k] = getattr(self, k) 446 | for k in self.references.keys(): 447 | h[k] = getattr(self, k) 448 | if 'id' not in self.attributes.keys() and not self.is_new(): 449 | h['id'] = self.id 450 | return h 451 | 452 | 453 | @property 454 | def id(self): 455 | """Returns the id of the instance. 456 | 457 | Raises MissingID if the instance is new. 458 | """ 459 | if not hasattr(self, '_id'): 460 | raise MissingID 461 | return self._id 462 | 463 | @id.setter 464 | def id(self, val): 465 | """ 466 | Setting the id for the object will fetch it from the datastorage. 467 | """ 468 | self._id = str(val) 469 | stored_attrs = self.db.hgetall(self.key()) 470 | attrs = self.attributes.values() 471 | for att in attrs: 472 | if att.name in stored_attrs and not isinstance(att, Counter): 473 | att.__set__(self, att.typecast_for_read(stored_attrs[att.name])) 474 | 475 | @property 476 | def attributes(self): 477 | """Return the attributes of the model. 478 | 479 | Returns a dict with models attribute name as keys 480 | and attribute descriptors as values. 481 | """ 482 | return dict(self._attributes) 483 | 484 | @property 485 | def lists(self): 486 | """ 487 | Returns the lists of the model. 488 | 489 | Returns a dict with models attribute name as keys 490 | and ListField descriptors as values. 491 | """ 492 | return dict(self._lists) 493 | 494 | @property 495 | def indices(self): 496 | """ 497 | Return a list of the indices of the model. 498 | ie: all attributes with index=True. 499 | """ 500 | return self._indices 501 | 502 | @property 503 | def references(self): 504 | """Returns the mapping of reference fields of the model.""" 505 | return self._references 506 | 507 | @property 508 | def db(self): 509 | """Returns the Redis client used by the model.""" 510 | return redisco.get_client() if not self._meta['db'] else self._meta['db'] 511 | 512 | @property 513 | def errors(self): 514 | """Returns the list of errors after validation.""" 515 | if not hasattr(self, '_errors'): 516 | self.is_valid() 517 | return self._errors 518 | 519 | @property 520 | def fields(self): 521 | """Returns the list of field names of the model.""" 522 | return (self.attributes.values() + self.lists.values() 523 | + self.references.values()) 524 | 525 | @property 526 | def counters(self): 527 | """Returns the mapping of the counters.""" 528 | return self._counters 529 | 530 | ################# 531 | # Class Methods # 532 | ################# 533 | 534 | @classmethod 535 | def exists(cls, id): 536 | """Checks if the model with id exists.""" 537 | return bool((cls._meta['db'] or redisco.get_client()).exists(cls._key[str(id)]) or 538 | (cls._meta['db'] or redisco.get_client()).sismember(cls._key['all'], str(id))) 539 | 540 | ################### 541 | # Private methods # 542 | ################### 543 | 544 | def _initialize_id(self): 545 | """Initializes the id of the instance.""" 546 | self._id = str(self.db.incr(self._key['id'])) 547 | 548 | def _write(self, _new=False): 549 | """Writes the values of the attributes to the datastore. 550 | 551 | This method also creates the indices and saves the lists 552 | associated to the object. 553 | """ 554 | pipeline = self.db.pipeline() 555 | self._create_membership(pipeline) 556 | self._update_indices(pipeline) 557 | h = {} 558 | # attributes 559 | for k, v in self.attributes.iteritems(): 560 | if isinstance(v, DateTimeField): 561 | if v.auto_now: 562 | setattr(self, k, datetime.now(tz=tzutc())) 563 | if v.auto_now_add and _new: 564 | setattr(self, k, datetime.now(tz=tzutc())) 565 | elif isinstance(v, DateField): 566 | if v.auto_now: 567 | setattr(self, k, datetime.now(tz=tzutc())) 568 | if v.auto_now_add and _new: 569 | setattr(self, k, datetime.now(tz=tzutc())) 570 | for_storage = getattr(self, k) 571 | if for_storage is not None: 572 | h[k] = v.typecast_for_storage(for_storage) 573 | # indices 574 | for index in self.indices: 575 | if index not in self.lists and index not in self.attributes: 576 | v = getattr(self, index) 577 | if callable(v): 578 | v = v() 579 | if v: 580 | try: 581 | h[index] = unicode(v) 582 | except UnicodeError: 583 | h[index] = unicode(v.decode('utf-8')) 584 | pipeline.delete(self.key()) 585 | if h: 586 | pipeline.hmset(self.key(), h) 587 | 588 | # lists 589 | for k, v in self.lists.iteritems(): 590 | l = List(self.key()[k], pipeline=pipeline) 591 | l.clear() 592 | values = getattr(self, k) 593 | if values: 594 | if v._redisco_model: 595 | l.extend([item.id for item in values]) 596 | else: 597 | l.extend(values) 598 | pipeline.execute() 599 | 600 | ############## 601 | # Membership # 602 | ############## 603 | 604 | def _create_membership(self, pipeline=None): 605 | """Adds the id of the object to the set of all objects of the same 606 | class. 607 | """ 608 | Set(self._key['all'], pipeline=pipeline).add(self.id) 609 | 610 | def _delete_membership(self, pipeline=None): 611 | """Removes the id of the object to the set of all objects of the 612 | same class. 613 | """ 614 | Set(self._key['all'], pipeline=pipeline).remove(self.id) 615 | 616 | ############ 617 | # INDICES! # 618 | ############ 619 | 620 | def _update_indices(self, pipeline=None): 621 | """Updates the indices of the object.""" 622 | self._delete_from_indices(pipeline) 623 | self._add_to_indices(pipeline) 624 | 625 | def _add_to_indices(self, pipeline): 626 | """Adds the base64 encoded values of the indices.""" 627 | for att in self.indices: 628 | self._add_to_index(att, pipeline=pipeline) 629 | 630 | def _add_to_index(self, att, val=None, pipeline=None): 631 | """ 632 | Adds the id to the index. 633 | 634 | This also adds to the _indices set of the object. 635 | """ 636 | index = self._index_key_for(att) 637 | if index is None: 638 | return 639 | t, index = index 640 | if t == 'attribute': 641 | pipeline.sadd(index, self.id) 642 | pipeline.sadd(self.key()['_indices'], index) 643 | elif t == 'list': 644 | for i in index: 645 | pipeline.sadd(i, self.id) 646 | pipeline.sadd(self.key()['_indices'], i) 647 | elif t == 'sortedset': 648 | zindex, index = index 649 | pipeline.sadd(index, self.id) 650 | pipeline.sadd(self.key()['_indices'], index) 651 | descriptor = self.attributes[att] 652 | score = descriptor.typecast_for_storage(getattr(self, att)) 653 | pipeline.zadd(zindex, self.id, score) 654 | pipeline.sadd(self.key()['_zindices'], zindex) 655 | 656 | def _delete_from_indices(self, pipeline): 657 | """Deletes the object's id from the sets(indices) it has been added 658 | to and removes its list of indices (used for housekeeping). 659 | """ 660 | s = Set(self.key()['_indices'], pipeline=self.db) 661 | z = Set(self.key()['_zindices'], pipeline=self.db) 662 | for index in s.members: 663 | pipeline.srem(index, self.id) 664 | for index in z.members: 665 | pipeline.zrem(index, self.id) 666 | pipeline.delete(s.key) 667 | pipeline.delete(z.key) 668 | 669 | def _index_key_for(self, att, value=None): 670 | """Returns a key based on the attribute and its value. 671 | 672 | The key is used for indexing. 673 | """ 674 | if value is None: 675 | value = getattr(self, att) 676 | if callable(value): 677 | value = value() 678 | if value is None: 679 | return None 680 | if att not in self.lists: 681 | return self._get_index_key_for_non_list_attr(att, value) 682 | else: 683 | return self._tuple_for_index_key_attr_list(att, value) 684 | 685 | def _get_index_key_for_non_list_attr(self, att, value): 686 | descriptor = self.attributes.get(att) 687 | if descriptor and isinstance(descriptor, ZINDEXABLE): 688 | sval = descriptor.typecast_for_storage(value) 689 | return self._tuple_for_index_key_attr_zset(att, value, sval) 690 | elif descriptor: 691 | val = descriptor.typecast_for_storage(value) 692 | return self._tuple_for_index_key_attr_val(att, val) 693 | else: 694 | # this is non-attribute index defined in Meta 695 | return self._tuple_for_index_key_attr_val(att, value) 696 | 697 | def _tuple_for_index_key_attr_val(self, att, val): 698 | return ('attribute', self._index_key_for_attr_val(att, val)) 699 | 700 | def _tuple_for_index_key_attr_list(self, att, val): 701 | return ('list', [self._index_key_for_attr_val(att, e) for e in val]) 702 | 703 | def _tuple_for_index_key_attr_zset(self, att, val, sval): 704 | return ('sortedset', 705 | (self._key[att], self._index_key_for_attr_val(att, sval))) 706 | 707 | def _index_key_for_attr_val(self, att, val): 708 | return self._key[att][val] 709 | 710 | ################## 711 | # Python methods # 712 | ################## 713 | 714 | def __hash__(self): 715 | return hash(self.key()) 716 | 717 | def __eq__(self, other): 718 | return isinstance(other, self.__class__) and self.key() == other.key() 719 | 720 | def __ne__(self, other): 721 | return not self.__eq__(other) 722 | 723 | def __repr__(self): 724 | if not self.is_new(): 725 | return "<%s %s>" % (self.key(), self.attributes_dict) 726 | return "<%s %s>" % (self.__class__.__name__, self.attributes_dict) 727 | 728 | 729 | 730 | def get_model_from_key(key): 731 | """Gets the model from a given key.""" 732 | _known_models = {} 733 | model_name = key.split(':', 2)[0] 734 | # populate 735 | for klass in Model.__subclasses__(): 736 | _known_models[klass.__name__] = klass 737 | return _known_models.get(model_name, None) 738 | 739 | 740 | def from_key(key): 741 | """Returns the model instance based on the key. 742 | 743 | Raises BadKeyError if the key is not recognized by 744 | redisco or no defined model can be found. 745 | Returns None if the key could not be found. 746 | """ 747 | model = get_model_from_key(key) 748 | if model is None: 749 | raise BadKeyError 750 | try: 751 | _, id = key.split(':', 2) 752 | id = int(id) 753 | except ValueError: 754 | raise BadKeyError 755 | except TypeError: 756 | raise BadKeyError 757 | return model.objects.get_by_id(id) 758 | 759 | 760 | class Mutex(object): 761 | def __init__(self, instance): 762 | self.instance = instance 763 | 764 | def __enter__(self): 765 | self.lock() 766 | return self 767 | 768 | def __exit__(self, exc_type, exc_value, traceback): 769 | self.unlock() 770 | 771 | def lock(self): 772 | o = self.instance 773 | _lock_key = o.key('_lock') 774 | with o.db.pipeline() as pipe: 775 | while True: 776 | try: 777 | pipe.watch(_lock_key) 778 | if o.db.exists(_lock_key) and not self.lock_has_expired(o.db.get(_lock_key)): 779 | continue 780 | 781 | pipe.multi() 782 | pipe.set(_lock_key, self.lock_timeout).execute() 783 | break 784 | 785 | except WatchError: 786 | time.sleep(0.5) 787 | continue 788 | 789 | def lock_has_expired(self, lock): 790 | if lock is None: 791 | lock = 0. 792 | return float(lock) < time.time() 793 | 794 | def unlock(self): 795 | self.instance.db.delete(self.instance.key('_lock')) 796 | 797 | @property 798 | def lock_timeout(self): 799 | return "%f" % (time.time() + 1.0) 800 | -------------------------------------------------------------------------------- /redisco/models/basetests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | from threading import Thread 4 | import redis 5 | import redisco 6 | import unittest 7 | from datetime import date 8 | from redisco import models 9 | from redisco.models import managers 10 | from redisco.models.base import Mutex 11 | from dateutil.tz import tzlocal 12 | 13 | 14 | class Person(models.Model): 15 | first_name = models.CharField(required=True) 16 | last_name = models.CharField() 17 | active = models.BooleanField(default=False) 18 | 19 | def full_name(self): 20 | return "%s %s" % (self.first_name, self.last_name,) 21 | 22 | class Meta: 23 | indices = ['full_name'] 24 | 25 | class HistoryManager(managers.Manager): 26 | __attr_name__ = "all_objects" 27 | def get_model_set(self): 28 | return super(Person.HistoryManager, self).\ 29 | get_model_set().filter(active=True) 30 | 31 | 32 | class RediscoTestCase(unittest.TestCase): 33 | def setUp(self): 34 | self.client = redisco.get_client() 35 | self.client.flushdb() 36 | 37 | def tearDown(self): 38 | self.client.flushdb() 39 | 40 | 41 | class ModelTestCase(RediscoTestCase): 42 | 43 | def test_key(self): 44 | self.assertEqual('Person', Person._key) 45 | 46 | def test_is_new(self): 47 | p = Person(first_name="Darken", last_name="Rahl") 48 | self.assertTrue(p.is_new()) 49 | 50 | def test_CharFields(self): 51 | person = Person(first_name="Granny", last_name="Goose") 52 | self.assertEqual("Granny", person.first_name) 53 | self.assertEqual("Goose", person.last_name) 54 | 55 | def test_save(self): 56 | person1 = Person(first_name="Granny", last_name="Goose") 57 | person1.save() 58 | person2 = Person(first_name="Jejomar") 59 | person2.save() 60 | 61 | self.assertEqual('1', person1.id) 62 | self.assertEqual('2', person2.id) 63 | 64 | jejomar = Person.objects.get_by_id('2') 65 | self.assertEqual(None, jejomar.last_name) 66 | 67 | def test_save_succeed(self): 68 | person = Person(first_name="Granny") 69 | self.assertTrue(person.save()) 70 | 71 | def test_save_fail(self): 72 | person = Person(last_name="Goose") 73 | self.assertFalse(person.save()) 74 | self.assertEqual([('first_name', 'required')], person.errors) 75 | 76 | def test_unicode(self): 77 | p = Person(first_name=u"Niña", last_name="Jose") 78 | self.assert_(p.save()) 79 | g = Person.objects.create(first_name="Granny", last_name="Goose") 80 | self.assert_(g) 81 | 82 | p = Person.objects.filter(first_name=u"Niña").first() 83 | self.assert_(p) 84 | self.assert_(isinstance(p.full_name(), unicode)) 85 | self.assertEqual(u"Niña Jose", p.full_name()) 86 | 87 | def test_repr(self): 88 | person1 = Person(first_name="Granny", last_name="Goose") 89 | self.assertEqual("", 90 | repr(person1)) 91 | 92 | self.assert_(person1.save()) 93 | self.assertEqual("", 94 | repr(person1)) 95 | 96 | def test_update(self): 97 | person1 = Person(first_name="Granny", last_name="Goose") 98 | person1.save() 99 | 100 | p = Person.objects.get_by_id('1') 101 | p.first_name = "Morgan" 102 | p.last_name = None 103 | assert p.save() 104 | 105 | p = Person.objects.get_by_id(p.id) 106 | self.assertEqual("Morgan", p.first_name) 107 | self.assertEqual(None, p.last_name) 108 | 109 | def test_default_CharField_val(self): 110 | class User(models.Model): 111 | views = models.IntegerField(default=199) 112 | liked = models.BooleanField(default=True) 113 | disliked = models.BooleanField(default=False) 114 | 115 | u = User() 116 | self.assertEqual(True, u.liked) 117 | self.assertEqual(False, u.disliked) 118 | self.assertEqual(199, u.views) 119 | assert u.save() 120 | 121 | u = User.objects.all()[0] 122 | self.assertEqual(True, u.liked) 123 | self.assertEqual(False, u.disliked) 124 | self.assertEqual(199, u.views) 125 | 126 | def test_callable_default_CharField_val(self): 127 | class User(models.Model): 128 | views = models.IntegerField(default=lambda: 199) 129 | liked = models.BooleanField(default=lambda: True) 130 | disliked = models.BooleanField(default=lambda: False) 131 | 132 | u = User() 133 | self.assertEqual(True, u.liked) 134 | self.assertEqual(False, u.disliked) 135 | self.assertEqual(199, u.views) 136 | assert u.save() 137 | 138 | u = User.objects.all()[0] 139 | self.assertEqual(True, u.liked) 140 | self.assertEqual(False, u.disliked) 141 | self.assertEqual(199, u.views) 142 | 143 | def test_getitem(self): 144 | person1 = Person(first_name="Granny", last_name="Goose") 145 | person1.save() 146 | person2 = Person(first_name="Jejomar", last_name="Binay") 147 | person2.save() 148 | 149 | p1 = Person.objects.get_by_id(1) 150 | p2 = Person.objects.get_by_id(2) 151 | 152 | self.assertEqual('Jejomar', p2.first_name) 153 | self.assertEqual('Binay', p2.last_name) 154 | 155 | self.assertEqual('Granny', p1.first_name) 156 | self.assertEqual('Goose', p1.last_name) 157 | 158 | def test_multiple_managers_exist(self): 159 | self.assertTrue(isinstance(Person.objects, managers.Manager)) 160 | self.assertTrue(isinstance(Person.all_objects, managers.Manager)) 161 | 162 | def test_history_manager(self): 163 | self.assertEqual(Person.all_objects.get_by_id(1), None) 164 | 165 | def test_manager_create(self): 166 | person = Person.objects.create(first_name="Granny", last_name="Goose") 167 | 168 | p1 = Person.objects.get_by_id(1) 169 | self.assertEqual('Granny', p1.first_name) 170 | self.assertEqual('Goose', p1.last_name) 171 | 172 | def test_indices(self): 173 | person = Person.objects.create(first_name="Granny", last_name="Goose") 174 | db = person.db 175 | key = person.key() 176 | ckey = Person._key 177 | 178 | index = 'Person:first_name:%s' % "Granny" 179 | self.assertTrue(index in db.smembers(key['_indices'])) 180 | self.assertTrue("1" in db.smembers(index)) 181 | 182 | def test_delete(self): 183 | Person.objects.create(first_name="Granny", last_name="Goose") 184 | Person.objects.create(first_name="Clark", last_name="Kent") 185 | Person.objects.create(first_name="Granny", last_name="Mommy") 186 | Person.objects.create(first_name="Granny", last_name="Kent") 187 | 188 | for person in Person.objects.all(): 189 | person.delete() 190 | 191 | self.assertEqual(0, self.client.scard('Person:all')) 192 | 193 | class Event(models.Model): 194 | name = models.CharField(required=True) 195 | created_on = models.DateField(required=True) 196 | 197 | from datetime import date 198 | 199 | Event.objects.create(name="Event #1", created_on=date.today()) 200 | Event.objects.create(name="Event #2", created_on=date.today()) 201 | Event.objects.create(name="Event #3", created_on=date.today()) 202 | Event.objects.create(name="Event #4", created_on=date.today()) 203 | 204 | for event in Event.objects.all(): 205 | event.delete() 206 | 207 | self.assertEqual(0, self.client.zcard("Event:created_on")) 208 | 209 | def test_filter(self): 210 | Person.objects.create(first_name="Granny", last_name="Goose") 211 | Person.objects.create(first_name="Clark", last_name="Kent") 212 | Person.objects.create(first_name="Granny", last_name="Mommy") 213 | Person.objects.create(first_name="Granny", last_name="Kent") 214 | persons = Person.objects.filter(first_name="Granny") 215 | 216 | self.assertEqual('1', persons[0].id) 217 | self.assertEqual(3, len(persons)) 218 | 219 | persons = Person.objects.filter(first_name="Clark") 220 | self.assertEqual(1, len(persons)) 221 | 222 | # by index 223 | persons = Person.objects.filter(full_name="Granny Mommy") 224 | self.assertEqual(1, len(persons)) 225 | self.assertEqual("Granny Mommy", persons[0].full_name()) 226 | 227 | def test_exclude(self): 228 | Person.objects.create(first_name="Granny", last_name="Goose") 229 | Person.objects.create(first_name="Clark", last_name="Kent") 230 | Person.objects.create(first_name="Granny", last_name="Mommy") 231 | Person.objects.create(first_name="Granny", last_name="Kent") 232 | persons = Person.objects.exclude(first_name="Granny") 233 | 234 | self.assertEqual('2', persons[0].id) 235 | self.assertEqual(1, len(persons)) 236 | 237 | persons = Person.objects.exclude(first_name="Clark") 238 | self.assertEqual(3, len(persons)) 239 | 240 | # by index 241 | persons = Person.objects.exclude(full_name="Granny Mommy") 242 | self.assertEqual(3, len(persons)) 243 | self.assertEqual("Granny Goose", persons[0].full_name()) 244 | self.assertEqual("Clark Kent", persons[1].full_name()) 245 | self.assertEqual("Granny Kent", persons[2].full_name()) 246 | 247 | # mixed 248 | Person.objects.create(first_name="Granny", last_name="Pacman") 249 | persons = (Person.objects.filter(first_name="Granny") 250 | .exclude(last_name="Mommy")) 251 | self.assertEqual(3, len(persons)) 252 | 253 | 254 | def test_first(self): 255 | Person.objects.create(first_name="Granny", last_name="Goose") 256 | Person.objects.create(first_name="Clark", last_name="Kent") 257 | Person.objects.create(first_name="Granny", last_name="Mommy") 258 | Person.objects.create(first_name="Granny", last_name="Kent") 259 | granny = Person.objects.filter(first_name="Granny").first() 260 | self.assertEqual('1', granny.id) 261 | lana = Person.objects.filter(first_name="Lana").first() 262 | self.assertFalse(lana) 263 | 264 | 265 | def test_iter(self): 266 | Person.objects.create(first_name="Granny", last_name="Goose") 267 | Person.objects.create(first_name="Clark", last_name="Kent") 268 | Person.objects.create(first_name="Granny", last_name="Mommy") 269 | Person.objects.create(first_name="Granny", last_name="Kent") 270 | 271 | for person in Person.objects.all(): 272 | self.assertTrue(person.full_name() in ("Granny Goose", 273 | "Clark Kent", "Granny Mommy", "Granny Kent",)) 274 | 275 | def test_sort(self): 276 | Person.objects.create(first_name="Zeddicus", last_name="Zorander") 277 | Person.objects.create(first_name="Richard", last_name="Cypher") 278 | Person.objects.create(first_name="Richard", last_name="Rahl") 279 | Person.objects.create(first_name="Kahlan", last_name="Amnell") 280 | 281 | res = Person.objects.order('first_name').all() 282 | self.assertEqual("Kahlan", res[0].first_name) 283 | self.assertEqual("Richard", res[1].first_name) 284 | self.assertEqual("Richard", res[2].first_name) 285 | self.assertEqual("Zeddicus Zorander", res[3].full_name()) 286 | 287 | res = Person.objects.order('-full_name').all() 288 | self.assertEqual("Zeddicus Zorander", res[0].full_name()) 289 | self.assertEqual("Richard Rahl", res[1].full_name()) 290 | self.assertEqual("Richard Cypher", res[2].full_name()) 291 | self.assertEqual("Kahlan Amnell", res[3].full_name()) 292 | 293 | def test_all(self): 294 | person1 = Person(first_name="Granny", last_name="Goose") 295 | person1.save() 296 | person2 = Person(first_name="Jejomar", last_name="Binay") 297 | person2.save() 298 | 299 | all = Person.objects.all() 300 | self.assertEqual(list([person1, person2]), list(all)) 301 | 302 | def test_limit(self): 303 | Person.objects.create(first_name="Zeddicus", last_name="Zorander") 304 | Person.objects.create(first_name="Richard", last_name="Cypher") 305 | Person.objects.create(first_name="Richard", last_name="Rahl") 306 | Person.objects.create(first_name="Kahlan", last_name="Amnell") 307 | 308 | res = Person.objects.order('first_name').all().limit(3) 309 | self.assertEqual(3, len(res)) 310 | self.assertEqual("Kahlan", res[0].first_name) 311 | self.assertEqual("Richard", res[1].first_name) 312 | self.assertEqual("Richard", res[2].first_name) 313 | 314 | res = Person.objects.order('first_name').limit(3, offset=1) 315 | self.assertEqual(3, len(res)) 316 | self.assertEqual("Richard", res[0].first_name) 317 | self.assertEqual("Richard", res[1].first_name) 318 | self.assertEqual("Zeddicus", res[2].first_name) 319 | 320 | 321 | def test_integer_field(self): 322 | class Character(models.Model): 323 | n = models.IntegerField() 324 | m = models.CharField() 325 | 326 | Character.objects.create(n=1998, m="A") 327 | Character.objects.create(n=3100, m="b") 328 | Character.objects.create(n=1, m="C") 329 | 330 | chars = Character.objects.all() 331 | self.assertEqual(3, len(chars)) 332 | self.assertEqual(1998, chars[0].n) 333 | self.assertEqual("A", chars[0].m) 334 | 335 | def test_sort_by_int(self): 336 | class Exam(models.Model): 337 | score = models.IntegerField() 338 | total_score = models.IntegerField() 339 | 340 | def percent(self): 341 | return int((float(self.score) / self.total_score) * 100) 342 | 343 | class Meta: 344 | indices = ('percent',) 345 | 346 | Exam.objects.create(score=9, total_score=100) 347 | Exam.objects.create(score=99, total_score=100) 348 | Exam.objects.create(score=75, total_score=100) 349 | Exam.objects.create(score=33, total_score=100) 350 | Exam.objects.create(score=95, total_score=100) 351 | 352 | exams = Exam.objects.order('score') 353 | self.assertEqual([9, 33, 75, 95, 99,], [exam.score for exam in exams]) 354 | filtered = Exam.objects.zfilter(score__in=(10, 96)) 355 | self.assertEqual(3, len(filtered)) 356 | 357 | 358 | def test_filter_date(self): 359 | from datetime import datetime 360 | 361 | class Post(models.Model): 362 | name = models.CharField() 363 | date = models.DateTimeField() 364 | 365 | dates = ( 366 | datetime(2010, 1, 20, 1, 40, 0), 367 | datetime(2010, 2, 20, 1, 40, 0), 368 | datetime(2010, 1, 26, 1, 40, 0), 369 | datetime(2009, 12, 21, 1, 40, 0), 370 | datetime(2010, 1, 10, 1, 40, 0), 371 | datetime(2010, 5, 20, 1, 40, 0), 372 | ) 373 | 374 | i = 0 375 | for date in dates: 376 | Post.objects.create(name="Post#%d" % i, date=date) 377 | i += 1 378 | 379 | self.assertEqual([Post.objects.get_by_id(4)], 380 | list(Post.objects.filter(date= 381 | datetime(2009, 12, 21, 1, 40, 0)))) 382 | 383 | lt = (0, 2, 3, 4) 384 | res = [Post.objects.get_by_id(l + 1) for l in lt] 385 | self.assertEqual(set(res), 386 | set(Post.objects.zfilter( 387 | date__lt=datetime(2010, 1, 30)))) 388 | 389 | def test_validation(self): 390 | class Person(models.Model): 391 | name = models.CharField(required=True) 392 | p = Person(name="Kokoy") 393 | self.assertTrue(p.is_valid()) 394 | 395 | p = Person() 396 | self.assertFalse(p.is_valid()) 397 | self.assertTrue(('name', 'required') in p.errors) 398 | 399 | def test_errors(self): 400 | class Person(models.Model): 401 | name = models.CharField(required=True, unique=True) 402 | p = Person.objects.create(name="Chuck") 403 | self.assertFalse(p.errors) 404 | 405 | p = Person(name="John") 406 | self.assertFalse(p.errors) 407 | p.name = "Chuck" # name should be unique 408 | # this doesn't work: 409 | #self.assertEquals(not p.errors, p.is_valid()) 410 | # but this works: 411 | self.assertEqual(p.is_valid(), not p.errors) 412 | 413 | def test_custom_validation(self): 414 | class Ninja(models.Model): 415 | def validator(field_name, age): 416 | if not age or age >= 10: 417 | return ((field_name, 'must be below 10'),) 418 | age = models.IntegerField(required=True, validator=validator) 419 | 420 | nin1 = Ninja(age=9) 421 | self.assertTrue(nin1.is_valid()) 422 | 423 | nin2 = Ninja(age=10) 424 | self.assertFalse(nin2.is_valid()) 425 | self.assertTrue(('age', 'must be below 10') in nin2.errors) 426 | 427 | def test_overriden_validation(self): 428 | class Ninja(models.Model): 429 | age = models.IntegerField(required=True) 430 | 431 | def validate(self): 432 | if self.age >= 10: 433 | self._errors.append(('age', 'must be below 10')) 434 | 435 | 436 | nin1 = Ninja(age=9) 437 | self.assertTrue(nin1.is_valid()) 438 | 439 | nin2 = Ninja(age=10) 440 | self.assertFalse(nin2.is_valid()) 441 | self.assertTrue(('age', 'must be below 10') in nin2.errors) 442 | 443 | def test_falsy_value_type_validation(self): 444 | class Person(models.Model): 445 | age = models.IntegerField() 446 | for val in ('', {}, []): 447 | p = Person(age=val) 448 | self.assertFalse(p.is_valid()) 449 | self.assertEqual([('age', 'bad type')], p.errors) 450 | 451 | 452 | def test_load_object_from_key(self): 453 | class Schedule(models.Model): 454 | att = models.CharField() 455 | 456 | class PaperType(models.Model): 457 | att = models.CharField() 458 | 459 | assert Schedule.objects.create(att="dinuguan") 460 | assert Schedule.objects.create(att="chicharon") 461 | assert Schedule.objects.create(att="Pizza") 462 | assert Schedule.objects.create(att="Pasta") 463 | assert Schedule.objects.create(att="Veggies") 464 | 465 | assert PaperType.objects.create(att="glossy") 466 | assert PaperType.objects.create(att="large") 467 | assert PaperType.objects.create(att="huge") 468 | assert PaperType.objects.create(att="A6") 469 | assert PaperType.objects.create(att="A9") 470 | 471 | o = models.from_key("Schedule:1") 472 | assert o 473 | self.assertEqual('1', o.id) 474 | self.assertEqual(Schedule, type(o)) 475 | o = models.from_key("PaperType:1") 476 | self.assertEqual('1', o.id) 477 | self.assertEqual(PaperType, type(o)) 478 | o = models.from_key("Schedule:4") 479 | self.assertEqual('4', o.id) 480 | self.assertEqual(Schedule, type(o)) 481 | o = models.from_key("PaperType:5") 482 | self.assertEqual('5', o.id) 483 | self.assertEqual(PaperType, type(o)) 484 | o = models.from_key("PaperType:6") 485 | self.assertTrue(o is None) 486 | 487 | def boom(): 488 | models.from_key("some arbitrary key") 489 | from redisco.models.exceptions import BadKeyError 490 | self.assertRaises(BadKeyError, boom) 491 | 492 | def test_uniqueness_validation(self): 493 | class Student(models.Model): 494 | student_id = models.CharField(unique=True) 495 | 496 | student = Student.objects.create(student_id="042231") 497 | self.assert_(student) 498 | 499 | student = Student(student_id="042231") 500 | self.assertFalse(student.is_valid()) 501 | self.assert_(('student_id', 'not unique') in student.errors) 502 | 503 | student = Student() 504 | self.assertTrue(student.is_valid()) 505 | 506 | def test_long_integers(self): 507 | class Tweet(models.Model): 508 | status_id = models.IntegerField() 509 | 510 | t = Tweet(status_id=int(u'14782201061')) 511 | self.assertTrue(t.is_valid()) 512 | t.save() 513 | 514 | t = Tweet.objects.get_by_id(t.id) 515 | self.assertEqual(14782201061, t.status_id) 516 | 517 | def test_slicing(self): 518 | Person.objects.create(first_name="Granny", last_name="Goose") 519 | Person.objects.create(first_name="Clark", last_name="Kent") 520 | Person.objects.create(first_name="Granny", last_name="Mommy") 521 | Person.objects.create(first_name="Lois", last_name="Kent") 522 | Person.objects.create(first_name="Jonathan", last_name="Kent") 523 | Person.objects.create(first_name="Martha", last_name="Kent") 524 | Person.objects.create(first_name="Lex", last_name="Luthor") 525 | Person.objects.create(first_name="Lionel", last_name="Luthor") 526 | 527 | # no slice 528 | a = Person.objects.all() 529 | self.assertEqual(8, len(a)) 530 | self.assertEqual(Person.objects.get_by_id('1'), a[0]) 531 | self.assertEqual("Lionel Luthor", a[7].full_name()) 532 | 533 | a = Person.objects.all()[3:] 534 | self.assertEqual(5, len(a)) 535 | self.assertEqual(Person.objects.get_by_id('4'), a[0]) 536 | self.assertEqual("Lionel Luthor", a[4].full_name()) 537 | 538 | a = Person.objects.all()[:6] 539 | self.assertEqual(6, len(a)) 540 | self.assertEqual(Person.objects.get_by_id('1'), a[0]) 541 | self.assertEqual("Martha Kent", a[5].full_name()) 542 | 543 | a = Person.objects.all()[2:6] 544 | self.assertEqual(4, len(a)) 545 | self.assertEqual(Person.objects.get_by_id('3'), a[0]) 546 | self.assertEqual("Martha Kent", a[3].full_name()) 547 | 548 | def test_get_or_create(self): 549 | Person.objects.create(first_name="Granny", last_name="Goose") 550 | Person.objects.create(first_name="Clark", last_name="Kent") 551 | Person.objects.create(first_name="Granny", last_name="Mommy") 552 | Person.objects.create(first_name="Lois", last_name="Kent") 553 | Person.objects.create(first_name="Jonathan", last_name="Kent") 554 | Person.objects.create(first_name="Martha", last_name="Kent") 555 | 556 | p = Person.objects.get_or_create(first_name="Lois", 557 | last_name="Kent") 558 | self.assertEqual('4', p.id) 559 | p = Person.objects.get_or_create(first_name="Jonathan", 560 | last_name="Weiss") 561 | self.assertEqual('7', p.id) 562 | 563 | 564 | def test_customizable_key(self): 565 | class Person(models.Model): 566 | name = models.CharField() 567 | 568 | class Meta: 569 | key = 'People' 570 | 571 | p = Person(name="Clark Kent") 572 | self.assert_(p.is_valid()) 573 | self.assert_(p.save()) 574 | 575 | self.assert_('1' in self.client.smembers('People:all')) 576 | 577 | 578 | class Event(models.Model): 579 | name = models.CharField(required=True) 580 | date = models.DateField(required=True) 581 | 582 | class DateFieldTestCase(RediscoTestCase): 583 | 584 | def test_CharField(self): 585 | event = Event(name="Legend of the Seeker Premiere", 586 | date=date(2008, 11, 12)) 587 | self.assertEqual(date(2008, 11, 12), event.date) 588 | 589 | def test_saved_CharField(self): 590 | instance = Event.objects.create(name="Legend of the Seeker Premiere", 591 | date=date(2008, 11, 12)) 592 | assert instance 593 | event = Event.objects.get_by_id(instance.id) 594 | assert event 595 | self.assertEqual(date(2008, 11, 12), event.date) 596 | 597 | def test_invalid_date(self): 598 | event = Event(name="Event #1") 599 | event.date = 1 600 | self.assertFalse(event.is_valid()) 601 | self.assertTrue(('date', 'bad type') in event.errors) 602 | 603 | def test_indexes(self): 604 | d = date.today() 605 | Event.objects.create(name="Event #1", date=d) 606 | self.assertTrue('1' in self.client.smembers(Event._key['all'])) 607 | # zfilter index 608 | self.assertTrue(self.client.exists("Event:date")) 609 | # other field indices 610 | self.assertEqual(2, self.client.scard("Event:1:_indices")) 611 | for index in self.client.smembers("Event:1:_indices"): 612 | self.assertTrue(index.startswith("Event:date") or 613 | index.startswith("Event:name")) 614 | 615 | def test_auto_now(self): 616 | class Report(models.Model): 617 | title = models.CharField() 618 | created_on = models.DateField(auto_now_add=True) 619 | updated_on = models.DateField(auto_now=True) 620 | 621 | r = Report(title="My Report") 622 | assert r.save() 623 | r = Report.objects.filter(title="My Report")[0] 624 | self.assertTrue(isinstance(r.created_on, date)) 625 | self.assertTrue(isinstance(r.updated_on, date)) 626 | self.assertEqual(date.today(), r.created_on) 627 | 628 | 629 | class CharFieldTestCase(RediscoTestCase): 630 | 631 | def test_max_length(self): 632 | class Person(models.Model): 633 | name = models.CharField(max_length=20, required=True) 634 | 635 | p = Person(name='The quick brown fox jumps over the lazy dog.') 636 | 637 | self.assertFalse(p.is_valid()) 638 | self.assert_(('name', 'exceeds max length') in p.errors) 639 | 640 | 641 | class Student(models.Model): 642 | name = models.CharField(required=True) 643 | average = models.FloatField(required=True) 644 | 645 | class FloatFieldTestCase(RediscoTestCase): 646 | def test_FloatField(self): 647 | s = Student(name="Richard Cypher", average=86.4) 648 | self.assertEqual(86.4, s.average) 649 | 650 | def test_saved_FloatField(self): 651 | s = Student.objects.create(name="Richard Cypher", 652 | average=3.14159) 653 | assert s 654 | student = Student.objects.get_by_id(s.id) 655 | assert student 656 | self.assertEqual(3.14159, student.average) 657 | 658 | def test_indexing(self): 659 | Student.objects.create(name="Richard Cypher", average=3.14159) 660 | Student.objects.create(name="Kahlan Amnell", average=92.45) 661 | Student.objects.create(name="Zeddicus Zorander", average=99.99) 662 | Student.objects.create(name="Cara", average=84.91) 663 | good = Student.objects.zfilter(average__gt=50.0) 664 | self.assertEqual(3, len(good)) 665 | self.assertTrue("Richard Cypher", 666 | Student.objects.filter(average=3.14159)[0].name) 667 | 668 | 669 | 670 | class Task(models.Model): 671 | name = models.CharField(default="Unknown") 672 | done = models.BooleanField() 673 | 674 | class BooleanFieldTestCase(RediscoTestCase): 675 | def test_CharField(self): 676 | t = Task(name="Cook dinner", done=False) 677 | assert t.save() 678 | self.assertFalse(t.done) 679 | 680 | def test_saved_CharField(self): 681 | t = Task(name="Cook dinner", done=False) 682 | assert t.save() 683 | 684 | t = Task.objects.all()[0] 685 | self.assertFalse(t.done) 686 | self.assertEqual(t.name, "Cook dinner") 687 | t.done = True 688 | assert t.save() 689 | 690 | t = Task.objects.all()[0] 691 | self.assertTrue(t.done) 692 | 693 | t_default = Task(done=False) 694 | assert t_default.save() 695 | 696 | t_default = Task.objects.get_by_id(t_default.id) 697 | self.assertTrue(t_default.name == "Unknown") 698 | 699 | 700 | def test_indexing(self): 701 | assert Task.objects.create(name="Study Lua", done=False) 702 | assert Task.objects.create(name="Read News", done=True) 703 | assert Task.objects.create(name="Buy Dinner", done=False) 704 | assert Task.objects.create(name="Visit Sick Friend", done=False) 705 | assert Task.objects.create(name="Play", done=True) 706 | assert Task.objects.create(name="Sing a song", done=False) 707 | assert Task.objects.create(name="Pass the Exam", done=True) 708 | assert Task.objects.create(name="Dance", done=False) 709 | assert Task.objects.create(name="Code", done=True) 710 | done = Task.objects.filter(done=True) 711 | unfin = Task.objects.filter(done=False) 712 | self.assertEqual(4, len(done)) 713 | self.assertEqual(5, len(unfin)) 714 | 715 | 716 | 717 | class ListFieldTestCase(RediscoTestCase): 718 | def test_basic(self): 719 | class Cake(models.Model): 720 | name = models.CharField() 721 | ingredients = models.ListField(str) 722 | sizes = models.ListField(int) 723 | 724 | Cake.objects.create(name="StrCake", 725 | ingredients=['strawberry', 'sugar', 'dough'], 726 | sizes=[1, 2, 5]) 727 | Cake.objects.create(name="Normal Cake", 728 | ingredients=['sugar', 'dough'], 729 | sizes=[1, 3, 5]) 730 | Cake.objects.create(name="No Sugar Cake", 731 | ingredients=['dough'], 732 | sizes=[]) 733 | cake = Cake.objects.all()[0] 734 | self.assertEqual(['strawberry', 'sugar', 'dough'], 735 | cake.ingredients) 736 | with_sugar = Cake.objects.filter(ingredients='sugar') 737 | self.assertTrue(2, len(with_sugar)) 738 | self.assertEqual([1, 2, 5], with_sugar[0].sizes) 739 | self.assertEqual([1, 3, 5], with_sugar[1].sizes) 740 | 741 | size1 = Cake.objects.filter(sizes=str(2)) 742 | self.assertEqual(1, len(size1)) 743 | 744 | cake.sizes = None 745 | cake.ingredients = None 746 | assert cake.save() 747 | 748 | cake = Cake.objects.get_by_id(cake.id) 749 | self.assertEqual([], cake.sizes) 750 | self.assertEqual([], cake.ingredients) 751 | 752 | def test_list_of_reference_fields(self): 753 | class Book(models.Model): 754 | title = models.CharField(required=True) 755 | date_published = models.DateField(required=True) 756 | 757 | class Author(models.Model): 758 | name = models.CharField(required=True) 759 | books = models.ListField(Book) 760 | 761 | book = Book.objects.create( 762 | title="University Physics With Modern Physics", 763 | date_published=date(2007, 4, 2)) 764 | assert book 765 | 766 | author1 = Author.objects.create(name="Hugh Young", 767 | books=[book]) 768 | author2 = Author.objects.create(name="Roger Freedman", 769 | books=[book]) 770 | 771 | assert author1 772 | assert author2 773 | author1 = Author.objects.get_by_id(1) 774 | author2 = Author.objects.get_by_id(2) 775 | self.assertTrue(book in author1.books) 776 | self.assertTrue(book in author2.books) 777 | 778 | book = Book.objects.create( 779 | title="University Physics With Modern Physics Paperback", 780 | date_published=date(2007, 4, 2)) 781 | 782 | author1.books.append(book) 783 | assert author1.save() 784 | 785 | author1 = Author.objects.get_by_id(1) 786 | self.assertEqual(2, len(author1.books)) 787 | 788 | def test_lazy_reference_field(self): 789 | class User(models.Model): 790 | name = models.CharField() 791 | likes = models.ListField('Link') 792 | 793 | def likes_link(self, link): 794 | if self.likes is None: 795 | self.likes = [link] 796 | self.save() 797 | else: 798 | if link not in self.likes: 799 | self.likes.append(link) 800 | self.save() 801 | 802 | class Link(models.Model): 803 | url = models.CharField() 804 | 805 | user = User.objects.create(name="Lion King") 806 | assert Link.objects.create(url="http://google.com") 807 | assert Link.objects.create(url="http://yahoo.com") 808 | assert Link.objects.create(url="http://github.com") 809 | assert Link.objects.create(url="http://bitbucket.org") 810 | 811 | links = Link.objects.all().limit(3) 812 | 813 | for link in links: 814 | user.likes_link(link) 815 | 816 | user = User.objects.get_by_id(1) 817 | self.assertEqual("http://google.com", user.likes[0].url) 818 | self.assertEqual("http://yahoo.com", user.likes[1].url) 819 | self.assertEqual("http://github.com", user.likes[2].url) 820 | self.assertEqual(3, len(user.likes)) 821 | 822 | 823 | class ReferenceFieldTestCase(RediscoTestCase): 824 | def test_basic(self): 825 | class Word(models.Model): 826 | placeholder = models.CharField() 827 | 828 | class Character(models.Model): 829 | n = models.IntegerField() 830 | m = models.CharField() 831 | word = models.ReferenceField(Word) 832 | 833 | Word.objects.create() 834 | word = Word.objects.all()[0] 835 | Character.objects.create(n=32, m='a', word=word) 836 | Character.objects.create(n=33, m='b', word=word) 837 | Character.objects.create(n=34, m='c', word=word) 838 | Character.objects.create(n=34, m='d') 839 | for char in Character.objects.all(): 840 | if char.m != 'd': 841 | self.assertEqual(word, char.word) 842 | else: 843 | self.assertEqual(None, char.word) 844 | a, b, c, d = list(Character.objects.all()) 845 | self.assertTrue(a in word.character_set) 846 | self.assertTrue(b in word.character_set) 847 | self.assertTrue(c in word.character_set) 848 | self.assertTrue(d not in word.character_set) 849 | self.assertEqual(3, len(word.character_set)) 850 | 851 | def test_reference(self): 852 | class Department(models.Model): 853 | name = models.Attribute(required=True) 854 | 855 | class Person(models.Model): 856 | name = models.Attribute(required=True) 857 | manager = models.ReferenceField('Person', related_name='underlings') 858 | department = models.ReferenceField(Department) 859 | 860 | d1 = Department.objects.create(name='Accounting') 861 | d2 = Department.objects.create(name='Billing') 862 | p1 = Person.objects.create(name='Joe', department=d1) 863 | p2 = Person.objects.create(name='Jack', department=d2) 864 | self.assertEqual(p1.department_id, p1.department.id) 865 | self.assertEqual(p2.department_id, p2.department.id) 866 | 867 | def test_lazy_reference_field(self): 868 | class User(models.Model): 869 | name = models.CharField() 870 | address = models.ReferenceField('Address') 871 | 872 | class Address(models.Model): 873 | street_address = models.CharField() 874 | city = models.CharField() 875 | zipcode = models.CharField() 876 | 877 | address = Address.objects.create(street_address="32/F Redisville", 878 | city="NoSQL City", zipcode="1.3.18") 879 | assert address 880 | user = User.objects.create(name="Richard Cypher", address=address) 881 | assert user 882 | 883 | a = Address.objects.all()[0] 884 | u = User.objects.all()[0] 885 | self.assertTrue(u in a.user_set) 886 | self.assertEqual("32/F Redisville", u.address.street_address) 887 | self.assertEqual("NoSQL City", u.address.city) 888 | self.assertEqual("1.3.18", u.address.zipcode) 889 | 890 | 891 | class DateTimeFieldTestCase(RediscoTestCase): 892 | 893 | def test_basic(self): 894 | from datetime import datetime 895 | n = datetime(2009, 12, 31).replace(tzinfo=tzlocal()) 896 | class Post(models.Model): 897 | title = models.CharField() 898 | date_posted = models.DateTimeField() 899 | created_at = models.DateTimeField(auto_now_add=True) 900 | post = Post(title="First!", date_posted=n) 901 | assert post.save() 902 | post = Post.objects.get_by_id(post.id) 903 | self.assertEqual(n, post.date_posted) 904 | assert post.created_at 905 | 906 | 907 | class TimeDeltaFieldTestCase(RediscoTestCase): 908 | 909 | def test_basic(self): 910 | from datetime import timedelta 911 | 912 | duration = timedelta(seconds=10) 913 | default_duration = timedelta(seconds=20) 914 | class DurationEvent(models.Model): 915 | name = models.CharField() 916 | started = models.DateTimeField() 917 | duration = models.TimeDeltaField(default=timedelta(seconds=20)) 918 | 919 | 920 | event_ten_sec = DurationEvent(name="Event 10 seconds", duration=timedelta(seconds=10)) 921 | assert event_ten_sec.is_valid(), [event_ten_sec.errors ] 922 | assert event_ten_sec.save() 923 | event_ten_sec = DurationEvent.objects.get_by_id(event_ten_sec.id) 924 | self.assertEqual(duration, event_ten_sec.duration) 925 | assert event_ten_sec.duration 926 | 927 | event_default_duration = DurationEvent(name="Event default duration") 928 | assert event_default_duration.save() 929 | event_default_duration = DurationEvent.objects.get_by_id(event_default_duration.id) 930 | self.assertEqual(default_duration, event_default_duration.duration) 931 | assert event_default_duration.duration 932 | 933 | 934 | class CounterFieldTestCase(RediscoTestCase): 935 | 936 | def test_basic(self): 937 | class Post(models.Model): 938 | title = models.CharField() 939 | body = models.CharField(indexed=False) 940 | liked = models.Counter() 941 | 942 | post = Post.objects.create(title="First!", 943 | body="Lorem ipsum") 944 | self.assert_(post) 945 | post.incr('liked') 946 | post.incr('liked', 2) 947 | post = Post.objects.get_by_id(post.id) 948 | self.assertEqual(3, post.liked) 949 | post.decr('liked', 2) 950 | post = Post.objects.get_by_id(post.id) 951 | self.assertEqual(1, post.liked) 952 | 953 | 954 | class MutexTestCase(RediscoTestCase): 955 | 956 | def setUp(self): 957 | super(MutexTestCase, self).setUp() 958 | self.p1 = Person.objects.create(first_name="Dick") 959 | self.p2 = Person.objects.get_by_id(self.p1.id) 960 | 961 | def test_instance_should_not_modify_locked(self): 962 | time1, time2 = {}, {} 963 | 964 | def f1(person, t): 965 | with Mutex(person): 966 | time.sleep(0.4) 967 | t['time'] = time.time() 968 | 969 | def f2(person, t): 970 | with Mutex(person): 971 | t['time'] = time.time() 972 | 973 | t1 = Thread(target=f1, args=(self.p1, time1,)) 974 | t2 = Thread(target=f2, args=(self.p2, time2,)) 975 | t1.start() 976 | time.sleep(0.1) 977 | t2.start() 978 | t1.join() 979 | t2.join() 980 | self.assert_(time2['time'] > time1['time']) 981 | 982 | def test_lock_expired(self): 983 | Mutex(self.p1).lock() 984 | with Mutex(self.p2): 985 | self.assert_(True) 986 | -------------------------------------------------------------------------------- /redisco/models/exceptions.py: -------------------------------------------------------------------------------- 1 | ########## 2 | # ERRORS # 3 | ########## 4 | from redis import WatchError 5 | 6 | class Error(Exception): 7 | pass 8 | 9 | class ValidationError(Error): 10 | pass 11 | 12 | class MissingID(Error): 13 | pass 14 | 15 | class AttributeNotIndexed(Error): 16 | pass 17 | 18 | class FieldValidationError(Error): 19 | 20 | def __init__(self, errors, *args, **kwargs): 21 | super(FieldValidationError, self).__init__(*args, **kwargs) 22 | self._errors = errors 23 | 24 | @property 25 | def errors(self): 26 | return self._errors 27 | 28 | class BadKeyError(Error): 29 | pass 30 | -------------------------------------------------------------------------------- /redisco/models/key.py: -------------------------------------------------------------------------------- 1 | try: 2 | unicode 3 | except NameError: 4 | # Python 3 5 | basestring = unicode = str 6 | 7 | class Key(unicode): 8 | def __getitem__(self, key): 9 | return Key(u"%s:%s" % (self, key)) 10 | -------------------------------------------------------------------------------- /redisco/models/managers.py: -------------------------------------------------------------------------------- 1 | from .modelset import ModelSet 2 | 3 | ############ 4 | # Managers # 5 | ############ 6 | 7 | class ManagerDescriptor(object): 8 | def __init__(self, manager): 9 | self.manager = manager 10 | 11 | def __get__(self, instance, owner): 12 | if instance != None: 13 | raise AttributeError 14 | return self.manager 15 | 16 | 17 | class Manager(object): 18 | 19 | def __init__(self, model_class): 20 | self.model_class = model_class 21 | 22 | def get_model_set(self): 23 | return ModelSet(self.model_class) 24 | 25 | def all(self): 26 | return self.get_model_set() 27 | 28 | def create(self, **kwargs): 29 | return self.get_model_set().create(**kwargs) 30 | 31 | def get_or_create(self, **kwargs): 32 | return self.get_model_set().get_or_create(**kwargs) 33 | 34 | def filter(self, **kwargs): 35 | return self.get_model_set().filter(**kwargs) 36 | 37 | def exclude(self, **kwargs): 38 | return self.get_model_set().exclude(**kwargs) 39 | 40 | def get_by_id(self, id): 41 | return self.get_model_set().get_by_id(id) 42 | 43 | def order(self, field): 44 | return self.get_model_set().order(field) 45 | 46 | def zfilter(self, **kwargs): 47 | return self.get_model_set().zfilter(**kwargs) 48 | 49 | 50 | -------------------------------------------------------------------------------- /redisco/models/modelset.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handles the queries. 3 | """ 4 | from .attributes import IntegerField, DateTimeField 5 | import redisco 6 | from redisco.containers import SortedSet, Set, List, NonPersistentList 7 | from .exceptions import AttributeNotIndexed 8 | from .attributes import ZINDEXABLE 9 | 10 | # Model Set 11 | class ModelSet(Set): 12 | def __init__(self, model_class): 13 | self.model_class = model_class 14 | self.key = model_class._key['all'] 15 | # We access directly _meta as .db is a property and should be 16 | # access from an instance, not a Class 17 | self._db = model_class._meta['db'] or redisco.get_client() 18 | self._filters = {} 19 | self._exclusions = {} 20 | self._zfilters = [] 21 | self._ordering = [] 22 | self._limit = None 23 | self._offset = None 24 | 25 | ################# 26 | # MAGIC METHODS # 27 | ################# 28 | 29 | def __getitem__(self, index): 30 | """ 31 | Will look in _set to get the id and simply return the instance of the model. 32 | """ 33 | if isinstance(index, slice): 34 | return map(lambda id: self._get_item_with_id(id), self._set[index]) 35 | else: 36 | id = self._set[index] 37 | if id: 38 | return self._get_item_with_id(id) 39 | else: 40 | raise IndexError 41 | 42 | def __repr__(self): 43 | if len(self._set) > 30: 44 | m = self._set[:30] 45 | else: 46 | m = self._set 47 | s = map(lambda id: self._get_item_with_id(id), m) 48 | return "%s" % s 49 | 50 | def __iter__(self): 51 | for id in self._set: 52 | yield self._get_item_with_id(id) 53 | 54 | def __len__(self): 55 | return len(self._set) 56 | 57 | def __contains__(self, val): 58 | return val.id in self._set 59 | 60 | ########################################## 61 | # METHODS THAT RETURN A SET OF INSTANCES # 62 | ########################################## 63 | 64 | def get_by_id(self, id): 65 | """ 66 | Returns the object definied by ``id``. 67 | 68 | :param id: the ``id`` of the objects to lookup. 69 | :returns: The object instance or None if not found. 70 | 71 | >>> from redisco import models 72 | >>> class Foo(models.Model): 73 | ... name = models.Attribute() 74 | ... 75 | >>> f = Foo(name="Einstein") 76 | >>> f.save() 77 | True 78 | >>> Foo.objects.get_by_id(f.id) == f 79 | True 80 | >>> [f.delete() for f in Foo.objects.all()] # doctest: +ELLIPSIS 81 | [...] 82 | """ 83 | if (self._filters or self._exclusions or self._zfilters) and str(id) not in self._set: 84 | return 85 | if self.model_class.exists(id): 86 | return self._get_item_with_id(id) 87 | 88 | def first(self): 89 | """ 90 | Return the first object of a collections. 91 | 92 | :return: The object or Non if the lookup gives no result 93 | 94 | 95 | >>> from redisco import models 96 | >>> class Foo(models.Model): 97 | ... name = models.Attribute() 98 | ... 99 | >>> f = Foo(name="toto") 100 | >>> f.save() 101 | True 102 | >>> Foo.objects.filter(name="toto").first() # doctest: +ELLIPSIS 103 | 104 | >>> [f.delete() for f in Foo.objects.all()] # doctest: +ELLIPSIS 105 | [...] 106 | """ 107 | try: 108 | return self.limit(1).__getitem__(0) 109 | except IndexError: 110 | return None 111 | 112 | 113 | ##################################### 114 | # METHODS THAT MODIFY THE MODEL SET # 115 | ##################################### 116 | 117 | def filter(self, **kwargs): 118 | """ 119 | Filter a collection on criteria 120 | 121 | >>> from redisco import models 122 | >>> class Foo(models.Model): 123 | ... name = models.Attribute() 124 | ... 125 | >>> Foo(name="toto").save() 126 | True 127 | >>> Foo(name="toto").save() 128 | True 129 | >>> Foo.objects.filter() # doctest: +ELLIPSIS 130 | [, ] 131 | >>> [f.delete() for f in Foo.objects.all()] # doctest: +ELLIPSIS 132 | [...] 133 | """ 134 | clone = self._clone() 135 | if not clone._filters: 136 | clone._filters = {} 137 | clone._filters.update(kwargs) 138 | return clone 139 | 140 | def exclude(self, **kwargs): 141 | """ 142 | Exclude a collection within a lookup. 143 | 144 | 145 | >>> from redisco import models 146 | >>> class Foo(models.Model): 147 | ... name = models.Attribute() 148 | ... exclude_me = models.BooleanField() 149 | ... 150 | >>> Foo(name="Einstein").save() 151 | True 152 | >>> Foo(name="Edison", exclude_me=True).save() 153 | True 154 | >>> Foo.objects.exclude(exclude_me=True).first().name 155 | u'Einstein' 156 | >>> [f.delete() for f in Foo.objects.all()] # doctest: +ELLIPSIS 157 | [...] 158 | """ 159 | clone = self._clone() 160 | if not clone._exclusions: 161 | clone._exclusions = {} 162 | clone._exclusions.update(kwargs) 163 | return clone 164 | 165 | def zfilter(self, **kwargs): 166 | clone = self._clone() 167 | if not clone._zfilters: 168 | clone._zfilters = [] 169 | clone._zfilters.append(kwargs) 170 | return clone 171 | 172 | # this should only be called once 173 | def order(self, field): 174 | """ 175 | Enable ordering in collections when doing a lookup. 176 | 177 | .. Warning:: This should only be called once per lookup. 178 | 179 | >>> from redisco import models 180 | >>> class Foo(models.Model): 181 | ... name = models.Attribute() 182 | ... exclude_me = models.BooleanField() 183 | ... 184 | >>> Foo(name="Abba").save() 185 | True 186 | >>> Foo(name="Zztop").save() 187 | True 188 | >>> Foo.objects.all().order("-name").first().name 189 | u'Zztop' 190 | >>> Foo.objects.all().order("name").first().name 191 | u'Abba' 192 | >>> [f.delete() for f in Foo.objects.all()] # doctest: +ELLIPSIS 193 | [...] 194 | """ 195 | fname = field.lstrip('-') 196 | if fname not in self.model_class._indices: 197 | raise ValueError("Order parameter should be an indexed attribute.") 198 | alpha = True 199 | if fname in self.model_class._attributes: 200 | v = self.model_class._attributes[fname] 201 | alpha = not isinstance(v, ZINDEXABLE) 202 | clone = self._clone() 203 | if not clone._ordering: 204 | clone._ordering = [] 205 | clone._ordering.append((field, alpha,)) 206 | return clone 207 | 208 | def limit(self, n, offset=0): 209 | """ 210 | Limit the size of the collection to *n* elements. 211 | """ 212 | clone = self._clone() 213 | clone._limit = n 214 | clone._offset = offset 215 | return clone 216 | 217 | def create(self, **kwargs): 218 | """ 219 | Create an object of the class. 220 | 221 | .. Note:: This is the same as creating an instance of the class and saving it. 222 | 223 | >>> from redisco import models 224 | >>> class Foo(models.Model): 225 | ... name = models.Attribute() 226 | ... 227 | >>> Foo.objects.create(name="Obama") # doctest: +ELLIPSIS 228 | 229 | >>> [f.delete() for f in Foo.objects.all()] # doctest: +ELLIPSIS 230 | [...] 231 | """ 232 | instance = self.model_class(**kwargs) 233 | if instance.save(): 234 | return instance 235 | else: 236 | return None 237 | 238 | def all(self): 239 | """ 240 | Return all elements of the collection. 241 | """ 242 | return self._clone() 243 | 244 | def get_or_create(self, **kwargs): 245 | """ 246 | Return an element of the collection or create it if necessary. 247 | 248 | >>> from redisco import models 249 | >>> class Foo(models.Model): 250 | ... name = models.Attribute() 251 | ... 252 | >>> new_obj = Foo.objects.get_or_create(name="Obama") 253 | >>> get_obj = Foo.objects.get_or_create(name="Obama") 254 | >>> new_obj == get_obj 255 | True 256 | >>> [f.delete() for f in Foo.objects.all()] # doctest: +ELLIPSIS 257 | [...] 258 | """ 259 | opts = {} 260 | for k, v in kwargs.iteritems(): 261 | if k in self.model_class._indices: 262 | opts[k] = v 263 | o = self.filter(**opts).first() 264 | if o: 265 | return o 266 | else: 267 | return self.create(**kwargs) 268 | 269 | # 270 | 271 | @property 272 | def db(self): 273 | return self._db 274 | 275 | ################### 276 | # PRIVATE METHODS # 277 | ################### 278 | 279 | @property 280 | def _set(self): 281 | """ 282 | This contains the list of ids that have been looked-up, 283 | filtered and ordered. This set is build hen we first access 284 | it and is cached for has long has the ModelSet exist. 285 | """ 286 | # For performance reasons, only one zfilter is allowed. 287 | if hasattr(self, '_cached_set'): 288 | return self._cached_set 289 | s = Set(self.key) 290 | if self._zfilters: 291 | s = self._add_zfilters(s) 292 | if self._filters: 293 | s = self._add_set_filter(s) 294 | if self._exclusions: 295 | s = self._add_set_exclusions(s) 296 | n = self._order(s.key) 297 | self._cached_set = n 298 | return self._cached_set 299 | 300 | def _add_set_filter(self, s): 301 | """ 302 | This function is the internal of the `filter` function. 303 | It simply creates a new "intersection" of indexed keys (the filter) and 304 | the previous filtered keys (if any). 305 | 306 | .. Note:: This function uses the ``Set`` container class. 307 | 308 | :return: the new Set 309 | """ 310 | indices = [] 311 | for k, v in self._filters.iteritems(): 312 | index = self._build_key_from_filter_item(k, v) 313 | if k not in self.model_class._indices: 314 | raise AttributeNotIndexed( 315 | "Attribute %s is not indexed in %s class." % 316 | (k, self.model_class.__name__)) 317 | indices.append(index) 318 | new_set_key = "~%s.%s" % ("+".join([self.key] + indices), id(self)) 319 | s.intersection(new_set_key, *[Set(n, db=self.db) for n in indices]) 320 | new_set = Set(new_set_key, db=self.db) 321 | new_set.set_expire() 322 | return new_set 323 | 324 | def _add_set_exclusions(self, s): 325 | """ 326 | This function is the internals of the `filter` function. 327 | It simply creates a new "difference" of indexed keys (the filter) and 328 | the previous filtered keys (if any). 329 | 330 | .. Note:: This function uses the ``Set`` container class. 331 | 332 | :return: the new Set 333 | """ 334 | indices = [] 335 | for k, v in self._exclusions.iteritems(): 336 | index = self._build_key_from_filter_item(k, v) 337 | if k not in self.model_class._indices: 338 | raise AttributeNotIndexed( 339 | "Attribute %s is not indexed in %s class." % 340 | (k, self.model_class.__name__)) 341 | indices.append(index) 342 | new_set_key = "~%s.%s" % ("-".join([self.key] + indices), id(self)) 343 | s.difference(new_set_key, *[Set(n, db=self.db) for n in indices]) 344 | new_set = Set(new_set_key, db=self.db) 345 | new_set.set_expire() 346 | return new_set 347 | 348 | def _add_zfilters(self, s): 349 | """ 350 | This function is the internals of the zfilter function. 351 | It will create a SortedSet and will compare the scores to 352 | the value provided. 353 | 354 | :return: a SortedSet with the ids. 355 | 356 | """ 357 | k, v = self._zfilters[0].items()[0] 358 | try: 359 | att, op = k.split('__') 360 | except ValueError: 361 | raise ValueError("zfilter should have an operator.") 362 | index = self.model_class._key[att] 363 | desc = self.model_class._attributes[att] 364 | zset = SortedSet(index, db=self.db) 365 | limit, offset = self._get_limit_and_offset() 366 | new_set_key = "~%s.%s" % ("+".join([self.key, att, op]), id(self)) 367 | new_set_key_temp = "#%s.%s" % ("+".join([self.key, att, op]), id(self)) 368 | members = [] 369 | if isinstance(v, (tuple, list,)): 370 | min, max = v 371 | min = float(desc.typecast_for_storage(min)) 372 | max = float(desc.typecast_for_storage(max)) 373 | else: 374 | v = float(desc.typecast_for_storage(v)) 375 | if op == 'lt': 376 | members = zset.lt(v, limit, offset) 377 | elif op == 'gt': 378 | members = zset.gt(v, limit, offset) 379 | elif op == 'gte': 380 | members = zset.ge(v, limit, offset) 381 | elif op == 'le': 382 | members = zset.le(v, limit, offset) 383 | elif op == 'lte': 384 | members = zset.le(v, limit, offset) 385 | elif op == 'in': 386 | members = zset.between(min, max, limit, offset) 387 | 388 | temp_set = Set(new_set_key_temp) 389 | if members: 390 | temp_set.add(*members) 391 | temp_set.set_expire() 392 | 393 | s.intersection(new_set_key, temp_set) 394 | new_set = Set(new_set_key) 395 | new_set.set_expire() 396 | return new_set 397 | 398 | def _order(self, skey): 399 | """ 400 | This function does not job. It will only call the good 401 | subfunction in case we want an ordering or not. 402 | """ 403 | if self._ordering: 404 | return self._set_with_ordering(skey) 405 | else: 406 | return self._set_without_ordering(skey) 407 | 408 | def _set_with_ordering(self, skey): 409 | """ 410 | Final call for finally ordering the looked-up collection. 411 | The ordering will be done by Redis itself and stored as a temporary set. 412 | 413 | :return: a Set of `id` 414 | """ 415 | num, start = self._get_limit_and_offset() 416 | old_set_key = skey 417 | for ordering, alpha in self._ordering: 418 | if ordering.startswith('-'): 419 | desc = True 420 | ordering = ordering.lstrip('-') 421 | else: 422 | desc = False 423 | new_set_key = "%s#%s.%s" % (old_set_key, ordering, id(self)) 424 | by = "%s->%s" % (self.model_class._key['*'], ordering) 425 | self.db.sort(old_set_key, 426 | by=by, 427 | store=new_set_key, 428 | alpha=alpha, 429 | start=start, 430 | num=num, 431 | desc=desc) 432 | if old_set_key != self.key: 433 | Set(old_set_key, db=self.db).set_expire() 434 | new_list = List(new_set_key, db=self.db) 435 | new_list.set_expire() 436 | return new_list 437 | 438 | def _set_without_ordering(self, skey): 439 | """ 440 | Final call for "non-ordered" looked up. 441 | We order by id anyway and this is done by redis (same as above). 442 | 443 | :returns: A Set of `id` 444 | """ 445 | # sort by id 446 | num, start = self._get_limit_and_offset() 447 | old_set_key = skey 448 | new_set_key = "%s#.%s" % (old_set_key, id(self)) 449 | self.db.sort(old_set_key, 450 | store=new_set_key, 451 | start=start, 452 | num=num) 453 | if old_set_key != self.key: 454 | Set(old_set_key, db=self.db).set_expire() 455 | new_list = List(new_set_key, db=self.db) 456 | new_list.set_expire() 457 | return new_list 458 | 459 | def _get_limit_and_offset(self): 460 | """ 461 | Return the limit and offset of the looked up ids. 462 | """ 463 | if (self._limit is not None and self._offset is None) or \ 464 | (self._limit is None and self._offset is not None): 465 | raise "Limit and offset must be specified" 466 | 467 | if self._limit is None: 468 | return (None, None) 469 | else: 470 | return (self._limit, self._offset) 471 | 472 | def _get_item_with_id(self, id): 473 | """ 474 | Fetch an object and return the instance. The real fetching is 475 | done by assigning the id to the Instance. See ``Model`` class. 476 | """ 477 | instance = self.model_class() 478 | instance.id = str(id) 479 | return instance 480 | 481 | def _build_key_from_filter_item(self, index, value): 482 | """ 483 | Build the keys from the filter so we can fetch the good keys 484 | with the indices. 485 | Example: 486 | Foo.objects.filter(name='bar') 487 | => 'Foo:name:bar' 488 | """ 489 | desc = self.model_class._attributes.get(index) 490 | if desc: 491 | value = desc.typecast_for_storage(value) 492 | return self.model_class._key[index][value] 493 | 494 | def _clone(self): 495 | """ 496 | This function allows the chaining of lookup calls. 497 | Example: 498 | Foo.objects.filter().filter().exclude()... 499 | 500 | :returns: a modelset instance with all the previous filters. 501 | """ 502 | klass = self.__class__ 503 | c = klass(self.model_class) 504 | if self._filters: 505 | c._filters = self._filters 506 | if self._exclusions: 507 | c._exclusions = self._exclusions 508 | if self._zfilters: 509 | c._zfilters = self._zfilters 510 | if self._ordering: 511 | c._ordering = self._ordering 512 | c._limit = self._limit 513 | c._offset = self._offset 514 | return c 515 | -------------------------------------------------------------------------------- /redisco/tests/README: -------------------------------------------------------------------------------- 1 | You will not find any tests herem but only the test suite. 2 | 3 | -------------------------------------------------------------------------------- /redisco/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from redisco.containerstests import (SetTestCase, ListTestCase, TypedListTestCase, 4 | SortedSetTestCase, HashTestCase) 5 | from redisco.models.basetests import (ModelTestCase, DateFieldTestCase, FloatFieldTestCase, 6 | BooleanFieldTestCase, ListFieldTestCase, ReferenceFieldTestCase, 7 | TimeDeltaFieldTestCase, DateTimeFieldTestCase, CounterFieldTestCase, 8 | CharFieldTestCase, MutexTestCase) 9 | 10 | import redisco 11 | REDIS_DB = int(os.environ.get('REDIS_DB', 15)) # WARNING TESTS FLUSHDB!!! 12 | REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379)) 13 | redisco.connection_setup(host="localhost", port=REDIS_PORT, db=REDIS_DB) 14 | 15 | typed_list_suite = unittest.TestLoader().loadTestsFromTestCase(TypedListTestCase) 16 | 17 | def all_tests(): 18 | suite = unittest.TestSuite() 19 | suite.addTest(unittest.makeSuite(SetTestCase)) 20 | suite.addTest(unittest.makeSuite(ListTestCase)) 21 | suite.addTest(unittest.makeSuite(TypedListTestCase)) 22 | suite.addTest(unittest.makeSuite(SortedSetTestCase)) 23 | suite.addTest(unittest.makeSuite(ModelTestCase)) 24 | suite.addTest(unittest.makeSuite(DateFieldTestCase)) 25 | suite.addTest(unittest.makeSuite(FloatFieldTestCase)) 26 | suite.addTest(unittest.makeSuite(BooleanFieldTestCase)) 27 | suite.addTest(unittest.makeSuite(ListFieldTestCase)) 28 | suite.addTest(unittest.makeSuite(ReferenceFieldTestCase)) 29 | suite.addTest(unittest.makeSuite(DateTimeFieldTestCase)) 30 | suite.addTest(unittest.makeSuite(TimeDeltaFieldTestCase)) 31 | suite.addTest(unittest.makeSuite(CounterFieldTestCase)) 32 | suite.addTest(unittest.makeSuite(MutexTestCase)) 33 | suite.addTest(unittest.makeSuite(HashTestCase)) 34 | suite.addTest(unittest.makeSuite(CharFieldTestCase)) 35 | return suite 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | DateUtils==0.6.6 2 | hiredis==0.1.1 3 | redis>=2.7.5 4 | redislite>=1.0.228 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-doctest=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | 4 | version = '0.2.9' 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | def read(fname): 12 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 13 | 14 | setup(name='redisco', 15 | version=version, 16 | description='Python Containers and Simple Models for Redis', 17 | url='http://kiddouk.github.com/redisco', 18 | download_url='', 19 | long_description=read('README.rst'), 20 | install_requires=read('requirements.txt').splitlines(), 21 | extras_require = { 22 | 'redislite': ["redislite>=1.0.228"] 23 | }, 24 | author='Tim Medina', 25 | author_email='iamteem@gmail.com', 26 | maintainer='Sebastien Requiem', 27 | maintainer_email='sebastien.requiem@gmail.com', 28 | keywords=['Redis', 'model', 'container'], 29 | license='MIT', 30 | packages=['redisco', 'redisco.models'], 31 | test_suite='nose.collector', 32 | classifiers=[ 33 | 'Development Status :: 4 - Beta', 34 | 'Environment :: Console', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: OS Independent', 38 | 'Programming Language :: Python'], 39 | ) 40 | 41 | --------------------------------------------------------------------------------