Skip to content

プラグイン開発

Recotem は Python エントリーポイントを通じて DataSource プラグインを検出します。プラグインは recotem.datasources グループに登録されたインストール済みパッケージです。

このリポジトリの examples/plugins/echo-source/ ディレクトリは最小限の動作する参考実装です。

プラグインコントラクト

プラグインは 3 つのクラスレベル属性と 1 つの必須メソッド (fetch) を持つクラスを提供しなければなりません。__init__ とオプションの probe については以下に説明します。

python
from __future__ import annotations

import random
from typing import ClassVar, Literal

import pandas as pd
from pydantic import BaseModel, Field
from recotem.datasource.base import DataSourceError, FetchContext


class EchoSource:
    """Returns a synthetic DataFrame — useful for testing and CI."""

    # 1. type_name: discriminator value matched against the recipe YAML
    #    `source.type` field.  Must be a non-empty string and unique across
    #    all installed plugins.  By convention use a short lower-case slug.
    type_name: ClassVar[str] = "echo"

    # 2. Config: pydantic BaseModel describing the recipe sub-fields for this
    #    source.  All fields appear under `source:` in the YAML alongside the
    #    `type:` discriminator.  Config MUST declare `type` as a Literal field
    #    whose single value equals type_name — recotem builds a pydantic
    #    discriminated union keyed on `type` across every registered plugin,
    #    and the training pipeline reads the field back to resolve the source
    #    class.  Omitting it is not an option: pydantic's default
    #    `extra="ignore"` would silently drop the YAML `type:` key and training
    #    would fail with "Recipe source has no discriminator 'type' field."
    #    `validate_plugin_contract` rejects a Config that omits it, types it as
    #    anything other than `Literal`, or whose Literal disagrees with
    #    type_name.
    class Config(BaseModel):
        type: Literal["echo"] = "echo"
        n_users: int = Field(default=10, ge=1)
        n_items: int = Field(default=20, ge=1)
        n_rows: int = Field(default=100, ge=1)
        seed: int = Field(default=42)

    # 3. extras_required: pip extras to suggest when optional dependencies
    #    are missing.  Leave empty if the plugin has no optional deps.
    extras_required: ClassVar[list[str]] = []

    # 4. no_expand_fields: frozenset of field names inside the source config
    #    whose string values must NEVER receive ${RECOTEM_RECIPE_*} env-var
    #    expansion.  List any fields that carry raw SQL, query parameters, or
    #    other content where ${} should be treated as literals.
    #    Use frozenset() (empty) when no fields need protection beyond the
    #    global baseline (query, query_parameters) that is always guarded.
    #    This attribute is REQUIRED — validate_plugin_contract enforces its
    #    presence and its type (frozenset).  A missing or wrong-type attribute
    #    raises DataSourceError at plugin discovery with a pointer to this doc.
    no_expand_fields: ClassVar[frozenset[str]] = frozenset()

    def __init__(self, config: "EchoSource.Config") -> None:
        self._config = config

    def fetch(self, ctx: FetchContext) -> pd.DataFrame:
        """Return a DataFrame whose columns include those named in
        the recipe `schema` block (user_column, item_column, optional
        time_column).

        Returns a DataFrame with columns: user_id (str), item_id (str),
        timestamp (int epoch seconds).
        """
        cfg = self._config
        max_possible = cfg.n_users * cfg.n_items
        if cfg.n_rows > max_possible:
            raise DataSourceError(
                f"EchoSource: n_rows ({cfg.n_rows}) exceeds n_users * n_items "
                f"({max_possible}).  Reduce n_rows or increase n_users/n_items."
            )
        rng = random.Random(cfg.seed)
        users = [f"user_{i}" for i in range(cfg.n_users)]
        items = [f"item_{j}" for j in range(cfg.n_items)]
        all_pairs = [(u, v) for u in users for v in items]
        sampled = rng.sample(all_pairs, cfg.n_rows)
        base_ts = 1_700_000_000
        rows = [
            {"user_id": u, "item_id": v, "timestamp": base_ts + idx}
            for idx, (u, v) in enumerate(sampled)
        ]
        return pd.DataFrame(rows, columns=["user_id", "item_id", "timestamp"])

    def probe(self) -> None:
        """Optional. Called by recotem validate to test connectivity.

        Should be cheap — never load full data.
        Raise DataSourceError on failure.
        Return value is ignored by recotem (Protocol declares -> None).
        """
        cfg = self._config
        max_possible = cfg.n_users * cfg.n_items
        if cfg.n_rows > max_possible:
            raise DataSourceError(
                f"EchoSource: n_rows ({cfg.n_rows}) exceeds n_users * n_items "
                f"({max_possible})."
            )
        # discarded by recotem validate — kept here for illustration only
        return {"status": "ok", "rows_to_emit": cfg.n_rows, "items": cfg.n_items}  # type: ignore[return-value]

ルール

  1. type_name はディスクリミネーター値です。レシピ内では source.type: echo として現れます。レジストリはこれが非空の文字列であり、ロードされたすべてのプラグイン間でユニークであることを検証します。type_name が重複していると、競合する完全修飾クラス名の両方が報告されます。recotem trainrecotem validate は終了コード 2 で終了します — 3 ではありません。プラグイン検出はレシピロードの内側で走るため、レジストリの DataSourceErrorRecipeError として再送出されるからです。recotem serve終了しません: recipe_load_error_skipped を記録し、該当レシピをロードしないまま稼働を続けます。

  2. Config は pydantic の BaseModel です。フィールドはレシピロード時に検証されます。制約には pydantic バリデーターを使用してください。デフォルト値なしの必須フィールドがレシピから欠落すると RecipeError が発生します。

    Config はディスクリミネーターフィールド type: Literal["<type_name>"] = "<type_name>"必ず宣言しなければならず、その値はクラスの type_name と完全に一致する必要があります。recotem は登録されたすべての Configtype をキーとする pydantic の判別可能ユニオンに組み立て (build_source_config_union)、recotem.training.pipeline がこのフィールドを読み戻してソースクラスを解決します。フィールドが欠落している場合、typing.Literal でない場合、または値が type_name と食い違う場合、validate_plugin_contract はプラグイン検出時に DataSourceError を発生させます。検出がレシピロードの内側で起きるため、このエラーは RecipeError にラップされ、プロセスは終了コード 2 で終了します — 下記の extra="ignore" の誤りと同じコードであり、データソースの失敗が返す 3 ではありません。

    代わりに pydantic のデフォルトである extra="ignore" に YAML の type: キーを吸収させることに依存しないでください。その組み合わせではレシピのロードは成功しますが、ディスクリミネーターが失われ、学習時に Recipe source has no discriminator 'type' field. (終了コード 2) で失敗します。

  3. extras_required純粋にドキュメント目的です。レジストリはこれが list[str] であることのみを検証します。recotem はこれらのエクストラを自動インストールまたは自動チェックしません。__init__ 内で役立つメッセージを自ら表示してください (以下の 遅延インポート を参照) — 属性の値がそこで引用するものです。

  4. no_expand_fields必須であり、frozenset[str] でなければなりません。これはソース Config 内のすべてのフィールドのうち、文字列値が ${RECOTEM_RECIPE_*} 環境変数展開を絶対に受けてはならないフィールドを命名します。validate_plugin_contract はこの属性が存在し frozenset であることを確認します。宣言が欠落または型が間違っている場合、プラグイン検出時にこのドキュメントへのポインターとともに DataSourceError が発生します。

    • ほとんどのプラグインでは no_expand_fields: ClassVar[frozenset[str]] = frozenset() を宣言してください — グローバルベースライン (queryquery_parameters) はレシピローダーによって常に保護されています。
    • SQL またはパラメーター化クエリフィールドを持つプラグインでは明示的にリストしてください: no_expand_fields: ClassVar[frozenset[str]] = frozenset({"sql", "bind_params"})。これにより多層防御が提供され、将来のメンテナーに対してセキュリティの意図が文書化されます。
  5. fetch(ctx)pandas.DataFrame を返さなければなりません。ソースがレシピのトップレベルの source ブロックを担う場合、DataFrame には recipe.schema で参照される列 (user_columnitem_column、オプションで time_column) が少なくとも含まれている必要があります。学習パイプラインはフェッチ直後にそれらの列に名前でアクセスします — 列が欠落すると KeyError として表面化し、学習実行が終了します。

    この recipe.schema のルールが適用されるのはインタラクションソースのみです。同じレジストリは features.item.source / features.user.source にも使われ、そこで必要な列は代わりにそのサイドの id_column と宣言されたすべての columns[].name になります。プラグイン側で特別な対応は不要です — FetchContext はインタラクション固有のフィールドを持たないため、登録済みのどのソースでもフィーチャーテーブルとして機能します — が、user_column / item_column が必ず要求されるという前提をハードコードしないでください。

  6. fetch() は外部または一時的な失敗 (認証エラー、ネットワークエラー、クエリエラー、空の結果) に対して DataSourceError を発生させなければなりません。 DataSourceError は終了コード 3 にマップされます。__init__ または fetch() から送出されたそれ以外の例外も Recotem がラップし、同じく終了コード 3 として報告されます — train では Data fetch failed: <exc> として (インスタンス化は fetch ステップの内側で行われるため、__init__ の失敗もこの文面で報告されます)、__init__ に限っては validate でも DataSource probe failed [source]: DataSource construction failed: <exc> として報告されます。両方のコマンドが呼び出すフックは __init__ だけです。validatefetch() を呼ばず、trainprobe() を呼びません。したがってこの 2 つのどちらかで失敗しても、片方のコマンドにしか届きません。とくに、probe() が例外を送出しても fetch() が動作するプラグインは、学習が終了コード 0 で完了し署名済みアーティファクトを書き出します。一方、同一のレシピに対する recotem validate は 3 を報告します。つまり probe() にだけ実装した事前条件は、学習実行のゲートにはなりません。したがってラップは終了コードではなくメッセージのためのものです: ラップしない例外は、欠けているエクストラも認証情報も示さないサードパーティライブラリ自身の文面でオペレーターに届きます。サードパーティの例外を明示的にラップしてください。

    python
    def fetch(self, ctx: FetchContext) -> pd.DataFrame:
        try:
            return self._do_fetch()
        except SomeLibraryError as exc:
            raise DataSourceError(str(exc)) from exc
  7. 遅延インポート。 オプションの依存関係をモジュールのトップレベルでインポートしないでください。__init__ または fetch() に遅延させてください。

    python
    def __init__(self, config: "MySource.Config") -> None:
        try:
            import my_optional_dep  # noqa: F401
        except ImportError as exc:
            raise DataSourceError(
                "MySource requires 'recotem[myextra]'. "
                "Install with: pip install 'recotem[myextra]'"
            ) from exc
        self.config = config

    これにより、欠落したエクストラは必要なエクストラ名を記載した明確な DataSourceError を生成します。ラップしない ImportError も同じ終了コード 3 を返しますが、オペレーターには No module named 'my_optional_dep' として届き、エクストラ名も対処方法も示しません。

パッケージ構成

examples/plugins/echo-source/ 配下の参考プラグインはこのレイアウトを使用しています:

recotem-echo-source/
├── pyproject.toml
└── src/
    └── recotem_echo/
        ├── __init__.py     # re-exports EchoSource so "recotem_echo:EchoSource" resolves
        └── source.py       # EchoSource class definition

クラスを直接含むフラットな recotem_echo/__init__.py も動作します — 重要なのはエントリーポイント文字列 <module>:<class> が解決できることです。

pyproject.toml:

toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "recotem-echo-source"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["recotem>=2.0,<3", "pandas>=2.2,<4"]

[project.entry-points."recotem.datasources"]
echo = "recotem_echo:EchoSource"

[tool.hatch.build.targets.wheel]
packages = ["src/recotem_echo"]

エントリーポイントキー (echo) はレジストリのログ/エラーメッセージで報告される名前ですが、ディスクリミネーターとしては使用されません — Recotem はロードされたクラスの type_name 属性を使用します。慣例として、両者を同じにしてください。

インストールと使用

bash
uv pip install -e examples/plugins/echo-source/

プラグインを使用するレシピに対して recotem validate を実行して検出を確認してください — ローダーはエントリーポイントレジストリを通じて source.type を解決し、プラグインが recotem と同じ環境にインストールされていない場合は Unknown DataSource type 'echo' を報告します。

ヒント — recotem schema にはプラグイン設定が含まれる

recotem schema は実行時に、登録されたすべての DataSource Config クラス (プラグインが提供するものを含む) の判別ユニオンを構築し、それを Recipe モデルに代入することで JSON Schema を生成します。プラグインの Config スキーマは出力に含まれます — これが source.* フィールドの IDE オートコンプリートを機能させる仕組みです。ユニオンは呼び出し時に build_source_config_union() を通じて組み立てられるため、プラグインは recotem と同じ Python 環境にインストールされている必要があります。

レシピ:

yaml
name: echo_test

source:
  type: echo
  n_users: 200
  n_items: 100
  n_rows: 6000    # see the note below before shrinking these
  seed: 42        # optional; omit to use the default seed

schema:
  user_column: user_id
  item_column: item_id
  time_column: timestamp   # EchoSource emits integer epoch-second timestamps
  time_unit: s             # required: a numeric time_column has no implied unit

training:
  algorithms: [TopPop]
  metric: ndcg
  cutoff: 10
  n_trials: 1

output:
  path: ./artifacts/echo_test.recotem

学習:

bash
recotem train recipe.yaml

注意 — 数値の time_column には time_unit が必須

schema.time_column が指す列が文字列や datetime ではなく数値を保持している場合、time_unit は必須です — 省略すると code: time_unit_required で終了コード 4 になります。recotem validate はこれを検出しません: レシピスキーマの検査とソースへのプローブは行いますが、単位が必要になるのは行がパースされた後だからです。そのため time_unit を欠いたレシピは validate をきれいに通過し、recotem train で失敗します。ソースがエポック整数を出力する場合は、レシピ作成者が最初から単位を設定できるよう、プラグインの README にその旨を記載してください。

ヒント — 行数がこれほど大きい理由

EchoSource はユーザーとアイテムのペアを一様ランダムにサンプリングするため、データにはレコメンダーが見つけられる信号がありません。報告される best_score はゼロ付近のノイズであり、モデルの品質については何も語りません。これは運用上の問題になります。最良トライアルのスコアがちょうど 0.0 のとき、学習は code: zero_score で終了コード 4 になるためです。小さな合成データセットではこれは現実に起こり得ます — n_users: 50 / n_rows: 500 ではおよそ 30 回に 1 回が 0.0 になり、同一条件の実行どうしでも結果が食い違いました。split.seed は irspack がホールドアウト集合を導出する ID の順序を制御しないためです。ホールドアウト集合のサイズは固定でも、どのインタラクションがそこに入るかは固定されません。上記の行数はホールドアウト集合のインタラクションを 31 件ではなく 511 件にするため、全件ミスの評価はほぼ起こり得なくなります。行数を減らすと、このウォークスルーはプラグインとは無関係の理由で断続的に失敗するようになります。

FetchContext

FetchContextfetch() がオプションで使用できるメタデータを保持します:

python
@dataclass
class FetchContext:
    recipe_name: str                            # the recipe's name field
    run_id: str                                 # unique ID for this training run (UUID)
    extra: dict[str, Any] = field(default_factory=dict)  # reserved for future use

ほとんどのプラグインは ctx を無視します。書き込みが多いソースからのフェッチにおけるロギングと冪等性キーに有用です。

fetch() の制約

  • 同期的で、単一の pandas.DataFrame を返すこと。ジェネレーター、Iterator[DataFrame]async def はサポートされていません — 学習パイプラインは fetch(ctx) を直接呼び出し、すぐに .columns を読み取ります。
  • DataFrame 全体をメモリに。 Recotem は全結果セットで学習します (irspack はそこからスパース行列を構築します)。メモリより大きいソースの場合は fetch() 内でチャンク処理と集計を行い、事前集計済みの DataFrame を返してください (例: (user, item) ペアのカウント)。
  • 認証情報は FetchContext.extra を通じて提供されません (予約済みです)。環境変数 (推奨 — K8s Secrets、systemd の EnvironmentFile、Docker の --env-file と連携) またはレシピで宣言した Config フィールド (ただし YAML にシークレットを記述しないこと — 代わりに ${RECOTEM_RECIPE_*} で環境変数を参照すること) から読み取ってください。

アイテムメタデータの読み込み

プラグインのレシピが item_metadata を使用する場合、メタデータは recotem.metadata.loader.load_item_metadata によってロードされます。失敗はソースフェッチの失敗と区別できるよう DataSourceError ではなく MetadataError として表面化します。例外には失敗の起点を示す .cause 属性があります:

.cause意味
"http_fetch"HTTP/HTTPS フェッチが失敗した (SSRF ガード、バイト上限、sha256 不一致)。__cause__HttpFetchError
"parse"ファイルが宣言された型 (CSV/Parquet) としてパースできなかった。
"field_missing"必須フィールドが存在せず on_field_missing="error" が設定されている。
"io"ローカルまたはオブジェクトストアの読み取りが失敗した。
"unknown"予期しない失敗のキャッチオール。

ローダーはオプションの recipe_name= キーワード引数を受け付けます。指定すると、レシピ名が HTTP フェッチャーのログコンテキストに組み込まれ、リダイレクトおよびバイト上限のログイベント (例: metadata_source_redirect) がトリガーしたレシピと関連付けられます。これはウォッチャーによって自動的に設定されます。load_item_metadata を直接呼び出す場合 (例: テスト内) のみ必要です。

互換性

プラグインコントラクトは recotem 2.x の公開サーフェスの一部です。プラグインの pyproject.tomlrecotem>=2.0,<3 をピン留めしてください — type_name / Config / fetch(ctx) の形状はメジャーバージョン内で安定しています。probe() フックは将来のマイナーリリースでオプションのパラメーターが追加される可能性があります。将来対応したい場合は **kwargs: Any を使用してください。

[project.entry-points."recotem.datasources"] のエントリーポイントキーは情報提供のみです (エラーメッセージで使用される)。ディスクリミネーターはクラスの type_name です。2 つのインストール済みプラグインが両方とも type_name = "csv" を宣言すると、recotem trainrecotem validate は両方の完全修飾クラス名を示して終了コード 2 で終了し、recotem serve は該当レシピをスキップしたまま稼働を続けます — どちらかをアンインストールするか type_name を変更してください。

recotem validate でのバリデーション

recotem validate recipes/my_recipe.yaml はソースクラスをインスタンス化し (__init__ の遅延インポート/エクストラチェックを実行)、fetch()呼び出しません。ソースにオプションの probe() メソッドが定義されている場合、recotem validate は軽量な接続/認証チェックのためにそれを呼び出します:

python
def probe(self) -> dict:
    """Optional. Called by recotem validate to test connectivity.

    Should be cheap (LIMIT 1, dry-run, fs.exists, ...) — never load full data.
    Raise DataSourceError on failure.  Return a small status dict that
    recotem validate logs (e.g. {"status": "ok", "rows_to_emit": n_rows}).
    """
    ...

probe() が定義されている場合、recotem validateDataSource: probe OK (<type_name>) を報告します。定義されていない場合は DataSource: extras OK (<type_name>, no probe defined) を報告します。ビルトインの CSVSource / ParquetSource は fsspec の exists() を使用し、BigQuerySource はドライランクエリジョブを使用します。

フィーチャーソースもプローブされます。 features: ブロックを持つレシピでは、features.item.source / features.user.source もトップレベルの source と同じ方法でプローブされ、報告される各行には [<where>] ラベル ([features.item.source] / [features.user.source]) が付くため、どのソースが失敗したかが分かります。プラグインがフィーチャーテーブルとして使われる可能性がある場合は、1 回の recotem validate で複数回実行されても問題ない程度に probe() を軽量に保ってください。

probe_columns() — スキーマカラムのチェック

recotem train は、データに存在しないカラムを指定したレシピを DataSourceError (終了コード 3) で拒否します。recotem validate は 2 つ目のオプションフックを通じて、トップレベルの source についてのみ同じ問いを投げます。

python
def probe_columns(self, ctx: FetchContext) -> bool:
    """Optional. Called by recotem validate with the recipe's schema columns.

    ctx.extra carries user_column / item_column / time_column, exactly as
    fetch() receives them.  Implement this only when the column list is
    cheap to obtain — a CSV header row, a Parquet footer schema — never by
    running the query or downloading the body.

    Return True when the check ran, False when this configuration cannot
    answer cheaply.  Raise DataSourceError when a required column is absent.
    """
    ...

recotem validate は 3 種類の異なる行のいずれかを出力するため、実行していないチェックを実行したかのように見せることはありません。

戻り値出力
TrueSchema columns: OK (<type_name>) [<where>]
FalseSchema columns: not checked (<type_name> cannot list columns without a full fetch) [<where>]
フックなしSchema columns: not checked (<type_name> has no header-only column probe; verified at train time) [<where>]

DataSourceError が送出された場合は Schema column check failed [<where>]: <error> として報告され、train と同じ終了コード 3 になります。

フィーチャーソース (features.item.source / features.user.source) はカラムチェックの対象です。フィーチャーテーブルがインタラクションのカラムを持たないのは正当だからです。BigQuerySourceSQLSource もこのフックを実装していません。カラム集合はクエリを実行して初めて分かるからです。

プラグインが実際に生成しうる終了コード

意図的に壊したプラグイン (1 パッケージにつき 1 つの違反、それぞれ独自の recotem.datasources エントリポイントを持つ) を素の pip install recotem 環境にインストールし、実際の CLI を実行して測定しました。

プラグインの状態trainvalidate
正常00
Configtype ディスクリミネータがない22
typeLiteral ではなく str22
typeLiteral 値が type_name と食い違う22
no_expand_fields がない22
no_expand_fieldsfrozenset ではなく set22
type_name が他のインストール済みプラグインと衝突22
__init__DataSourceError を送出33
__init__ がそれ以外の例外を送出33
probe() が何らかの例外を送出0 ‡3
probe()HttpFetchError を送出 (またはラップ)0 ‡7
fetch()DataSourceError を送出30 †
fetch() がそれ以外の例外を送出30 †
fetch() が DataFrame 以外を返す30 †
fetch()schema: で指定されたカラムを欠く30 †
fetch()HttpFetchError を送出 (またはラップ)70 †

validatefetch() を呼びません。‡ trainprobe() を呼びません。

見落としやすい帰結が 4 つあります。

  • コントラクト違反はすべて終了コード 3 ではなく 2 です。 プラグインの探索はレシピのロード内部で実行されるため、レジストリの DataSourceErrorRecipeError として再送出されます。終了コード 3 は、ロードには成功したソースがデータの生成に失敗した場合のものです。
  • プラグインがどう振る舞っても終了コード 1 にはなりません。 ラップされていない例外は、到達したコマンドの側で Recotem がラップし、3 として報告されます。2/3 の枠から上に抜ける唯一のコードが 7 で、HttpFetchError__cause__ チェーンを通じてこれを保持します。したがってプラグイン内部での SSRF ガードによる拒否は、平坦化されずに 7 として報告されます。ただしその失敗の構造化 code フィールドは依然として datasource_error です。7 は __cause__ チェーン由来なので、code を grep しても専用の値は見つかりません。
  • trainvalidate は、両方が到達しうる失敗についてはすべて一致します。 食い違う行は、ちょうど一方が実行しない行だけです。
  • probe()train に対するゲートではありません。 チェックを probe() にだけ置いても得られるのは validate の失敗だけです。probe() が例外を送出しても fetch() が動作するプラグインは、学習に成功して署名付きアーティファクトを書き出します。学習を止めなければならない前提条件は __init__fetch() に置いてください。

1 つの壊れたプラグインがホスト上の全レシピを壊します

探索は source.type ごとの遅延評価ではなく、recotem.datasources エントリポイントグループ全体に対して先行して実行されます。まったく別の正常なソースを指定したレシピも失敗します。

Recipe '.../ok.yaml' source: plugin source discovery failed for type 'ok':
DataSource plugin 'MismatchSource' ... declares Config.type as Literal['something_else'],
which does not match its type_name 'mismatch'.

このレシピは MismatchSource を一切参照していません。つまり、インストール済みのどれか 1 つのプラグインのコントラクト違反が、trainvalidate にとってホスト全体の障害になります。しかもエラーが名指しするのは実行したレシピではなく、違反したプラグインクラスです。ファイルパスではなくメッセージ中のクラス名を読んでください。

危険 — serve では同じ障害が素朴なプローブから見えません

serve は意図的に寛容です。壊れたレシピ 1 つが、他のレシピをホストするサーバー全体を落とすことは許されません。type_name の衝突と単一のレシピディレクトリで測定した結果:

console
$ curl -s http://127.0.0.1:8080/v1/health
{"status":"ok","total":0,"loaded":0,"skipped":1}

HTTP 200"status": "ok"、そしてロード済みレシピ 0 件。プロセスは生きていて応答もしますが、何も配信していません。ログには recipe_load_error_skippedrecipes_directory_loaded_lenient が出ています。ステータスコードだけ、あるいは status フィールドだけを見る liveness / readiness チェックは「正常」と報告します。プロセスが生きているかではなく、loadedskipped でアラートしてください。

テスト

CLI を使用せずに fetch() を直接テストしてください:

python
from recotem_echo import EchoSource
from recotem.datasource.base import FetchContext

source = EchoSource(EchoSource.Config(n_users=20, n_items=50, n_rows=200))
ctx = FetchContext(recipe_name="test", run_id="abc")
df = source.fetch(ctx)
assert {"user_id", "item_id", "timestamp"}.issubset(df.columns)
assert len(df) == 200

完全な YAML → Recipe → DataSource パスを確認するには recotem.recipe.load_recipe を統合テストで使用してください。recipe.source はプラグインの Config モデルのインスタンスです:

python
from recotem.recipe import load_recipe
from recotem_echo import EchoSource

recipe = load_recipe("tests/fixtures/echo_recipe.yaml")
assert isinstance(recipe.source, EchoSource.Config)

プラグインの信頼

警告

サードパーティの DataSource プラグインは完全なプロセス権限で実行されます。悪意のあるプラグインは RECOTEM_SIGNING_KEYSRECOTEM_API_KEYS を含む環境変数を読み取れます。プラグインのバージョンをピン留めし、ロックファイルでハッシュピン留めし、デプロイ前にソースコードをレビューしてください。セキュリティ — プラグインの信頼 を参照してください。