This document tours a Schevo database from the vantage point of building a database navigator that takes full advantage of the deep introspection Schevo offers.
It covers operations to read from, and execute transactions upon, an open database. It presents those operations in the usage order that is typical of building such a comprehensive application.
This document also functions as a doctest:
>>> from schevo.test import DocTest, DocTestEvolve
When creating an instance of DocTest passing a schema body string, we get an object t that has a done method to call when finished with the test object, and a db attribute that contains the in-memory open database based on the schema.
DocTestEvolve is similar, except we pass the name of a schema package and a schema version number to the constructor instead of a body string.
In each example, we work with the open database to demonstrate how its API reflects the schema.
Pass a database to the schevo.label:label function to get its persistent label.
The default label of a database is Schevo Database.
>>> from schevo.label import label >>> t = DocTest(""" ... """); db = t.db >>> label(db) u'Schevo Database'
Pass the database to the schevo.label:relabel function to change its label:
>>> from schevo.label import relabel >>> relabel(db, 'My Database') >>> label(db) u'My Database' >>> t.done()
Note
Persistent labels cannot be changed while executing a transaction.
>>> t = DocTest(""" ... from schevo.label import relabel ... class ChangeLabel(T.Transaction): ... def _execute(self, db): ... db.label = 'New Label' ... def t_change_label(): ... return ChangeLabel() ... """); db = t.db >>> tx = db.t.change_label() >>> db.execute(tx) Traceback (most recent call last): ... DatabaseExecutingTransaction: Cannot change database label... >>> t.done() Traceback (most recent call last): ... DatabaseExecutingTransaction: Cannot change database label...
Call the database’s extent_names method to get an alphabetically-ordered list of extent names:
>>> t = DocTest(""" ... class Clown(E.Entity): ... pass ... ... class Acrobat(E.Entity): ... pass ... ... class Balloon(E.Entity): ... pass ... """); db = t.db >>> db.extent_names() ['Acrobat', 'Balloon', 'Clown']
Call the database’s extent_names method to get an alphabetically-ordered list of extent objects:
>>> db.extents() [<Extent 'Acrobat' in <Database u'Schevo Database' :: V 1>>, <Extent 'Balloon' in <Database u'Schevo Database' :: V 1>>, <Extent 'Clown' in <Database u'Schevo Database' :: V 1>>]
Pass an extent name to the database’s extent method or accessing it as an attribute of the database to get an individual extent by name:
>>> db.extent('Balloon') <Extent 'Balloon' in <Database u'Schevo Database' :: V 1>> >>> db.Balloon is db.extent('Balloon') True >>> t.done()
Call the database’s pack method to pack the database:
>>> db.pack()
Note
The in-memory storage backend used for unit tests does not support the pack method, so it is skipped above.
The engine populates a database with an initial data set when it first creates a database.
>>> t = DocTest(""" ... class Book(E.Entity): ... ... name = f.string() ... ... _initial = [ ... ('Schevo and You',), ... ] ... ... _sample = [ ... ('The Art of War',), ... ] ... ... _sample_custom = [ ... ('Iliad',), ... ] ... """); db = t.db >>> sorted(book.name for book in db.Book) [u'Schevo and You']
Call a database’s populate method with no arguments to populate a database with the default sample data set:
>>> db.populate() >>> sorted(book.name for book in db.Book) [u'Schevo and You', u'The Art of War']
Call a database’s populate method with a string argument to populate a database with a named sample data set:
>>> db.populate('custom') >>> sorted(book.name for book in db.Book) [u'Iliad', u'Schevo and You', u'The Art of War'] >>> t.done()
Access the schema_source property of a database to get its schema source:
>>> t = DocTest(""" ... # Even an empty database has schema source. ... """); db = t.db >>> print db.schema_source from schevo.schema import * schevo.schema.prep(locals()) # Even an empty database has schema source. >>> t.done()
Access the version property of a database to get its current schema version:
>>> t = DocTestEvolve('schevo.test.testschema_evolve', 1) >>> t.db.version 1 >>> t.done() >>> t = DocTestEvolve('schevo.test.testschema_evolve', 2) >>> t.db.version 2 >>> t.done()
Schevo provides optional multiple-reader-one-writer locking to safely allow multiple threads access to the database.
By default, a database does not have locking facilities. Instead, it contains dummy objects so that code written to be multi-thread-ready may still be run when no locking facilities are installed on the database:
>>> t = DocTest(""" ... """); db = t.db >>> db.read_lock <class 'schevo.mt.dummy.dummy_lock'> >>> db.write_lock <class 'schevo.mt.dummy.dummy_lock'>
If using Schevo in a multi-threaded environment, be sure to install locking support onto the database by using the schevo.mt:install function:
>>> import schevo.mt >>> schevo.mt.install(db) >>> db.read_lock <bound method RWLock._acquire_locked_wrapper ... >>> db.write_lock <bound method RWLock._acquire_locked_wrapper ...
Note
After installing locking support, be sure to consistently use locks to wrap all read and write operations.
If you do not wrap multiple read operation, and a write operation occurs in another thread, you may get inconsistent results during your read.
If you do not wrap write operations, then they may conflict with writes occurring in another thread or cause inconsistent results during reads in another thread.
Note
The locking API does not yet take advantage of the Python 2.5 with statement.
Acquire a read lock by calling the database’s read_lock object to acquire a lock, performing the desired read operation(s), then calling the release method of the acquired lock:
>>> lock = db.read_lock() >>> try: ... # Do reading stuff here. ... pass ... finally: ... lock.release()
Note
When a thread attempts to acquire a read lock, it will block until pending write locks have been released.
Acquire a read lock by calling the database’s read_lock object to acquire a lock, performing the desired read operation(s), then calling the release method of the acquired lock:
>>> lock = db.write_lock() >>> try: ... # Do reading and writing stuff here. ... pass ... finally: ... lock.release() >>> t.done()
Note
When a thread attempts to acquire a write lock, it will block until pending read and write locks have been released.
If you acquire a read lock, and find you need to acquire a write lock within the same thread, acquiring a write lock while the thread still has the read lock will “upgrade” the read lock to a write lock for the remainder of the life of the thread’s outermost lock:
>>> lock_outer = db.read_lock() >>> try: ... # Do reading stuff here. ... lock_inner = db.write_lock() ... try: ... # Do reading and writing stuff here. ... pass ... finally: ... lock_inner.release() ... finally: ... lock_outer.release() >>> t.done()
Note
When a thread upgrades a read lock to a write lock, it will block until pending read locks have been released.
After an outer read lock has been upgraded by an inner write lock, it will continue to act as an exclusive write lock for the remainder of its lifespan, even after the inner lock’s release method has been called.
Access the t attribute of an object that may have transaction methods to get the transaction method namespace of that object:
>>> t = DocTest(""" ... class Plant(E.Entity): ... ... common_name = f.string() ... ... _initial = [ ... ('Fern',), ... ] ... """); db = t.db >>> db.t <'t' namespace on <Database u'Schevo Database' :: V 1>> >>> db.Plant.t <'t' namespace on <Extent 'Plant' in <Database ...>>> >>> db.Plant[1].t <'t' namespace on <Plant entity oid:1 rev:0>> >>> db.Plant[1].v.default().t <'t' namespace on <schevo.entity._DefaultView ...>> >>> t.done()
Iterate over the t namespace, such as by transforming it to a sorted list, to get the names of available, non-hidden transaction methods.
By default, databases do not have any transaction methods.
>>> t = DocTest(""" ... class Plant(E.Entity): ... ... common_name = f.string() ... ... _initial = [ ... ('Fern',), ... ] ... """); db = t.db >>> sorted(db.t) []
By default, extents have a transaction method used to create new Create transactions.
>>> sorted(db.Plant.t) ['create']
By default, entities have transaction methods used to create new Delete and Update transactions:
>>> sorted(db.Plant[1].t) ['clone', 'delete', 'update']
By default, entity views have transaction methods that reflect those of the parent entity:
>>> sorted(db.Plant[1].v.default().t) ['clone', 'delete', 'update']
Note
If a transaction method is hidden, it is still usable, but it does not show up when iterating over the t namespace. This is to allow programmatic usage of transactions that the schema designer does not feel appropriate to expose in a dynamic user interface.
>>> t2 = DocTest(""" ... class Plant(E.Entity): ... ... common_name = f.string() ... ... _hide('t_update') ... ... _initial = [ ... ('Fern',), ... ] ... """); db2 = t2.db >>> sorted(db2.Plant[1].t) ['clone', 'delete'] >>> t2.done()
Use the __getitem__ protocol to get a transaction method from a t namespace:
>>> method_name = 'create' >>> method = db.Plant.t[method_name] >>> method <extentclassmethod Plant.t_create ...
You may also use the __getattr__ protocol:
>>> db.Plant.t.create <extentclassmethod Plant.t_create ... >>> db.Plant.t.create is db.Plant.t['create'] True
Each transaction method has a label.
If the schema does not manually assign a label to a transaction method, Schevo automatically computes one.
>>> label(db.Plant.t.create) u'New' >>> label(db.Plant[1].t.update) u'Edit' >>> label(db.Plant[1].t.delete) u'Delete' >>> t.done()
Each extent in a database keeps useful information about itself.
>>> t = DocTest(""" ... class Food(E.Entity): ... ... common_name = f.string() ... fancy_name = f.string() ... high_in_sugar = f.boolean() ... ... _key(common_name) ... _key(fancy_name) ... ... _index(high_in_sugar, common_name) ... _index(high_in_sugar, fancy_name) ... ... _initial = [ ... ('Lettuce', 'Lactuca sativa', False), ... ('Date', 'Phoenix dactylifera', True), ... ('Broccoli', 'Brassica oleracea', False), ... ] ... ... class Person(E.Entity): ... ... name = f.string() ... favorite_food = f.entity('Food') ... ... _key(name) ... ... _plural = 'People' ... ... _initial = [ ... ('Jill', ('Date',)), ... ('Jack', ('Lettuce',)), ... ('Jen', ('Broccoli',)), ... ('Jeff', ('Date',)), ... ] ... ... class EatingRecord(E.Entity): ... ... person = f.entity('Person') ... food = f.entity('Food') ... when = f.datetime() ... ... _key(person, when) ... ... _initial = [ ... (('Jill',), ('Date',), '2008-01-15 13:05:00'), ... (('Jack',), ('Date',), '2008-01-15 13:10:00'), ... (('Jack',), ('Lettuce',), '2008-01-15 13:15:00'), ... (('Jen',), ('Broccoli',), '2008-02-02 11:13:00'), ... (('Jeff',), ('Lettuce',), '2008-03-05 14:45:00'), ... ] ... """); db = t.db
Access the db attribute of an extent to determine which database it belongs to:
>>> db.Food.db is db True
Access the index_spec attribute of an extent to get a tuple of index specs for indices maintained for the extent:
>>> sorted(db.Food.index_spec) [('high_in_sugar', 'common_name'), ('high_in_sugar', 'fancy_name')]
Access the key_spec attribute of an extent to get a tuple of key specs for keys maintained for the extent:
>>> sorted(db.Food.key_spec) [('common_name',), ('fancy_name',)]
Access the default_key attribute of an extent to get the key spec for the default key maintained for the extent:
>>> db.Food.default_key ('common_name',)
Pass an extent object to the schevo.label:label function to get the singular form of the extent’s label:
>>> label(db.Food) u'Food' >>> label(db.EatingRecord) u'Eating Record'
Use the schevo.label:plural function to get the plural form:
>>> from schevo.label import plural >>> plural(db.Food) u'Foods' >>> plural(db.Person) u'People'
Access the relationships attribute of an extent to get a list of its relationships:
>>> sorted(db.Food.relationships) [('EatingRecord', 'food'), ('Person', 'favorite_food')] >>> db.Person.relationships [('EatingRecord', 'person')]
Pass an extent to the built-in len function to get the current size in entities of the extent:
>>> len(db.Food) 3 >>> len(db.Person) 4 >>> len(db.EatingRecord) 5
Call the as_unittest_code method of an extent to get a string containing the values of all entities in that extent, formatted in a manner that you can paste into schema source code to use those entities as sample data for unit tests:
>>> print db.Food.as_unittest_code() E.Food._sample_unittest = [ (u'Broccoli', u'Brassica oleracea', False), (u'Date', u'Phoenix dactylifera', True), (u'Lettuce', u'Lactuca sativa', False), ] >>> print db.Person.as_unittest_code() E.Person._sample_unittest = [ (u'Jack', (u'Lettuce',)), (u'Jeff', (u'Date',)), (u'Jen', (u'Broccoli',)), (u'Jill', (u'Date',)), ]
Use the __getitem__ protocol on an extent with an OID to retrieve the entity from that extent that has that OID:
>>> jack = db.Person.findone(name='Jack') >>> oid = jack.s.oid >>> type(oid) <type 'int'> >>> person = db.Person[oid] >>> person == jack True
If no entity with that OID exists in the extent, the operation raises schevo.error:EntityDoesNotExist:
>>> person = db.Person[12345] Traceback (most recent call last): ... EntityDoesNotExist: "OID 12345 does not exist in extent 'Person'." Traceback (most recent call last): ... EntityDoesNotExist: "OID 12345 does not exist in extent 'Person'."
Pass keyword arguments to the findone method of an extent to retrieve the entity from that extent where the fields named as keys in the keyword arguments have values equal to the corresponding values in the keyword arguments:
>>> person = db.Person.findone(name='Jack') >>> jack.name u'Jack'
If no matching entity is found, the method returns None:
>>> person = db.Person.findone(name='John') >>> person is None True
If multiple entities match, the method raises schevo.error:FindoneFoundMoreThanOne:
>>> date = db.Food.findone(common_name='Date') >>> person = db.Person.findone( ... favorite_food=date ... ) Traceback (most recent call last): ... FindoneFoundMoreThanOne: Found more than one match in extent 'Person' ... Traceback (most recent call last): ... FindoneFoundMoreThanOne: Found more than one match in extent 'Person' ...
>>> t.done()