macaron-0.3.1/0000755000076400007640000000000011723030207012476 5ustar nobrinnobrinmacaron-0.3.1/MANIFEST.in0000644000076400007640000000011411721324752014241 0ustar nobrinnobrininclude README.* include LICENSE include MANIFEST.in include test/test_*.py macaron-0.3.1/PKG-INFO0000644000076400007640000000315011723030207013572 0ustar nobrinnobrinMetadata-Version: 1.0 Name: macaron Version: 0.3.1 Summary: Simple object-relational mapper for SQLite3, includes plugin for Bottle web framework Home-page: http://nobrin.github.com/macaron Author: Nobuo Okazaki Author-email: nobrin@biokids.org License: MIT Description: Macaron is a small object-relational mapper (ORM) for SQLite on Python. It is distributed as a single file module which has no dependencies other than the Python Standard Library. Macaron provides easy access way to SQLite database as standalone. And also it can work in Bottle web framework through the plugin mechanism. Example:: >>> import macaron >>> macaron.macaronage("members.db") >>> team = Team.create(name="Houkago Tea Time") >>> team.members.append(name="Ritsu", part="Dr") >>> mio = team.members.append(name="Mio", part="Ba") >>> print mio >>> for member in team.members: print member ... >>> macaron.bake() >>> macaron.cleanup() Platform: any Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Database Classifier: Topic :: Database :: Front-Ends Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 macaron-0.3.1/README.rst0000644000076400007640000000517111722545267014211 0ustar nobrinnobrin.. _Python: http://python.org/ .. _SQLite: http://www.sqlite.org/ .. _Bottle: http://bottlepy.org/ ===================== Macaron: O/R Mapper ===================== Overview ======== *Macaron* is a small and simple object-relational mapper (ORM) for SQLite_ and Python_. It is distributed as a single file module which has no dependencies other than the `Python Standard Library `_. *Macaron* provides provides easy access methods to SQLite database. And it supports Bottle_ web framework through plugin mechanism. Example:: >>> import macaron >>> macaron.macaronage(dbfile="members.db") >>> team = Team.create(name="Houkago Tea Time") >>> team.members.append(first_name="Ritsu", last_name="Tainaka", part="Dr") >>> mio = team.members.append(first_name="Mio", last_name="Akiyama", part="Ba") >>> print mio >>> for member in team.members: print member ... Macaron supports **Many-To-One** relationships and reverse reference. Many-To-Many relationships have not been supported yet. To realize simple implementation, Macaron does not provide methods for creation of tables. MacaronPlugin class for Bottle_ web framework is implemented. External resources ================== - Homepage and documentation: http://nobrin.github.com/macaron/ - Documentation in Japanese: http://biokids.org/?Macaron - Python Package Index (PyPI): http://pypi.python.org/pypi/macaron - GitHub: https://github.com/nobrin/macaron Installation and Dependencies ============================= :: tar zxvf macaron-0.3.0.tar.gz cd macaron-0.3.0 python setup.py or using easy_install:: easy_install macaron Use for Web Applications ======================== Macaron in the Bottle --------------------- Bottle_ is a lightweight web framework for Python. Macaron can be used with Bottle through :class:`MacaronPlugin`, which is tested with Bottle 0.10.9. Example ------- :: #!/usr/bin/env python from bottle import * import macaron install(macaron.MacaronPlugin("address.db")) class Address(macaron.Model): _table_name = "address" @route("/hello") def index(): addr = Address.get(1) return "

Hello!!

My address is %s" % addr.address run(host="localhost", port=8080) Implementation -------------- :class:`MacaronPlugin` create lazy connection. So the :class:`sqlite3.Connection` object is create at call Macaron methods. In case of no use the methods in :meth:`bottle.route`, any connection is created. macaron-0.3.1/setup.cfg0000644000076400007640000000007311723030207014317 0ustar nobrinnobrin[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 macaron-0.3.1/macaron.py0000644000076400007640000007157311723025434014513 0ustar nobrinnobrin# -*- coding: utf-8 -*- """ Macaron is a small object-relational mapper (ORM) for SQLite on Python. It is distributed as a single file module which has no dependencies other than the Python Standard Library. Macaron provides easy access way to SQLite database as standalone. And also it can work in Bottle web framework through the plugin mechanism. Example:: >>> import macaron >>> macaron.macaronage("members.db") >>> team = Team.create(name="Houkago Tea Time") >>> team.members.append(name="Ritsu", part="Dr") >>> mio = team.members.append(name="Mio", part="Ba") >>> print mio >>> for member in team.members: print member ... >>> macaron.bake() >>> macaron.cleanup() """ __author__ = "Nobuo Okazaki" __version__ = "0.3.1" __license__ = "MIT License" import sqlite3, re import copy import logging from datetime import datetime # --- Exceptions class ObjectDoesNotExist(Exception): pass class ValidationError(Exception): pass # TODO: fix behavior class MultipleObjectsReturned(Exception): pass class NotUniqueForeignKey(Exception): pass # --- Module global attributes _m = None # Macaron object history = None #: Returns history of SQL execution. You can get history like a list (index:0 is latest). # --- Module methods def macaronage(dbfile=":memory:", lazy=False, autocommit=False, logger=None, history=-1): """ :param dbfile: SQLite database file name. :param lazy: Uses :class:`LazyConnection`. :param autocommit: Commits automatically when closing database. :param logger: Uses for logging SQL execution. :param history: Sets max count of SQL execution history (0 is unlimited, -1 is disabled). Default: disabled :type logger: :class:`logging.Logger` Initializes macaron. This sets Macaron instance to module global variable *_m* (don't access directly). If *lazy* is ``True``, :class:`LazyConnection` object is used for connection, which will connect to the DB when using. If *autocommit* is ``True``, this will commits when this object will be unloaded. """ globals()["_m"] = Macaron() globals()["history"] = ListHandler(-1) conn = None if history >= 0: # enable history logger logger = logger or logging.getLogger() logger.setLevel(logging.DEBUG) globals()["history"].set_max_count(history) logger.addHandler(globals()["history"]) if lazy: conn = LazyConnection(dbfile, factory=_create_wrapper(logger)) else: conn = sqlite3.connect(dbfile, factory=_create_wrapper(logger)) if not conn: raise Exception("Can't create connection.") _m.connection["default"] = conn _m.autocommit = autocommit def execute(*args, **kw): """Wrapper for ``Cursor#execute()``.""" return _m.connection["default"].cursor().execute(*args, **kw) def bake(): _m.connection["default"].commit() # Commits def rollback(): _m.connection["default"].rollback() # Rollback def cleanup(): _m = None # Closes database and tidies up Macaron # --- Classes class Macaron(object): """Macaron controller class. Do not instance this class by user.""" def __init__(self): #: ``dict`` object holds :class:`sqlite3.Connection` self.connection = {} self.used_by = [] self.sql_logger = None def __del__(self): """Closing the connections""" while len(self.used_by): # Removes references from TableMetaClassProperty. # If the pointer leaved, closing connection causes status mismatch # between TableMetaClassProperty#table_meta and Macaron#connection. self.used_by.pop(0).table_meta = None for k in self.connection.keys(): if self.autocommit: self.connection[k].commit() self.connection[k].close() def get_connection(self, meta_obj): """Returns Connection and adds reference to the object which uses it.""" self.used_by.append(meta_obj) return self.connection[meta_obj.conn_name] # --- Connection wrappers def _create_wrapper(logger): """Returns ConnectionWrapper class""" class ConnectionWrapper(sqlite3.Connection): def __init__(self, *args, **kw): super(ConnectionWrapper, self).__init__(*args, **kw) self.execute("PRAGMA foreign_keys = ON") # fkey support ON (>=SQLite-3.6.19) def cursor(self): self.logger = logger return super(ConnectionWrapper, self).cursor(CursorWrapper) return ConnectionWrapper class CursorWrapper(sqlite3.Cursor): """Subclass of sqlite3.Cursor for logging""" def execute(self, sql, parameters=[]): if self.connection.logger: self.connection.logger.debug("%s\nparams: %s" % (sql, str(parameters))) if(isinstance(history, ListHandler)): history.lastsql = sql history.lastparams = parameters return super(CursorWrapper, self).execute(sql, parameters) class LazyConnection(object): """Lazy connection wrapper""" def __init__(self, *args, **kw): self.args = args self.kwargs = kw self._conn = None def __getattr__(self, name): if not self._conn and (name in ["commit", "rollback", "close"]): return self.noop self._conn = self._conn or sqlite3.connect(*self.args, **self.kwargs) return getattr(self._conn, name) def noop(self): return # NO-OP for commit, rollback, close # --- Logging class ListHandler(logging.Handler): """SQL history listing handler for ``logging``. :param max_count: max count of SQL history (0 is unlimited, -1 is disabled) """ def __init__(self, max_count=100): logging.Handler.__init__(self, logging.DEBUG) self.lastsql = None self.lastparams = None self._max_count = max_count self._list = [] def emit(self, record): if self._max_count < 0: return if self._max_count > 0: while len(self._list) >= self._max_count: self._list.pop() self._list.insert(0, record.getMessage()) def _get_max_count(self): return self._max_count def set_max_count(self, max_count): self._max_count = max_count if max_count > 0: while len(self._list) > self._max_count: self._list.pop() max_count = property(_get_max_count) def count(self): return len(self._list) def __getitem__(self, idx): if self._max_count < 0: raise RuntimeError("SQL history is disabled. Use macaronage() with 'history' parameter.") if len(self._list) <= idx: raise IndexError("SQL history max_count is %d." % len(self._list)) return self._list.__getitem__(idx) # --- Table and field information class FieldInfoCollection(list): """FieldInfo collection""" def __init__(self): self._field_dict = {} def append(self, fld): super(FieldInfoCollection, self).append(fld) self._field_dict[fld.name] = fld def __getitem__(self, name): if isinstance(name, (int, long)): return super(FieldInfoCollection, self).__getitem__(name) return self._field_dict[name] def keys(self): return self._field_dict.keys() class ClassProperty(property): """Using class property wrapper class""" def __get__(self, owner_obj, cls): return self.fget.__get__(owner_obj, cls)() class FieldFactory(object): @staticmethod def create(row, cls): rec = dict(zip(["cid", "name", "type", "not_null", "default", "is_primary_key"], row)) cdict = cls.__dict__ if cdict.has_key(rec["name"]) and cdict[rec["name"]].is_user_defined: fld = cls.__dict__[rec["name"]] else: fldkw = { "null" : not rec["not_null"], "is_primary_key": rec["is_primary_key"], } use_field_class = Field for fldcls in TYPE_FIELDS: for regex in fldcls.TYPE_NAMES: if re.search(regex, row[2]): use_field_class = fldcls break fld = use_field_class(**fldkw) fld.cid, fld.name, fld.type = row[0:3] fld.initialize_after_meta() # convert default from 'PRAGMA table_info()'. if fld.default == None and rec["default"] != None: fld.default = fld.cast(rec["default"]) setattr(cls, rec["name"], fld) return fld class TableMetaClassProperty(property): """Using TableMetaInfo class property wrapper class""" def __init__(self): super(TableMetaClassProperty, self).__init__() self.table_meta = None self.table_name = None self.conn_name = "default" #: for future use. multiple databases? def __get__(self, owner_obj, cls): if not self.table_meta: self.table_meta = TableMetaInfo(_m.get_connection(self), self.table_name, cls) return self.table_meta class TableMetaInfo(object): """Table information class. This object has table information, which is set to ModelClass._meta by :class:`ModelMeta`. If you use ``Bookmark`` class, you can access the table information with ``Bookmark._meta``. """ def __init__(self, conn, table_name, cls): self._conn = conn # Connection for the table #: Table fields collection self.fields = FieldInfoCollection() #: Primary key :class:`Field` self.primary_key = None #: Table name self.table_name = table_name cur = conn.cursor() rows = cur.execute("PRAGMA table_info(%s)" % table_name).fetchall() for row in rows: fld = FieldFactory.create(row, cls) self.fields.append(fld) if fld.is_primary_key: self.primary_key = fld # --- Field converting and validation class Field(property): is_user_defined = False def __init__(self, null=False, default=None, is_primary_key=False): self.null = null self.default = default self.is_primary_key = bool(is_primary_key) def cast(self, value): return value def set(self, obj, value): return value def to_database(self, obj, value): return value def to_object(self, row, value): return value def validate(self, obj, value): if not self.null and value == None: raise ValidationError("Field '%s' does not accept None value." % self.name) return True def initialize_after_meta(self): pass def __get__(self, owner_obj, cls): return owner_obj._data.get(self.name, None) def __set__(self, owner_obj, value): self.validate(self, value) owner_obj._data[self.name] = value @staticmethod def default_convert(typename, value): for regex in FloatField.TYPE_NAMES: if re.search(regex, typename, re.I): return float(value) for regex in IntegerField.TYPE_NAMES: if re.search(regex, typename, re.I): return int(value) return value class AtCreate(Field): pass class AtSave(Field): pass class TimestampField(Field): TYPE_NAMES = (r"^TIMESTAMP$", r"^DATETIME$") def to_database(self, obj, value): return value.strftime("%Y-%m-%d %H:%M:%S") def to_object(self, row, value): return datetime.strptime(value, "%Y-%m-%d %H:%M:%S") class DateField(Field): TYPE_NAMES = (r"^DATE$",) def to_database(self, obj, value): return value.strftime("%Y-%m-%d") def to_object(self, row, value): return datetime.strptime(value, "%Y-%m-%d").date() class TimeField(Field): TYPE_NAMES = (r"^TIME$",) def to_database(self, obj, value): return value.strftime("%H-%M-%S") def to_object(self, row, value): return datetime.strptime(value, "%H-%M-%S").time() class TimestampAtCreate(TimestampField, AtCreate): def __init__(self, **kw): kw["null"] = True super(TimestampAtCreate, self).__init__(**kw) def set(self, obj, value): return datetime.now() class DateAtCreate(DateField, AtCreate): def __init__(self, **kw): kw["null"] = True super(DateAtCreate, self).__init__(**kw) def set(self, obj, value): return datetime.now().date() class TimeAtCreate(TimeField, AtCreate): def __init__(self, **kw): kw["null"] = True super(TimeAtCreate, self).__init__(**kw) def set(self, obj, value): return datetime.now().time() class TimestampAtSave(TimestampAtCreate, AtSave): pass class DateAtSave(DateAtCreate, AtSave): pass class TimeAtSave(TimeAtCreate, AtSave): pass class FloatField(Field): TYPE_NAMES = ("REAL", "FLOA", "DOUB") def __init__(self, max=None, min=None, **kw): super(FloatField, self).__init__(**kw) self.max, self.min = max, min def cast(self, value): if value == None: return None return float(value) def validate(self, obj, value): super(FloatField, self).validate(obj, value) if value == None: return True try: self.cast(value) except (ValueError, TypeError): raise ValidationError("Value is not a number.") if self.max != None and value > self.max: raise ValidationError("Max value is exceeded. %d" % value) if self.min != None and value < self.min: raise ValidationError("Min value is underrun. %d" % value) return True class IntegerField(FloatField): TYPE_NAMES = ("INT",) def initialize_after_meta(self): if re.match(r"^INTEGER$", self.type, re.I) and self.is_primary_key: self.null = True def cast(self, value): if value == None: return None return int(value) def validate(self, obj, value): super(IntegerField, self).validate(obj, value) if value == None: return True try: self.cast(value) except (ValueError, TypeError): raise ValidationError("Value is not an integer.") return True class CharField(Field): TYPE_NAMES = ("CHAR", "CLOB", "TEXT") def __init__(self, max_length=None, min_length=None, **kw): super(CharField, self).__init__(**kw) self.max_length, self.min_length = max_length, min_length def initialize_after_meta(self): m = re.search(r"CHAR\s*\((\d+)\)", self.type, re.I) if m and (not self.max_length or self.max_length > int(m.group(1))): self.max_length = int(m.group(1)) def validate(self, obj, value): super(CharField, self).validate(obj, value) if value == None: return True if self.max_length and len(value) > self.max_length: raise ValidationError("Text is too long.") if self.min_length and len(value) < self.min_length: raise ValidationError("Text is too short.") return True # --- Relationships class ManyToOne(property): """Many to one relation ship definition class""" def __init__(self, ref, related_name=None, fkey=None, ref_key=None): # in this state, db has been not connected! self.ref = ref #: reference table ('one' side) self.fkey = fkey #: foreign key name ('many' side) self.ref_key = ref_key #: reference key ('one' side) self.related_name = related_name #: accessor name for one to many relation def __get__(self, owner, cls): reftbl = self.ref._meta.table_name clstbl = cls._meta.table_name self.fkey = self.fkey or "%s_id" % self.ref._meta.table_name self.ref_key = self.ref_key or self.ref._meta.primary_key.name sql = "SELECT %s.* FROM %s LEFT JOIN %s ON %s = %s.%s WHERE %s.%s = ?" \ % (reftbl, clstbl, reftbl, self.fkey, reftbl, self.ref_key, \ clstbl, cls._meta.primary_key.name) cur = cls._meta._conn.cursor() cur = cur.execute(sql, [owner.pk]) row = cur.fetchone() if cur.fetchone(): raise NotUniqueForeignKey("Reference key '%s.%s' is not unique." % (reftbl, self.ref_key)) return self.ref._factory(cur, row) def set_reverse(self, rev_cls): """Sets up one to many definition method. This method will be called in ``ModelMeta#__init__``. To inform the model class to ManyToOne and _ManyToOne_Rev classes. The *rev_class* means **'many(child)' side class**. """ self.related_name = self.related_name or "%s_set" % rev_cls.__name__.lower() setattr(self.ref, self.related_name, _ManyToOne_Rev(self.ref, self.ref_key, rev_cls, self.fkey)) class _ManyToOne_Rev(property): """The reverse of many to one relationship.""" def __init__(self, ref, ref_key, rev, rev_fkey): self.ref = ref # Reference table (parent) self.ref_key = ref_key # Key column name of parent self.rev = rev # Child table (many side) self.rev_fkey = rev_fkey # Foreign key name of child def __get__(self, owner, cls): self.rev_fkey = self.rev_fkey or "%s_id" % self.ref._meta.table_name self.ref_key = self.ref_key or self.ref._meta.primary_key.name qs = self.rev.select("%s = ?" % self.rev_fkey, [getattr(owner, self.ref_key)]) return ManyToOneRevSet(qs, owner, self) # --- QuerySet class QuerySet(object): """This class generates SQL which like QuerySet in Django""" def __init__(self, parent): if isinstance(parent, QuerySet): self.cls = parent.cls self.clauses = copy.deepcopy(parent.clauses) else: self.cls = parent self.clauses = {"type": "SELECT", "where": [], "order_by": [], "values": [], "distinct": False} self.clauses["offset"] = 0 self.clauses["limit"] = 0 self.clauses["select_fields"] = "*" self.factory = self.cls._factory # Factory method converting record to object self._initialize_cursor() def _initialize_cursor(self): """Cleaning cache and state""" self.cur = None # cursor self._index = -1 # pointer self._cache = [] # cache list def _generate_sql(self): if self.clauses["distinct"]: distinct = "DISTINCT " else: distinct = "" if self.clauses["type"] == "DELETE": sqls = ["DELETE FROM %s" % self.cls._meta.table_name] else: sqls = ["SELECT %s%s FROM %s" % (distinct, self.clauses["select_fields"], self.cls._meta.table_name)] if len(self.clauses["where"]): sqls.append("WHERE %s" % " AND ".join(["(%s)" % c for c in self.clauses["where"]])) if self.clauses["type"] == "SELECT": if len(self.clauses["order_by"]): sqls.append("ORDER BY %s" % ", ".join(self.clauses["order_by"])) if self.clauses["offset"]: sqls.append("OFFSET %d" % self.clauses["offset"]) if self.clauses["limit"]: sqls.append("LIMIT %d" % self.clauses["limit"]) return "\n".join(sqls) sql = property(_generate_sql) #: Generating SQL def _execute(self): """Getting and setting a new cursor""" self._initialize_cursor() self.cur = self.cls._meta._conn.cursor().execute(self.sql, self.clauses["values"]) def __iter__(self): self._execute() return self def next(self): if not self.cur: self._execute() row = self.cur.fetchone() self._index += 1 if not row: raise StopIteration() self._cache.append(self.factory(self.cur, row)) return self._cache[-1] def get(self, where, values=None): if values == None: values = [where] where = "%s = ?" % self.cls._meta.primary_key.name qs = self.select(where, values) try: obj = qs.next() except StopIteration: raise self.cls.DoesNotExist("%s object is not found." % cls.__name__) try: qs.next() except StopIteration: return obj raise MultipleObjectsReturned("The 'get()' requires single result.") def select(self, where=None, values=[]): newset = self.__class__(self) if where: newset.clauses["where"].append(where) if values: newset.clauses["values"] += values return newset def all(self): return self.select() def delete(self): self.clauses["type"] = "DELETE" self._execute() def distinct(self): """EXPERIMENTAL: I don't know what situation this distinct method is used in. """ newset = self.__class__(self) newset.clauses["distinct"] = True return newset def order_by(self, *args): newset = self.__class__(self) newset.clauses["order_by"] += [re.sub(r"^-(.+)$", r"\1 DESC", n) for n in args] return newset def __getitem__(self, index): newset = self.__class__(self) if isinstance(index, slice): start, stop = index.start or 0, index.stop or 0 newset.clauses["offset"], newset.clauses["limit"] = start, stop - start return newset elif self._index >= index: return self._cache[index] for obj in self: if self._index >= index: return obj # Aggregation methods def aggregate(self, agg): def single_value(cur, row): return row[0] newset = self.__class__(self) newset.clauses["select_fields"] = "%s(%s)" % (agg.name, agg.field_name) newset.factory = single_value # Change factory method for single value return newset.next() def count(self): return self.aggregate(Count("*")) def __str__(self): objs = self._cache + [obj for obj in self] return str(objs) class ManyToOneRevSet(QuerySet): """Reverse relationship of ManyToOne""" def __init__(self, parent_query, parent_object=None, rel=None): super(ManyToOneRevSet, self).__init__(parent_query) if parent_object and rel: self.parent = parent_object self.parent_key = rel.ref_key self.cls_fkey = rel.rev_fkey def append(self, *args, **kw): """Append a new member""" kw[self.cls_fkey] = getattr(self.parent, self.parent_key) return self.cls.create(*args, **kw) # --- BaseModel and Model class class ModelMeta(type): """Meta class for Model class""" def __new__(cls, name, bases, dict): dict["DoesNotExist"] = type("DoesNotExist", (ObjectDoesNotExist,), {}) dict["_meta"] = TableMetaClassProperty() dict["_meta"].table_name = dict.pop("_table_name", name.lower()) return type.__new__(cls, name, bases, dict) def __init__(cls, name, bases, dict): for k in dict.keys(): if isinstance(dict[k], ManyToOne): dict[k].set_reverse(cls) if isinstance(dict[k], Field): dict[k].is_user_defined = True class Model(object): """Base model class. Models must inherit this class.""" __metaclass__ = ModelMeta _table_name = None #: Database table name (the property will be deleted in ModelMeta) _meta = None #: accessor for TableMetaInfo (set in ModelMeta) # Accessing to _meta triggers initializing TableMetaInfo and Class attributes. def __init__(self, **kw): self._data = {} for fld in self.__class__._meta.fields: self._data[fld.name] = fld.default for k in kw.keys(): if k not in self.__class__._meta.fields.keys(): ValueError("Invalid column name '%s'." % k) setattr(self, k, kw[k]) def get_key_value(self): """Getting value of primary key field""" return getattr(self, self.__class__._meta.primary_key.name) pk = property(get_key_value) #: accessor for primary key value @classmethod def _factory(cls, cur, row): """Convert raw values to object""" h = dict([[d[0], row[i]] for i, d in enumerate(cur.description)]) for fld in cls._meta.fields: h[fld.name] = fld.to_object(sqlite3.Row(cur, row), h[fld.name]) return cls(**h) @classmethod def get(cls, where, values=None): """Getting single result by ID""" return QuerySet(cls).get(where, values) @classmethod def all(cls): return QuerySet(cls).select() @classmethod def select(cls, where, values): """Getting QuerySet instance by WHERE clause""" return QuerySet(cls).select(where, values) @classmethod def create(cls, **kw): """Creating new record""" names = [] obj = cls(**kw) for fld in cls._meta.fields: if fld.is_primary_key and not getattr(obj, fld.name): continue names.append(fld.name) Model._before_before_store(obj, "set", AtCreate) # set value obj.before_create() obj.validate() Model._before_before_store(obj, "to_database", Field) # convert object to database values = [getattr(obj, n) for n in names] holder = ", ".join(["?"] * len(names)) sql = "INSERT INTO %s (%s) VALUES (%s)" % (cls._meta.table_name, ", ".join(names), holder) cls._save_and_update_object(obj, sql, values) obj.after_create() return obj def save(self): """Updating the record""" cls = self.__class__ names = [] for fld in cls._meta.fields: if fld.is_primary_key: continue names.append(fld.name) holder = ", ".join(["%s = ?" % n for n in names]) Model._before_before_store(self, "set", AtSave) # set value self.validate() self.before_save() Model._before_before_store(self, "to_database", Field) # convert object to database values = [getattr(self, n) for n in names] sql = "UPDATE %s SET %s WHERE %s = ?" % (cls._meta.table_name, holder, cls._meta.primary_key.name) cls._save_and_update_object(self, sql, values + [self.pk]) self.after_save() @staticmethod def _save_and_update_object(obj, sql, values): cls = obj.__class__ cur = cls._meta._conn.cursor().execute(sql, values) if obj.pk == None: current_id = cur.lastrowid else: current_id = obj.pk newobj = cls.get(current_id) for fld in cls._meta.fields: setattr(obj, fld.name, getattr(newobj, fld.name)) def delete(self): """Deleting the record""" cls = self.__class__ sql = "DELETE FROM %s WHERE %s = ?" % (cls._meta.table_name, cls._meta.primary_key.name) cls._meta._conn.cursor().execute(sql, [self.pk]) @staticmethod def _before_before_store(obj, meth_name, at_cls): cls = obj.__class__ # set value with at_cls object for fld in cls._meta.fields: if isinstance(fld, at_cls): converter = getattr(fld, meth_name) setattr(obj, fld.name, converter(cls, getattr(obj, fld.name))) def validate(self): cls = self.__class__ for fld in cls._meta.fields: value = getattr(self, fld.name) if not fld.validate(self, value): raise ValidationError("%s.%s is invalid value. '%s'" % (cls.__name__, fld.name, str(value))) # These hooks are triggered at Model.create() and Model#save(). # Model.create(): before_create -> INSERT -> after_create # Model#save() : bofore_save -> UPDATE -> after_save def before_create(self): pass # Called before INSERT def before_save(self): pass # Called before UPDATE def after_create(self): pass # Called after INSERT def after_save(self): pass # Called after UPDATE def __repr__(self): return "<%s object %s>" % (self.__class__.__name__, self.pk) # --- Aggregation functions class AggregateFunction(object): def __init__(self, field_name): self.field_name = field_name class Avg(AggregateFunction): name = "AVG" class Max(AggregateFunction): name = "MAX" class Min(AggregateFunction): name = "MIN" class Sum(AggregateFunction): name = "SUM" class Count(AggregateFunction): name = "COUNT" # --- Plugin for Bottle web framework class MacaronPlugin(object): """Macaron plugin for Bottle web framework This plugin handled Macaron. """ name = "macaron" api = 2 def __init__(self, dbfile=":memory:", autocommit=True): self.dbfile = dbfile self.autocommit = autocommit def setup(self, app): pass def apply(self, callback, ctx): conf = ctx.config.get("macaron") or {} dbfile = conf.get("dbfile", self.dbfile) autocommit = conf.get("autocommit", self.autocommit) import traceback as tb def wrapper(*args, **kwargs): macaronage(dbfile, lazy=True) try: ret_value = callback(*args, **kwargs) if autocommit: bake() # commit except sqlite3.IntegrityError, e: rollback() try: import bottle traceback = None if bottle.DEBUG: traceback = (history.lastsql, history.lastparams) sqllog = "[Macaron]LastSQL: %s\n[Macaron]Params : %s\n" % traceback bottle.request.environ["wsgi.errors"].write(sqllog) raise bottle.HTTPError(500, "Database Error", e, tb.format_exc()) except ImportError: raise e return ret_value return wrapper TYPE_FIELDS = [IntegerField, FloatField, CharField] macaron-0.3.1/macaron.egg-info/0000755000076400007640000000000011723030207015610 5ustar nobrinnobrinmacaron-0.3.1/macaron.egg-info/PKG-INFO0000644000076400007640000000315011723030207016704 0ustar nobrinnobrinMetadata-Version: 1.0 Name: macaron Version: 0.3.1 Summary: Simple object-relational mapper for SQLite3, includes plugin for Bottle web framework Home-page: http://nobrin.github.com/macaron Author: Nobuo Okazaki Author-email: nobrin@biokids.org License: MIT Description: Macaron is a small object-relational mapper (ORM) for SQLite on Python. It is distributed as a single file module which has no dependencies other than the Python Standard Library. Macaron provides easy access way to SQLite database as standalone. And also it can work in Bottle web framework through the plugin mechanism. Example:: >>> import macaron >>> macaron.macaronage("members.db") >>> team = Team.create(name="Houkago Tea Time") >>> team.members.append(name="Ritsu", part="Dr") >>> mio = team.members.append(name="Mio", part="Ba") >>> print mio >>> for member in team.members: print member ... >>> macaron.bake() >>> macaron.cleanup() Platform: any Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Database Classifier: Topic :: Database :: Front-Ends Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 macaron-0.3.1/macaron.egg-info/top_level.txt0000644000076400007640000000001011723030207020331 0ustar nobrinnobrinmacaron macaron-0.3.1/macaron.egg-info/SOURCES.txt0000644000076400007640000000044711723030207017501 0ustar nobrinnobrinLICENSE MANIFEST.in README.rst macaron.py setup.py macaron.egg-info/PKG-INFO macaron.egg-info/SOURCES.txt macaron.egg-info/dependency_links.txt macaron.egg-info/top_level.txt test/test_basic.py test/test_class_attr.py test/test_convert.py test/test_fields.py test/test_history.py test/testall.pymacaron-0.3.1/macaron.egg-info/dependency_links.txt0000644000076400007640000000000111723030207021656 0ustar nobrinnobrin macaron-0.3.1/setup.py0000644000076400007640000000246111723025725014224 0ustar nobrinnobrin#!/usr/bin/env python # -*- coding: utf-8 -*- # Project: # Module: try: from setuptools import setup # for development to use 'setup.py develop' command except ImportError: from distutils.core import setup import sys if sys.version_info < (2, 5): raise NotImplementedError("Sorry, you need at least Python 2.5 to use Macaron.") import macaron setup( name = "macaron", version = macaron.__version__, description = "Simple object-relational mapper for SQLite3, includes plugin for Bottle web framework", long_description = macaron.__doc__, author = macaron.__author__, author_email = "nobrin@biokids.org", url = "http://nobrin.github.com/macaron", py_modules = ["macaron"], scripts = ["macaron.py"], license = "MIT", platforms = "any", classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Database", "Topic :: Database :: Front-Ends", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", ], ) macaron-0.3.1/LICENSE0000644000076400007640000000204311721324752013513 0ustar nobrinnobrinCopyright (c) 2012, Nobuo Okazaki. 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. macaron-0.3.1/test/0000755000076400007640000000000011723030207013455 5ustar nobrinnobrinmacaron-0.3.1/test/testall.py0000755000076400007640000000117111721324752015513 0ustar nobrinnobrin#!/usr/bin/env python # -*- coding: utf-8 -*- # test all import unittest import sys, os, glob test_root = os.path.dirname(os.path.abspath(__file__)) test_files = glob.glob(os.path.join(test_root, "test_*.py")) os.chdir(test_root) sys.path.insert(0, os.path.dirname(test_root)) sys.path.insert(0, test_root) test_names = [os.path.basename(name)[:-3] for name in test_files] suite = unittest.defaultTestLoader.loadTestsFromNames(test_names) def run(): import macaron result = unittest.TextTestRunner(verbosity=2).run(suite) sys.exit((result.errors or result.failures) and 1 or 0) if __name__ == '__main__': run() macaron-0.3.1/test/test_history.py0000644000076400007640000000271511723022401016571 0ustar nobrinnobrin#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test for HistoryLogger. """ import unittest import macaron import sqlite3 import logging DB_FILE = ":memory:" SQL_TEST = """CREATE TABLE IF NOT EXISTS t_test ( id INTEGER PRIMARY KEY, name TEXT, value TEXT )""" class TestHistoryLogger(unittest.TestCase): def setUp(self): pass def tearDown(self): pass def testLogger(self): logger = logging.getLogger() logger.setLevel(logging.DEBUG) sql_logger = macaron.ListHandler(10) logger.addHandler(sql_logger) conn = sqlite3.connect(DB_FILE, factory=macaron._create_wrapper(logger)) conn.execute(SQL_TEST) self.assertEqual(sql_logger[0], "%s\nparams: []" % SQL_TEST) conn.close() def testLogger_content(self): macaron.macaronage(DB_FILE, history=10) macaron.execute(SQL_TEST) self.assertEqual(macaron.history[0], "%s\nparams: []" % SQL_TEST) macaron.cleanup() def testMacaronOption_disabled(self): macaron.macaronage(DB_FILE) def _history_is_disabled(): macaron.history[0] self.assertRaises(RuntimeError, _history_is_disabled) macaron.cleanup() def testMacaronOption_index(self): macaron.macaronage(DB_FILE, history=10) def _index_error(): macaron.history[1] self.assertRaises(IndexError, _index_error) macaron.cleanup() if __name__ == "__main__": unittest.main() macaron-0.3.1/test/test_convert.py0000644000076400007640000000335411721324752016564 0ustar nobrinnobrin#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test converter """ import sys, os sys.path.insert(0, "../") import unittest, time try: import simplejson as json except ImportError: import json import macaron DB_FILE = ":memory:" sql_t_myrecord = """CREATE TABLE IF NOT EXISTS myrecord ( id INTEGER PRIMARY KEY, name TEXT, value TEXT, created TIMESTAMP, modified TIMESTAMP )""" class StoreJSON(macaron.Field): def to_database(self, obj, value): return json.dumps(value) def to_object(self, row, value): return json.loads(value) class MyRecord(macaron.Model): value = StoreJSON() created = macaron.TimestampAtCreate() modified = macaron.TimestampAtSave() def __str__(self): return "" % (self.name, str(self.value)) class TestConverter(unittest.TestCase): def setUp(self): macaron.macaronage(dbfile=DB_FILE, lazy=True) macaron.execute(sql_t_myrecord) def tearDown(self): macaron.bake() macaron.cleanup() def testCRUD(self): newrec = MyRecord.create(name="My test", value={"Macaron":"Good!"}) self.assert_(newrec.created) self.assert_(newrec.modified) created = newrec.created modified = newrec.modified time.sleep(2) # wait for changing time rec = MyRecord.get(1) self.assertEqual(rec.value["Macaron"], "Good!") rec.value = {"Macaron":"Excellent!!"} rec.save() self.assertEqual(rec.created, created, "When saving, created time is not changed") self.assertNotEqual(rec.modified, modified, "When saving, modified time is updated") if __name__ == "__main__": if os.path.isfile(DB_FILE): os.unlink(DB_FILE) unittest.main() macaron-0.3.1/test/test_basic.py0000755000076400007640000001210211723021556016155 0ustar nobrinnobrin#!/usr/bin/env python # -*- coding: utf-8 -*- """ Testing for basic usage. """ import unittest import macaron DB_FILE = ":memory:" SQL_TEAM = """CREATE TABLE IF NOT EXISTS team ( id INTEGER PRIMARY KEY, name TEXT )""" SQL_MEMBER = """CREATE TABLE IF NOT EXISTS member ( id INTEGER PRIMARY KEY, team_id INTEGER REFERENCES team (id), first_name TEXT, last_name TEXT, part TEXT, age INT )""" class Team(macaron.Model): def __str__(self): return "" % self.name class Member(macaron.Model): team = macaron.ManyToOne(Team, related_name="members") def __str__(self): return "" % (self.first_name, self.last_name, self.part) class TestMacaron(unittest.TestCase): names = [ ("Ritsu", "Tainaka", "Dr", "Ritsu Tainaka : Dr"), ("Mio", "Akiyama", "Ba", "Mio Akiyama : Ba"), ("Yui", "Hirasawa", "Gt", "Yui Hirasawa : Gt"), ("Tsumugi", "Kotobuki", "Kb", "Tsumugi Kotobuki : Kb"), ] def setUp(self): macaron.macaronage(DB_FILE, lazy=True) macaron.execute(SQL_TEAM) macaron.execute(SQL_MEMBER) def tearDown(self): macaron.bake() def testCRUD(self): # create team name = "Houkago Tea Time" team = Team.create(name=name) self.assertEqual(str(team), "" % name) # create members for idx, n in enumerate(self.names): member = Member.create(team_id=team.pk, first_name=n[0], last_name=n[1], part=n[2]) self.assertEqual(str(member), "" % n[3]) self.assertEqual(member.id, idx + 1) # get member with id ritsu = Member.get(1) self.assertEqual(str(ritsu), "") # get team the member Ritsu belongs to is Houkago Tea Time team = member.team self.assertEqual(str(team), "") # get members with iterator for idx, m in enumerate(team.members): self.assertEqual(str(m), "" % self.names[idx][3]) macaron.bake() # Yui changes instrument to castanets yui = Member.get("first_name=? AND last_name=?", ["Yui", "Hirasawa"]) yui.part = "Castanets" yui.save() # re-fetch Yui member = Member.get(3) self.assertEqual(member.part, "Castanets") # Delete all members self.assertEqual(team.members.count(), 4) team.members.select("first_name=?", ["Ritsu"]).delete() self.assertEqual(team.members.count(), 3) team.members.delete() self.assertEqual(team.members.count(), 0) # cancel the changes macaron.rollback() # Add another member 'Sawako' as Gt1 team = Team.get(1) Member.create(team_id=team.pk, first_name="Sawako", last_name="Yamanaka", part="Gt1") # re-fetch Sawako with index sawako = team.members[4] self.assertEqual(str(sawako), "") # but Sawako is not a member of the team sawako.delete() # Add another member Azusa through reverse relation of ManyToOne team.members.append(first_name="Azusa", last_name="Nakano", part="Gt2") azu = Member.get("first_name=? AND last_name=?", ["Azusa", "Nakano"]) self.assertEqual(str(azu), "") # Okay, Yui changes part to Gt1 yui = Member.get("first_name=? AND last_name=?", ["Yui", "Hirasawa"]) yui.part = "Gt1" yui.save() # At last, there are five menbers nm = self.names[:] nm[2] = ("Yui", "Hirasawa", "Gt1", "Yui Hirasawa : Gt1") nm.append(("Azusa", "Nakano", "Gt2", "Azusa Nakano : Gt2")) for idx, m in enumerate(team.members): self.assertEqual(str(m), "" % nm[idx][3]) def testAggregation(self): team = Team.create(name="Houkago Tea Time") team.members.append(first_name="Ritsu" , last_name="Tainaka" , part="Dr" , age=17) team.members.append(first_name="Mio" , last_name="Akiyama" , part="Ba" , age=17) team.members.append(first_name="Yui" , last_name="Hirasawa", part="Gt1", age=17) team.members.append(first_name="Tsumugi", last_name="Kotobuki", part="Kb" , age=16) team.members.append(first_name="Azusa" , last_name="Nakano" , part="Gt2", age=17) a = ("Akiyama", "Hirasawa", "Kotobuki", "Nakano", "Tainaka") for i, m in enumerate(Team.get(1).members.order_by("last_name")): self.assertEqual(m.last_name, a[i]) cnt = team.members.all().count() self.assertEqual(cnt, 5) sum_of_ages = team.members.all().aggregate(macaron.Sum("age")) self.assertEqual(sum_of_ages, 84) # sorry, I can't imagene what situation the distinct is used in. qs = Member.all().distinct() self.assertEqual(qs.sql, "SELECT DISTINCT * FROM member") if __name__ == "__main__": import os if os.path.isfile(DB_FILE): os.unlink(DB_FILE) unittest.main() macaron.cleanup() macaron-0.3.1/test/test_class_attr.py0000644000076400007640000001320011723022422017221 0ustar nobrinnobrin#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test for class attributes. """ import unittest import macaron DB_FILE = ":memory:" SQL_TEAM = """CREATE TABLE team ( id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, created TIMESTAMP NOT NULL, start_date DATE NOT NULL )""" SQL_MEMBER = """CREATE TABLE member ( id INTEGER PRIMARY KEY NOT NULL, team_id INTEGER REFERENCES team (id) ON DELETE SET NULL, first_name VARCHAR(20), last_name VARCHAR(20), part VARCHAR(10), age INT DEFAULT 16 NOT NULL CHECK (15 <= age and age <= 18), created TIMESTAMP, joined DATE, modified TIMESTAMP )""" class Team(macaron.Model): created = macaron.TimestampAtCreate() start_date = macaron.DateAtCreate() def __str__(self): return "" % self.name class Member(macaron.Model): team = macaron.ManyToOne(Team, related_name="members") age = macaron.IntegerField(max=18, min=15) created = macaron.TimestampAtCreate() joined = macaron.DateAtCreate() modified = macaron.TimestampAtSave() class TestMacaron(unittest.TestCase): names = [ ("Ritsu", "Tainaka", "Dr", 17, "Ritsu Tainaka : Dr"), ("Mio", "Akiyama", "Ba", 17, "Mio Akiyama : Ba"), ("Yui", "Hirasawa", "Gt", 17, "Yui Hirasawa : Gt"), ("Tsumugi", "Kotobuki", "Kb", 17, "Tsumugi Kotobuki : Kb"), ] def setUp(self): macaron.macaronage(DB_FILE) macaron.execute(SQL_TEAM) macaron.execute(SQL_MEMBER) def tearDown(self): macaron.bake() def testClassProperties(self): # initializes Team class prop = Team.__dict__["_meta"] self.assertEqual(type(prop), macaron.TableMetaClassProperty) self.assertEqual(prop.table_meta, None) self.assertEqual(prop.table_name, "team") team = Team.create(name="Houkago Tea Time") self.assertEqual(type(prop.table_meta), macaron.TableMetaInfo) self.assertEqual(prop.table_name, "team") # tests attributes of class properties prop = Team.__dict__["id"] self.assertEqual(type(prop), macaron.IntegerField) self.assertEqual(prop.null, True) self.assertEqual(prop.default, None) self.assertEqual(prop.is_primary_key, True) prop = Team.__dict__["created"] self.assert_(type(prop) is macaron.TimestampAtCreate) self.assertEqual(prop.null, True, "AtCreate accepted None value.") self.assertEqual(prop.default, None) self.assertEqual(prop.is_primary_key, False) prop = Team.__dict__["start_date"] self.assert_(type(prop) is macaron.DateAtCreate) self.assertEqual(prop.null, True, "AtCreate accepted None value.") self.assertEqual(prop.default, None) self.assertEqual(prop.is_primary_key, False) # tests ManyToOne relationship # _ManyToOne_Rev is a class which should not be initialized by user. # It is initialized by ManyToOne object. prop = Team.__dict__["members"] self.assertEqual(type(prop), macaron._ManyToOne_Rev) self.assertEqual(prop.ref, Team) self.assertEqual(prop.ref_key, None, "This is None for setting at delay (in _ManyToOne_Rev#__get__).") self.assertEqual(prop.rev, Member) self.assertEqual(prop.rev_fkey, None, "This is None for setting at delay (in _ManyToOne_Rev#__get__).") members = team.members # this triggers setting for ref_key and rev_fkey self.assertEqual(type(members), macaron.ManyToOneRevSet) self.assert_(members.parent is team) self.assertEqual(members.parent_key, "id", "parent_key == prop.ref_key") self.assertEqual(members.cls_fkey, "team_id", "cls_fkey == prop.rev_fkey") self.assertEqual(prop.ref_key, "id") self.assertEqual(prop.rev_fkey, "team_id") # tests Member member = team.members.append(first_name="Azusa", last_name="Nakano", part="Gt2", age=16) self.assertEqual(member.team_id, team.pk) for k in ["first_name", "last_name"]: prop = Member.__dict__[k] self.assertEqual(type(prop), macaron.CharField) self.assertEqual(prop.max_length, 20) self.assertEqual(prop.min_length, None) self.assertEqual(prop.null, True) self.assertEqual(prop.default, None) self.assertEqual(prop.is_primary_key, False) prop = Member.__dict__["age"] self.assertEqual(type(prop), macaron.IntegerField) self.assertEqual(prop.max, 18) self.assertEqual(prop.min, 15) self.assertEqual(prop.null, False) self.assertEqual(prop.default, 16) self.assertEqual(prop.is_primary_key, False) prop = Member.__dict__["created"] self.assertEqual(type(prop), macaron.TimestampAtCreate) self.assertEqual(prop.null, True, "AtCreate accepts None value.") self.assertEqual(prop.default, None) self.assertEqual(prop.is_primary_key, False) prop = Member.__dict__["joined"] self.assert_(type(prop) is macaron.DateAtCreate) self.assertEqual(prop.null, True, "AtCreate accepts None value.") self.assertEqual(prop.default, None) self.assertEqual(prop.is_primary_key, False) prop = Member.__dict__["modified"] self.assertEqual(type(prop), macaron.TimestampAtSave) self.assertEqual(prop.null, True, "AtSave accepts None value.") self.assertEqual(prop.default, None) self.assertEqual(prop.is_primary_key, False) if __name__ == "__main__": import os if os.path.isfile(DB_FILE): os.unlink(DB_FILE) unittest.main() macaron.cleanup() macaron-0.3.1/test/test_fields.py0000644000076400007640000000660611723016006016345 0ustar nobrinnobrin#!/usr/bin/env python # -*- coding: utf-8 -*- import unittest import time import macaron DB_FILE = ":memory:" SQL_TEAM = """CREATE TABLE team ( id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(40) NOT NULL, created TIMESTAMP NOT NULL )""" SQL_MEMBER = """CREATE TABLE member ( id INTEGER PRIMARY KEY NOT NULL, team_id INTEGER REFERENCES team (id) NOT DEFERRABLE INITIALLY IMMEDIATE, first_name TEXT, last_name TEXT, age INT DEFAULT 16, part VARCHAR(10) NOT NULL, joined TIMESTAMP NOT NULL, modified TIMESTAMP NOT NULL )""" class Team(macaron.Model): created = macaron.TimestampAtCreate() def __str__(self): return "" % self.name class Member(macaron.Model): team = macaron.ManyToOne(Team, "members") joined = macaron.TimestampAtCreate() modified = macaron.TimestampAtSave() age = macaron.IntegerField(max=18, min=15) class TestMacaron(unittest.TestCase): def setUp(self): macaron.macaronage(dbfile=DB_FILE, lazy=True) macaron.execute(SQL_TEAM) macaron.execute(SQL_MEMBER) def tearDown(self): macaron.bake() def testProperties(self): chks = ( {"name":"id", "default":None, "null":True}, {"name":"name", "default":None, "null":False, "max_length":40}, {"name":"created", "default":None, "null":True}, ) clss = (macaron.IntegerField, macaron.CharField, macaron.TimestampAtCreate) for idx in range(0, len(Team._meta.fields)): fld = Team._meta.fields[idx] for n in chks[idx].keys(): self.assertEqual(getattr(fld, n), chks[idx][n]) self.assert_(fld.__class__ is clss[idx], \ "Field '%s' is %s not %s" % (fld.name, fld.__class__.__name__, clss[idx].__name__)) team1 = Team.create(name="Houkago Tea Time") team2 = Team.get(1) for n in ("id", "name", "created"): self.assertEqual(getattr(team1, n), getattr(team2, n)) def _part_is_not_set(): team1.members.append(first_name="Azusa", last_name="Nakano") self.assertRaises(macaron.ValidationError, _part_is_not_set) member1 = team1.members.append(first_name="Azusa", last_name="Nakano", part="Gt") member2 = Member.get(1) for n in("id", "team_id", "first_name", "last_name", "age", "joined", "modified"): self.assertEqual(getattr(member1, n), getattr(member2, n)) self.assertEqual(member1.id, 1) self.assertEqual(member1.team_id, 1) self.assertEqual(member1.first_name, "Azusa") self.assertEqual(member1.last_name, "Nakano") self.assertEqual(member1.age, 16) self.assert_(member1.joined) self.assert_(member1.modified) member1.age += 1 time.sleep(2) member1.save() self.assertNotEqual(member1.modified, member2.modified) def _age_exceeded(): member1.age = 19 def _age_underrun(): member1.age = 14 self.assertRaises(macaron.ValidationError, _age_exceeded) self.assertRaises(macaron.ValidationError, _age_underrun) def _too_long_part_name(): member1.part = "1234567890A" self.assertRaises(macaron.ValidationError, _too_long_part_name) if __name__ == "__main__": # if os.path.isfile(DB_FILE): os.unlink(DB_FILE) unittest.main() macaron.db_close()