-
Notifications
You must be signed in to change notification settings - Fork 529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate with Sqlalchemy #141
Comments
Hi @botzill, could you give an example of what behaviour you are interested in? Right now, a machine will add a from transitions import Machine
states = ['A', 'B']
transitions = [['go', 'A', 'B']]
class Model(object):
def __init__(self):
self.statusA = self.statusB = 'A'
def status_changed(self):
self.statusA = self.state # version with callback
@property
def state(self):
return self.statusB
@state.setter
def state(self, value):
self.statusB = value # version without callback
model = Model()
machine = Machine(model, states=states, transitions=transitions, initial='A',
after_state_change='status_changed')
print model.state # A
model.go()
print model.state # B
print model.statusA # B
print model.statusB # B |
Hi @aleneum, Here is a small example of what I mean: from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from transitions import Machine
from sqlalchemy import Column, Integer, String
engine = create_engine('sqlite:///test.sqlite')
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
state = Column(String)
name = Column(String)
if __name__ == '__main__':
states = ['created', 'validated', 'active', 'inactive']
transitions = [
['validate', 'created', 'validated'],
['enable', 'validated', 'active'],
['disable', 'active', 'inactive'],
]
Base.metadata.create_all(engine)
user = User(name="User1")
machine = Machine(model=user, states=states, transitions=transitions, initial='created')
print(user.state)
# output: created
# but not saved into DB
user.validate()
print(user.state)
# output: validated
# but not saved into DB
user.enable()
print(user.state)
# output: active
# but not saved into DB
# only at this point we actually save the state into DB
session.add(user)
session.commit() What would be great to have is for the Of course we can achieve this by methods you mentioned above, but having a standard support for this would be really nice. Here we can also encounter scenarios like status = Column(String, name="state") so, the fields which holds the state can have different name in the DB. By having a sort of mapping would help to integrate the p.s: I just discovered this awesome library today and maybe it already has some of the features implemented that I'm asking. Thx. |
While I can see the utility in what you're suggesting, @botzill, I worry that adding support for common ORMs could get messy pretty quickly (see also #142), and maintaining some uniformity in the API might take some effort. Given that @aleneum's suggestions are quite elegant and minimalistic, I think you could probably solve your problem with minimal code by subclassing I'm willing to reconsider adding support for this kind of thing if we get a lot of requests for it, but I think for that to happen I'd want to know that someone is willing to commit the time to maintaining and updating the various mixins in an ORM integration module, and I'm not sure that I or any of the other current maintainers want to take that on. |
Thx @tyarkoni, Of course adding support for this feature is pretty straight forward currently without any additional helpers. But it's always surprisingly good when you get such functionality out of the box :). I will play with it and see what I can come up with. Thx again for the great library which is not yet added in awesome python? |
I am not sure about how picky class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
# renamed state -> status to allow that getter/setter solution
status = Column(String)
name = Column(String)
def __init__(self, session, *args, **kwargs):
self._session = session
self._session.add(self)
super(User, self).__init__(*args, **kwargs)
@property
def state(self):
return self.status
@state.setter
def state(self, value):
self.status = value
self._session.commit() I tested it with your example code and it seems to work (as in 'No exceptions are thrown'). I have not checked if the content in test.sqlite is correct though. |
Thx @aleneum, You solution is good. I made a small change so that we don't need to pass in the Base._session = scoped_session(sessionmaker(bind=engine))
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
status = Column(String)
name = Column(String)
def __init__(self, *args, **kwargs):
super(User, self).__init__(*args, **kwargs)
self._session.add(self)
@property
def state(self):
return self.status
@state.setter
def state(self, value):
self.status = value
self._session.commit() Another solution is to use a mixin, here is a working example: Base = declarative_base()
Base._session = scoped_session(sessionmaker(bind=engine))
class StatusMixin(object):
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
@declared_attr
def status(cls):
return Column(String())
@property
def state(self):
return self.status
@state.setter
def state(self, value):
if self.status != value:
self.status = value
self._session.add(self)
self._session.commit()
class User(Base, StatusMixin):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String) This way we can add state to an existing model easily. Thx. |
Nice to hear that you have found a way that suits your needs :). I will close this issue for now. Feel free to reopen it if you have further feedback or face any issues with transitions. Best regards! |
OK, just a small update here. The examples above works OK when we initialize the model object ourself(and from sqlalchemy import Column, Integer, String
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from transitions import Machine
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy import event
engine = create_engine('sqlite:///test.sqlite')
Base = declarative_base()
session = scoped_session(sessionmaker(bind=engine))
Base._session = session
class StateMixin(object):
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
@declared_attr
def status(cls):
return Column(String())
@property
def state(self):
return self.status
@state.setter
def state(self, value):
if self.status != value:
self.status = value
def after_state_change(self):
self._session.add(self)
self._session.commit()
@classmethod
def init_state_machine(cls, obj, *args, **kwargs):
# when we load data from the DB(via query) we need to set the proper initial state
initial = obj.status or 'created'
machine = Machine(model=obj, states=states, transitions=transitions, initial=initial,
after_state_change='after_state_change')
# in case that we need to have machine obj in model obj
setattr(obj, 'machine', machine)
class User(Base, StateMixin):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
event.listen(User, 'init', User.init_state_machine)
event.listen(User, 'load', User.init_state_machine)
if __name__ == '__main__':
states = ['created', 'validated', 'active', 'inactive']
transitions = [
['validated', 'created', 'validated'],
['enable', ['validated', 'created'], 'active'],
['disable', 'active', 'inactive'],
]
# Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
user = User(name="User1")
user.validated()
print(user.state)
# output: validated
# extract object from DB and init state machine
user_db = session.query(User).first()
print(user_db.state)
# output: validated
user_db.enable()
print(user.state)
# output: active |
The documentation suggests a @orm.reconstructor decorator. This might also be a solution. |
I found the @botzill's answer super helpful! I'm now using that pattern throughout my app to drive model state changes off of state machine transitions. Thanks a ton! For those who come after and are curious about exactly how/why this code works, I added a bit of commenting. Assume that on our core model, class StateMachineMixins:
# When we access the state property, we want to be shown the value of the model_state
# field, not the state of the state machine. This ensures we're always driving changes
# through the state machine, and reading out state through the model_state field.
@property
def state(self):
return self.model_state
# When the state machine writes to the state field (a side-effect that occurs when you've called
# a transition and the machine was instantiated with a reference to the model), have it update
# the underlying model_state field.
@state.setter
def state(self, value):
if self.model_state != value:
self.model_state = value
The rest is fairly self-explanatory. After a state change, actually persist the changes to the database, etc. Thanks again @botzill! |
Hi.
I wanted to ask if you have any plans to make this work easily with Sqlalchemuy model? Let say we have a model which has a
status
which needs to be updated every time we change the status using the machine. Of course we can implement this at the moment by using the trigger callbacks and update the state manually. I'm just wandering if you have such plans for the future.Thx.
The text was updated successfully, but these errors were encountered: