Scene 01
Two pieces
A host is a program that runs other people's code, for example an application server. Asqueel gives a host two pieces. The first piece lets the users of the host declare databases inside the host configuration. The second piece lets the host and its applications own databases by name. It also gives every call a clean unit of work.
Example & screen notes
Slide "Asqueel inside a host". Bullets:
AsqueelDb.grammar: the database grammar, mounted in the host configurationAsqueelDb(node): the database built from the mounteddbnodeAsqueelDbMixin: databases owned by name, one session per call
Scene 02
The host grammar
The host has its own configuration grammar, written with the same builder elements as lesson 12. This host has a configuration element, a databases collection keyed by code, and a database element. The database element carries the metadata subbuilder, set to db_class colon grammar. Below a database element, the grammar is the one found on the db_class attribute, in its member named grammar. AsqueelDb dot grammar is SqlDatabaseConfig, the grammar of every asqueel recipe.
Example & screen notes
Code block, the _meta line highlighted.
from genro_builders.builder import element
from genro_builders.contrib.config import ConfigBuilder
class HostElements:
@element(sub_tags='databases[0:1]', node_label='configuration')
def configuration(self): ...
@element(parent_tags='configuration', sub_tags='database', collection_key='code')
def databases(self): ...
@element(parent_tags='databases', _meta={'subbuilder': 'db_class:grammar'})
def database(self, db_class: type, code: str | None = None): ...
class Host(HostElements, ConfigBuilder):
pass
Scene 03
The model as a function of a db node
The course recipe writes its model inside its main method, below root dot db. To mount it, we need the same body as a function that receives a db node. invoicing_db repeats the body of Invoicing dot main, using the table functions of the course module. A standalone recipe can call the same function, so the command line of lesson 31 can still plan and apply migrations. The model then exists once.
Example & screen notes
Code block.
import invoicing
from genro_bag.resolvers import EnvResolver
def invoicing_db(db):
"""The body of Invoicing.main, written under any db node."""
db.connection(name='main', implementation='postgres',
dsn=EnvResolver('ASQUEEL_DSN', default='dbname=asqueel_course'))
invc = db.packages().package('invc', sqlschema='invc', sqlprefix=True)
invc.options().type('money', dtype='N', size='14,2')
invc.options().type('perc', dtype='N', size='5,2')
for declare in (invoicing.region, invoicing.state, invoicing.customer_type,
invoicing.vat_type, invoicing.customer, invoicing.product,
invoicing.invoice, invoicing.invoice_row, invoicing.invoice_note):
declare(invc)
Scene 04
Mount and build
The host configuration declares one database, with code invoicing and db_class AsqueelDb. Below it, the asqueel grammar applies, starting from db. The host reads its configuration with ConfigHandler. AsqueelDb accepts the db node of that configuration. It replays the subtree, element by element, into a recipe. The model and the connection settings are those of the course recipe. Migrations work on this database too: the plan is empty because the database is up to date. A node other than db raises TypeError.
Example & screen notes
Code block and output.
from genro_builders.contrib.config import ConfigHandler
from asqueel import AsqueelDb
class ServerConfiguration(Host):
def main(self, root):
database = root.configuration().databases().database(
db_class=AsqueelDb, code='invoicing')
invoicing_db(database.db('course', implementation='postgres'))
config = ConfigHandler(ServerConfiguration)
node = config.builder.source.get_node('configuration.databases.invoicing.db')
db = AsqueelDb(node)
len(db.model.tables) # 9
db.migration_plan().empty # True
db.closeConnection()
AsqueelDb(config.builder.source.get_node('configuration.databases.invoicing'))
# TypeError: a mounted Asqueel database starts at a 'db' node, not 'database'
Scene 05
Owners
A host mixes AsqueelDbMixin into its server class and into its application class. The host defines two members. asqueel_owner_name is an empty string for the server, and the application code for an application. asqueel_owner returns the owner with a given name. The empty name means the server. The mixin keeps the databases of each owner in a dictionary named asqueel_dbs.
Example & screen notes
Code block.
from asqueel import AsqueelDbMixin
class Server(AsqueelDbMixin):
def __init__(self):
super().__init__()
self.applications = {}
@property
def asqueel_owner_name(self):
return ''
def asqueel_owner(self, name):
return self if name == '' else self.applications[name]
class App(AsqueelDbMixin):
def __init__(self, server, code):
super().__init__()
self.server, self.code = server, code
server.applications[code] = self
@property
def asqueel_owner_name(self):
return self.code
def asqueel_owner(self, name):
return self.server.asqueel_owner(name)
Scene 06
Registering and taking databases
set_asqueel_db builds an AsqueelDb and registers it under a local name. The source is anything AsqueelDb accepts: a mounted node, a recipe class, a registered name or a file. A local name cannot contain a colon, and cannot be registered twice for one owner. The property db returns the database named default of the owner. When the owner has none, it returns the default of the server. get_db takes a name. A name without a colon is a database of the server. A name with a colon is owner name, colon, local name. A missing database raises KeyError.
Example & screen notes
Code block, then a table.
server = Server()
billing = App(server, 'billing')
server.set_asqueel_db('default', node)
billing.set_asqueel_db('archive', invoicing.Invoicing)
billing.set_asqueel_db('archive', invoicing.Invoicing)
# ValueError: database 'archive' is already registered for this owner
| Expression | Database |
|---|---|
billing.db |
the default of billing, else the server's default |
billing.get_db('default') |
the server's default |
billing.get_db('billing:archive') |
the archive of application billing |
Scene 07
One call, one session
The first take of a database during a call enters session on it, as in lesson 27. The session clears currentEnv, so a call never sees the context of the previous call on that thread. Later takes in the same call return the same database without a new session. A database already in a session on the thread, taken through another owner, is used as it is. The call does its work and commits what it wants to keep. At the end of the call, the host calls release_databases on the owner the call used. It leaves the sessions opened through that owner. Leaving a session rolls back what was not committed and closes the connections. Call 1 did not commit, so call 2 finds no region and an empty environment.
Example & screen notes
Code block and output.
# call 1
db = billing.db
db.currentEnv['user'] = 'ada'
db.table('invc.region').insert({'code': 'NO', 'name': 'North'})
with session(db):
...
# TransactionStateError: a session is already open on this thread for this database
billing.release_databases()
# call 2
db = billing.db
dict(db.currentEnv) # {}
db.table('invc.region').query().count() # 0
billing.release_databases()
Scene 08
The per-request pattern
This is the shape of a call in a host. The names handle_request and do_the_work stand for host and application code. The call takes its database, sets its context, works, and commits. The finally clause releases the databases, also after an error. One call runs on one thread. release_databases acts on the current thread only, so each thread releases its own takes.
Example & screen notes
Code block.
def handle_request(app, request):
try:
db = app.db
db.updateEnv(user=request.user)
result = do_the_work(db, request)
db.commit()
return result
finally:
app.release_databases()
Bullets:
- One call runs on one thread
- Each thread releases only its own takes
- The host decides where the call starts and ends
Scene 09
Recap
A host mounts the asqueel grammar in its configuration and builds each database from its db node. AsqueelDbMixin owns databases by name, for the server and for each application. The first take of a call opens a session, and release_databases closes it. The next lesson puts the whole course together in one example.
Example & screen notes
Slide "Recap". Bullets:
- Mount
AsqueelDb.grammarwith_meta={'subbuilder': 'db_class:grammar'}, build withAsqueelDb(node) AsqueelDbMixin:set_asqueel_db,db,get_db('owner:name')- First take enters
session(db);release_databases()leaves it
Next: "Lesson 33 — The complete example".