Плагины и расширения odpm (4.4+)¶
odpm 4.4+ добавляет extension API на host: prepare steps, compose fragments и lifecycle hooks. Плагины не получают прямой доступ к mutable Config — только frozen ExtensionHostContext.
Версия API (4.6+)¶
Текущая версия extension API: EXTENSION_API_VERSION = "1.1" (dev_project/extensions/api.py). Поддерживаются 1.0 и 1.1; модули без EXTENSION_API_VERSION считаются 1.0. Host проверяет версию при загрузке entry points и .odpm/plugins/*.py. В плагине:
from dev_project.extensions.api import EXTENSION_API_VERSION, assert_extension_api_compatible
assert_extension_api_compatible(EXTENSION_API_VERSION)
Breaking changes в протоколах pluggy или manifest hooks требуют major bump API. Политика: ADR-004.
Три способа расширить проект¶
| Механизм | Где объявляется | Когда выполняется |
|---|---|---|
Manifest services |
odpm.json v2 → services |
Prepare compose.fragments + odpm plan → compose.fragment.<name> |
Manifest hooks |
odpm.json v2 → hooks |
post_clone после git materialize; post_prepare после prepare; pre_up перед compose up |
| Python entry points | pyproject.toml пакета |
Pluggy: odpm.prepare_steps, odpm.hooks |
| Project-local plugins | .odpm/plugins/*.py или extensions.local |
Импорт при bootstrap (только внутри project_dir) |
Подробнее о полях v2: odpm.json. ADR: adr-001-extensions-and-manifest-v2.md.
Декларативный сервис (Mailpit)¶
Тестовый SMTP с веб-интерфейсом на порту 8025. Добавьте в nested manifest v2:
{
"manifest_schema": 2,
"requires_odpm": "4.5.0",
"services": {
"mailpit": {
"image": "axllent/mailpit",
"restart": "unless-stopped",
"ports": ["8025:8025", "1025:1025"]
}
}
}
Для sidecar допустимы user и tty (как в service_patches):
"services": {
"armtek_test": {
"image": "autoparts_env:emulator",
"user": "root",
"tty": true,
"volumes": ["${DIGITAL_AUTOPARTS_ENV_DIR}/data:/data:Z"]
}
}
Тот же spec в коде: dev_project.extensions.reference.mailpit.MAILPIT_SERVICE_SPEC.
После odpm up сервис появится в сгенерированном docker-compose.yml (блок {COMPOSE_SERVICE_FRAGMENTS}). Артефакты materialize: .odpm/compose/fragments/mailpit.yml (gitignored).
Patch built-in сервисов (service_patches, 4.6+)¶
Имена odoo, db, postgres нельзя объявлять в services — только patch. Политика: ADR-009.
"service_patches": {
"odoo": {
"environment": {
"CUSTOM_METRIC": "1"
}
}
}
odpm plan показывает compose.patch.odoo (preview); patch применяется при compose.generate.
command / entrypoint для sidecar (4.6+)¶
Только exec form (JSON-массив строк) в services.<name>:
"services": {
"worker": {
"image": "busybox:latest",
"command": ["sh", "-c", "sleep infinity"]
}
}
Команда odoo по-прежнему задаётся generator; override — через service_patches.odoo.command при явной необходимости.
Lifecycle hooks в manifest¶
"hooks": {
"post_prepare": [
["./scripts/notify.sh", "prepare-done"],
"mycompany.odpm.hooks.warmup"
],
"pre_up": [
["docker", "network", "create", "odpm-dev", "||", "true"]
]
}
Каждый элемент — либо argv (массив строк, выполняется в project_dir без shell), либо plugin id (строка) для pluggy hook runner.
В argv поддерживается ${VAR} / ${VAR:-default} (как в Compose): раскрытие при выполнении hook из process env → project .env → default в строке. Subprocess получает merged env (process + недостающие ключи из .env). Пример сборки образа sidecar:
"hooks": {
"post_prepare": [[
"docker", "build",
"-f", "${DIGITAL_AUTOPARTS_ENV_DIR}/server_launch_system/alpine_dockerfile",
"-t", "autoparts_env:emulator",
"${DIGITAL_AUTOPARTS_ENV_DIR}"
]]
}
В services / service_patches те же правила подстановки для строковых полей (image, volumes[], command[], environment, …) — раскрытие при чтении manifest с EnvResolver.
Порядок по ADR-004:
- Git materialize
hooks.post_clone(если задан)- Все prepare steps (built-in +
odpm.prepare_steps+ local plugins), сортировка поorder hooks.post_prepare- Runtime: debug profile, IDE, database drift
hooks.pre_updocker compose up
odpm plan показывает шаги hooks.*, compose.fragment.<service> и compose.patch.<service> когда они настроены.
Поле order у prepare steps¶
Меньшее значение order выполняется раньше среди extension steps (built-in порядок фиксирован в registry). Конфликт id с built-in step → ValueError при регистрации.
Ошибка shell-hook → PipelineError с кодом выхода команды.
Python-плагин: compose fragment¶
# my_odpm_mailpit/__init__.py
from dev_project.extensions import ExtensionHostContext, register_compose_fragment
from dev_project.extensions.reference.mailpit import MAILPIT_SERVICE_SPEC
class MailpitFragment:
name = "mailpit"
def compose_services(self, ctx: ExtensionHostContext) -> dict:
return {"mailpit": dict(MAILPIT_SERVICE_SPEC)}
def _register():
register_compose_fragment("mailpit", MailpitFragment())
Регистрация при импорте пакета или через entry point (см. ниже).
Patch built-in из Python (API 1.1+)¶
Опциональный метод compose_service_patches на compose fragment (те же правила, что manifest service_patches):
def compose_service_patches(self, ctx: ExtensionHostContext) -> dict:
return {"odoo": {"environment": {"MY_FLAG": "1"}}}
Nested dependency services (4.6+)¶
При use_oca_dependencies v2 services / service_patches из odpm.json зависимостей наследуются в host compose после project.map_folders. При конфликте имён побеждает host manifest. См. ADR-004.
Python-плагин: prepare step¶
[project.entry-points."odpm.prepare_steps"]
my_step = "my_odpm_plugin.steps:plugin_factory"
from dataclasses import dataclass
from dev_project.prepare.helpers import make_plan_step
from dev_project.prepare.types import PrepareContext
@dataclass(frozen=True)
class MyPrepareStep:
id: str = "mycompany.custom.step"
description: str = "Custom prepare work"
order: int = 500
def evaluate(self, ctx: PrepareContext):
return make_plan_step(self.id, self.description, "run", True, "always run")
def execute(self, ctx: PrepareContext) -> None:
...
def plugin_factory():
return MyPrepareStep()
evaluate должен быть без side effects — odpm plan вызывает только evaluate.
Python-плагин: lifecycle hook runner¶
[project.entry-points."odpm.hooks"]
warmup = "my_odpm_plugin.hooks:WarmupRunner"
class WarmupRunner:
name = "mycompany.odpm.hooks.warmup"
def run_post_clone(self, ctx) -> None:
...
def run_post_prepare(self, ctx) -> None:
...
def run_pre_up(self, ctx) -> None:
...
Id в manifest ("mycompany.odpm.hooks.warmup") должен совпадать с name runner.
Пример pyproject.toml пакета¶
[project]
name = "odpm-services-mailpit"
version = "0.1.0"
dependencies = ["odpm>=4.4"]
[project.entry-points."odpm.hooks"]
# optional lifecycle runners
[project.entry-points."odpm.prepare_steps"]
# optional prepare steps
Установите пакет в venv host (pip install -e .) рядом с odpm.
Project-local plugins (4.5+)¶
Без отдельного pip-пакета можно положить модули в .odpm/plugins/ проекта (только этот каталог, без .. в именах). Опциональный allow-list в manifest v2:
"extensions": {
"local": ["mailpit_local"]
}
Загружается project_dir/.odpm/plugins/mailpit_local.py. Пример fixture: tests/fixtures/sample_plugin/.
Cookbook (минимальный плагин)¶
- Manifest v2
services.mailpitилиregister_compose_fragmentв Python. - Prepare step с
evaluateбез side effects +executeдля записи файлов. - Hook runner с
name, совпадающим с id вhooks.post_prepare. pip install -e .или.odpm/plugins/my_plugin.py.odpm plan— проверить шагиcompose.fragment.*,hooks.*, extension prepare step.
Шаблон pyproject.toml: tests/fixtures/sample_plugin/pyproject.toml.
Ограничения¶
- Container-side
ContainerConfigостаётся stdlib-only (без новых PyPI deps в образе). - Compose YAML генерируется host-движком
dev_project/yaml/(ruamel.yaml); YAML anchors/merge aliases не поддерживаются. - Конфликт имён: plugin compose fragment перезаписывает одноимённый service из manifest.
См. также¶
- Сгенерированные файлы —
.odpm/compose/fragments/ - Declarative sidecar-сервисы — секция Mailpit выше и таблица механизмов расширения
- Устаревшие черновики в
dev_project/plugins/(services_ru.md,todo_ru.md) — redirect сюда