User guide¶
Create models¶
You can use models library for two different things:
Create data representation and validate incoming data quickly.
Display data schema in UI (forms, schemas, etc.).
To create a custom model just inherit from the base Model class. Recommended way to
do it, which would preserve python typing hints, is shown below.
>>> from kaiju_models import *
>>> class User(Model["User.Fields"]):
... class Fields(Model.Fields):
... name: str = StringField(min_length=3, required=True)
... registered: bool = BooleanField(default=False)
... nicknames: list[str] = ListField(StringField())
Since model type is a subclass of a field type, you can use models in fields as well. Adding a model as a field will result
to this field being a msgspec struct. It’s recommended to type hint such inner models with their Fields attribute.
Fields that accept other fields as arguments (for example ListField) also can accept
models.
Here is a more complex example of a model above which uses these features.
>>> class Identifier(Model["Phone.Fields"]):
... class Fields(Model.Fields):
... id: str = StringField(required=True)
... type: str = EnumField(["phone", "email"], required=True)
... verified: bool = BooleanField(default=False)
>>> class Address(Model["Address.Fields"]):
... class Fields(Model.Fields):
... city: str = StringField(required=True)
... street: str = StringField()
>>> class User(Model["UserModel.Fields"]):
... class Fields(Model.Fields):
... name: str = StringField(min_length=3, required=True)
... registered: bool = BooleanField(default=False)
... nicknames: list[str] = ListField(StringField())
... identifier: Identifier.Fields = Identifier(required=True)
... address: list[Address.Fields] = ListField(Address())
Validate data¶
Once the class is initialized, a msgspec.struct object will be generated based on the model fields description.
You can either access it by __base_type__ attribute, or you can directly validate
or load data using the class methods get_struct(), decode(),
decode_msgpack().
The latter two are basically shortcuts for converting binary data from HTTP requests and other sources in the most efficient way. It’s recommended to use it when converting from JSON since it retains type information and therefore faster. The resulting object is always a msgspec.struct object.
>>> user_model = UserModel()
>>> user = user_model.decode(b'{"name":"John","registered":true,"identifier":{"type":"phone","id":"888769"}}')
>>> assert user.name == 'John'
get_struct() method can be used both with dictionary like objects or normal objects
with attributes, as long as it matches the expected structure interface.
>>> data = {"name": "John", "registered": True, "identifier": {"type": "phone", "id": "888769"}}
>>> user = user_model.decode(data)
>>> assert user.name == 'John'
>>> from collections import namedtuple
>>> data = namedtuple('Userdata', ('name', 'registered', 'identifier'), ('John', True, {'type': 'phone', 'id': '888769'}))
>>> user = user_model.decode(data)
>>> assert user.name == 'John'
If the data is invalid then a conversion method raises ModelValidationError with a bunch
of additional data about error type and constraints. Note that it only checks for one value at a time.
To dump data once again it’s efficient to use the model encoder since it is already pre-initialized for this data type.
Use encode() or encode_msgpack() to convert
a struct to binary data.
>>> data = user_model.encode(user)
Msgpack¶
Msgpack encode_msgpack() and decode_msgpack()
are differ slightly from normal encode methods. Aside from using messagepack data
format they also use compact data representation. Each struct is encoded as an array with all the default values
skipped. It also uses certain optimization in encoding particular data types. Keep that in mind whenever you want to
manually decode and parse msgpack model output.
Msgpack encoding can safe up to 50% space / traffic and is also pretty CPU efficient.
Integrate with UI¶
There is a specific method get_schema() which returns a model schema in UI friendly
format. One can use it to dynamically create a form for model data. Only fields and models marked with ui_visible=True
are returned in the list of fields. The output of the method would look like this in JSON:
{
"type": "model",
"title": null,
"schema": [
{
"id": "name",
"type": "str",
"default": null,
"title": null,
"description": null,
"meta": {
"form_properties": {
"has_feedback": false,
"placeholder": null,
"disable_label": false,
"mark_required": false
}
},
"validation": {
"required": null,
"min_length": 3,
"max_length": null,
"pattern": null,
"pattern_error_text": null
}
},
...
],
"meta": {
"form_properties": {
"has_feedback": false,
"placeholder": null,
"disable_label": false,
"mark_required": false
}
},
"ordering": [
"registered",
"nicknames",
"identifier",
"has_money",
"address",
"sex",
"birthdate",
"bio",
"name"
]
}
Each model field or submodel has its own sub-schema. The ordering field at each model is a special containing an ordered list of attributes is a special type of field which can only present in models and tells the API in which order the fields should be if using compact array data format.
The proper way to use models API with UI is to create a UI validation form to pre-validate data on the client. The
model validation methods should be reserved for data conversion and API calls, so if data doesn’t pass validation on the
backend it may be a signal that there’s something wrong going on. However it’s still a good idea to process backend
validation errors on the client side at least to some extent. See ErrorData for the
description of data provided by ModelValidationError class.
Dynamic models¶
Model classes can be created dynamically from fields using create_model_type(). The function
returns a new model class and also registers it by its name in TYPES registry of field
and model classes.
>>> new_user_model = create_model_type('NewUserModel', user_model.Fields.__fields__)
Model sharing¶
For dynamic models (or sometimes even for static ones) it may prove useful to share them across instances, or store them
at a file / database. You can use repr_() and
from_repr() for that purpose, or rather encode_model()
and decode_model() if you need to convert models from / to binary data.
The process itself is straightforward.
>>> data = encode_model(user_model)
>>> user_model = decode_model(data)
Note that obviously it needs to have all the respectful model and field types defined in the system to be able to load.
By default serialization functions use TYPES global store for all model and field types
where classes automatically added on class creation using __init_subclass__ hook.
You can pass different types mapping to the function if needed.