Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit a0f7fbc8ec535c2e09a3af57bb3df52bd86e1bf8 @danielholmstrom committed Dec 26, 2012
Showing with 353 additions and 0 deletions.
  1. +7 −0 .gitignore
  2. +7 −0 LICENSE
  3. +32 −0 README.md
  4. +6 −0 iteralchemy/__init__.py
  5. +67 −0 iteralchemy/classes.py
  6. +128 −0 iteralchemy/tests/__init__.py
  7. +66 −0 iteralchemy/tests/test_asdict.py
  8. +5 −0 setup.cfg
  9. +35 −0 setup.py
@@ -0,0 +1,7 @@
+*.pyc
+*.egg
+*.mo
+*.egg-info
+.coverage
+doc/_build
+\#*#
@@ -0,0 +1,7 @@
+Copyright (C) 2012 Daniel Holmström
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,32 @@
+# Converts SQLAlchemy declarative models to dict
+
+Currently this works with synonyms and simple relations-ships as one-to-many and many-to-many. Relationships will only be followed one level.
+
+The only collection currently supported is sqlalchemy.orm.collections.InstrumentedList.
+
+## License
+
+iteralchemy is released under the MIT license.
+
+## Examples
+
+Mixin iterachlemy.IterableModel in a declarative class or use it as a base class for declarative\_base. Each class can have the following attibutes set:
+
+* asdict\_exclude: List of properties that should be excluded by default(default empty)
+* asdict\_exclude\_underscore: Exclude properties starting with an underscore(default True)
+
+ >>> dict(user)
+ {'id': 3, 'name': 'Gerald'}
+ >>> user.asdict(exclude=['id'])
+ {'name': 'Gerald'}
+ >>> user.asdict(follow=['roles'])
+ {'id': 3, 'name': 'Gerald', 'roles': [{'id': 'admin'}, {'id': 'user'}]}
+
+
+
+See iteralchemy/test\_todict.py for more examples.
+
+## TODO
+
+* Write docs
+* Support more collections
@@ -0,0 +1,6 @@
+# vim: set fileencoding=utf-8 :
+from __future__ import absolute_import, division
+
+from iteralchemy.classes import IterableModel
+
+__all__ = [IterableModel]
@@ -0,0 +1,67 @@
+# vim: set fileencoding=utf-8 :
+from __future__ import absolute_import, division
+
+from sqlalchemy.orm import RelationshipProperty, ColumnProperty,\
+ SynonymProperty
+from sqlalchemy.orm.collections import InstrumentedList
+
+
+class IterableModel(object):
+ """Adds iteration and asdict() method to an sqlalchemy class
+
+ """
+
+ asdict_exclude = None
+ """List of properties that always will be excluded"""
+ asdict_exclude_underscore = True
+ """Exclude properties starting with underscore"""
+
+ def asdict(self, exclude=None, exclude_underscore=None, follow=None):
+ """Get a dict from a model
+
+ :param follow: List of relationships that should be followed
+ :param exclude: List of properties that should be excluded, will be
+ merged with self.asdict_exclude
+ :param exclude_underscore: Overides self.exclude_underscore if set
+
+ :returns: dict
+ """
+
+ follow = follow or []
+ exclude = exclude or []
+ exclude += self.asdict_exclude or []
+ if exclude_underscore is None:
+ exclude_underscore = self.asdict_exclude_underscore
+
+
+ # Get relationships, columns and synonyms
+ relations = [k.key for k in self.__mapper__.iterate_properties if
+ isinstance(k, RelationshipProperty)]
+ columns = [k.key for k in self.__mapper__.iterate_properties if
+ isinstance(k, ColumnProperty)]
+ synonyms = [k.key for k in self.__mapper__.iterate_properties if
+ isinstance(k, SynonymProperty)]
+
+ if self.asdict_exclude_underscore:
+ # Exclude everything starting with underscore
+ exclude += [k for k in self.__mapper__._props if k[0] == '_']
+
+ data = dict([(k, getattr(self, k)) for k in columns + synonyms\
+ if k not in exclude])
+ if follow:
+ for k in relations:
+ rel = getattr(self, k)
+ if hasattr(rel, 'asdict'):
+ data.update({k: rel.asdict()})
+ elif isinstance(rel, InstrumentedList):
+ data.update({k: [dict(i) for i in rel]})
+
+ return data
+
+ def __iter__(self):
+ """Iterates
+
+ yields tuples that can be used to create a dict
+ """
+ for i in self.asdict().iteritems():
+ yield i
@@ -0,0 +1,128 @@
+# vim: set fileencoding=utf-8 :
+from __future__ import absolute_import, division
+
+from iteralchemy import IterableModel
+import unittest
+
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy import Table, Column, String, Integer, ForeignKey
+from sqlalchemy.orm import relationship, backref, synonym
+
+# Setup sqlalchemy
+engine = create_engine('sqlite:///:memory:', echo=False)
+from sqlalchemy.ext.declarative import declarative_base
+Base = declarative_base(engine, cls=IterableModel)
+
+class TestCase(unittest.TestCase):
+
+ def setUp(self):
+ """ Recreate the database """
+ Base.metadata.create_all(engine)
+ Session = sessionmaker(bind=engine)
+ self.session = Session()
+
+ def tearDown(self):
+ Base.metadata.drop_all()
+
+
+class Named(Base):
+ __tablename__ = 'named'
+ id = Column(Integer, primary_key=True)
+ name = Column(String)
+
+ def __init__(self, name):
+ self.name = name
+
+
+class NamedOtherColumnName(Base):
+ __tablename__ = 'named_with_other_column'
+
+ id = Column(Integer, primary_key=True)
+ name = Column('namecolumn', String)
+
+ def __init__(self, name):
+ self.name = name
+
+class NamedWithSynonym(Base):
+ __tablename__ = 'named_with_synonym'
+
+ id = Column(Integer, primary_key=True)
+ _name = Column(String)
+
+ def _setname(self, name):
+ self._name = name
+
+ def _getname(self):
+ return self._name
+
+ name = synonym('_name', descriptor=property(_getname, _setname))
+
+ def __init__(self, name):
+ self.name = name
+
+
+class OneToManyChild(Base):
+
+ __tablename__ = 'onetomanychild'
+
+ id = Column(Integer, primary_key=True)
+
+ name = Column(String)
+
+ def __init__(self, name):
+ self.name = name
+
+
+class OneToManyParent(Base):
+
+ __tablename__ = 'onetomanyparent'
+
+ id = Column(Integer, primary_key=True)
+
+ name = Column(String)
+
+ _child_id = Column(Integer, ForeignKey(OneToManyChild.id))
+
+ child = relationship(OneToManyChild,
+ primaryjoin=_child_id == OneToManyChild.id,
+ backref=backref('parent'))
+
+ def __init__(self, name):
+ self.name = name
+
+
+m2m_table = Table('m2m_table',
+ Base.metadata,
+ Column('left_id', Integer,
+ ForeignKey('m2mleft.id'),
+ primary_key=True),
+ Column('right_id', Integer,
+ ForeignKey('m2mright.id'),
+ primary_key=True),
+ )
+
+
+class M2mLeft(Base):
+ __tablename__ = 'm2mleft'
+
+ id = Column(Integer, primary_key=True)
+
+ name = Column(String)
+
+ def __init__(self, name):
+ self.name = name
+
+ rights = relationship('M2mRight', secondary=m2m_table,
+ backref=backref('lefts'))
+
+
+class M2mRight(Base):
+ __tablename__ = 'm2mright'
+
+ id = Column(Integer, primary_key=True)
+
+ name = Column(String)
+
+ def __init__(self, name):
+ self.name = name
@@ -0,0 +1,66 @@
+# vim: set fileencoding=utf-8 :
+from __future__ import absolute_import, division
+from iteralchemy.tests import TestCase, Named, NamedOtherColumnName,\
+ NamedWithSynonym, OneToManyChild, OneToManyParent,\
+ M2mLeft, M2mRight
+
+
+class TestAsdict(TestCase):
+
+ def test_dict(self):
+ named = Named('a name')
+ named.asdict() == dict(named)
+
+ def test_exclude_flag(self):
+ named = Named('a name')
+ assert named.asdict(exclude=['id']) == {'name': 'a name'}
+
+ def test_named_without_save(self):
+ named = Named('a name')
+ assert named.asdict() == {'id': None, 'name': named.name}
+
+ def test_named_with_save(self):
+ named = Named('a name')
+ self.session.add(named)
+ self.session.commit()
+ assert named.asdict() == {'id': named.id, 'name': named.name}
+
+ def test_named_other_columnname(self):
+ named = NamedOtherColumnName('a name')
+ assert named.asdict() == {'id': None, 'name': named.name}
+
+ def test_named_synonym(self):
+ named = NamedWithSynonym('a name')
+ assert named.asdict() == {'id': None, 'name': named.name}
+
+ def test_one_to_many_plain(self):
+ child = OneToManyChild('child')
+ parent = OneToManyParent('parent')
+ parent.child = child
+ self.session.add(parent)
+ self.session.commit()
+ assert parent.asdict() == {'id': parent.id, 'name': parent.name}
+
+ def test_one_to_many_deep(self):
+ child = OneToManyChild('child')
+ parent = OneToManyParent('parent')
+ parent.child = child
+ self.session.add(parent)
+ self.session.commit()
+ assert parent.asdict(follow=['child']) ==\
+ {'id': parent.id, 'name': parent.name,
+ 'child': child.asdict()}
+
+ def test_many_to_many(self):
+ s = self.session
+ l1 = M2mLeft('l1')
+ r1 = M2mRight('r1')
+ r2 = M2mRight('r2')
+ l1.rights.append(r1)
+ l1.rights.append(r2)
+ s.add(l1)
+ s.commit()
+ assert l1.asdict(follow=['rights']) == {'id': l1.id, 'name': l1.name,
+ 'rights': [r1.asdict(), r2.asdict()]}
+ assert r1.asdict(follow=['lefts']) == {'id': r1.id, 'name': r1.name,
+ 'lefts': [l1.asdict()]}
@@ -0,0 +1,5 @@
+[nosetests]
+with-doctest=1
+detailed-errors=1
+with-coverage=1
+cover-package=iteralchemy
@@ -0,0 +1,35 @@
+"""
+Setup the package
+
+`include_package_data' will add all files in MANIFEST.in that is prefixed
+'recursive-include'.
+
+"""
+import os
+from setuptools import find_packages
+from distutils.core import setup
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.md')).read()
+
+# Requirements for the package
+install_requires = ['Sphinx==1.1.3',
+ 'nose==1.2.1',
+ 'coverage==3.5.2',
+ 'SQLAlchemy==0.8.0b2',
+ ]
+
+
+# Requirement for running tests
+test_requires = install_requires
+
+setup(name='Iteralchemy',
+ version='0.1',
+ long_description=README,
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=install_requires,
+ tests_require=test_requires,
+ test_suite='iteralchemy',
+ )

0 comments on commit a0f7fbc

Please sign in to comment.