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