├── .gitignore ├── .travis.yml ├── HISTORY.rst ├── LICENSE ├── README.rst ├── dev-requirements.in ├── dev-requirements.txt ├── ok ├── .gitignore ├── __init__.py ├── keys.py └── tests │ ├── __init__.py │ ├── module_test.py │ └── test_key.py ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.5 3 | env: 4 | - TOXENV=flake8 5 | - TOXENV=py27 6 | - TOXENV=py34 7 | - TOXENV=py35 8 | install: 9 | - pip install tox 10 | script: tox 11 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | 0.1.2 (2016-07-28) 2 | ++++++++++++++++++ 3 | 4 | **Bugfixes** 5 | 6 | - Accessing a field or subkey on a Key that doesn't exist will now raise an 7 | AttributeError instead of returning None. 8 | 9 | 0.1.1 (2016-07-07) 10 | ++++++++++++++++++ 11 | 12 | **Features and Improvements** 13 | 14 | - The string representation of a `Key` is now the key itself. 15 | - Keys now accepts IDs that aren't strings. 16 | 17 | 0.1.0 (2016-06-26) 18 | ++++++++++++++++++ 19 | 20 | - Initial release 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mitchel Cabuloy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ok |latest-version| 2 | ============================== 3 | 4 | |build-status| |python-support| |downloads| |license| 5 | 6 | Object-Key Mapper for Redis 7 | 8 | If you’ve used redis on python, you’ve had to deal with redis keys. 9 | Sometimes, *lots* of redis keys. With so many keys, it’s easy to make 10 | mistakes, especially since keys are just strings. I built ``ok`` so that 11 | I didn’t have to work with strings for redis keys. 12 | 13 | Here’s how you use it: 14 | 15 | .. code:: python 16 | 17 | import ok 18 | import redis 19 | 20 | 21 | class User(ok.Key): 22 | fields = ['timeline', 'followers', 'following'] 23 | 24 | 25 | # Get user mixxorz' timeline 26 | r = redis.StrictRedis() 27 | r.zrevrange(User('mixxorz').timeline, 0, 50) 28 | # ZREVRANGE User:mixxorz:timeline 0 50 29 | 30 | Managing your keys just became a lot less fragile. 31 | 32 | Installation 33 | ------------ 34 | 35 | Install it from pypi 36 | 37 | :: 38 | 39 | $ pip install ok-redis 40 | 41 | Usage 42 | ----- 43 | 44 | Access fields. 45 | 46 | .. code:: python 47 | 48 | class User(ok.Key): 49 | fields = ['timeline', 'followers', 'following'] 50 | 51 | 52 | print(User('mixxorz').timeline) 53 | # User:mixxorz:timeline 54 | 55 | Chain keys. 56 | 57 | .. code:: python 58 | 59 | class City(ok.Key): 60 | fields = ['tweets_hll'] 61 | 62 | 63 | class Country(ok.Key): 64 | subkeys = [City] 65 | 66 | 67 | print(Country('PH').City('Manila').tweets_hll) 68 | # Country:PH:City:Manila:tweets 69 | 70 | Subkeys can be an absolute or relative path to a key. 71 | 72 | .. code:: python 73 | 74 | # mod_one.py 75 | class Refer(Key): 76 | fields = ['elements'] 77 | 78 | 79 | # mod_two.py 80 | class Parent(Key): 81 | subkeys = ['..mod_one.Refer'] 82 | 83 | 84 | print(Parent('foo').Refer('bar').elements) 85 | # Parent:foo:Refer:bar:elements 86 | 87 | The string representation of a `Key` instance is the key, so you can use it like 88 | this: 89 | 90 | .. code:: python 91 | 92 | class User(ok.Key): 93 | pass 94 | 95 | 96 | r.get(User('mixxorz')) 97 | 98 | But you can also access the key explicitly. 99 | 100 | .. code:: python 101 | 102 | User('mixxorz').key 103 | # >>> User:mixxorz 104 | 105 | IDs are optional. 106 | 107 | .. code:: python 108 | 109 | class User(ok.Key): 110 | fields = ['rankings'] 111 | 112 | 113 | print(User().rankings) 114 | # User:rankings 115 | 116 | IDs don't have to be strings 117 | 118 | .. code:: python 119 | 120 | class User(ok.Key) 121 | pass 122 | 123 | print(User(123)) 124 | # User:123 125 | 126 | You can change the string used for the key. 127 | 128 | .. code:: python 129 | 130 | class Facebook(ok.Key): 131 | fields = ['all_posts'] 132 | class_key = 'fb' 133 | 134 | 135 | print(Facebook().all_posts) 136 | # fb:all_posts 137 | 138 | License 139 | ------- 140 | 141 | MIT 142 | 143 | .. |latest-version| image:: https://img.shields.io/pypi/v/ok-redis.svg 144 | :target: https://pypi.python.org/pypi/ok-redis/ 145 | :alt: Latest version 146 | .. |build-status| image:: https://img.shields.io/travis/mixxorz/ok-redis/master.svg 147 | :target: https://travis-ci.org/mixxorz/ok-redis 148 | :alt: Build status 149 | .. |python-support| image:: https://img.shields.io/pypi/pyversions/ok-redis.svg 150 | :target: https://pypi.python.org/pypi/ok-redis 151 | :alt: Python versions 152 | .. |downloads| image:: https://img.shields.io/pypi/dm/ok-redis.svg 153 | :target: https://pypi.python.org/pypi/ok-redis/ 154 | :alt: Monthly downloads 155 | .. |license| image:: https://img.shields.io/pypi/l/ok-redis.svg 156 | :target: https://github.com/behave/ok-redis/blob/master/LICENSE 157 | :alt: Software license 158 | -------------------------------------------------------------------------------- /dev-requirements.in: -------------------------------------------------------------------------------- 1 | autopep8 2 | docutils 3 | pip-tools 4 | pygments 5 | pytest 6 | tox 7 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file dev-requirements.txt dev-requirements.in 6 | # 7 | 8 | autopep8==1.2.4 9 | click==6.6 # via pip-tools 10 | docutils==0.12 11 | first==2.0.1 # via pip-tools 12 | pep8==1.7.0 # via autopep8 13 | pip-tools==1.6.5 14 | pluggy==0.3.1 # via tox 15 | py==1.4.31 # via pytest, tox 16 | pygments==2.1.3 17 | pytest==2.9.2 18 | six==1.10.0 # via pip-tools 19 | tox==2.3.1 20 | virtualenv==15.0.2 # via tox 21 | -------------------------------------------------------------------------------- /ok/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixxorz/ok-redis/8480b4764437f9c578411be496e381ad1f8cf4f8/ok/.gitignore -------------------------------------------------------------------------------- /ok/__init__.py: -------------------------------------------------------------------------------- 1 | """Object-Key Mapper for Redis""" 2 | 3 | from .keys import Key 4 | 5 | __all__ = ['Key'] 6 | __version__ = '0.1.2' 7 | -------------------------------------------------------------------------------- /ok/keys.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from importlib import import_module 3 | 4 | import six 5 | 6 | 7 | def keygen(*args, **kwargs): 8 | """Joins strings together by a colon (default) 9 | 10 | This function doesn't include empty strings in the final output. 11 | """ 12 | 13 | kwargs['sep'] = ':' 14 | 15 | cleaned_list = [arg for arg in args if arg != ''] 16 | 17 | return kwargs['sep'].join(cleaned_list) 18 | 19 | 20 | def import_class(name, relative_to): 21 | module = '.'.join(name.split('.')[:-1]) 22 | class_name = name.split('.')[-1] 23 | if module: 24 | mod = import_module(module, relative_to.__module__) 25 | else: 26 | mod = import_module(relative_to.__module__) 27 | return getattr(mod, class_name) 28 | 29 | 30 | @six.python_2_unicode_compatible 31 | class Key(object): 32 | class_key = None 33 | fields = [] 34 | subkeys = [] 35 | 36 | def __init__(self, id='', prefix='', class_key=''): 37 | self.id = six.text_type(id) 38 | self.prefix = prefix 39 | self.class_key = type(self).class_key or type(self).__name__ 40 | self.key = keygen(self.prefix, self.class_key, self.id) 41 | 42 | def __call__(self, id='', prefix=''): 43 | id = six.text_type(id) 44 | 45 | if not id: 46 | id = self.id 47 | 48 | if not prefix: 49 | prefix = self.prefix 50 | 51 | return type(self)(id, prefix) 52 | 53 | def __getattr__(self, attr): 54 | # Access fields 55 | if attr in self.fields: 56 | return keygen(self.key, attr) 57 | 58 | # Access subkeys 59 | string_subkeys = [subkey for subkey in self.subkeys 60 | if type(subkey) == str] 61 | subkeys = [subkey for subkey in self.subkeys 62 | if type(subkey) != str and issubclass(subkey, Key)] 63 | 64 | for subkey in string_subkeys: 65 | subkeys.append(import_class(subkey, self)) 66 | 67 | if attr in [subkey.__name__ for subkey in subkeys]: 68 | return next(subkey for subkey in subkeys 69 | if subkey.__name__ == attr)(prefix=self.key) 70 | 71 | raise AttributeError 72 | 73 | def __str__(self): 74 | return self.key 75 | -------------------------------------------------------------------------------- /ok/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixxorz/ok-redis/8480b4764437f9c578411be496e381ad1f8cf4f8/ok/tests/__init__.py -------------------------------------------------------------------------------- /ok/tests/module_test.py: -------------------------------------------------------------------------------- 1 | from ok import Key 2 | 3 | 4 | class ReferRelative(Key): 5 | pass 6 | 7 | 8 | class Refer(Key): 9 | pass 10 | -------------------------------------------------------------------------------- /ok/tests/test_key.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ok import Key 4 | 5 | 6 | class KeyTest(unittest.TestCase): 7 | 8 | def test_key(self): 9 | class User(Key): 10 | pass 11 | 12 | self.assertEqual(User().key, 'User') 13 | 14 | def test_field_access(self): 15 | class User(Key): 16 | fields = ['timeline', 'followers', 'following'] 17 | 18 | self.assertEqual(User('mixxorz').timeline, 'User:mixxorz:timeline') 19 | 20 | def test_class_key_access(self): 21 | class User(Key): 22 | pass 23 | 24 | self.assertEqual(User('mixxorz').key, 'User:mixxorz') 25 | 26 | def test_subkey_access(self): 27 | class City(Key): 28 | pass 29 | 30 | class Province(Key): 31 | subkeys = [City] 32 | 33 | class Country(Key): 34 | subkeys = [Province] 35 | 36 | self.assertEqual(Country('PH').Province('Metro Manila').City('Manila').key, # noqa 37 | 'Country:PH:Province:Metro Manila:City:Manila') 38 | 39 | def test_string_subkey_access(self): 40 | self.assertEqual(Parent('foo').Refer('bar').key, 41 | 'Parent:foo:Refer:bar') 42 | 43 | def test_string_subkey_relative_access(self): 44 | self.assertEqual(ParentRelative('foo').ReferRelative('bar').key, 45 | 'ParentRelative:foo:ReferRelative:bar') 46 | 47 | def test_string_subkey_same_file_relative_access(self): 48 | self.assertEqual(ParentSame('foo').ReferSame('bar').key, 49 | 'ParentSame:foo:ReferSame:bar') 50 | 51 | def test_empty_id(self): 52 | class Twitter(Key): 53 | fields = ['tweets'] 54 | 55 | self.assertEqual(Twitter().tweets, 'Twitter:tweets') 56 | 57 | def test_class_key(self): 58 | class Twitter(Key): 59 | class_key = 'twitter' 60 | 61 | self.assertEqual(Twitter().key, 'twitter') 62 | 63 | def test_should_accept_non_strings(self): 64 | class User(Key): 65 | fields = ['tweets'] 66 | 67 | self.assertEqual(User(123).tweets, 'User:123:tweets') 68 | 69 | def test_class_str(self): 70 | class User(Key): 71 | pass 72 | 73 | self.assertEqual('%s' % User('mixxorz'), 'User:mixxorz') 74 | 75 | def test_attribute_error(self): 76 | class Tweet(Key): 77 | pass 78 | 79 | class User(Key): 80 | fields = ['tweets'] 81 | subkeys = [Tweet] 82 | 83 | with self.assertRaises(AttributeError): 84 | User('mixxorz').followers 85 | 86 | with self.assertRaises(AttributeError): 87 | User('mixxorz').Profile() 88 | 89 | 90 | class Parent(Key): 91 | subkeys = ['ok.tests.module_test.Refer'] 92 | 93 | 94 | class ParentRelative(Key): 95 | subkeys = ['..module_test.ReferRelative'] 96 | 97 | 98 | class ParentSame(Key): 99 | subkeys = ['.ReferSame'] 100 | 101 | 102 | class ReferSame(Key): 103 | pass 104 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .ropeproject/* 3 | 4 | [pytest] 5 | python_files = test_key.py 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | import os 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 5 | README = readme.read() 6 | 7 | setup(name='ok-redis', 8 | version='0.1.2', 9 | packages=find_packages(exclude=['test*']), 10 | license='MIT', 11 | description='Object-Key Mapper for Redis', 12 | long_description=README, 13 | url='https://github.com/mixxorz/ok-redis', 14 | author='Mitchel Cabuloy (mixxorz)', 15 | author_email='mixxorz@gmail.com', 16 | maintainer='Mitchel Cabuloy (mixxorz)', 17 | maintainer_email='mixxorz@gmail.com', 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Operating System :: OS Independent', 23 | 'Programming Language :: Python :: 2.7', 24 | 'Programming Language :: Python :: 3.4', 25 | 'Programming Language :: Python :: 3.5', 26 | 'Topic :: Database', 27 | 'Topic :: Utilities', 28 | ], 29 | install_requires=['six'], 30 | zip_safe=False) 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = 8 | py{27,34,35} 9 | flake8 10 | 11 | [testenv] 12 | basepython = 13 | py27: python2.7 14 | py34: python3.4 15 | py35: python3.5 16 | deps = 17 | pytest 18 | commands = 19 | py.test 20 | 21 | [testenv:flake8] 22 | basepython = python 23 | deps = flake8 24 | commands = 25 | flake8 ok 26 | --------------------------------------------------------------------------------