Data model

Example data model in quickstart

Overview

Data model definition is a core part of any blargh-based application. In brief, there are four steps:

from blargh import data_model

#   STEP 1 - initialize the data model
dm = data_model.DataModel('some_name')

#   STEP 2 - create some objects
object_1 = dm.create_object('object_1_name')
object_2 = dm.create_object('object_2_name')

#   STEP 3 - add fields to objects
object_1.add_field(data_model.fields.Scalar('some_field_name'))
object_2.add_field(data_model.fields.Scalar('other_field_name'))

#   STEP 4 - (optional) connect different objects
dm.connect(object_1, 'some_field_name', object_2, 'other_field_name')

Steps 1,2 and 4 are simple declarations, all the magic happens in data_model.fields.

Fields

class blargh.data_model.fields.Field(name, ext_name=None, readonly=False, default=None, writable=True, hidden=False)

Base class of all fields, implements common attributes.

name

Internal (stored) field name

ext_name

External field name, defaults to name

readonly

Boolean, if True user will not be allowed to set value of this field directly. Readonly field can be changed either by other Calc field, or by updates to a connected Rel field. Compare with writable.

default

Default value of a field.

_writable

Either boolean, or a function accepting blargh.engine.Instance as only argument. If evaluates to False, field can’t be changed in any way - compare with readonly. Public interface: Field.writable(instance).

hidden

Boolean, if True field is not visible (so also can’t be accessed directly), but still stored. Hidden field can be changed either by other Calc field, or by updates to a connected Rel field.

class blargh.data_model.fields.Scalar(*args, pkey=False, type_=None, **kwargs)

Field storing a single value. This value can be complex (e.g. list or json), but is not parsed in any way - stored value is always the same as visible value.

Beside attributes inherited from Field, there are also:

pkey

If True, this field will be used in blargh.engine.Instance.id(). Every object must have exactly one pkey field. Pkey fields are always readonly.

type_

If anything else than None, values of this type will be accepted for this field. Values that can be reversibly casted to type_ are also accepted. E.g. '1' is accepted for int field, because str(int('1')) == '1', but '1 ' is not.

class blargh.data_model.fields.Calc(*args, getter=None, setter=None, **kwargs)

Field that is not stored, but:

  • when requested, it’s value is computed (e.g. based on other fields)
  • when updated, new value is passed to a function that updates other fields

Main purpose is to allow interface that is independent from storage. For example, we might want to store kilometers, but allow end users to read/write miles.

getter

function accepting blargh.engine.Instance as first and only argument, and returning some value, e.g.

def is_empty(jar):
    cookies_field = jar.model.field('cookies')
    cookies = jar.get_val(cookies_field).repr(0)
    return not len(cookies)

Default getter returns None.

setter

function accepting blargh.engine.Instance as first argument and anything as second, should return dictionary {'other_field_name': new_value_of_field}, e.g:

def ingredients(cookie, ingredients):
    if 'milk' in ingredients:
        new_type = 'muffin'
    else:
        new_type = 'shortbread'
    return {'type': new_type}

Default setter raises an exception.

class blargh.data_model.fields.Rel(*args, stores, multi, cascade=False, **kwargs)

Field representing other objects.

Relation could be one-way (e.g. jar knows it’s cookies, but cookie has no idea if it is in a jar), or two-way. Two-way relations are created with DataModel.connect().

stores

Defined type of objects on the other side of the relation (heterogeneous relations are not allowed). Value should be engine.data_model.object.Object (the thing returned by DataModel.create_object('some_name')).

multi

if True, any number of related objects is allowed, False - 0 or 1.

cascade

if True, when this instance is deleted all related instances are also deleted.

It is forbidden to set both multi=True and cascade=True.

Creating custom fields

More-or-less any behaviour could be achived by extending blargh.data_model.fields.* classes, or maybe by creating independent field class. Few examples are in the cookbook:

Connecting Rel fields

Let’s consider mother-child data model:

from blargh.data_model import DataModel
from blargh.data_model.fields import Scalar, Rel

dm = data_model.DataModel('mother_and_child')

mother = dm.create_object('mother')
child = dm.create_object('child')

mother.add_field(Scalar('id', pkey=True, type_=int))
mother.add_field(Rel('children', stores=child, multi=True))

child.add_field(Scalar('id', pkey=True, type_=int))
child.add_field(Rel('mother', stores=mother, multi=False))

In this data model, mother has any number of children, and child could have at most one mother. What’s still missing is the connection between “A is mother of B” and “B is one of A’s children”. In standard mother-has-child-data-model those two statements are interchangeable, which is coded by:

dm.connect(mother, 'children', child, 'mother')

This way, e.g:

  • if we remove an element from mother.children, removed child looses mother
  • if we set child.mother, mother.children field also get’s updated for both new mother and (possible) previous mother.

NOTE: This mother-child connection is by no way necesary, e.g. mother.children could refer to biological children only, and child could have adoptive mother.

Utilities

Data model code

dm.as_code() returns code lines that create identical data model

dm = ... # any data model declaration
with open('some_file.py', 'w') as f:
    f.write("\n".join(dm.as_code()))

from some_file import dm as dm_2
assert dm == dm_2

Data model from PostgreSQL schema

If you want to use blargh with already existint PostgreSQL database, there’s a tool blargh.data_model.PGSchemaImport that will create appropriate data model. For example, our favourite cookie-jar data model could be obtained this way:

psql -c '
    CREATE SCHEMA cookies;
    CREATE TABLE  cookies.jar    (id   serial PRIMARY KEY);
    CREATE TABLE  cookies.cookie (id   serial PRIMARY KEY,
                                  type text,
                                  jar  integer REFERENCES cookies.jar(id));
'
python3 -c '
import psycopg2
from blargh.data_model import PGSchemaImport

conn = psycopg2.connect("")  # correct connection string

dm = PGSchemaImport(conn, "cookies").data_model()
print("\n".join(dm.as_code()))
' > cookies_data_model.py

This was not extensively tested, so expect bugs. It is not possible to process more than one schema, although you can run it few times and combine results on your own.