Attributes

Sheraf can store a lot of different objet types. Each type has its own Attribute to take care of it. In this section we will briefly see the most commonly used attributes. For each class the reference will provide more complete information.

Introduction

The most simple attribute type is the SimpleAttribute. It can hold anything that an OOBTree can store. It does not apply any type coercion or transformation on the data you put in it. It will just try to save it as it is.

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.SimpleAttribute()
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(name="George Abitbol")
...     george.name
'George Abitbol'

Default values

Attributes have a defaut parameter that can be used to set a default value. For instance let us set the default age for cowboys at 30.

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.SimpleAttribute()
...     age = sheraf.SimpleAttribute(default=30)
...
>>> with sheraf.connection(commit=True):
...     peter = Cowboy.create(name="Peter", age=35)
...     steven = Cowboy.create(name="Steven")
...     peter.age
...     steven.age
35
30

Here Steven has not been set an age, so he is 30.

If the default value is callable, then it will be called. The callable can have zero or one positionnal parameter (that will be the model instance).

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.SimpleAttribute(default=lambda: "John Doe")
...
>>> with sheraf.connection(commit=True):
...     john = Cowboy.create()
...     john.name
'John Doe'

Lazyness

Default attributes values are lazy. This means they are not stored in the database until the first read or write access on the attribute. It allows to save some space in the database, and some calculations at the model instance creation. However, this behavior can be disable with the lazy parameter:

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.SimpleAttribute(default="John Doe")
...     age = sheraf.SimpleAttribute(default=30, lazy=False)
...
>>> with sheraf.connection(commit=True):
...     john = Cowboy.create()
...     "age" in john.mapping
...     "name" in john.mapping
...     john.name
...     "name" in john.mapping
True
False
'John Doe'
True

Here we can see that the age was stored as soon as the instance was created, but we had to wait to an access to the name attribute before it was stored.

Note

Indexed attributes are always lazy, not matter the lazy argument value.

Basic attributes

The simple types such as int, float, str, bool have their matching IntegerAttribute FloatAttribute, StringAttribute and BooleanAttribute.

All those typed attributes cast their inputs in the type they refers to:

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     age = sheraf.IntegerAttribute()
...     height = sheraf.FloatAttribute()
...     sherif = sheraf.BooleanAttribute()
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(name="George", age=50, height=1.80, sherif=True)
...
...     george.age = 51.5
...     george.age
51

Here a float 51.5 has been passed to an IntegerAttribute and thus has been casted to int.

Collections

Sheraf can also store collection of items. dict, list and set have their matching DictAttribute, ListAttribute and SetAttribute.

Basic usage

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     surnames = sheraf.ListAttribute(persistent_type=sheraf.types.SmallList)
...     horse_breeds = sheraf.DictAttribute(persistent_type=sheraf.types.LargeDict)
...     favorite_numbers = sheraf.SetAttribute()
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(
...         name="George Abitbol",
...         surnames=["georgy", "the classiest man in the world"],
...         horse_breeds={
...             "jolly jumper": "mustang",
...             "polly pumper": "shetland",
...         },
...         favorite_numbers={13, 11, 17},
...     )
...     george.surnames[0]
...     george.horse_breeds["jolly jumper"]
...     13 in george.favorite_numbers
'georgy'
'mustang'
True

The collection attributes behave the same way than the python types their refer to. You can iterate over a ListAttribute the same way that you can iterate a list, you can access data from a DictAttribute the same way you do with a dict.

The collection type take a persistent_type parameter that is the persistent type that will be used to store the data. Sheraf provide some shortcuts to avoid passing this parameter each time you need a collection attribute. You can check SmallDictAttribute, LargeDictAttribute, SmallListAttribute and LargeListAttribute.

Nesting attributes

Collection attributes can hold other attributes. For instance, you can nest a IntegerAttribute inside a LargeListAttribute:

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     favorite_numbers = sheraf.LargeListAttribute(sheraf.IntegerAttribute())
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(
...         name="george",
...         favorite_numbers=[15, 3.5],
...     )
...     list(george.favorite_numbers)
[15, 3]

You can see here that the float 3.5 value has been casted into an int by the IntegerAttribute.

But you can also nest collections in collection. For instance a DictAttribute can hold another DictAttribute.

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     animal_breeds = sheraf.SmallDictAttribute(
...         sheraf.SmallDictAttribute(
...             sheraf.StringAttribute()
...         )
...     )
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(
...         name="george",
...         animal_breeds={
...             "horses": {
...                  "jolly jumper": "mustang",
...             },
...         },
...     )
...     george.animal_breeds["horses"]["jolly jumper"]
'mustang'

There is no limit on how much attributes can be nested.

Models

Models have several ways to reference to other models.

Externals references

The most basic way to reference another model is by using ModelAttribute.

>>> class Horse(sheraf.Model):
...     table = "horse"
...     name = sheraf.StringAttribute()
...     breed = sheraf.StringAttribute()
...
>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     horse = sheraf.ModelAttribute(Horse)
...
>>> with sheraf.connection(commit=True):
...     jolly = Horse.create(name="Jolly Jumper", breed="mustang")
...     george = Cowboy.create(name="George Abitbol", horse=jolly)
...     george.horse.name
'Jolly Jumper'

The id of the Horse instance will be stored in the Cowboy instance. Accessing to the horse thus makes a second access to the database.

Note that create() can make instances for both models. The inner model should be passed as a dictionnary matching the attribute names to their values:

>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(name="George Abitbol", horse={
...         "name": "Jolly Jumper",
...         "breed": "mustang",
...     })
...     george.horse.name
'Jolly Jumper'

Inlines models

External references to models reach performances limits when scaling. The more the number of refered models is high, the longer it takes to access one of them. This is due to how BTrees works.

If the model you refers is very dependant on the referer, you might prefer using a InlineModelAttribute instead.

>>> class Horse(sheraf.InlineModel):
...     name = sheraf.StringAttribute()
...     breed = sheraf.StringAttribute()
...
>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     horse = sheraf.InlineModelAttribute(Horse)
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(name="George Abitbol", horse={
...         "name": "Jolly Jumper",
...         "breed": "mustang",
...     })
...     george.horse.name
'Jolly Jumper'

InlineModelAttribute works in a very similar way than ModelAttribute. The InlineModel is very dependant on its host model. It does not have an id attribute, an cannot be accessed by another way than using the InlineModelAttribute on its host.

If you need to store several InlineModel, you might want to use it in combination with a collection attribute such as DictAttribute or ListAttribute.

Note that you can define anonymous InlineModel:

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     horse = sheraf.InlineModelAttribute(sheraf.InlineModel(
...         name=sheraf.StringAttribute(),
...         breed=sheraf.StringAttribute(),
...     ))
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(name="George Abitbol", horse={
...         "name": "Jolly Jumper",
...         "breed": "mustang",
...     })
...     george.horse.name
'Jolly Jumper'

Indexed models

InlineModelAttribute are great, and using them in combination with collection attributes gives a good way to handle several of them. However sometimes you may need more advanced indexation behavior, like with first-level models.

IndexedModelAttribute does not store just one model, but a whole model indexation machine. It handles a AttributeModel and allows you to use the create() and read() methods from IndexableModel, and take advantages of the filter() and order() methods from QuerySet.

>>> class Horse(sheraf.AttributeModel):
...     name = sheraf.StringAttribute().index(primary=True)
...     age = sheraf.IntegerAttribute().index(unique=True)
...     breed = sheraf.StringAttribute()
...
>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     horses = sheraf.IndexedModelAttribute(Horse)
...
>>> with sheraf.connection(commit=True):
...     george = Cowboy.create(name="George Abitbol")
...     jolly = george.horses.create(name="Jolly Jumper", breed="mustang", age=15)
...     polly = george.horses.create(name="Polly Pumper", breed="shetland", age=20)
...
...     george.horses.read("Jolly Jumper").breed
...     george.horses.get(age=20).name
...     george.horses.count()
'mustang'
'Polly Pumper'
2

Note that the AttributeModel must have one primary index.

Files

Sheraf offers two ways to store binary files in the database: BlobAttribute and FileAttribute.

Blobs

BlobAttribute makes use of ZODB Blob objects to store binary files.

>>> class Cowboy(sheraf.Model):
...     table = "cowboy"
...     name = sheraf.StringAttribute()
...     fax = sheraf.BlobAttribute()
...
>>> with sheraf.connection(commit=True): 
...     fax = sheraf.Blob(data=b"Hello George!", filename="fax.txt")
...     george = Cowboy.create(name="George", fax=fax)
...     george.fax.filename
...     george.fax.data
'fax.txt'
b'Hello George!'

The file content can either be passed to the Blob object by the data or the stream parameter, depending on the format.

As it uses ZODB Blob, files will be removed from the filesystem after a database pack if delete() is called on the BlobAttribute.

Files

TODO