Skip to content

oarepo/oarepo-model

Repository files navigation

OARepo Model

Model customizations and builders for Invenio framework.

Overview

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.

Installation

pip install oarepo-model

Requirements

  • Python 3.13+
  • Invenio 14.x

High-level API

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()

Adding customizations

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.

Extending class with a mixin

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),
    ],
)

Adding a new service component

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),
    ],
)

Generating metadata schema via data types

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",
)

Behind the scenes

When the model is created, the following steps are performed:

  1. An instance of InvenioModel is created. This instance holds the basic configuration of the model, such as its name, version, api and ui slugs.

  2. An instance of an InvenioModelBuilder is created.

  3. All presets are collected and sorted according to their dependencies.

  4. For each preset:

    1. Dependencies of the preset are collected, including those that were passed as customizations to the model. If the dependency has not yet been built, it is built at this moment.
    2. The apply method is called with the builder and the model. The method returns a list of customizations that are applied to the model.
  5. If there are any unapplied customizations, they are applied to the model.

  6. During the application of the customizations, instances of Partial are created within the builder, such as BuilderClass, 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.

  7. The result of the model building process is transformed into a SimpleNamespace and returned to the caller. The returned object also provides helpers:

    • register() / unregister() — to register the in-memory model for import/entry points
    • get_resources() — to retrieve the in-memory files as a {path: content} mapping

Registering the model

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.

Design decisions

Late binding

The classes within the model should be as loosely coupled as possible. This is implemented by using dependency injection wherever possible.

Dependency descriptor

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")

builder.get_runtime_dependencies()

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)

Injected properties

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.

Early binding (for system fields and similar)

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)

Development

Setup

# Clone repository
git clone https://github.com/oarepo/oarepo-model.git
cd oarepo-model

./run.sh venv

Running Tests

./run.sh test

License

Copyright (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.

Links

Acknowledgments

This project builds upon Invenio Framework and is developed as part of the OARepo ecosystem.

About

Proof of concept of a runtime-built Invenio model

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 9

Languages