コンテンツにスキップ

pymaiml.builders

builders

pymaiml.builders

Ergonomic helpers on top of the raw maiml_domain classes, aimed at six sources of boilerplate/mistakes identified while hand-building MaiML files directly against maiml_domain during MaiML-Domain's own development:

  1. Picking the right one of ~70 property/content classes for a Python value (infer_property / infer_content).
  2. A protocol element's generic data container is usually a placeholder -- it declares what will eventually be measured, but (unlike the corresponding element built later) has no value/values of its own yet. xsi:type is still required by the schema even then, so it must be possible to say it explicitly instead of inferring it from a value that doesn't exist (infer_property/infer_content's xsi_type= parameter).
  3. Keeping that placeholder's xsi:type and the xsi:type of the matching property/content for the same key in sync -- a protocol declaring ex:temperature as floatType must not end up with a <data> recording of ex:temperature that serializes as intType just because the measured Python value happened to be a whole number (XsiTypeRegistry, shared across the infer_property/infer_content calls for both the protocol and the data side).
  4. Keeping id/uuid values unique across a large object tree (IdFactory).
  5. Remembering that recording a material/condition/result in requires a matching lifecycle:transition="complete" in , plus the exact XES namespace URI for the "lifecycle:" prefix -- this is EVT-02 in the maiml-schema-validator skill's rule set, and was hand-written three separate times in MaiML-Domain's own test script before this module existed (new_complete_event).
  6. Turning a materialTemplate/conditionTemplate/resultTemplate into the material/condition/result instance it describes -- assigning the instance's own id/uuid (never reused from the template), copying the template's generic data container, and re-pointing every templateRef to the right instanceRef once its target has been instantiated too (create_instance / create_instances).

This is the pymaiml.query -> pymaiml.builders handoff for this kind of work: pymaiml.query.get_templates() selects which templates to act on (by kind, by instruction_id, ...); pymaiml.builders.create_instance(s)() turns the selected maiml_domain.protocol template objects into maiml_domain.data instance objects. Neither module reaches into the other for this -- the caller passes query's output as builders' input.

This module does not attempt a full "session"/fluent builder over the entire object graph -- that would need to model every XSD content model's ordering and cardinality a second time on top of maiml_domain and pymaiml.serialization, and untested code for a ~70-class surface is worse than no code. What's here is scoped to what could be written and verified against real validation output.

IdFactory

IdFactory()

Generates unique xs:ID values and MaiML uuid values.

MaiML requires every id attribute to be unique within the file (xs:ID) and every global object to carry a uuid (a real, randomly generated one -- name-based v3/v5 UUIDs are a separate, deliberate choice for entities that must stay stable across files, so this factory does not attempt to guess when that applies).

ids = IdFactory() ids.new_id("material") 'material1' ids.new_id("material") 'material2' isinstance(ids.new_uuid(), m.Uuid) True

Every id this factory generates is prefix + an integer, so prefix itself must already be a valid xs:ID (NCName) on its own -- appending digits to a valid NCName always yields another valid NCName, but e.g. new_id("123") would produce "1231", which starts with a digit and is not a valid xs:ID. new_id() rejects such a prefix with ValueError the first time it's used, rather than silently handing back an id that passes through this SDK fine but fails XSD validation much later:

IdFactory().new_id("123") Traceback (most recent call last): ... ValueError: IdFactory.new_id: prefix='123' would not produce a valid xs:ID (NCName) -- xs:ID must not start with a digit, and must not contain ':' or other characters outside [A-Za-z0-9_.-] (plus Unicode letters). Pass a prefix that is itself a valid xs:ID.

When adding new elements to a file that already has other elements in it (e.g. loading an existing protocol via pymaiml.serialization.load() and building new data/eventLog on top of it), reserve the loaded ids first so this factory's own numbering can never collide with them, even if a prefix happens to coincide:

ids = IdFactory.from_existing_ids(["material_template1", "place1"]) ids.new_id("material_template") # skips 1 -- already reserved 'material_template2'

ソースコード位置: pymaiml/builders.py
def __init__(self) -> None:
    self._counters: Dict[str, itertools.count] = {}
    self._issued: set = set()

reserve

reserve(ids) -> None

Mark ids already used elsewhere (typically every id in a file loaded via pymaiml.serialization.load/loads -- see LoadedMaiml.ids) so new_id() will never return one of them. Safe to call more than once, and safe to call with ids this factory has already issued itself.

ソースコード位置: pymaiml/builders.py
def reserve(self, ids) -> None:
    """
    Mark ids already used elsewhere (typically every id in a file
    loaded via pymaiml.serialization.load/loads -- see LoadedMaiml.ids)
    so new_id() will never return one of them. Safe to call more than
    once, and safe to call with ids this factory has already issued
    itself.
    """
    self._issued.update(ids)

from_existing_ids classmethod

from_existing_ids(ids) -> 'IdFactory'

Convenience constructor: build a fresh IdFactory with ids pre-reserved. Equivalent to IdFactory() followed by .reserve(ids).

ソースコード位置: pymaiml/builders.py
@classmethod
def from_existing_ids(cls, ids) -> "IdFactory":
    """Convenience constructor: build a fresh IdFactory with `ids`
    pre-reserved. Equivalent to IdFactory() followed by .reserve(ids)."""
    factory = cls()
    factory.reserve(ids)
    return factory

XsiTypeRegistry

XsiTypeRegistry()

Remembers, per key, which concrete maiml_domain property/content class was chosen the first time infer_property()/infer_content() saw that key -- so a protocol's value-less placeholder (built with xsi_type= since there is no value to infer from) and the actual measured property/ content recorded later in for the same key are guaranteed to share one xsi:type, even when the measured Python value wouldn't by itself infer to that same class (e.g. a measured value of 20 (an int) for a key the protocol declared as floatType).

Pass one shared XsiTypeRegistry instance to every infer_property()/ infer_content() call across a document's protocol and data sections. This relies on MaiML's usual convention that a given key denotes one semantic property throughout a document (per the Annex A/B thesaurus) -- if a key genuinely needs a different type in a different place, give it a distinct key instead; registering a second, different class for a key already registered raises ValueError rather than silently overwriting it.

reg = XsiTypeRegistry() placeholder = infer_property("ex:temperature", xsi_type=m.FloatType, registry=reg) measured = infer_property("ex:temperature", value=20, registry=reg) type(measured) is m.FloatType True

Note -- repeated keys under one parent: MaiML-Schema-1_0 places no uniqueness constraint on key at all (genericDataContainerGroup is just property*, content* -- no xs:unique/xs:key anywhere in the schema), so the same key appearing more than once directly under the same parent (e.g. two temperature readings both keyed "ex:temperature" in one 's properties) is schema-valid and passes validation. XsiTypeRegistry is fine with that as long as every occurrence resolves to the same xsi:type (which is the normal case, and all repeats after the first will simply reuse what's already registered). If you deliberately want a different xsi:type for a repeated key -- an unusual case -- pass registry= only on the calls where you want the shared-type check, and omit it (call infer_property()/infer_content() without registry=) for the one(s) that should be exempt from it.

ソースコード位置: pymaiml/builders.py
def __init__(self) -> None:
    self._by_key: Dict[str, type] = {}

InsertionValue dataclass

InsertionValue(uri: str, hash: 'm.HashType', uuid: Optional['m.Uuid'] = None, format: Optional[str] = None)

The new uri/hash (and optionally uuid/format) create_instance() should give one of a template's elements when building the matching instance's own copy of it.

A template's insertion is never copied to an instance as-is -- an names an external file, and the instance's copy of an inserted file is a distinct file from the template's (the template's insertion is typically a placeholder or an example; the instance's is the file this particular measurement/analysis actually produced) -- so its uri and hash must always be supplied fresh by the caller, who is the only one who knows what that new file's uri/hash actually are. uuid, if omitted, is generated fresh (via the same IdFactory create_instance()/create_instances() were given); format, if omitted, is copied from the template's insertion (the file format -- e.g. "text/csv" -- is the one part of an insertion that often does stay the same between a template's placeholder and an instance's real file, unlike uri/hash which by definition cannot).

infer_property

infer_property(key: str, value: Any = None, values: Optional[List[Any]] = None, *, xsi_type: Optional[Union[str, type]] = None, registry: Optional[XsiTypeRegistry] = None, **kwargs)

Build a maiml_domain property instance.

Normally the concrete class is chosen from the Python type of value (scalar) or the elements of values (list -- must be non-empty and homogeneous; this is enforced -- every element must belong to the same inferred type as values[0], e.g. all int or all str, or a TypeError is raised naming the offending element, rather than silently choosing a type from values[0] alone and letting the rest through unchecked). xsi:type is required by the schema regardless, so when there is no value yet to infer it from -- the common case for a protocol element's placeholder property -- pass it explicitly via xsi_type= (either the maiml_domain class, e.g. m.FloatType, or the xsi:type name, e.g. "floatType"). It is an error to have neither: a value/values to infer from, nor an explicit xsi_type=.

Pass a shared registry= (an XsiTypeRegistry) across a document's protocol and data sections to guarantee the placeholder declared in the protocol and the actual measurement recorded later in for the same key end up with the same xsi:type -- see XsiTypeRegistry.

Extra kwargs (description=, units=, format_string=, encryption=, ...) are forwarded to the chosen class's constructor -- pass whatever that concrete class accepts; an unsupported kwarg (e.g. units= on a non-numeric type) raises TypeError from the underlying constructor, same as calling it directly.

At most one of value/values may be given. For anything the inference table doesn't cover (xs:QName/IDREF/token/uri/language scalars, enumerations, list-of-mixed-types), pass xsi_type= explicitly or construct the maiml_domain class directly.

ソースコード位置: pymaiml/builders.py
def infer_property(
    key: str,
    value: Any = None,
    values: Optional[List[Any]] = None,
    *,
    xsi_type: Optional[Union[str, type]] = None,
    registry: Optional[XsiTypeRegistry] = None,
    **kwargs,
):
    """
    Build a maiml_domain property instance.

    Normally the concrete class is chosen from the Python type of `value`
    (scalar) or the elements of `values` (list -- must be non-empty and
    homogeneous; this is enforced -- every element must belong to the same
    inferred type as values[0], e.g. all int or all str, or a TypeError is
    raised naming the offending element, rather than silently choosing a
    type from values[0] alone and letting the rest through unchecked).
    xsi:type is required by the schema regardless, so when there is no
    value yet to infer it from -- the common case for a protocol element's
    placeholder property -- pass it explicitly via `xsi_type=` (either the
    maiml_domain class, e.g. `m.FloatType`, or the xsi:type name, e.g.
    `"floatType"`). It is an error to have neither: a value/values to infer
    from, nor an explicit xsi_type=.

    Pass a shared `registry=` (an `XsiTypeRegistry`) across a document's
    protocol and data sections to guarantee the placeholder declared in the
    protocol and the actual measurement recorded later in <data> for the
    same key end up with the same xsi:type -- see XsiTypeRegistry.

    Extra kwargs (description=, units=, format_string=, encryption=, ...)
    are forwarded to the chosen class's constructor -- pass whatever that
    concrete class accepts; an unsupported kwarg (e.g. units= on a
    non-numeric type) raises TypeError from the underlying constructor,
    same as calling it directly.

    At most one of `value`/`values` may be given. For anything the
    inference table doesn't cover (xs:QName/IDREF/token/uri/language
    scalars, enumerations, list-of-mixed-types), pass xsi_type= explicitly
    or construct the maiml_domain class directly.
    """
    if value is not None and values is not None:
        raise ValueError("infer_property: pass at most one of value= or values=")

    cls = _resolve_xsi_type(xsi_type)
    if cls is not None and not is_property_class(cls):
        raise TypeError(f"infer_property: xsi_type={cls!r} is not a maiml_domain property class")

    if cls is None and registry is not None:
        cls = registry.get(key)
        if cls is not None and not is_property_class(cls):
            raise TypeError(
                f"infer_property: registry has key={key!r} registered as "
                f"{cls.__name__}, which is not a property class -- the same key is "
                "being used for both a property and a content container"
            )

    if cls is None:
        if values is not None:
            if not values:
                raise ValueError("infer_property: values must be non-empty to infer a type")
            cls = _infer_homogeneous_list_class(
                values, _LIST_CLASS_BY_TYPE, caller="infer_property", kind_noun="property"
            )
        elif value is not None:
            cls = _match(type(value), _SCALAR_CLASS_BY_TYPE)
            if cls is None:
                raise TypeError(
                    f"infer_property: no known scalar property type for {type(value)!r}; "
                    "pass xsi_type= explicitly, or construct the maiml_domain class directly."
                )
        else:
            raise ValueError(
                f"infer_property: xsi:type is required by the schema but could not be "
                f"determined for key={key!r} -- no value=/values= to infer it from. Pass "
                "xsi_type=<maiml_domain class or xsi:type name> explicitly; this is the "
                "common case for a protocol placeholder property that has no value yet."
            )

    if registry is not None:
        registry.register(key, cls)

    return _instantiate(cls, key, value, values, kwargs)

infer_content

infer_content(key: str, values: Optional[List[Any]] = None, *, xsi_type: Optional[Union[str, type]] = None, registry: Optional[XsiTypeRegistry] = None, **kwargs)

Build a maiml_domain content instance (always a list type -- MaiML has no scalar content type).

Normally the concrete class is chosen from the Python type of values' elements (must be non-empty and homogeneous; this is enforced -- every element must belong to the same inferred type as values[0], or a TypeError is raised naming the offending element). xsi:type is required by the schema regardless, so when there are no values yet -- the common case for a protocol element's placeholder content, which may only describe axis=/size= for now -- pass it explicitly via xsi_type= (either the maiml_domain class, e.g. m.ContentFloatListType, or the xsi:type name, e.g. "contentFloatListType"). It is an error to have neither: non-empty values= to infer from, nor an explicit xsi_type=.

Pass a shared registry= (an XsiTypeRegistry) across a document's protocol and data sections to guarantee the placeholder declared in the protocol and the actual measurement recorded later in for the same key end up with the same xsi:type -- see XsiTypeRegistry.

Extra kwargs (axis=, size=, units=, format_string=, id=, ref=, ...) are forwarded to the chosen class's constructor.

ソースコード位置: pymaiml/builders.py
def infer_content(
    key: str,
    values: Optional[List[Any]] = None,
    *,
    xsi_type: Optional[Union[str, type]] = None,
    registry: Optional[XsiTypeRegistry] = None,
    **kwargs,
):
    """
    Build a maiml_domain content instance (always a list type -- MaiML has
    no scalar content type).

    Normally the concrete class is chosen from the Python type of `values`'
    elements (must be non-empty and homogeneous; this is enforced -- every
    element must belong to the same inferred type as values[0], or a
    TypeError is raised naming the offending element). xsi:type is
    required by the schema regardless, so when there are no values yet --
    the common case for a protocol element's placeholder content, which
    may only describe axis=/size= for now -- pass it explicitly via
    `xsi_type=`
    (either the maiml_domain class, e.g. `m.ContentFloatListType`, or the
    xsi:type name, e.g. `"contentFloatListType"`). It is an error to have
    neither: non-empty values= to infer from, nor an explicit xsi_type=.

    Pass a shared `registry=` (an `XsiTypeRegistry`) across a document's
    protocol and data sections to guarantee the placeholder declared in the
    protocol and the actual measurement recorded later in <data> for the
    same key end up with the same xsi:type -- see XsiTypeRegistry.

    Extra kwargs (axis=, size=, units=, format_string=, id=, ref=, ...) are
    forwarded to the chosen class's constructor.
    """
    cls = _resolve_xsi_type(xsi_type)
    if cls is not None and not is_content_class(cls):
        raise TypeError(f"infer_content: xsi_type={cls!r} is not a maiml_domain content class")

    if cls is None and registry is not None:
        cls = registry.get(key)
        if cls is not None and not is_content_class(cls):
            raise TypeError(
                f"infer_content: registry has key={key!r} registered as "
                f"{cls.__name__}, which is not a content class -- the same key is "
                "being used for both a property and a content container"
            )

    if cls is None:
        if not values:
            raise ValueError(
                f"infer_content: xsi:type is required by the schema but could not be "
                f"determined for key={key!r} -- no non-empty values= to infer it from. Pass "
                "xsi_type=<maiml_domain class or xsi:type name> explicitly; this is the "
                "common case for a protocol placeholder content that has no values yet."
            )
        cls = _infer_homogeneous_list_class(
            values, _CONTENT_LIST_CLASS_BY_TYPE, caller="infer_content", kind_noun="content"
        )

    if registry is not None:
        registry.register(key, cls)

    return _instantiate(cls, key, None, values, kwargs)

new_complete_event

new_complete_event(id: str, ref: str, *, id_factory: Optional[IdFactory] = None, extra_properties: Optional[list] = None) -> 'm.EventType'

Build an EventType carrying the lifecycle:transition="complete" property that maiml-schema-validator's EVT-02 rule requires whenever records a material/condition/result. ref must point at the id of the instruction (or program) this event completes.

Remember to declare xmlns:lifecycle="" on the root element (see pymaiml.serialization.dumps' extra_namespaces=) -- NS-01 rejects the "lifecycle:" prefix if it resolves to any other URI.

ソースコード位置: pymaiml/builders.py
def new_complete_event(
    id: str,
    ref: str,
    *,
    id_factory: Optional[IdFactory] = None,
    extra_properties: Optional[list] = None,
) -> "m.EventType":
    """
    Build an EventType carrying the lifecycle:transition="complete"
    property that maiml-schema-validator's EVT-02 rule requires whenever
    <data> records a material/condition/result. `ref` must point at the
    id of the instruction (or program) this event completes.

    Remember to declare xmlns:lifecycle="<LIFECYCLE_NS>" on the root
    element (see pymaiml.serialization.dumps' extra_namespaces=) -- NS-01
    rejects the "lifecycle:" prefix if it resolves to any other URI.
    """
    complete_prop = m.StringType(key="lifecycle:transition", value="complete")
    properties = [complete_prop, *(extra_properties or [])]
    content_uuid = id_factory.new_uuid() if id_factory is not None else m.Uuid(str(_uuidlib.uuid4()))
    return m.EventType(
        id=id,
        ref=ref,
        content=m.GlobalObjectContent(uuid=content_uuid, properties=properties),
    )

create_instance

create_instance(template: Template, *, id: str, id_factory: IdFactory, template_instance_map: Optional[Mapping[str, str]] = None, insertion_values: Optional[Sequence[InsertionValue]] = None) -> Instance

Build the material/condition/result instance that template (a MaterialTemplateType/ConditionTemplateType/ResultTemplateType -- see pymaiml.query.Template, typically obtained from pymaiml.query.get_templates()) describes, as the matching maiml_domain instance class (MaterialType/ConditionType/ResultType -- see pymaiml.query.Instance) determined automatically from template's own type; callers never pick the instance class themselves.

id is the new instance's own id -- this function does not generate it itself (see create_instances(), which reserves ids for a whole batch up front via an IdFactory before building any instance, so every templateRef in that batch can already resolve to a real instance id by the time it is needed). id_factory is still required here too: it is used to mint the instance's own fresh content uuid, every rebuilt 's uuid (unless InsertionValue supplies one), and every wrapper element's own id.

What is copied from template to the new instance, and what is not: - template.id becomes instance.ref (this is how an instance says which template it is an instance of -- see maiml_domain.data). - template.content's name/description/annotation are copied as-is; its insertions/properties/contents (or encryption) are copied via a fresh GlobalObjectContent, never the same mutable list/object the template holds (see _build_instance_content()) -- editing the returned instance afterwards can never mutate template. - template.content's insertions are NOT copied as-is: each becomes a brand new InsertionType with a caller-supplied uri/hash (via insertion_values, a Sequence[InsertionValue] matched to template.content.insertions by POSITION -- not by uri, which MaiML-Schema-1_0 does not guarantee is unique among one generic data container's insertions; see InsertionValue's docstring for why the template's uri/hash can never just be reused). A length mismatch between insertion_values and template.content.insertions raises ValueError. - template.template_refs become instance.instance_refs, each translated via template_instance_map (see _convert_template_refs() and template_instance_map's own docstring below) rather than copied by id -- a templateRef with no entry in template_instance_map raises ValueError rather than silently pointing at a template's id (which is never a valid instance id) or being dropped. - The instance's own id (the id parameter) and its content's uuid are always newly assigned, never copied from template -- an instance is a distinct object from the template it instantiates, with its own identity, even though it shares the template's name/description/annotation/generic-data-container content. - template.place_refs is NOT copied: MaterialType/ConditionType/ ResultType has no place_refs attribute at all (a template's connection to PNML topology has no instance-side equivalent).

template_instance_map, if given, maps every OTHER template's id this template's own template_refs might point to, to that other template's already-decided instance id -- it does not need (and for a single-template call, will not have) an entry for template.id itself. Pass it whenever template.template_refs is non-empty; create_instances() builds and passes it automatically when instantiating a batch of related templates together.

ソースコード位置: pymaiml/builders.py
def create_instance(
    template: Template,
    *,
    id: str,
    id_factory: IdFactory,
    template_instance_map: Optional[Mapping[str, str]] = None,
    insertion_values: Optional[Sequence[InsertionValue]] = None,
) -> Instance:
    """
    Build the material/condition/result instance that `template`
    (a MaterialTemplateType/ConditionTemplateType/ResultTemplateType --
    see pymaiml.query.Template, typically obtained from
    pymaiml.query.get_templates()) describes, as the matching maiml_domain
    instance class (MaterialType/ConditionType/ResultType -- see
    pymaiml.query.Instance) determined automatically from `template`'s own
    type; callers never pick the instance class themselves.

    `id` is the new instance's own id -- this function does not generate
    it itself (see create_instances(), which reserves ids for a whole
    batch up front via an IdFactory before building any instance, so every
    templateRef in that batch can already resolve to a real instance id by
    the time it is needed). `id_factory` is still required here too: it is
    used to mint the instance's own fresh content uuid, every rebuilt
    <insertion>'s uuid (unless InsertionValue supplies one), and every
    <instanceRef> wrapper element's own id.

    What is copied from `template` to the new instance, and what is not:
      - template.id becomes instance.ref (this is *how* an instance says
        which template it is an instance of -- see maiml_domain.data).
      - template.content's name/description/annotation are copied as-is;
        its insertions/properties/contents (or encryption) are copied via
        a fresh GlobalObjectContent, never the same mutable list/object
        the template holds (see _build_instance_content()) -- editing the
        returned instance afterwards can never mutate `template`.
      - template.content's insertions are NOT copied as-is: each becomes a
        brand new InsertionType with a caller-supplied uri/hash (via
        insertion_values, a Sequence[InsertionValue] matched to
        template.content.insertions by POSITION -- not by uri, which
        MaiML-Schema-1_0 does not guarantee is unique among one generic
        data container's insertions; see InsertionValue's docstring for
        why the template's uri/hash can never just be reused). A length
        mismatch between insertion_values and template.content.insertions
        raises ValueError.
      - template.template_refs become instance.instance_refs, each
        translated via template_instance_map (see _convert_template_refs()
        and template_instance_map's own docstring below) rather than
        copied by id -- a templateRef with no entry in template_instance_map
        raises ValueError rather than silently pointing at a template's id
        (which is never a valid instance id) or being dropped.
      - The instance's own id (the `id` parameter) and its content's uuid
        are always newly assigned, never copied from `template` -- an
        instance is a distinct object from the template it instantiates,
        with its own identity, even though it shares the template's
        name/description/annotation/generic-data-container content.
      - template.place_refs is NOT copied: MaterialType/ConditionType/
        ResultType has no place_refs attribute at all (a template's
        connection to PNML topology has no instance-side equivalent).

    template_instance_map, if given, maps every OTHER template's id this
    template's own template_refs might point to, to that other template's
    already-decided instance id -- it does not need (and for a
    single-template call, will not have) an entry for `template.id`
    itself. Pass it whenever `template.template_refs` is non-empty;
    create_instances() builds and passes it automatically when
    instantiating a batch of related templates together.
    """
    instance_cls = _TEMPLATE_TO_INSTANCE.get(type(template))
    if instance_cls is None:
        raise TypeError(
            f"create_instance: {type(template).__name__} is not a template class -- "
            f"expected one of {[c.__name__ for c in _TEMPLATE_TO_INSTANCE]}"
        )

    content = _build_instance_content(
        template.content, id_factory=id_factory, insertion_values=insertion_values
    )
    instance_refs = _convert_template_refs(
        template.template_refs, template_instance_map=template_instance_map, id_factory=id_factory
    )
    return instance_cls(id=id, ref=template.id, content=content, instance_refs=instance_refs)

create_instances

create_instances(templates: Sequence[Template], *, id_factory: IdFactory, insertion_values: Optional[Mapping[str, Sequence[InsertionValue]]] = None, existing_instance_map: Optional[Mapping[str, str]] = None) -> List[Instance]

Build one instance per template in templates (e.g. the result of pymaiml.query.get_templates(xml_text, instruction_id=...)), handling the two-phase sequencing create_instance() itself deliberately does not: every instance's id must be decided before any instance is built, because a template's templateRef can point at another template in the same batch whose own instance doesn't exist yet at the point this template is instantiated (see create_instance()'s template_instance_map parameter -- this is exactly why that map is not create_instance()'s own responsibility to build).

Processing order: 1. Reserve a fresh instance id for every template in templates (via id_factory, under the "material"/"condition"/"result" prefix matching each template's own kind) -- this is own_ids, {template.id: new_instance_id}. Raises ValueError if the same template.id appears more than once in templates (each must be instantiated at most once per call). 2. Build template_instance_map = {(existing_instance_map or {}), own_ids} -- own_ids' entries win on a key collision, since a template being instantiated in this very call always takes precedence over a stale existing_instance_map entry for the same template id. 3. Call create_instance() once per template, passing its reserved id from own_ids, the shared template_instance_map from step 2, and (if given) that template's own entry from insertion_values.

insertion_values, if given, is keyed by template.id first, and then -- for that template -- a Sequence[InsertionValue] matched to its own content.insertions by position (see create_instance()'s insertion_values, and _build_instance_insertions()'s docstring for why position, not uri, is what identifies one insertion: MaiML-Schema-1_0 does not guarantee uri is unique among a generic data container's insertions). This is create_instance()'s same, single-template-scoped parameter, just nested one level to cover a whole batch of templates that may each have their own insertions.

existing_instance_map, if given, supplies instance ids for templates OUTSIDE this batch that some template here might still templateRef -- e.g. a template instantiated in an earlier, separate call. Without an entry (here or in the freshly built own_ids) for every templateRef target, create_instance() raises ValueError (see its own template_instance_map docstring) rather than silently leaving a dangling or template-id reference in the output.

Returns instances in the same order as templates.

ソースコード位置: pymaiml/builders.py
def create_instances(
    templates: Sequence[Template],
    *,
    id_factory: IdFactory,
    insertion_values: Optional[Mapping[str, Sequence[InsertionValue]]] = None,
    existing_instance_map: Optional[Mapping[str, str]] = None,
) -> List[Instance]:
    """
    Build one instance per template in `templates` (e.g. the result of
    pymaiml.query.get_templates(xml_text, instruction_id=...)), handling
    the two-phase sequencing create_instance() itself deliberately does
    not: every instance's id must be decided *before* any instance is
    built, because a template's templateRef can point at another template
    in the same batch whose own instance doesn't exist yet at the point
    this template is instantiated (see create_instance()'s
    template_instance_map parameter -- this is exactly why that map is
    not create_instance()'s own responsibility to build).

    Processing order:
      1. Reserve a fresh instance id for every template in `templates`
         (via id_factory, under the "material"/"condition"/"result"
         prefix matching each template's own kind) -- this is `own_ids`,
         {template.id: new_instance_id}. Raises ValueError if the same
         template.id appears more than once in `templates` (each must be
         instantiated at most once per call).
      2. Build template_instance_map = {**(existing_instance_map or {}),
         **own_ids} -- own_ids' entries win on a key collision, since a
         template being instantiated in this very call always takes
         precedence over a stale existing_instance_map entry for the same
         template id.
      3. Call create_instance() once per template, passing its reserved id
         from own_ids, the shared template_instance_map from step 2, and
         (if given) that template's own entry from insertion_values.

    insertion_values, if given, is keyed by template.id first, and then --
    for that template -- a Sequence[InsertionValue] matched to its own
    content.insertions by position (see create_instance()'s
    insertion_values, and _build_instance_insertions()'s docstring for why
    position, not uri, is what identifies one insertion: MaiML-Schema-1_0
    does not guarantee uri is unique among a generic data container's
    insertions). This is create_instance()'s same, single-template-scoped
    parameter, just nested one level to cover a whole batch of templates
    that may each have their own insertions.

    existing_instance_map, if given, supplies instance ids for templates
    OUTSIDE this batch that some template here might still templateRef --
    e.g. a template instantiated in an earlier, separate call. Without an
    entry (here or in the freshly built own_ids) for every templateRef
    target, create_instance() raises ValueError (see its own
    template_instance_map docstring) rather than silently leaving a
    dangling or template-id reference in the output.

    Returns instances in the same order as `templates`.
    """
    templates = list(templates)

    seen_template_ids = set()
    own_ids: Dict[str, str] = {}
    for template in templates:
        if template.id in seen_template_ids:
            raise ValueError(
                f"create_instances: template id {template.id!r} appears more than "
                "once in `templates` -- each template can be instantiated at most "
                "once per call."
            )
        seen_template_ids.add(template.id)

        prefix = _INSTANCE_ID_PREFIX.get(type(template))
        if prefix is None:
            raise TypeError(
                f"create_instances: {type(template).__name__} is not a template "
                f"class -- expected one of {[c.__name__ for c in _INSTANCE_ID_PREFIX]}"
            )
        own_ids[template.id] = id_factory.new_id(prefix)

    template_instance_map: Dict[str, str] = {**(existing_instance_map or {}), **own_ids}

    return [
        create_instance(
            template,
            id=own_ids[template.id],
            id_factory=id_factory,
            template_instance_map=template_instance_map,
            insertion_values=(insertion_values or {}).get(template.id),
        )
        for template in templates
    ]