# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011 Piston Cloud Computing, Inc. # Copyright 2012 Cloudscaling Group, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ SQLAlchemy models. """ import six from oslo_utils import timeutils from sqlalchemy import Column from sqlalchemy import DateTime from sqlalchemy.orm import object_mapper from oslo_db.sqlalchemy import types class ModelBase(six.Iterator): """Base class for models.""" __table_initialized__ = False def save(self, session): """Save this object.""" # NOTE(boris-42): This part of code should be look like: # session.add(self) # session.flush() # But there is a bug in sqlalchemy and eventlet that # raises NoneType exception if there is no running # transaction and rollback is called. As long as # sqlalchemy has this bug we have to create transaction # explicitly. with session.begin(subtransactions=True): session.add(self) session.flush() def __setitem__(self, key, value): setattr(self, key, value) def __getitem__(self, key): return getattr(self, key) def __contains__(self, key): # Don't use hasattr() because hasattr() catches any exception, not only # AttributeError. We want to passthrough SQLAlchemy exceptions # (ex: sqlalchemy.orm.exc.DetachedInstanceError). try: getattr(self, key) except AttributeError: return False else: return True def get(self, key, default=None): return getattr(self, key, default) @property def _extra_keys(self): """Specifies custom fields Subclasses can override this property to return a list of custom fields that should be included in their dict representation. For reference check tests/db/sqlalchemy/test_models.py """ return [] def __iter__(self): columns = list(dict(object_mapper(self).columns).keys()) # NOTE(russellb): Allow models to specify other keys that can be looked # up, beyond the actual db columns. An example would be the 'name' # property for an Instance. columns.extend(self._extra_keys) return ModelIterator(self, iter(columns)) def update(self, values): """Make the model object behave like a dict.""" for k, v in values.items(): setattr(self, k, v) def _as_dict(self): """Make the model object behave like a dict. Includes attributes from joins. """ local = dict((key, value) for key, value in self) joined = dict([(k, v) for k, v in self.__dict__.items() if not k[0] == '_']) local.update(joined) return local def iteritems(self): """Make the model object behave like a dict.""" return self._as_dict().items() def items(self): """Make the model object behave like a dict.""" return self._as_dict().items() def keys(self): """Make the model object behave like a dict.""" return [key for key, value in self.iteritems()] class ModelIterator(six.Iterator): def __init__(self, model, columns): self.model = model self.i = columns def __iter__(self): return self # In Python 3, __next__() has replaced next(). def __next__(self): n = six.advance_iterator(self.i) return n, getattr(self.model, n) class TimestampMixin(object): created_at = Column(DateTime, default=lambda: timeutils.utcnow()) updated_at = Column(DateTime, onupdate=lambda: timeutils.utcnow()) class SoftDeleteMixin(object): deleted_at = Column(DateTime) deleted = Column(types.SoftDeleteInteger, default=0) def soft_delete(self, session): """Mark this object as deleted.""" self.deleted = self.id self.deleted_at = timeutils.utcnow() self.save(session=session)