Package: python-object-persistence
Here, a persistent object is one that persists or lives beyond the life of
a program by being stored in a database in a serialized form.  The object
can be loaded which deserializes its stored form back into its original class
type with all attributes restoredConceptsYour regular Python objects are serialized withjsonpickleSerialized objects are stored in aSQLite3databaseEach object must have a globally (across all class types) unique identifier
in itsidproperty.Referencesbetween persistent objects are supported. See below for details.Installationpipinstallpython-object-persistenceSystem RequirementsSQLite 3.9.0 or later is required for the JSON functionality which was added
about 2 months prior to this writing.Python’s standard library version is older than the required minimum SQLite3 version.However,Python will use whatever SQLite3 version you have installed.  Thus, if
you update SQLite3 to the latest stable version you are good to go.Eventually, Python will bundle SQLite 3.9.0+ and none of this will be required.OS XInstallHomebrewif you haven’t already.$brewupdate$brewinstallsqlite3--with-json1$python>>>importsqlite3>>>sqlite3.sqlite_version'3.11.0'LinuxInstall latest SQLite3 from source to home directory:$LOCAL=$HOME/local$sqlite_version=3110000# or whatever$wgethttp://sqlite.org/2016/sqlite-autoconf-${sqlite_version}.tar.gz-O-|tarxzv$cdsqlite-autoconf-${sqlite_version}$./configure--enable-json1--prefix=$LOCAL$make$makeinstallI usepyenvto manage install Python versions.$curl-Lhttps://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer|bash$echo'export PATH="$HOME/.pyenv/bin:$PATH"'>>~/.bash_profile$echo'eval "$(pyenv init -)"'>>~/.bash_profile$echo'eval "$(pyenv virtualenv-init -)"'>>~/.bash_profile$echo'export PYENV_VERSION=3.5.1'>>~/.bash_profile$source~/.bash_profile$LD_RUN_PATH=$HOME/local/libpyenvinstall$PYENV_VERSION$pyenvversion3.5.1$python>>>importsqlite3>>>sqlite3.sqlite_version'3.11.0'UsageConnect to the database to use via:importpersistentpersistent.connect(db_path=':memory:',debug=True)SubclassPersistent.  Create objects of your class type.  Callsaveon them.classFoo(persistent.Persistent):passf=Foo()f.bar='hello'f.save()Load an object byid:x=persistent.get(f.id)assertx.id==f.idassertx.bar==f.barMake updates and save the object.x.bar='monkey'assertx.save()Inter-Object ReferencesA persistent object may refer to another persistent object by setting an
attribute to the referenced object as in any other Python program. By default,
when the source object is saved, acopyof referenced objects is saved with
it.If you would prefer to save an explicit reference, add the source object
attributes that contain references to the source class’sreferences. On
save, the attributes on the source object are stored as the referenced object’sid. On load, the source object’sreferencesare scanned and the referenced
objects loaded, replacing the corresponding attribute on the source object.classBar(persistent.Persistent):references=['a_ref']b=Bar()c=Bar()c.baz='yes'b.a_ref=cb.save()x=persistent.get(b.id)assertx.id==b.idasserttype(x.a_ref)isBarassertx.a_ref.id==c.idassertx.a_ref.baz==c.bazTimestampsThe system automatically addscreated_atandupdated_atwhich aredatetimeobjects in UTC.classBaz(persistent.Persistent):passx=Baz()x.save()fromdatetimeimportdatetimeasserttype(x.created_at)isdatetimeassertnothasattr(x,'updated_at')x.quux='doo'x.save()asserttype(x.updated_at)isdatetimeCachingA least-recently-used (LRU) cache is used to hold the latest copy of each object by
objectid.  On a cache miss, the desired object is loaded from the database
and placed into the cache.  If more than N objects (by default, N=1000) objects
are stored in the cache, the least-recently-used object is evicted from the
cache.To change the default size of the cache, use thecache_sizeparameter when
callingpersistent.connect.  To disable caching entirely, set thecache_sizeto0.IndexingTo enforce that only a single object may contain some value for a set of “key
paths”, create a “unique index”:persistent.add_index(['a','b.c'],unique=True)x=Bar()x.a=1x.b=dict(c=1)x.save()# OKy=Bar()y.a=1y.b=dict(c=1)try:y.save()exceptpersistent.UniquenessErroraserr:assertTrue# Fails as y is non-unique for ['a', 'b.c']Note that such an index is scoped to the same object class.  If you wish to
make the index span all persistent objects stored, passglobal_scope=Truetoadd_index.By default, an index has a generated name which is returned byadd_index.A non-unique index can be created to speed up queries.QueryingTo query or find objects, create apersist.Queryobject, passing the class of
object. Only objects of the given class will be returned.q=persist.Query(Bar)q.equal_to(key_path,value)objects=q.find()Key PathsAkey pathis a string with elements separated by a period (.).
Following a key path in an object leads to a particular value.
The value at a key path is what is used as the test value.Consider key path “a.b.c”:o=Persistent()o.a=dict(b=dict(c=1))The value at the key path “a.b.c” is1Seekeypathfor more details.Filtersq.equal_to(key_path,value)q.not_equal_to(key_path,value)q.exists(key_path)q.does_not_exist(key_path)q.contained_in(key_path,values)q.not_contained_in(key_path,values)q.starts_with(key_path,substr,case_insensitive=False)q.contains(key_path,substr,case_insensitive=False)q.ends_with(key_path,substr,case_insensitive=False)q.greater_than(key_path,n,is_list=False)q.greater_than_or_equal_to(key_path,n,is_list=False)q.less_than(key_path,n,is_list=False)q.less_than_or_equal_to(key_path,n,is_list=False)q.matches(key_path,regex_pattern,case_insensitive=False)Note: whenis_listisTruethe test/comparison is between thelengthof
the list atkey_pathand the operandn.Sortingq.ascending(key_path)q.descending(key_path)These can be called multiple times to sort on multiple key paths.Paginationq.limit(n)q.skip(n)Running the Queryobjs=q.find()obj=q.first()n=q.count()findandfirstreturnNoneif no object(s) were found.AND or OR QueriesAQueryis in effect anANDquery in that all conditions specified
must be met by an object for that object to be included in the result set.To create anORquery, useOrQuery:q=OrQuery(Query(A).equal_to('foo','bar'),Query(B).equal_to('baz','buz'),Query(C).equal_to('buz','quux'))objs=q.find()You may pass an arbitrary number of queries to anOrQuery.DebuggingPassdebug=Truetopersistent.connectand submitted SQL statements will be
logged using Python’s built-inloggingmodule at thedebuglevel.DevelopmentRunmake initto install Python package dependencies withpip.TestingRunmake testto run the test suite withpytestincluding coverage reporting.I aim for 100% code coverage in tests.  See tests.py.When Python’s standard library version of SQLite3 is updated, I will include Tox reports here.