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:
- Picking the right one of ~70 property/content classes for a Python
value (
infer_property/infer_content). - 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'sxsi_type=parameter). - 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:temperatureas floatType must not end up with a<data>recording ofex:temperaturethat serializes as intType just because the measured Python value happened to be a whole number (XsiTypeRegistry, shared across theinfer_property/infer_contentcalls for both the protocol and the data side). - Keeping id/uuid values unique across a large object tree
(
IdFactory). - 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). - 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 ¶
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
reserve ¶
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
from_existing_ids
classmethod
¶
Convenience constructor: build a fresh IdFactory with ids
pre-reserved. Equivalent to IdFactory() followed by .reserve(ids).
ソースコード位置: pymaiml/builders.py
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 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.
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
A template's insertion is never copied to an instance as-is -- an
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
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 | |
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
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="
ソースコード位置: pymaiml/builders.py
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
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
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 | |
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
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 | |