Changelog¶
このプロジェクトの変更点はこのファイルに記録します。 形式は Keep a Changelog に、バージョニングは Semantic Versioning に準拠します。
MaiML-Library organization の方針により、MaiML仕様(業務ルール・シリアライズ 形式など)に影響する変更は、必ずGitHub Issue/PRでの議論を経てから、このファ イルへの記載とあわせて行ってください。
バージョニングポリシー¶
このリポジトリは x.y.z 形式の Semantic Versioning
に準拠しますが、現在はまだ正式リリース前(0.y.z)の段階です。SemVerの
慣習に従い、メジャーバージョン x が 0 の間は x を固定し、以下の基準で
バージョンを管理します(MaiML-Domainと共通の方針です)。
- 破壊的変更(既存APIの必須引数の追加・変更、公開インターフェースの
変更など):
y(マイナー)を上げる。あわせて GitHub の Release 機能 でリリースノートを作成する。Releaseを作成するとリポジトリをWatchしている メンバーに通知が届くため、破壊的変更を能動的に知らせる目的も兼ねる。 - 後方互換を保った変更(機能追加・バグ修正など):
z(パッチ)を上げる。 この場合は軽量なgit tagのみでよく、GitHub Releaseの作成は不要。
1.0.0 へ上げるタイミング(=正式リリース)は、APIが安定し外部からの利用に
耐えると判断した時点とします。1.0.0 以降は標準的なSemVerに従い、
破壊的変更は x(メジャー)、機能追加は y(マイナー)、バグ修正は
z(パッチ)を上げます。
なお、依存先である maiml-domain のバージョンを上げる(pyproject.tomlの
@vX.Y.Z指定を更新する)こと自体も、このSDKにとっての変更として扱い、
その内容(Domain側で何が変わったか)に応じて上記の基準でバージョンを
判断してください。
[Unreleased]¶
[0.2.0] - 2026-09-14¶
-
リリース前レビューで見つかった、バージョン文字列・タグ参照の 同期漏れを修正しました。
pyproject.tomlのversionを0.2.0へ 上げた際、pymaiml/__init__.pyの__version__(0.1.0のまま)と、tests/test_smoke.pyの対応するアサーション・コメントが未更新 でした。また、maiml-domain依存をv0.3.0へ上げた際、README.md/CONTRIBUTING.md内のコード例・説明文中に残っていたv0.2.0という 記述(計6箇所)も古いままでした。いずれもこの0.2.0タグ自体が まだGitHub Releaseを作成していない段階で発見したため、新しい バージョンを切らず本リリースに含めています。 -
依存パッケージ
maiml-domainのバージョン指定をv0.2.0からv0.3.0へ更新しました。 MaiML-Domain側のv0.3.0は、これまで 無検証で構築できていた値を拒否するようになる破壊的変更(xs:decimal 非有限値の拒否、xs:ID/IDREF/QName/languageのローカルな字句検証の 追加、UncertaintyBaseType.keyへのQName検証適用、NCNAME_PATTERNの astral-plane完全対応)であり、CONTRIBUTING.mdの方針(依存先の バージョンを上げること自体もこのSDKにとっての変更として扱う)に 従い、この更新を本リポジトリの0.2.0リリースへ含めました。 新しいmaiml-domainに対してpytest(177件)を実行し、既存のテストが すべて通ることを確認済みです。 -
README.mdのGitHub Pages案内から、実態と食い違っていた記述を修正しました。 「このリポジトリではまだこの設定を行っていません」という一文が残ったまま でしたが、https://maiml-library.github.io/PyMaiML/ は実際にはすでに公開 済み(Settings > Pages > SourceがGitHub Actionsに設定済み)であることを 確認したため、「すでに設定済みで、閲覧できます」という記述に更新しました。 コード変更はなく、ドキュメントのみの修正です。
-
README.mdの「MaiML-Domainとの関係」節の記載を更新しました。
maiml_domain/pymaimlそれぞれの紹介文の冒頭に、両者の役割を一文で 説明する文章(「JIS K 0200 / MaiML XSDに対応する、言語SDK共通の ドメインモデル」「MaiML-Domainを基盤として、MaiML文書の読み書き・検証・ 検索・構築・意味的操作を提供するPython SDK」)を追加しました。既存の 技術的な説明部分はそのまま残しています。コード変更はありません。 -
同梱XSD(
pymaiml/schema/MaiML-Schema-1_0/)の再配布条件を明文化しました。 これらのXSDファイルはJAIMA(日本分析機器工業会)の第三者著作物であり、 無改変での再配布は許諾されていますが改変は許諾されていません。この 制約をpymaiml/schema/MaiML-Schema-1_0/NOTICE(新規)へ明記し、README.md(該当箇所・ライセンス節)とCONTRIBUTING.md(スキーマ 更新手順の先頭)からも参照するようにしました。あわせて、maiml.xsd/maiml-core.xsd/maiml-document.xsd/maiml-property.xsd/xenc-schema.xsdの5本が(<Signature>/<EncryptedData>検証のため) 既にxs:importを追記された改変版であることを、この制約と矛盾する 既知の・未解消の逸脱として明記しました。今回はこの5本を元に 戻さず維持する判断としましたが、今後の追加改変は行わない方針です。 コード変更はなく、ドキュメント(NOTICE新規作成・README.md・ CONTRIBUTING.md)のみの追記です。 -
署名済みMaiMLファイルの保存に関するルールを明文化しました。 署名済みMaiMLを保存する際は、XMLの再整形(pretty print)・コメント削除・ 不要な空白削除など、署名対象XMLの正準化結果を変化させる処理を行っては ならないというルールを
CONTRIBUTING.mdの「XML Signature(電子署名)の 扱い」節に追記し、pymaiml.serialization.dumps()のdocstringにも corollaryとして追記しました。dumps()/dump()は既存のSignatureを 常に出力から除外するため現状はこの問題に直面しませんが、将来 「署名済みファイルをそのまま保存・コピーする」機能や署名対応モジュール を追加する際に必ず守るべき制約です。コード変更はなく、ドキュメント (CONTRIBUTING.md・docstring)のみの追記です。 -
署名済みMaiMLの保存ルールを、MaiML-Signer(外部の署名ツール)の README記載内容を踏まえて再レビュー・拡充しました。 「XMLとして 意味的に同一に見える変更でも、正準化(C14N)結果が変われば署名は保持 されない」という同ツールの記載を踏まえ、
CONTRIBUTING.mdのルールに 改行コード(CRLF/LF)の変換・属性の並び替え・空要素の記法変更も明記し、 Gitのautocrlf設定など実務上ありがちな落とし穴と、対策としての.gitattributesでの-text指定を追記しました。pymaiml.serialization.dumps()のdocstringにも改行コード変換の注意を 追記しています。コード変更はありません。 -
tests/test_validation.pyの回帰テストを拡充しました(3テスト→27テスト)。MaiML_Domain_PyMaiML_required_fixes.mdのレビューで指摘された通り、pymaiml.validationに実装済みの検証ルールに対して直接的な回帰テストが 不足していたため、ENCR-01・CHN-01・CHN-02・NS-01・UUID-01・ROOT-01〜 ROOT-07・REF-01〜REF-03(いずれも正常系/異常系)・EVT-01/EVT-02・ XSD-01・XML-01・ENC-01・EXT-01・IO-01を含む、実装済みFindingコード ほぼ全てに専用テストを追加しました。XSD-02(lxml内部エラーに依存する 狭い経路)のみ、意図的にテスト対象外とし、その理由をモジュール docstringに明記しています。コード変更(validation.py本体)はなく、 テストの追加のみです。 -
Signature抽出仕様(バイト同一性の非保証)をdocstringへ明記しました。
_read_document()が<Signature>要素をlxml.etree.tostring()で 再シリアライズする際、親スコープの名前空間宣言が付与される場合が あり、抽出結果は入力XML中の元の部分木とバイト単位で完全一致すると は限りません。この点を_read_document()とloads()のdocstringに 明記し、CONTRIBUTING.mdの「XML Signature(電子署名)の扱い」節を 参照するよう案内を追加しました。コード変更はなく、ドキュメント (docstring)のみの追記です。
[0.1.0] - 2026-09-11¶
Added¶
- リポジトリの雛形を作成。
maiml_domain(MaiML-Domain, v0.1.0タグ)への pip依存をpyproject.tomlに定義。 - 依存関係の疎通確認用スモークテスト(
tests/test_smoke.py)。 pymaiml.serialization:maiml_domainのオブジェクトツリーを実際の.maimlXMLへ書き出すdumps()/dump()を実装。MaiML-Domainの検証用 スクリプト(tests/build_sample_maiml.py)にあった変換ロジックを、 document/protocol(method・program階層のtemplateも含む)/data (material/condition/result)/eventLog(extension/global/classifier含む) /pnmlの全構造要素、およびproperty/content約70種類全てに一般化した。pymaiml.serialization: 読み込み方向loads()/load()を実装。LoadedMaiml(root/namespaces/ids)を返す。「既存のprotocolのみの MaiMLファイルを読み込み、document/protocolをそのまま引き継いで 新たなdata/eventLogを組み立てる」というユースケースに対応するため、namespaces(ルート要素が宣言していた名前空間の再現用)とids(IdFactory.from_existing_ids()と組み合わせた新規id採番時の 衝突回避用)をあわせて提供する。dumps()の出力をloads()で読み戻し、 再度dumps()した結果が元の出力とバイト単位で一致することを確認済み。pymaiml.validation: 公式MaiML-Schema-1_0(pymaiml/schema/に同梱) によるXSD検証と、MaiML AI Common Specificationの補足ルール (EVT-02のlifecycle complete判定、ref参照先の型チェック、XES名前空間 の厳密一致、秘匿禁止要素など)をvalidate(path) -> ValidationResultとして提供。maiml-schema-validator Claude skillの検証スクリプトと 同等のチェックをライブラリAPI化したもの。pymaiml.builders:IdFactory(id/uuid採番)、infer_property()/infer_content()(Pythonの値の型からproperty/content クラスを推定)、new_complete_event()(EVT-02対応のlifecycle:transition="complete"イベント組み立て)を追加。pymaiml.builders.IdFactory:reserve()/from_existing_ids()を追加。pymaiml.serialization.load()で読み込んだ既存ファイルのid一覧を そのまま渡すことで、新規に採番するidが既存ファイルのidと衝突しないこと を保証できる。pymaiml.builders.infer_property()/infer_content()にxsi_type=(maiml_domainのクラス、またはxsi:type名の文字列)を追加。protocol要素の汎用データコンテナ(材料テンプレート等)はほとんどの場合、値が まだ無いプレースホルダーだが、xsi:typeはスキーマ上必須のため、値から 推定できないこのケースに対応できるようにした。xsi_type=・value=/values=のいずれも与えなかった場合は(推定不能として) ValueErrorを送出する。PropertyListTypeのようにvalue/valuesパラメータ自体を持たないクラスも、コンストラクタの実シグネチャを見て 正しく組み立てられる。pymaiml.builders.XsiTypeRegistryを追加。infer_property()/infer_content()にregistry=として共有インスタンスを渡すことで、protocol側のプレースホルダーと対応するdata側の実測値記録とで、 同じkeyが常に同じxsi:typeになることを保証する(実測値のPython型 から推定した場合と食い違う可能性がある場合でも、登録済みの型を優先 する)。同じkeyに異なるxsi:typeを再登録しようとした場合、または property/contentの種類を跨いで同じkeyを使おうとした場合はエラーに なる。- 依存関係に
lxmlを追加(pymaiml.validationとpymaiml.serializationの 読み込み側が使用)。 - 上記3モジュールに対するテスト(
tests/test_serialization.py・tests/test_validation.py・tests/test_builders.py)を追加。生成した.maimlが実際にスキーマ検証を通ることに加え、「既存protocolファイルを 読み込んで新規data/eventLogを追加し、スキーマ検証まで通す」ユースケース、 および「protocol側の値なしプレースホルダーと対応するdata側の実測値が 同じxsi:typeになる」ことをend-to-endで検証するテストを含む。 pymaiml.serialization.dumps()にdrop_stale_signature=引数を追加。document.signatureはloads()/dumps()がただの1フィールドとして機械的 に往復させるだけの生XML文字列であり、data/protocolなど他の部分が 編集されたかどうかは一切関知しない。そのため「ロード→編集→出力」の ワークフローで、編集後にdocument.signatureを明示的にクリアし忘れると、 内容的にはもう無効なはずの古い署名がそのまま出力に残ってしまう (署名生成・暗号学的検証そのものはCONTRIBUTING.mdの方針どおりpymaiml外 (MaiML-TOOLS層)の責務だが、「編集されたかどうか」はSDK内でloads()→ ミューテーションが完結するpymaimlでしか検知できない)。loads()が返すLoadedMaimlはロード直後のrootのcopy.deepcopy()を 非公開の_snapshotとして保持するようになり、dumps(root, drop_stale_signature=loaded)は現在のrootとloaded._snapshotを(document.signatureを除いて)比較し、署名以外の 内容が変わっていれば出力から<Signature>要素を省く (root.document.signature自体は書き換えない)。maiml_domainのDocumentType/MaimlRootType等はdataclassではなく独自__init__の クラスで__eq__も未定義のため、比較は_build_maiml_element()(dumps()本体から切り出した木構築処理を_write_document(..., suppress_signature=True)付きで両者に適用)によるXMLフィンガープリント 比較で行う。drop_stale_signature=None(デフォルト)では従来どおり 署名は無条件に素通しされ、後方互換。loads()を経由しない(スナップ ショットを持たない)LoadedMaimlを渡した場合は分かりやすいValueErrorを送出する。回帰防止テストを4件追加 (test_drop_stale_signature_keeps_signature_when_nothing_changed、test_drop_stale_signature_drops_signature_when_content_edited、test_drop_stale_signature_without_edits_matches_plain_dumps、test_drop_stale_signature_requires_a_snapshot_from_loads)。tests/test_xsd_completeness.pyを追加。MaiML-Schema-1_0(pymaiml/schema/同梱版)を正として、(1)全<xs:complexType>が同名(先頭大文字化)のmaiml_domainクラスを持つか、(2)maiml-property.xsdの全property/ content xsi:type(propertyBaseType/contentBaseTypeを直接継承する complexType)がpymaiml._xsi_registryに登録されているか、をそれぞれ 双方向(過不足なし)でチェックする。「XSDが更新されたのに、対応する クラスをMaiML-Domain側に追加し忘れる」ケースをCIで検出できるようにする ためのテスト。maiml_domain側のシンプル型(Uuid等)・xs:group由来の mixin(GlobalObjectContent/EncryptionType)・実装詳細 (_StrictAttributesMixin)は、XSDのcomplexTypeに対応しないことが既知の 例外として明示的に許容リスト化してある。現時点(MaiML-Domainv0.2.0) では過不足なし(5件すべて合格)。
Fixed¶
pymaiml.serialization:<uncertainty>要素が書き込み・読み込みの両方で 無視されていた不具合を修正。スキーマのuncertaintyBaseTypeはpropertyBaseType/contentBaseType共通の抽象基底型であり、maiml_domain側のuncertaintiesパラメータは元々全てのproperty/content クラスにモデル化されていた(pymaiml側の実装漏れであり、maiml_domainの制限ではなかった)。_write_property_or_content()/_read_property_or_content()にtag=引数を追加し、同じ具象クラスを<uncertainty>タグとしても書き出し/読み込みできるようにした。pymaiml.serialization:_parse_scalar_text()/_format_value()の xsi:type判定が大文字小文字を区別する部分文字列一致 (例:"Float" in xsi_type)だったため、pymaiml._xsi_registryが クラス名の先頭一文字だけを小文字化して生成するxsi:type名 (FloatType→floatType)に対しては判定が常に一致せず、bareな scalar/list型(float/double/decimal/int/long/short/byte/boolean/ dateTime/uuid/hexBinary/base64Binary)の実測値がloads()後もPython型 へ変換されず文字列のまま返っていた不具合を修正。判定をxsi_type.lower()同士の比較に変更し、大文字小文字の位置に依存しない ようにした(Content*/unsigned*接頭辞を持つ型はクラス名中の位置が ずれていたため偶然影響を受けていなかった)。- 上記2件の回帰防止テストを
tests/test_serialization.pyに追加 (test_uncertainty_round_trips_through_dumps_and_loads、test_bare_scalar_types_round_trip_with_correct_python_type)。 ユーザー提供の実データファイル6件(ESEMstandard.1/.2、FSEMgold.2、 DAFMgoldcorrected.1/.2、CCORRELATION)をpymaiml.builders/pymaiml.serializationのみで再構築するテストを通じて発見した 不具合(特に後者はCCORRELATION.maimlの<uncertainty>付き measurement値で顕在化した)。 pymaiml.serialization:documentにSignatureを持つファイルをloads()→dumps(..., extra_namespaces=loaded.namespaces)という、LoadedMaiml.namespacesのdocstringが案内する手順どおりに再書き出しする と、ルート<maiml>要素に同じxmlns:nsN宣言が二重に現れxml.parsers.expat.ExpatError: duplicate attributeで失敗していた 不具合を修正。<Signature>/<EncryptedData>はET.fromstring()で 読み直してそのまま木に追加する生XMLであり、そこで実際に使われている 名前空間をxml.etree.ElementTreeが木全体走査で発見してルート要素に 自動でxmlns:nsN宣言を追加する一方、こちらがextra_namespacesから 設定した同名のリテラル属性の存在をElementTreeは関知しないため、両者が 同じプレフィックスを使うと二重宣言になっていた。dumps()に_dedupe_root_namespace_decls()を追加し、ルート要素上で同名・同値の 宣言が重複したときは1つにまとめ、同名で値が異なる(真の競合)場合は 分かりやすいValueErrorを送出するようにした。pymaiml.serialization:<description>/<format>のようなxs:string minOccurs="0"の要素が、存在するが空(<description/>)の 場合と要素そのものが存在しない場合を区別できず、どちらもNoneとして 読み込まれ、往復後に要素が消えていた不具合を修正 (value側は空文字列として正しく保たれており、ライブラリ内で挙動が 不統一だった)。_text_of()と、独自に同じ判定を行っていた_read_property_or_content()/_read_insertion()のそれぞれで、 「子要素が存在しない」場合のみNoneを返し、存在する場合は.text or ""で空文字列を返すよう統一した。- README.md: 「ローカルでMaiML-Domainと同時に開発する場合」の手順を
記載どおりに実行すると、
pip install -e ../MaiML-Domainで入れた editable版が、続くpip install -e ".[dev]"によってエラーなく アンインストールされ、git+...@v0.1.0タグ由来の固定版に静かに 差し替わってしまう(Domain側を編集しても反映されない状態に気づけない) 不具合を修正。dependenciesのdirect URL指定(maiml-domain @ git+...)がある限りpip install -e ".[dev]"は毎回このタグ版を 再インストールするため、pip install --no-deps -e .を使い、[dev]の依存(lxml/pytest)は個別にインストールする手順に修正した。 - 上記のうち
pymaiml.serialization側2件について、回帰防止テストをtests/test_serialization.pyに追加 (test_signature_round_trips_without_duplicate_namespace_error、test_dumps_rejects_genuinely_conflicting_root_namespace_declaration、test_empty_property_value_and_description_round_trip_as_empty_string、test_absent_property_description_still_round_trips_as_none、test_empty_insertion_format_round_trips_as_empty_string、test_absent_insertion_format_still_round_trips_as_none)。 いずれも外部レビュー(2026-09-07、PyMaiML690edd6/MaiML-Domainbe11e5c時点)で報告された「要修正」所見3件の再現コードに基づく。 pymaiml.serialization:units/formatString/scaleFactor属性を 持つ<property>/<content>をloads()する際、書き込み側はhasattr(obj, "units")等で対応クラスかどうかを確認しているのに対し、 読み込み側は無条件にkwargsへ積んでいたため、対応しないxsi:type (例:stringTypeにunits)だと生のTypeError(__init__() got an unexpected keyword argument 'units')がそのまま呼び出し側に漏れて いた不具合を修正。_read_property_or_content()でinspect.signature(cls.__init__).parametersを使って対象クラスが そのパラメータを受け付けるか事前に確認し、受け付けない場合はpymaiml.validation.validate()の利用を促す分かりやすいValueErrorを送出するようにした。回帰防止テストを2件追加 (test_units_on_a_class_that_does_not_accept_it_raises_a_clear_error、test_units_formatstring_scalefactor_on_a_class_that_accepts_them_still_work)。pymaiml.builders.infer_property()/infer_content():values=の homogeneous判定が実際にはvalues[0]しか見ておらず、例えばinfer_property("ex:value", values=[1, 2, "abc"])のような型の 混在したリストでもエラーにならずIntListTypeが選ばれ、"abc"が そのまま紛れ込んでいた不具合を修正(docstringには元々 "must be non-empty and homogeneous"と明記されていたが、実装がそれを 満たしていなかった)。_infer_homogeneous_list_class()を追加し、 values[0]から選んだクラスに、残りの全要素も_match()と同じ規則で 一致するかを確認するようにした。「Pythonのclassが完全一致」ではなく 「選択されたMaiML型に(同じ推定テーブル上で)変換可能か」で判定して いるため、bytes/bytearray混在(どちらもBase64Binary*ListType)は 引き続き許可されるが、bool/int混在(boolはintのサブクラスだが 別のMaiML型BooleanListType/IntListTypeに対応するため)は拒否される。 回帰防止テストをtests/test_builders.pyに5件追加 (test_infer_property_rejects_heterogeneous_values、test_infer_content_rejects_heterogeneous_values、test_infer_property_rejects_bool_mixed_with_int、test_infer_property_accepts_bytes_and_bytearray_together、test_infer_property_heterogeneous_error_names_the_offending_element)。 外部レビューで報告された所見。pymaiml.builders.IdFactory.new_id(): 生成されるidは常にprefix + 連番の 整数であるにもかかわらず、prefix自体がxs:ID(NCName)として妥当かを 一切確認していなかったため、ids.new_id("123")のように数字始まりのprefixを渡すと"1231"のようなxs:ID違反のidをそのまま生成できてしまう 問題を修正。_is_valid_ncname()を追加し、new_id()が各prefixを (そのprefixで最初に呼ばれた時点で1回だけ)検証、数字始まり・:を含む・ 空文字列など、NCNameとして不正なprefixは分かりやすいValueErrorで 即座に拒否するようにした(IdFactoryのdocstringにdoctest例を追記)。 回帰防止テストをtests/test_builders.pyに7件追加 (test_new_id_rejects_prefix_that_would_not_be_a_valid_xs_id×5パターン、test_new_id_accepts_a_prefix_that_is_itself_a_valid_xs_id×6パターン、test_new_id_rejects_bad_prefix_even_after_a_good_prefix_was_already_used、test_new_id_only_validates_a_prefix_once_per_factory)。 外部レビューで報告された所見。
Changed¶
- README.md:
pymaiml.serializationの節に既知の制限を2点追記。 (1)loads()はスキーマ妥当な入力のみを対象としており、基数 (minOccurs)違反のファイルはmaiml_domain側のValueErrorで読み込みが 止まるため、診断・修復用途にはpymaiml.validation.validate()を先に 使うべきこと。(2)documentにSignatureを持つファイルをloads()→dumps()で往復させるとdumps()のpretty-print整形により署名対象の バイト列が変わり、他の準拠実装が付与した署名は無効化されること (pymaiml自身が書いた署名を読み直す場合はC14N出力が一致するため 影響を受けない)。 - README.md:
pymaiml.validationの節に、同梱XSD(pymaiml/schema/ MaiML-Schema-1_0/)のうち5本(maiml.xsd/maiml-core.xsd/maiml-document.xsd/maiml-property.xsd/xenc-schema.xsd)が公式 配布版に無いxs:import(xmldsig/xmlenc名前空間)を追記した改変版 であることを明記。公式配布版はこれらのimportを欠いておりlxmlで スキーマオブジェクトを構築できないための実務的な補完であることと、maiml-schema-validatorスキルのreference/側にも同じ差分を適用した コピーを保持していることを併記。 以上4件は外部レビュー(2026-09-07、PyMaiML690edd6/MaiML-Domainbe11e5c時点)の「要検討」所見のうち、コード修正が妥当と判断した 1件(units/formatString/scaleFactor)と、設計上の割り切りとして ドキュメント化に留めるのが妥当と判断した3件(署名の再整形、 loads()の対象範囲、同梱XSDの差分)への対応。 pyproject.tomlのdependencies(maiml-domain @ git+...@v0.2.0という direct reference)の上に、PyPI公開時にはこの形式のまま使えない旨と、 公開後に書き換えるべき形(maiml-domain>=0.2,<0.3)をコメントで併記。 PyPAの仕様上、public index serverはアップロードされたdistributionの 依存関係にdirect referenceを含めることを許可すべきではないとされて おり、実際PyPIへのアップロードもこの形式のままでは拒否される。 開発段階の現在はタグ固定のgit依存(main追従より安全)のままで問題 ないため、dependencies自体は変更していない。CONTRIBUTING.mdに 「PyPI公開前の対応」節を新設し、(1) MaiML-Domainを先にPyPI公開、 (2)dependenciesをバージョン範囲指定へ書き換え、(3)pymaiml自体を PyPI公開、という順序を明文化した。外部レビューで報告された所見。
Security¶
- 信頼できない(未検証の)MaiML XMLを解析するlxmlパーサーに
resolve_entities=Falseとno_network=Trueを明示するハードニングを 実施(新モジュールpymaiml._xml_security.make_untrusted_input_parser())。 従来pymaiml.validation._xsd_validate()はetree.XMLParser(remove_blank_ text=False)のみで、lxmlの既定値(resolve_entities=True、no_network=False)に依存していたため、DOCTYPEで宣言した外部 エンティティ(例:<!ENTITY xxe SYSTEM "file:///etc/passwd">)や ネットワーク越しの外部リソース参照が解決されうる状態だった(いわゆる XXE/entity-expansion脆弱性)。現状validate()はローカルファイルしか 扱わないためすぐに悪用可能というわけではないが、pymaiml.validationが将来APIやアップロードファイルなど未検証の入力を扱う可能性を見込み、 「このライブラリは外部エンティティ・ネットワークリソースを一切解決 しない」という方針をコードで明示的に固定した。同様に未検証入力を 読むpymaiml.serialization.loads()(_lxml_etree.fromstring(data)、 従来パーサー未指定=lxml既定値)も同じmake_untrusted_input_parser()を 使うよう変更。 - 一方、同梱の信頼済みXSDスキーマ本体を読み込む
pymaiml.validation._load_schema()(etree.parse(str(maiml_xsd)))は 意図的に上記のハードニング済みパーサーを共有せず、従来どおりの パーサーのまま維持した。こちらはスキーマファイル間のxs:import/xs:include(ローカルファイルパスによる相互参照)を解決する必要が あり、未検証のMaiML入力とは信頼レベルが異なるため。両者を意図的に 別々の、名前の付いたコード経路として分離しておくことで、一方への 変更がもう一方へ静かに波及することを防ぐ設計とした。 - 上記の変更に伴い、
pymaiml.validation._xsd_validate()でschema.validate(doc)がlxml.etree.XMLSchemaValidateError(internal error)を送出するケースを新たに捕捉するよう修正。resolve_entities=Falseにより未解決のまま残ったDOCTYPE由来の エンティティ参照ノードを含む木は、libxml2のスキーマバリデータが 正常に走査できず内部エラー例外を送出することがある(実際に 回帰テストで発生を確認)。これを捕捉せずに伝播させるとvalidate()が例外で落ちてしまうため、通常のXSD違反と同様にFinding(code="XSD-02")として報告するよう変更した。 - 回帰テスト
tests/test_xml_security.pyを追加。ハードニング済み パーサーが外部ファイルエンティティを展開しないこと・ネットワーク リソースへアクセスしようとしないことを直接確認するテスト、validate()/loads()がそのような入力に対して例外を送出したり 秘匿情報をエラーメッセージ経由で漏らしたりしないことを確認する テスト、通常の正当なMaiMLファイルの読み込み・検証がハードニング後も 引き続き成功することを確認するテスト、_load_schema()が引き続き ローカルXSD間のxs:import/xs:includeを解決できることを確認する テストを含む。ユーザー提案。 pymaiml._xsi_registry: モジュールレベルのXSI_TYPE_TO_CLASS/CLASS_TO_XSI_TYPE初期化が、_build_registries()(maiml_domain. property.__all__のリフレクション)を2回呼び出していた(それぞれ.update(_build_registries()[0])/[1]という書き方だったため)重複を 解消。XSI_TYPE_TO_CLASS, CLASS_TO_XSI_TYPE = _build_registries()と 1回の呼び出しで両方を受け取る形に変更。結果は元々同じ辞書の内容に なるため動作上のバグではなく、インポート時に無駄なリフレクション処理を もう一度実行していた分のコードの明快さの改善。ユーザー指摘。
Changed(破壊的変更)¶
pymaiml.serialization.dumps()/dump()は、既存のdocument.signature(<Signature>)を常に出力から除外するよう変更しました。drop_stale_signature=引数(署名以外の内容が変わっていなければ署名を 維持する、変わっていれば省く)は廃止し、除外に条件分岐はなくなりました。 これは本ファイル冒頭の### Addedに記載した、drop_stale_signature=引数の追加エントリを置き換える変更です。 理由は「署名以外が変わっていなければ安全に維持できる」という前提 そのものがpymaimlの立場からは保証できないと判断したためです。MaiMLの<Signature>はJIS X 5093 / ETSI TS 101 903(XAdES)準拠のenveloped 署名であり、Digestは署名時点の厳密なバイト列に対して計算されます。dumps()はmaiml_domainのオブジェクトツリーからXMLを再構築する際、 インデント・namespace宣言位置・属性順序・空要素表現などを含めて 出力を作り直すため、内容(document.signature以外のフィールド)が 一切変わっていなくても、署名時点の厳密なバイト列を再現できるとは 保証できません。pymaimlは署名の生成・検証そのものを実装しておらず (CONTRIBUTING.mdの「XML Signature(電子署名)の扱い」節を新設し、 明文化しました)、この「バイト列が変わっていないかどうか」を判断する 資格自体がpymaimlにはない、という整理です。 影響:pymaiml.serialization.loads()は<Signature>を読み込み、DocumentType.signatureに文字列として保持する動作(検証等への 受け渡し用)は変更していません。変わるのは書き出し側のみで、 「署名済みファイルをloads()→(無編集で)dumps()しても、出力に<Signature>は含まれない」という点が、以前(署名以外に変更が無ければ 維持されていた)から変わります。署名済みファイルが必要な場合は、dumps()/dump()で内容を確定させた後に、その出力バイト列へ 外部の署名ツールで改めて署名してください。pymaimlへ将来 署名対応を追加する場合も、pymaiml.serializationとは独立した モジュール(例:pymaiml.signature)に分離することをCONTRIBUTING.mdで推奨事項として明記しました。 内部実装としては、_write_document()からsuppress_signature引数と シグネチャ書き込み分岐そのものを削除、_build_maiml_element()からも 同引数を削除、_content_changed_since_snapshot()(スナップショットとの 差分検出)を削除、LoadedMaiml._snapshot(ロード時copy.deepcopy()) を削除しました。回帰テストはtests/test_serialization.pyの drop_stale_signature系4件を、常時除外の挙動を確認する4件 (test_dumps_never_writes_a_document_signature、test_loads_still_reads_a_signature_dumps_never_wrote、test_dumps_drops_a_loaded_signature_even_with_no_further_edits、test_dumps_no_longer_accepts_drop_stale_signature)に置き換えました。 また、既存のtest_signature_round_trips_without_duplicate_namespace_ error(<Signature>往復時の名前空間重複バグの回帰テスト)は、dumps()がもう<Signature>を書き戻さないため前提が崩れたので、 同じ_dedupe_root_namespace_decls()の保護を<EncryptedData>(_write_encryption()が同様に埋め込みXML断片をそのまま追記する経路) で検証するtest_encrypted_data_round_trips_without_duplicate_ namespace_errorに置き換えました。ユーザー指摘・提案。
Added¶
- README.md(
pymaiml.serialization節)とpymaiml/serialization.pyの モジュールdocstring(Known limitations)に、XMLコメント (<!-- ... -->)・処理命令(<?...?>)がload/dumpの往復で保持されない ことを明記。現在のloads()は対象のXSD要素だけを明示的に拾ってmaiml_domainオブジェクトへ変換する設計で、コメント・処理命令は そもそもモデル化していないため、loads()→dumps()で往復させると 元のファイルにあったコメント・処理命令は失われる(XMLとして完全に losslessなround-tripは保証しない)。これは単なる開発者向けメモの 消失には留まらない。コメントが「データの一部を意図的に省略している」 といった、それ自体が意味を持つ情報を担っている場合、その情報ごと 失われる点を明示的に注意喚起する。ユーザー指摘。 pymaiml.queryモジュールを新設。serialization/validation/buildersとは独立した、読み取り専用の「ファイル内の一覧を取得する」 ユーティリティ群として、以下4関数を提供します。get_uuids(xml_text)--<uuid>要素のテキストを一覧取得 (出現箇所の種類を問わない: オブジェクトの識別uuid、insertion自身のuuid、chain/parentのuuidをすべて含む)。重複除去はしない (下記参照)。get_keys(xml_text)--key=属性値を一覧取得 (<property>/<content>/<chain>/<parent>のいずれも対象)get_namespaces(xml_text)-- 宣言されているカスタム名前空間を{接頭辞: URI}のdictで取得(デフォルト名前空間とxsi:は除外)get_insertion_uris(xml_text)--<insertion>/<uri>のテキストを 一覧取得(外部ファイル参照のURI)
リスト系関数はいずれも出現順を保持しますが、重複の扱いはget_uuids()
だけ異なります。get_keys()/get_insertion_uris()は出現順を保持しつつ
重複を除去します(dict.fromkeys()による先頭優先の重複除去)。
get_namespaces()はdictを返すため、キーの挿入順がそのまま出現順に
なります。一方get_uuids()は重複を除去せず、出現した<uuid>要素の
テキストを全件そのまま返します(ユーザー指摘により変更)。uuidは本来
オブジェクトを一意に識別するためのものであり、同じ値が複数回出現する
こと自体が検出したい事実になり得るためです。重複除去した一覧が必要な
場合は呼び出し側でset(...)やlist(dict.fromkeys(...))を使うことを
想定しています。
設計上の要点は次のとおりです。
- pymaiml.serialization.loads()を経由しません。loads()はスキーマ
妥当な入力のみを対象とし、maiml_domainオブジェクトツリーの構築を
要求しますが、pymaiml.queryは生のXMLを直接(lxml.etreeの
.iter()による汎用的なタグ名/属性名の走査で)読むため、まだ
スキーマ検証していないファイルに対する軽量な下調べとしても使えます。
- get_namespaces()は、LoadedMaiml.namespaces(ルート<maiml>要素
のみを見る)とは異なり、木全体を走査します。pymaiml自身の
dumps()出力であればxml.etree.ElementTreeのシリアライザが
使用中の名前空間をすべてルートへ引き上げるためLoadedMaiml.
namespacesでも十分ですが、pymaimlのdumps()を経由していない
外部生成ファイル(例: ルート以外の要素でxmlns:dsを宣言したまま
署名されたファイル -- dumps()は署名を書き出さないため、
署名付きファイルは必然的に外部由来です)では、名前空間がルート以外
の要素に宣言されている可能性があり、get_namespaces()はそのケースも
正しく検出します。同じ接頭辞に異なるURIが束縛されている場合は
ValueErrorを送出します(_dedupe_root_namespace_decls()の
「サイレントに片方を選ばず失敗する」という既存方針を踏襲)。
- 4関数とも、pymaiml.serialization.loads()と同じ
pymaiml._xml_security.make_untrusted_input_parser()(XXE/
entity-expansion/networkハードニング済み)でXMLを解析します。
「このファイルに何が入っているか一覧を取る」という用途は、
未検証・未信頼な入力に対してまさに使われがちな操作であるため。
pymaiml/__init__.pyのモジュール概要とREADME.mdの「モジュール構成」に
追記し、tests/test_query.py(13件、上記の重複除去・走査範囲・
エラー送出・XXEハードニングをそれぞれ検証)を追加しました。
ユーザー要望・設計指定。
- pymaiml.queryを、生のXMLを直接走査する独立実装から
maiml_domainベースの実装へ書き換え(破壊的変更)。 上記のエントリで
記述した「serialization.loads()を経由しない」という設計は撤回します。
get_uuids()/get_keys()/get_insertion_uris()は、いまは
pymaiml.serialization.loads(xml_text)を呼び出し、その結果の
maiml_domainオブジェクトツリー(loaded.root)から値を読み取ります。
理由は、「このファイルに何が入っているか」というPython向けの見え方は、
このプロジェクトのPython系ツールが共有する唯一の正しいドメインモデルで
あるmaiml_domain経由で提供すべきであり、同じタグ/属性名を偶然一致
させているだけの独立したXML走査実装をもう1つ持つべきではない、という
より強いルールに従うためです。
この変更の直接的な結果として、以下の3点があります。
- xml_textはスキーマ妥当なMaiMLでなければなりません。
maiml_domainのコンストラクタが要求するカーディナリティを満たさない
入力に対しては、loads()と同じ例外(多くはValueError、整形式でない
XMLの場合はlxmlのパースエラー)がそのまま送出されます。以前この
3関数がサポートしていた「スキーマ検証前のファイルに対する軽量な
下調べ」という用途は、もう使えません(先にvalidate()または
load()してください)。
- この3関数のXXE/entity-expansion/networkハードニングは、内部で
呼び出すserialization.loads()のものがそのまま適用されます。この
3関数自体はもうXMLを直接パースしないため、独自に守るべきハードニング
経路自体が存在しません。
- URIを持たない<insertion>という不正な形は、もうget_insertion_uris()
に到達し得ません。InsertionType.__post_init__が空/欠落したuriを
拒否するため、そのようなinsertionはそもそもmaiml_domainオブジェクト
として構築できないからです(以前の実装が持っていた、URIなしの
insertionをスキップする防御的な分岐は不要になり、削除しました)。
get_namespaces()だけは例外として、今も生XMLをlxml.etreeで直接
解析します。名前空間宣言はXMLレベルの概念であり、maiml_domainの
どのクラスにも保持されていない(serializationの「既知の制限」参照:
dumps()はextra_namespaces=から再構築するだけで、maiml_domain側は
一切名前空間を持たない)ため、読み替えるべきオブジェクトツリー上の
情報がそもそも存在しないためです。
内部実装として、_iter_domain_objects()という汎用ヘルパーを追加
しました。maiml_domainの約30の構造クラス・約50のproperty/content
リーフクラスを個別に分岐せず、vars(obj)を各クラスの__init__が
属性を代入した順序のまま再帰的に辿ることで、uuid/key属性を持つ
オブジェクトやInsertionTypeインスタンスを汎用的に収集します。これに
よりmaiml_domainが将来クラスを追加してもpymaiml.query側の追随が
不要になります(循環参照に備えたid()ベースの訪問済みガード付き。
ただしmaiml_domain自身が循環するオブジェクトグラフを生成することは
ありません)。
README.mdのpymaiml.query節、pymaiml/__init__.pyのモジュール概要、
tests/test_query.py(既存の生XMLスニペットを使うテストを、
minimal_rootフィクスチャを土台にした実オブジェクトツリー経由のテストへ
全面的に置き換え。「URIなしのinsertion」テストは、その形自体が
maiml_domainでは構築不能になったため削除し、代わりに
InsertionTypeがuriを必須とすることを検証するテストを追加)を
更新しました。ユーザー要望(「MaiML-Domainを使用するというルールの
もと、コードを修正して」)。
- pymaiml.queryにget_templates()/get_instances()を追加。
上記4関数(文字列のフラットな一覧を返す)とは別の、もう1つの関数群です。
こちらは文字列ではなくmaiml_domainのオブジェクトそのものを返し、
キーワード引数でフィルタする設計にしました(ユーザーが選択: 「オブジェクト
返却+キーワード引数フィルタ」。他候補として、機能ごとに個別関数を並べる
設計、fluentなクエリビルダ設計も提示しましたが、「全て取得・idだけ取得・
instructionに紐づくものを取得…など機能を増やせるようにしたい」という
要望に対し、新しい絞り込み軸をキーワード引数の追加だけで拡張できる
この設計が最も適すると判断されました)。
- get_templates(xml_text, *, kind=None) -- material/condition/
resultTemplateそのもの(MaterialTemplateType/ConditionTemplateType/
ResultTemplateType)を返します。kindは"material"/"condition"/
"result"のいずれか(未指定・Noneなら3種類まとめて)。
- get_instances(xml_text, *, kind=None, instruction_id=None) --
material/condition/resultそのもの(MaterialType/ConditionType/
ResultType)を返します。kindはget_templates()と同様。
instruction_idを指定すると、その<instruction id=...>に紐づく
インスタンスだけに絞り込みます。
「idだけ取得」は専用関数を設けず、返ってきたオブジェクトから呼び出し側が
.idを読むだけで済む設計です([t.id for t in get_templates(xml_text)])。
kindに"material"/"condition"/"result"以外を渡すとValueError。
instruction_idの解決は、MaiMLスキーマがinstructionからインスタンスへ
辿れる唯一の経路である<instruction> → (refで参照する)<event> →
<event>のresults_refs → <results> → <results>の
materials/conditions/resultsという連鎖をそのまま辿ります
(maiml_domain.event_log.EventType.ref/results_refs、
maiml_domain.data.ResultsType参照)。instructionからテンプレートへの
直接的な参照はスキーマ上存在せず、PNMLのplace/transition/arcトポロジー
経由の間接的なものにとどまるため、get_templates()には
instruction_id=フィルタを設けていません。
instruction_idに、ファイル内のどの<instruction>のidとも一致しない
値を渡すとValueErrorを送出します(タイプミスを黙って[]にせず検出する
ため)。一方、instruction_id自体は実在するインスタンスがまだ何も
紐づいていない場合は、これとは区別してエラーにせず[]を返します。
protocolFileRootType(<data>/<eventLog>を持たない手法単体ファイル
-- maiml_domain.root.ProtocolFileRootType)には見つけられる
インスタンスがそもそも存在しないため、get_instances()は
kind/instruction_idによらず常に[]を返します。実在する
instruction_idを渡した場合もエラーにはなりません
(<instruction>自体は存在するため)。単に紐づく<event>が
1つも無いだけです。
両関数とも_iter_domain_objects()(上記エントリで追加した汎用の木構造
走査ヘルパー)をそのまま再利用しており、pymaiml.query内に新しい
走査ロジックは追加していません。
pymaiml/query.pyの__all__とモジュールdocstring、README.mdの
pymaiml.query節、pymaiml/__init__.pyのモジュール概要を更新し、
tests/test_query.pyに14件のテスト
(kind=によるフィルタ・未知のkindでのValueError・
instruction_idによる絞り込みとそのevent→results_refs連鎖・
未知のinstruction_idでのValueError・実在するが何も紐づいていない
instruction_idでの[]・protocolFileRootTypeでの挙動)を追加しました。
ユーザー要望(「次は、template一覧、インスタンス一覧を取得する機能を
つけたい。全て取得・idだけ取得・instructionに紐づくものを取得、、、
など機能を増やせるようにしたい。」)。
- get_templates()にもinstruction_id=を追加し、get_instances()の
instruction_id=をPNMLトポロジー経由の経路と合流させました。 直前の
エントリで追加したget_instances(xml_text, *, kind=None,
instruction_id=None)は、instructionからインスタンスへの経路として
event連鎖(instruction → event → results_refs → results →
materials/conditions/results)のみを実装していましたが、それとは
独立したもう1つの経路 -- instruction → transitionRef → transition
→ arc → place → テンプレートのplaceRef -- による絞り込みを追加
してほしいというユーザー要望を受け、以下のとおり変更しました。
- get_templates(xml_text, *, kind=None, instruction_id=None) --
instruction_idを新設。指定した<instruction id=...>の
transitionRefが指す<transition>に触れる<arc>から、その
もう一方の<place>をplaceRefで指すテンプレートまでを辿って
絞り込みます(内部ヘルパー_templates_linked_to_instruction())。
- get_instances()のinstruction_id=は、上記のテンプレート絞り込みに
さらにもう1段(テンプレートをrefで指すインスタンスまで)進めた経路
と、既存のevent連鎖経路の和集合を返すよう変更しました
(どちらの経路からも見つかるインスタンスは1回だけ列挙されます)。
この2つの経路は独立して意味を持ちます。event連鎖は「このinstructionの 実行が実際に記録したインスタンスは何か」、PNMLトポロジー経路は 「このinstructionの遷移が配線上どのテンプレート(のインスタンス)に 繋がっているか」を答えるもので、一方が他方の部分集合とは限りません (テンプレートは配線上繋がっているが、まだ1件もeventが記録していない こともあれば、逆に配線を見ただけでは分からない対応がevent側に記録 されていることもあります)。
ArcType.source/targetはplace/transitionのどちらのidも取り得るため
(maiml_domain.pnml参照)、内部実装ではinstructionのtransition群と
一致する側をarcの両端どちらからでも検出し、もう一方をplace候補として
扱います。
instruction_idの存在チェック(未知のidならValueError、実在するが
何も見つからない場合は[])は両関数・両経路を通じて一貫しています。
protocolFileRootType(手法単体ファイル、<data>/<eventLog>なし)
でも、PNMLトポロジー経路自体は<protocol>側だけで完結するため
get_templates(instruction_id=...)は通常どおり動作するようになり
ました。get_instances()はテンプレートまでは辿れても実インスタンスが
存在しないため、この場合も常に[]のままです。
pymaiml/query.pyのモジュールdocstring・両関数のdocstringを更新し、
tests/test_query.pyに、PNMLトポロジー単体での絞り込み・kindとの
組み合わせ・未配線instructionでの[]・未知のinstruction_idでの
ValueError・protocolFileRootTypeでの動作・event経路とPNML経路の
和集合が重複しないことの確認・両経路が互いに見つけられないものを
それぞれ見つけられることの確認、を検証するテストを追加しました
(README.mdのpymaiml.query節、pymaiml/__init__.pyのモジュール
概要も更新)。ユーザー要望(「instruction→transitionRef→transition→
place→template→インスタンスという経路で絞り込んでください」
「templateの場合も同様に」)。
- レビュー指摘を受け、_templates_linked_to_instruction()のPNML
トポロジー解決を、id文字列同士の一致だけでなく実在するmaiml_domain
オブジェクトの確認を挟むように修正しました。 また
get_templates()/get_instances()の戻り値型をList[object]から、
具体的なTemplate/Instance型エイリアスへ変更し、pymaiml.queryの
モジュールdocstringを現在の仕様中心に整理しました。レビュー文書
PyMaiML_query_review.md(3項目、優先度順)への対応です。
- (優先度: 高) これまでの実装は、instruction.transition_refsの
refとArcType.source/targetの文字列を直接照合し、一致した反対側
のIDをそのままplace_idsとして扱っていました。maiml_domain自体は
IDREFの解決可能性を保証しない(MaiML-Schema-1_0のXSDのような強制が
ない)ため、実在しないTransitionType/PlaceTypeを指す偶然の文字列
一致だけでもテンプレートまで辿り着いてしまう可能性がありました。
修正後は、instruction.transition_refsが指すIDのうち実在する
TransitionType.idのみを対象にし、<arc>の反対側から得たIDも実在
するPlaceType.idのみを対象にしてから、初めてテンプレートの
place_refsと照合するようにしました(all_objsに実際に存在する
オブジェクトの集合と&を取る形)。「Domainを正として問い合わせる」
というPyMaiMLの設計方針に一致させるための変更で、get_templates()/
get_instances()の両方のPNML経路に影響します。
tests/test_query.pyに、実在しないTransitionTypeへの偶然のID
一致だけでは何も見つからないことを検証する回帰テストを追加しました
(_templates_linked_to_instruction()を直接、手作りのオブジェクトで
呼び出すテスト -- maiml_domain自身のコンストラクタではこの
ぶら下がった参照形状を通常再現できないため)。
- (優先度: 中) get_templates()/get_instances()の戻り値型を
List[object]から、新設した型エイリアスTemplate = Union[
MaterialTemplateType, ConditionTemplateType, ResultTemplateType]・
Instance = Union[MaterialType, ConditionType, ResultType]
(pymaiml.queryから__all__経由でエクスポート)を使った
List[Template]/List[Instance]へ変更しました。動作は変わりません
が、公開SDK APIとしてIDEの補完や静的型チェックが効くようになります。
軽量なTemplateInfo/InstanceInfoのような別DTOは新設せず、
maiml_domainオブジェクトをそのまま返す既存方針は維持しています。
- (優先度: 低) pymaiml/query.pyのモジュールdocstringを、
旧raw XML実装からの変更経緯(なぜ書き換えたか、以前の実装との比較)
を中心とした説明から、現在の6関数の役割・入力・戻り値・主要な
filter条件・意味論を中心とした説明に整理しました。変更経緯自体は
削除せず、本CHANGELOGを参照するよう一文だけ残しています。
pymaiml.buildersにTemplate→Instance変換機能create_instance()/create_instances()/InsertionValueを追加しました。materialTemplate/conditionTemplate/resultTemplate(maiml_domainのMaterialTemplateType/ConditionTemplateType/ResultTemplateType、 典型的にはpymaiml.query.get_templates()の戻り値)から、対応するmaterial/condition/resultインスタンス(MaterialType/ConditionType/ResultType)を組み立てます。設計文書PyMaiML_template_to_instance_design.mdへの対応です。
query.get_templates() -- どのTemplateを対象にするか選ぶ
↓
builders.create_instance(s)() -- 選ばれたTemplateを実体化する
↓
maiml_domain Instance object
query(選択)とbuilders(組み立て)の責務分離に沿って、
pymaiml.builders側に配置しています(pymaiml.queryには置いていません)。
create_instance(template, *, id, id_factory, template_instance_map=None, insertion_values=None)-- 1つのTemplateから1つのInstanceを生成する 下位プリミティブ。Templateの実型(MaterialTemplateType等)から対応する Instance型を自動判定するため、呼び出し側がTemplateの種類ごとに別の 関数を呼ぶ必要はありません。create_instances(templates, *, id_factory, insertion_values=None, existing_instance_map=None)-- 複数Templateを一括でInstance化する 上位関数。templatesと同じ順序でlist[Instance]を返します。
引き継ぐ内容・引き継がない内容は以下のとおりです。
template.id→instance.ref(Instanceが「どのTemplateの実体か」を 表す方法そのもの)。- Instance自身の
id(呼び出し側/create_instances()が新規採番)とcontent.uuid(id_factoryで新規採番)は、Templateの値を流用せず 常に新規生成します。 content.name/description/annotationはそのままコピーします (immutableな文字列のため)。content.properties/content.contents、および(排他的に設定されて いる場合の)content.encryptionはcopy.deepcopy()します。生成後に Instance側を変更してもTemplate側が意図せず変更されないようにする ためです。content.insertionsはそのままコピーしません。各insertionは Instance用の新しいInsertionTypeとして再生成し、uri/hashは 呼び出し側がInsertionValue(uri/hash必須、uuid/formatは 省略可)で新しい値を指定します(uuid省略時はid_factoryで新規 生成、format省略時はTemplate側のinsertionのformatを継承)。 対応するInsertionValueが無いinsertionが存在する場合はValueErrorになります。template.template_refs(TemplateRefTypeのリスト)はinstance.instance_refs(InstanceRefTypeのリスト)へ変換します。 変換にはtemplate_instance_map(Template ID→Instance IDの対応表)を 使い、Template IDの文字列をそのままinstanceRefにコピーすることは しません(Template IDとInstance IDは別の値であり、黙ってコピーすると 意味的に不正な参照になり得るため)。対応表に無いtemplateRefはValueErrorになります。template.place_refsはコピーしません(MaterialType/ConditionType/ResultTypeにはそもそもplace_refsという属性自体が存在しません)。
template_instance_mapの構築はcreate_instance()自身の責務ではなく、
create_instances()が担います。理由は、あるTemplateをInstance化して
いる時点では、そのTemplateがtemplateRefで参照している別のTemplateの
Instance IDがまだ決まっていない可能性があるためです。そのため
create_instances()は次の2段階で処理します。
templatesに含まれる全Templateについて、先に(どのInstanceも 組み立てる前に)Instance IDをid_factoryで採番する (material/condition/resultプレフィックスはTemplateの種類に 応じて自動選択)。同じTemplate IDがtemplates内に2回以上現れた 場合はValueErrorにします。template_instance_map = {**(existing_instance_map or {}), **上記で採番したid}を組み立ててから、Template1つにつき1回create_instance()を呼び出す。
この順序により、同じバッチ内で後方(リストの後ろ)にあるTemplateを
参照するtemplateRefも正しく解決できます。existing_instance_mapは、
今回のバッチ外(例えば以前の別呼び出しで既にInstance化済み)の
Templateへの参照を解決するために渡せる任意の対応表で、同じTemplate
IDが両方に存在する場合は今回のバッチ側の採番が優先されます。
templateRef/instanceRefの対応関係について: MaiML-Schema-1_0は
templateRef(親がmaterialTemplate)を「同じmaterialTemplate」の
参照として定義していますが、これは「同じ種類(同種)のTemplateを指す」
という型の制約であり、「親Template自身を指す」という自己参照の意味
ではありません。したがって、Template AのtemplateRefがTemplate Bを
指し、Template A/BをそれぞれInstance化するとInstance Aの
instanceRefはInstance Bを指す、というTemplate間参照が正常なケース
として扱われます。
tests/test_builders.pyに、Template種類ごとのInstance型自動判定、
id/uuid新規生成、name/description/annotationのコピー、
properties/contentsのdeep copyによる分離、encryption排他ケース、
insertionの再生成(新uri/hash、format継承、uuid省略時の新規生成、
対応するInsertionValueが無い場合のValueError)、
templateRef→instanceRef変換(対応表による解決、対応が無い場合の
ValueError)、create_instances()の2段階処理(バッチ内で後方の
Templateへの前方参照の解決、existing_instance_mapによるバッチ外
参照の解決、バッチ内の採番がexisting_instance_mapより優先されること、
同一Template IDの重複検出)を検証するテストを追加しました。
ユーザー要望(設計文書PyMaiML_template_to_instance_design.mdの
「この実装をお願いします」)への対応です。
pymaiml.builders.create_instance()/create_instances()のinsertion_values引数を、Template側insertionの旧uriをキーにしたMappingから、template.content.insertionsと同じ順序で対応付けるSequence[InsertionValue]へ変更しました。 設計修正案PyMaiML_insertion_mapping_revision.mdへの対応です。
変更前はinsertion_valuesを{旧uri: InsertionValue(...), ...}という
辞書として渡し、内部でinsertion_values.get(old.uri)のように
Template側InsertionType.uriをキーとして対応するInstance用の新しい
uri/hashを探していました。しかしMaiML-Schema-1_0は、1つの汎用
データコンテナ内に複数のinsertionが存在する場合でもuriの一意性を
一切保証していません(genericDataContainerGroupのスキーマ定義上、
同じuriを持つinsertionが2つ以上存在することを妨げるものはあり
ません)。InsertionType自体もidを持たないため、uriをキーにした
対応付けは、同じuriを持つinsertionが複数存在するケースで、
どちらのInstance用の値がどちらのTemplate側insertionに対応するのかを
一意に決定できないという欠陥がありました。
修正後は、insertion_valuesをSequence[InsertionValue]として受け取り、
template.content.insertionsとzip()して出現順序(位置)で対応
付けます(insertion_values[0]はtemplate.content.insertions[0]、
insertion_values[1]はtemplate.content.insertions[1]、という具合)。
InsertionTypeにはそもそも順序以外に安定した識別子が無いため、
スキーマが実際に保証している「出現順序」だけを対応付けの根拠にする
方針です。
create_instances()側のinsertion_valuesは、Template IDをキーにした
第1階層はそのまま維持し、値の型だけMapping[str, InsertionValue]から
Sequence[InsertionValue]に変更しています
(Mapping[template_id, Sequence[InsertionValue]])。
Template側insertionの個数とinsertion_valuesの要素数が一致しない
場合(不足・過剰いずれも)、およびTemplateにinsertionがあるのに
insertion_values自体を渡さなかった場合は、これまでどおり
ValueErrorになります(不足分を推測したり、Template側のuri/hash
をそのまま使い回したりはしません)。Templateにinsertionが1つも無い
場合はinsertion_valuesを省略でき(渡しても無視されます)、この
ショートサーキットは変更前から変わりません。uuid省略時の新規生成・
format省略時のTemplate側からの継承という既存の挙動も変更していません。
tests/test_builders.pyに、同じuriを持つ2つのinsertionが存在する
場合でも位置によって正しく異なる新しいuri/hashに対応付けられる
ことを検証する回帰テスト、およびinsertion_valuesの要素数が
Template側insertionの個数と一致しない場合にValueErrorになる
ことを検証するテストを追加しました。
- MkDocs + mkdocstringsによるAPI Reference一式(
mkdocs.yml・docs/)を 追加しました。pymaiml.serialization/validation/builders/queryの 各モジュールのdocstringから自動生成する構成で、create_instance()/create_instances()のような長文docstring(「何を・なぜ」まで説明する 地の文スタイル)も書き換えずそのままAPI Referenceの本文として使えます (mkdocs.ymlでdocstring_style: nullを指定し、Google/NumPy/Sphinx いずれのセクション見出し形式への統一も要求していません)。各モジュールが 既に定義している__all__をmkdocstrings-pythonがそのまま公開APIの一覧 として使うため、_始まりの内部ヘルパー関数(_build_instance_content()等)は表示されません。
README.md・CHANGELOG.md・CONTRIBUTING.mdはpymdownx.snippetsで
そのまま取り込んで表示する構成にしており(docs/index.mdが
--8<-- "README.md"のように参照するだけ)、内容の二重管理は発生し
ません。
pyproject.tomlにdocs extra(mkdocs/mkdocs-material/
mkdocstrings[python])を追加し、.github/workflows/docs.ymlで
push/pull_request時にmkdocs build --strictによるビルド確認、
mainブランチへのpush時にgh-pagesブランチへの自動デプロイを行い
ます(リポジトリ側でのGitHub Pages有効化(Settings > Pages)は別途
必要です)。ローカルでのビルド・プレビュー手順はREADME.mdの
「ドキュメント」節に追加しました。.gitignoreにmkdocs buildの
生成物site/を追加しています。
- ドキュメントの自動デプロイを、
mkdocs gh-deploy(gh-pagesブランチへの push)方式から、GitHub Pages Actionsデプロイ方式 (actions/upload-pages-artifact+actions/deploy-pages)へ切り替え ました。 設計文書PyMaiML_docs_auto_deploy.mdへの対応です。
.github/workflows/docs.ymlを全面的に書き換え、mainへのpush/merge時に
次の流れでビルド・公開まで自動で行うようにしました。
main へ push / merge
↓
GitHub Actions
↓
Python環境をセットアップ / pip install -e ".[docs]"
↓
mkdocs build --strict (mkdocstringsによるAPI Reference生成を含む)
↓
site/ を Pages artifact としてアップロード
↓
GitHub Pages へ deploy
gh-pagesブランチを生成物置き場として管理する必要がなくなり、git
identityの設定やブランチへのpush権限(contents: write)も不要に
なりました(代わりにpages: write/id-token: write権限を使用)。
設計文書には無い変更点として、pull_requestトリガーを残し、PR時は
mkdocs build --strictによるビルド確認のみを行い、deploy jobは
github.event_name == 'push' && github.ref == 'refs/heads/main'の
場合のみ実行するようガードを追加しています(PRの内容を誤って本番の
GitHub Pagesへデプロイしてしまわないようにするためで、設計文書自身が
採用理由として挙げている「mkdocs build --strictによるビルドエラー
のCI上での検出」をPRでも維持する意図です)。
Action本体のバージョン(actions/checkout@v6・actions/setup-python@v6・
actions/configure-pages@v5・actions/upload-pages-artifact@v4・
actions/deploy-pages@v4)は設計文書の指定どおりで、実在するタグである
ことをGitHub APIで確認済みです。
GitHub Pages側の設定(Settings > Pages > Build and deployment > Source を
「GitHub Actions」にする)は、この切り替え後も引き続き最初の1回だけ
人手での設定が必要です(GitHub Pagesの仕様上、ワークフローからは
自動化できません)。README.mdの「ドキュメント」節をこの新しい方式に
合わせて更新しました。ローカルでのmkdocs build --strict実行による
ビルド確認・全テストスイート(153件)の再実行は完了しています。