Package: eventsourcing-sqlalchemy
Event Sourcing in Python with SQLAlchemyThis package supports using the Pythoneventsourcinglibrary
withSQLAlchemy.Table of contentsTable of contentsQuick startInstallationGetting startedManaging transactions outside the applicationManaging sessions with Flask-SQLAlchemyManaging sessions with FastAPI-SQLAlchemyUsing SQLAlchemy scoped sessionsGoogle Cloud SQL Python ConnectorMore informationQuick startTo use SQLAlchemy with your Python eventsourcing applications:install the Python packageeventsourcing_sqlalchemyset the environment variablePERSISTENCE_MODULEto'eventsourcing_sqlalchemy'set the environment variableSQLALCHEMY_URLto an SQLAlchemy database URLSee below for more information.InstallationUse pip to install thestable distributionfrom the Python Package Index. Please note, it is recommended to
install Python packages into a Python virtual environment.$ pip install eventsourcing_sqlalchemyGetting startedDefine aggregates and applications in the usual way.fromeventsourcing.applicationimportApplicationfromeventsourcing.domainimportAggregate,eventfromuuidimportuuid5,NAMESPACE_URLclassTrainingSchool(Application):defregister(self,name):dog=Dog(name)self.save(dog)defadd_trick(self,name,trick):dog=self.repository.get(Dog.create_id(name))dog.add_trick(trick)self.save(dog)defget_tricks(self,name):dog=self.repository.get(Dog.create_id(name))returndog.tricksclassDog(Aggregate):@event('Registered')def__init__(self,name):self.name=nameself.tricks=[]@staticmethoddefcreate_id(name):returnuuid5(NAMESPACE_URL,f'/dogs/{name}')@event('TrickAdded')defadd_trick(self,trick):self.tricks.append(trick)To use this module as the persistence module for your application, set the environment
variablePERSISTENCE_MODULEto'eventsourcing_sqlalchemy'.When using this module, you need to set the environment variableSQLALCHEMY_URLto an
SQLAlchemy database URL for your database.
Please refer to theSQLAlchemy documentationfor more information about SQLAlchemy Database URLs.importosos.environ['PERSISTENCE_MODULE']='eventsourcing_sqlalchemy'os.environ['SQLALCHEMY_URL']='sqlite:///:memory:'Construct and use the application in the usual way.school=TrainingSchool()school.register('Fido')school.add_trick('Fido','roll over')school.add_trick('Fido','play dead')tricks=school.get_tricks('Fido')asserttricks==['roll over','play dead']Managing transactions outside the applicationSometimes you might need to update an SQLAlchemy ORM model atomically with updates to
your event-sourced application. You can manage transactions outside the application.
Just call the application recorder'stransaction()method and use the returnedTransactionobject as a context manager to obtain an SQLAlchemySessionobject. You canadd()your ORM objects to the session. Everything will commit
atomically when theTransactioncontext manager exits. This effectively implements
thread-scoped transactions.withschool.recorder.transaction()assession:# Update CRUD model....# session.add(my_orm_object)# Update event-sourced application.school.register('Buster')school.add_trick('Buster','fetch ball')tricks=school.get_tricks('Buster')asserttricks==['fetch ball']Please note, the SQLAlchemy "autoflush" ORM feature is enabled by default.app=Application()withapp.recorder.transaction()assession:assertsession.autoflushisTrueIf you need "autoflush" to be disabled, you can set the environment variableSQLALCHEMY_NO_AUTOFLUSH.app=Application(env={'SQLALCHEMY_AUTOFLUSH':'False'})withapp.recorder.transaction()assession:assertsession.autoflushisFalseAlternatively, you can set the autoflush option directly on the SQLAlchemy session maker.app=Application()app.recorder.datastore.session_maker.kw["autoflush"]=Falsewithapp.recorder.transaction()assession:assertsession.autoflushisFalseAlternatively, you can use the session'sno_autoflushcontext manager.app=Application()withapp.recorder.transaction()assession:withsession.no_autoflush:assertsession.autoflushisFalseAlternatively, you can set theautoflushattribute of the session object.app=Application()withapp.recorder.transaction()assession:session.autoflush=False# Add CRUD objects to the session....Using SQLAlchemy scoped sessionsIt's possible to configure the application to use an SQLAlchemyscoped_sessionobject which will scope the session to standard threads, or other things such
as Web requests in a Web application framework.Define an adapter for ascoped_sessionobject and configure the event-sourced
application using the environment variableSQLALCHEMY_SCOPED_SESSION_TOPIC.fromeventsourcing.applicationimportAggregateNotFoundfromeventsourcing.utilsimportget_topicfromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportsessionmaker,scoped_session# Create engine.engine=create_engine('sqlite:///:memory:')# Create a scoped_session object.session=scoped_session(sessionmaker(autocommit=False,autoflush=False,bind=engine))# Define an adapter for the scoped session.classMyScopedSessionAdapter:def__getattribute__(self,item:str)->None:returngetattr(session,item)# Produce the topic of the scoped session adapter class.scoped_session_topic=get_topic(MyScopedSessionAdapter)# Construct an event-sourced application.app=Application(env={'SQLALCHEMY_SCOPED_SESSION_TOPIC':scoped_session_topic})# During request.aggregate=Aggregate()app.save(aggregate)app.repository.get(aggregate.id)session.commit()# After request.session.remove()# During request.app.repository.get(aggregate.id)# After request.session.remove()# During request.aggregate=Aggregate()app.save(aggregate)# forget to commit# After request.session.remove()# During request.try:# forgot to commitapp.repository.get(aggregate.id)exceptAggregateNotFound:passelse:raiseException("Expected aggregate not found")# After request.session.remove()As you can see, you need to callcommit()during a request, and callremove()after the request completes. Packages that integrate SQLAlchemy with Web application
frameworks tend to automate this call toremove(). Some of them also callcommit()automatically if an exception is not raised during the handling of a request.Managing sessions with Flask-SQLAlchemyThe packageFlask-SQLAlchemy(full docs) provides a class
calledSQLAlchemywhich has asessionattribute which is an SQLAlchemyscoped_session. This can be adapted in a similar way.fromflaskimportFlaskfromflask_sqlalchemyimportSQLAlchemytry:fromsqlalchemy.ormimportdeclarative_base# type: ignoreexceptImportError:fromsqlalchemy.ext.declarativeimportdeclarative_base# Define a Flask app.flask_app=Flask(__name__)flask_app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///:memory:'# Integration between Flask and SQLAlchemy.Base=declarative_base()db=SQLAlchemy(flask_app,model_class=Base)# Define an adapter for the scoped session.classFlaskScopedSession:def__getattribute__(self,item:str)->None:returngetattr(db.session,item)# Run the Flask application in a Web application server.withflask_app.app_context():# Produce the topic of the scoped session adapter class.scoped_session_adapter_topic=get_topic(FlaskScopedSession)# Construct event-sourced application to use scoped sessions.es_app=Application(env={"SQLALCHEMY_SCOPED_SESSION_TOPIC":scoped_session_adapter_topic})# During request.aggregate=Aggregate()es_app.save(aggregate)db.session.commit()# After request (this is done automatically).db.session.remove()# During request.es_app.repository.get(aggregate.id)# After request (this is done automatically).db.session.remove()Managing sessions with FastAPI-SQLAlchemyThe packageFastAPI-SQLAlchemydoesn't actually use an SQLAlchemyscoped_session, but instead has a globaldbvariable that has asessionattribute which returns request-scoped sessions when
accessed. This can be adapted in a similar way. Sessions are committed automatically
after the request has been handled successfully, and not committed if an exception
is raised.fromfastapiimportFastAPIfromfastapi_sqlalchemyimportdb,DBSessionMiddleware# Define a FastAPI application.fastapi_app=FastAPI()# Add SQLAlchemy integration middleware to the FastAPI application.fastapi_app.add_middleware(DBSessionMiddleware,db_url='sqlite:///:memory:')# Build the middleware stack (happens automatically when the FastAPI app runs in a Web app server).fastapi_app.build_middleware_stack()# Define an adapter for the scoped session.classFastapiScopedSession:def__getattribute__(self,item:str)->None:returngetattr(db.session,item)# Construct an event-sourced application within a scoped session.withdb(commit_on_exit=True):# Produce the topic of the scoped session adapter class.scoped_session_adapter_topic=get_topic(FlaskScopedSession)# Construct event-sourced application to use scoped sessions.es_app=Application(env={"SQLALCHEMY_SCOPED_SESSION_TOPIC":get_topic(FastapiScopedSession)})# Create a new event-sourced aggregate.withdb(commit_on_exit=True):# This happens automatically before handling a route.# Handle request.aggregate=Aggregate()es_app.save(aggregate)es_app.repository.get(aggregate.id)# The aggregate has been committed.withdb(commit_on_exit=True):# This happens automatically in a route.# Handle request.es_app.repository.get(aggregate.id)# Raise exception after creating aggregate.try:withdb(commit_on_exit=True):# Handle request.aggregate=Aggregate()es_app.save(aggregate)es_app.repository.get(aggregate.id)raiseTypeError("An error occurred!!!")exceptTypeError:# Web framework returns an error....else:raiseException("Expected type error")# The aggregate hasn't been committed.withdb(commit_on_exit=True):try:es_app.repository.get(aggregate.id)exceptAggregateNotFound:passelse:raiseException("Expected aggregate not found")Google Cloud SQL Python ConnectorYou can set the environment variableSQLALCHEMY_CONNECTION_CREATOR_TOPICto a topic
that will resolve to a callable that will be used to create database connections.For example, you can use theCloud SQL Python Connectorin the following way.First install the Cloud SQL Python Connector package from PyPI.$ pip install 'cloud-sql-python-connector[pg8000]'Then define agetconn()function, following the advice in the Cloud SQL
Python Connector README page.fromgoogle.cloud.sql.connectorimportConnector# initialize Connector objectconnector=Connector()# function to return the database connectiondefget_google_cloud_sql_conn():returnconnector.connect("project:region:instance","pg8000",user="postgres-iam-user@gmail.com",db="my-db-name",enable_iam_auth=True,)Set the environment variable'SQLALCHEMY_CONNECTION_CREATOR_TOPIC', along with'PERSISTENCE_MODULE'and'SQLALCHEMY_URL'.fromeventsourcing.utilsimportget_topicos.environ['PERSISTENCE_MODULE']='eventsourcing_sqlalchemy'os.environ['SQLALCHEMY_URL']='postgresql+pg8000://'os.environ['SQLALCHEMY_CONNECTION_CREATOR_TOPIC']=get_topic(get_google_cloud_sql_conn)More informationSee the library'sdocumentationand theSQLAlchemyproject for more information.