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 withreadonly
. Public interface:Field.writable(instance)
.
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 forint
field, becausestr(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 byDataModel.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
andcascade=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.