Customizing Cassini
The Basics
Cassini is written to be customizable.
This is done by making changes to your cas_project.py
file.
Warning
Whilst Cassini is written to be customisable, straying from the default configuration makes it more likely you will run into bugs or issues that may be harder to debug or get help on.
The main entrypoint for customisation is changing your hierarchy from the defaults.
For example, if you want to change your naming format, you can change WorkPackage.name_part_template
:
from cassini import Home, WorkPackage, Experiment, Sample, DataSet
WorkPackage.name_part_template = 'HR{}' # (1)!
project = Project([Home, WorkPackage, Experiment, Sample, DataSet], __file__)
...
- This replaces the old
name_part_template
, which wasWP{}
withHR{}
.
This will make your names start with HR
, e.g. HR1.5a
.
Warning
Be wary making changes after your project is set up. e.g. if you are changing how things are named, old refrences will break.
Creating New Tier Classes
You can define your own Tier
classes from scratch, by inheriting from the base classes FolderTierBase
or NotebookTierBase
.
The FolderTierBase
is for tier's with no notebook or meta, just a folder e.g. DataSets.
The NotebookTierBase
is, as you'd expect, for tier's with a folder, notebook and meta.
Simply define your own Tier
classes by either subclassing some of the defaults, or using
the base class TierBase
:
from cassini import Project, Home, NotebookTierBase
class Part(NotebookTierBase):
pretty_type = 'Part' # (1)!
name_part_template = 'Part: {}' # (2)!
project = Project([Home, Part], __file__)
if __name__ == '__main__':
project.launch()
- This is like the full name of this tier.
- With this template,
Part
will be named likePart: 1
, this allows gettingparts
withpart = project['Part: 1']
.
Check out the FolderTierBase and NotebookTierBase for information on all the different attributes you can overwrite to customise Cassini's behaviour. You can also look at the implementations of the defaults to see how this can be done.
Naming Conventions
The naming system in Cassini is complex, to allow names to be as compact as possible, whilst still parseable!
The name of a tier is created by taking its tier.id
, and inserting that into the name_part_template
. This is done for each of its parents, prepending the resulting string until we reach the tier before Home.
For example:
>>> dset = project['WP5.2c-ABC']
>>> dset.id
'ABC'
>>> dset.identifiers
('5', '2', 'c', 'ABC')
>>> dset.name_part_template
'-{}'
dset
is a DataSet
instance, which has name_part_template="-{}"
, as dset.id = 'ABC'
, this gets inserted into -{}
resulting in -ABC
.
>>> smpl = dset.parent
>>> smpl.name
'WP5.2c'
>>> smpl.name_part_template
'{}'
Giving just the 'c'
character
>>> exp = smpl.parent
>>> exp.name
'WP5.2'
>>> exp.name_part_template
'.{}'
Giving '.2'
.
>>> wp = wp.parent
>>> wp.name
'WP5'
>>> exp.name_part_template
'WP{}'
Giving 'WP5'
.
Hence the name has the form 'WP5' + '.2' + 'c' + '-ABC' = 'WP5.2c-ABC'
!
That's how names are build... but how are they parsed? e.g. Why aren't the Experiment id and Sample id lumped together?
The answer is we add a final class attribute, the tier.id_regex
. This restricts the form of the id. E.g. for WorkPackages and Experiments, the id
must always be a digit. For Samples, the id mustn't start with a number or end in a dash and DataSets, id can actually be anything!
>>> dset.id_regex # (1)!
'(.+)'
>>> smpl.id_regex # (2)!
'([^0-9^-][^-]*)'
>>> exp.id_regex # (3)!
'(\d+)'
>>> wp.id_regex # (4)!
'(\d+)'
- Any character allowed.
- Cannot start with a number and cannot include a dash
-
. - Must be a number.
- Must be a number.
Through setting name_part_template
and id_regex
, it is possible to create your own naming convention.
The Notebook Gui
To make changes to the gui, you can create you own gui class and then simply set the gui_cls
attribute of your Tier
:
from cassini import Home, WorkPackage, Experiment, Sample, DataSet
from cassini.core import TierGuiProtocol # (1)!
class MyGui(TierGuiProtocol):
def __init__(self, tier):
self.tier = tier
def header(self):
print(self.tier.name)
WorkPackage.gui_cls = MyGui # (2)!
project = Project([Home, WorkPackage, Experiment, Sample, DataSet], __file__)
...
- The TierGuiProtocol helps enforce the right interface of the Gui class.
- A bit naughty.
from cassini import Home, WorkPackage, Experiment, Sample, DataSet
from cassini.core import TierGuiProtocol # (1)!
class MyGui(TierGuiProtocol):
def __init__(self, tier):
self.tier = tier
def header(self):
print(self.tier.name)
class MyWorkPackage(WorkPackage): # (2)!
gui_cls = MyGui
project = Project([Home, MyWorkPackage, Experiment, Sample, DataSet], __file__)
...
Each Tier
creates its own gui_cls
instance upon __init__
, passing itself as the first argument.
>>> WP1 = project['WP1']
>>> WP1.gui
<MyGui instance>
>>> mytier.gui.header()
WP1
Meta Attributes (MetaAttr
)
You may have noticed smpl.description
and smpl.started
are attributes, that are linked to the contents of the smpl
's meta file.
These also have strict types:
>>> smpl = project['WP1.1a']
>>> smpl.description = 10
ValidationError: 1 validation error for WorkPackageMetaCache
description
Input should be a valid string [type=string_type, input_value=3, input_type=int]
Including if you try to directly set on the meta:
>>> smpl.meta['description'] = 10
ValidationError: 1 validation error for WorkPackageMetaCache
description
Input should be a valid string [type=string_type, input_value=3, input_type=int]
These special stricter attributes, linked to the meta file are defined using MetaAttr
, which are an example of a descriptor.
>>> smpl.__class__.description
<MetaAttr ...>
All the MetaAttr
for a Tier
are used to create a Pydantic model for the meta file:
>>> smpl.meta_model.model_fields
{'started': FieldInfo(annotation=AwareDatetime, required=False, default=None, json_schema_extra={'x-cas-field': 'core'}),
'description': FieldInfo(annotation=str, required=False, default=None, json_schema_extra={'x-cas-field': 'core'}),
'conclusion': FieldInfo(annotation=str, required=False, default=None, json_schema_extra={'x-cas-field': 'core'})}
This model is then used by Cassini to ensure the meta file stays in a valid state. A schema of this model is shared with the browser-side JupyterLab extension to also perform validation.
You can add your own MetaAttr
to Tiers
, for example, the cassini_lib
extension adds a cas_lib_version
attribute:
Tier.cas_lib_version = MetaAttr(
json_type=str, # (1)!
attr_type=Version, # (2)!
post_get=lambda v: Version(v) if v else v, # (3)!
pre_set=str, # (4)!
name="cas_lib_version", # (5)!
cas_field="private", # (6)!
)
- The type passed to
pydantic
. - The type the attribute returns when accessed (this is mostly used to help with internal type checking).
- Function called after the value is loaded from the
json
file. - Function called before the value is inserted back into the
json
file. - The name/ key used for this attribute in the
json
file. (If none, this takes the same value as the attribute name). - Indicates this attribute should not be displayed to the user and is internal.
Whilst Pydantic provides its own methods for customising serialisation and deserialisation, these can be a bit confusing(!), we provide post_get
and pre_set
parameters, which are callables, which act on the values after they are fetched from the file (post_get
) and before the attributes are set (pre_set
).
The final parameter cas_field
is a custom parameter which tells Cassini who should see/ access this attribute. From above, you can see for 'core' meta values, such as description, these are set to 'core'
. If you don't want your MetaAttr to appear in the Cassini UI, you should set this to 'private'
. Otherwise MetaAtt
will always be included in the Meta Editor, both in the New Child Dialogue and the Tier Viewer. The Cassini UI will enforce the same type constraints you set in json_type
and will try and render the most appropriate widget for setting this value e.g. a calendar for a date.
You will notice that all meta values are optional, this is a requirement of Cassini.
You can find more information on Meta and MetaAttr in the meta api docs.
Extensions
If you've come up with a great set of customizations, you might want to turn them into an extension.