Model customizations and builders for Invenio framework.
This package provides a way of building an Invenio model with user customizations. It allows you to add mixins, classes to components, routes, and other customizations to the model while ensuring that the model remains consistent, functional and upgradable.
pip install oarepo-model- Python 3.13+
- Invenio 14.x
First, create a model using the model function from oarepo_model.api and include the necessary presets.
# mymodel.py
from oarepo_model.api import model
from oarepo_model.presets.records_resources import records_resources_preset
from oarepo_model.presets.drafts import drafts_preset
my_model = model(
"my_model",
version="1.0.0",
presets=[
records_resources_preset,
drafts_preset,
],
customizations=[
],
)Then you need to register the model before Invenio is initialized. The best way is
to do it in the invenio.cfg file:
# invenio.cfg
from mymodel import my_model
my_model.register()You can add customizations to the model by using the customizations parameter
of the model function. The following customizations are available, importable from
oarepo_model.customizations:
| Name | Description |
|---|---|
| classes | |
AddClass(name) |
Adds a new class to the model. |
AddBaseClass(name, base_class) |
Adds a single base class to a model class. Call multiple times to add multiple base classes in order. |
AddClassField(name, field_name, field_value) |
Adds a field (attribute, method, or property) to an existing class. |
PrependMixin(name, mixin) |
Prepends a single mixin to a model class (adds it as the first parent). Call multiple times in reverse order for multiple mixins. |
ReplaceBaseClass(name, old_base, new_base) |
Replaces one base class with another in a model class. |
| modules | |
AddModule(name, exists_ok=False) |
Adds a module to the model. |
AddToModule(module_name, property_name, value, exists_ok=False) |
Adds a property to a module in the model. |
AddFileToModule(symbolic_name, module_name, file_path, payload, exists_ok=False) |
Adds a file to the module with specified content. |
AddJSONFile(symbolic_name, module_name, file_path, payload, exists_ok=False) |
Adds a JSON file to the module (automatically serializes dictionary to JSON). |
CopyFile(source_symbolic_name, target_symbolic_name, target_module_name, target_file_path, exists_ok=False) |
Copies content from one symbolic file location to another. |
PatchJSONFile(symbolic_name, payload) |
Patches/modifies an existing JSON file by merging new data with existing content. |
| lists | |
AddList(name, exists_ok=False) |
Adds a new list to the model. |
AddClassList(name, exists_ok=False) |
Adds a new class list to the model. A class list keeps an MRO-consistent order of classes and can be used later as bases for a generated class. If this ordering functionality is not required, use AddList. |
AddToList(list_name, value, exists_ok=False) |
Appends a value to an existing list in the model. Set exists_ok=True to allow duplicates. |
| dicts | |
AddDictionary(name, default=None, exists_ok=False) |
Adds a dictionary to the model. |
AddToDictionary(name, {..}...) or AddToDictionary(name, key=..., value=..., patch=False) |
Adds entries to a dictionary in the model (optionally merge with patch=True). |
| entry points | |
AddEntryPoint(group, name, module_path) |
Adds an entry point to the model. |
| facets | |
AddFacetGroup(name, facets, exists_ok=False) |
Adds a facet group to the model for search result filtering. |
| high-level | |
AddMetadataExport(code, name, mimetype, serializer, ...) |
Adds a serializer for metadata exports. |
AddMetadataImport(code, name, mimetype, deserializer, ...) |
Adds a deserializer for metadata imports. |
AddPIDRelation(name, path, keys, pid_field, ...) |
Declares a PID relation system field based on a path (supports list and nested-list relations). |
SetDefaultSearchFields(*search_fields) |
Specifies a set of default search fields for the index. |
PatchIndexSettings(settings) |
Patches/modifies OpenSearch/Elasticsearch index settings. |
SetIndexTotalFieldsLimit(limit) |
Sets the index.mapping.total_fields.limit setting. |
SetIndexNestedFieldsLimit(limit) |
Sets the index.mapping.nested_fields.limit setting. |
SetPermissionPolicy(policy_class) |
Sets the permission policy for the model. |
To add a mixin to a class in the model, you can use the PrependMixin customization.
Mixins are prepended to the class, so they take precedence in the MRO. If the resulting
MRO would be inconsistent, it is automatically reordered to a consistent order.
from oarepo_model.customizations import PrependMixin
from my_mixins import BaseMixin
my_model = model(
"my_model",
version="1.0.0",
presets=[
records_resources_preset,
drafts_preset,
],
customizations=[
PrependMixin("Record", BaseMixin),
],
)from oarepo_model.customizations import AddToList
class MyComponent:
...
my_model = model(
"my_model",
version="1.0.0",
presets=[
records_resources_preset,
drafts_preset,
],
customizations=[
AddToList("record_service_components", MyComponent),
],
)To generate RecordSchema/MetadataSchema from a type definition, pass types and set metadata_type to the name of the root type:
from oarepo_model.api import model
from oarepo_model.presets.records_resources import records_resources_preset
my_model = model(
"my_model",
version="1.0.0",
presets=[records_resources_preset],
types=[
{
"RecordMetadata": {
"properties": {
"title": {"type": "fulltext+keyword", "required": True},
},
}
}
],
metadata_type="RecordMetadata",
)When the model is created, the following steps are performed:
-
An instance of
InvenioModelis created. This instance holds the basic configuration of the model, such as its name, version, api and ui slugs. -
An instance of an
InvenioModelBuilderis created. -
All presets are collected and sorted according to their dependencies.
-
For each preset:
- Dependencies of the preset are collected, including those that were passed
as
customizationsto the model. If the dependency has not yet been built, it is built at this moment. - The
applymethod is called with the builder and the model. The method returns a list of customizations that are applied to the model.
- Dependencies of the preset are collected, including those that were passed
as
-
If there are any unapplied customizations, they are applied to the model.
-
During the application of the customizations, instances of
Partialare created within the builder, such asBuilderClass,BuilderList,BuilderModule. These instances provide a recipe for a part of the final model. The part is built either if it is needed by a preset/customization or at the end of the model building process. -
The result of the model building process is transformed into a
SimpleNamespaceand returned to the caller. The returned object also provides helpers:register()/unregister()— to register the in-memory model for import/entry pointsget_resources()— to retrieve the in-memory files as a{path: content}mapping
Invenio needs some parts of the model to be registered via entry points. We provide
a register method on the model instance that automatically adds the model to the
entry points via registering a new importer to sys.meta_path. This allows
Invenio to find model components in the entry points and use them during the
initialization process.
The call needs to be done before Invenio is initialized, which is why the best place
to do it is in the invenio.cfg file.
The classes within the model should be as loosely coupled as possible. This is implemented by using dependency injection wherever possible.
A dependency descriptor makes sure that the class is loaded from the model during runtime. This allows adding circular dependencies between classes, for example.
Note: This does not work with Invenio's system fields, as these are handled in
a special way by Invenio and are skipped. For example, a pid field on a record might
not be created in this way.
class A:
b = Dependency("B")This call returns an object that can return resolved dependencies during runtime via
its get method. This is useful, for example, when you want to access a model artifact from
within a function or a method. This cannot be used in static initialization.
class MyPreset:
def apply(self, builder, model, ...):
runtime_deps = builder.get_runtime_dependencies()
class A:
def __init__(self):
self.b = runtime_deps.get("B")
yield AddClass("A", A)oarepo_model and oarepo_model_namespace are injected into every generated class. Imported
modules created by the model expose whatever attributes you added via customizations, but do not
receive special injections automatically.
In system fields, due to the way they are initialized in Invenio, we cannot use late binding. This means that we need to use the classes directly in the system fields, and they have to be built before the system fields are declared. This is done via reordering the presets and customizations so that the system fields' classes are built before the classes that use them.
Each preset has two properties: provides and depends_on. The provides property
is a list of classes that the preset provides or modifies, while the depends_on
property is a list of classes that the preset depends on. The presets are sorted
by their dependencies, so that the dependencies are built before the preset itself.
You can then get the built dependencies from the 3rd argument of the apply method
of the preset. This is a dictionary of classes that were built during the model building process.
Example:
class MyPreset(Preset):
provides = ["MyClass"]
depends_on = ["Record"]
def apply(self, builder, model, dependencies):
# dependencies is a dict of classes that were built during the model building process
class MyClass(metaclass=MetaThatNeedsToHaveBProperty):
b = dependencies["Record"] # The Record has been built at this point and is a valid class
yield AddClass("MyClass", MyClass)# Clone repository
git clone https://github.com/oarepo/oarepo-model.git
cd oarepo-model
./run.sh venv./run.sh testCopyright (c) 2025 CESNET z.s.p.o.
OARepo Model is free software; you can redistribute it and/or modify it under the terms of the MIT License. See LICENSE file for more details.
- Documentation: https://github.com/oarepo/oarepo-model
- PyPI: https://pypi.org/project/oarepo-model/
- Issues: https://github.com/oarepo/oarepo-model/issues
- OARepo Project: https://github.com/oarepo
This project builds upon Invenio Framework and is developed as part of the OARepo ecosystem.