metaobjects 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""object.* → Pydantic v2 model module (sub-project A)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from metaobjects.meta.core.field.meta_field import MetaField
|
|
5
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
6
|
+
from metaobjects.meta.core.field import field_constants as fc
|
|
7
|
+
from metaobjects.meta.core.validator import validator_constants as vc
|
|
8
|
+
from metaobjects.shared.base_types import TYPE_VALIDATOR
|
|
9
|
+
from metaobjects.shared.separators import PACKAGE_SEP
|
|
10
|
+
from metaobjects.codegen.constants import generated_header
|
|
11
|
+
from metaobjects.codegen.type_map import py_type_for
|
|
12
|
+
from metaobjects.codegen.format import ruff_format
|
|
13
|
+
from metaobjects.codegen.generator import EmittedFile, GenContext, Generator, per_entity
|
|
14
|
+
from metaobjects.codegen.generators.m2m_codegen import (
|
|
15
|
+
build_object_index,
|
|
16
|
+
resolve_m2m_descriptors,
|
|
17
|
+
)
|
|
18
|
+
from metaobjects.codegen.generators.tph_plan import tph_subtype_binding
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_int(value: object) -> bool:
|
|
22
|
+
"""A real int (bool is an int subclass, so `@maxLength: true` etc. must not count)."""
|
|
23
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validators(field: MetaField, sub_type: str) -> list[MetaField]:
|
|
27
|
+
"""The field's own ``validator.<sub_type>`` children (effective, supers included)."""
|
|
28
|
+
return [
|
|
29
|
+
c
|
|
30
|
+
for c in field.children()
|
|
31
|
+
if c.type == TYPE_VALIDATOR and c.sub_type == sub_type
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _first_attr(field: MetaField, sub_type: str, attr_name: str) -> object | None:
|
|
36
|
+
"""First int-valued *attr_name* across the field's ``validator.<sub_type>`` children."""
|
|
37
|
+
for v in _validators(field, sub_type):
|
|
38
|
+
val = v.attr(attr_name)
|
|
39
|
+
if _is_int(val):
|
|
40
|
+
return val
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validator_constraints(field: MetaField) -> dict[str, object]:
|
|
45
|
+
"""Map the field's ``validator.*`` children + field attrs to Pydantic ``Field``
|
|
46
|
+
kwargs. Cross-port-canonical semantics (TS is the reference):
|
|
47
|
+
|
|
48
|
+
- ``validator.regex @pattern`` -> ``pattern=``
|
|
49
|
+
- ``validator.numeric @min/@max`` -> ``ge=``/``le=`` (numeric value bounds)
|
|
50
|
+
- ``validator.length @min`` + field ``@maxLength`` -> ``min_length=``/``max_length=``
|
|
51
|
+
- ``validator.array @min/@max`` -> list ``min_length=``/``max_length=`` (element count)
|
|
52
|
+
"""
|
|
53
|
+
kwargs: dict[str, object] = {}
|
|
54
|
+
|
|
55
|
+
# String length: validator.length @min/@max + field @maxLength (max wins per field attr).
|
|
56
|
+
min_len = _first_attr(field, vc.VALIDATOR_SUBTYPE_LENGTH, vc.VALIDATOR_ATTR_MIN)
|
|
57
|
+
max_len = _first_attr(field, vc.VALIDATOR_SUBTYPE_LENGTH, vc.VALIDATOR_ATTR_MAX)
|
|
58
|
+
field_max = field.attr(fc.FIELD_ATTR_MAX_LENGTH)
|
|
59
|
+
if _is_int(field_max):
|
|
60
|
+
max_len = field_max
|
|
61
|
+
if min_len is not None:
|
|
62
|
+
kwargs["min_length"] = min_len
|
|
63
|
+
if max_len is not None:
|
|
64
|
+
kwargs["max_length"] = max_len
|
|
65
|
+
|
|
66
|
+
# Array element count: validator.array @min/@max -> list min_length/max_length.
|
|
67
|
+
arr_min = _first_attr(field, vc.VALIDATOR_SUBTYPE_ARRAY, vc.VALIDATOR_ATTR_MIN)
|
|
68
|
+
arr_max = _first_attr(field, vc.VALIDATOR_SUBTYPE_ARRAY, vc.VALIDATOR_ATTR_MAX)
|
|
69
|
+
if arr_min is not None:
|
|
70
|
+
kwargs["min_length"] = arr_min
|
|
71
|
+
if arr_max is not None:
|
|
72
|
+
kwargs["max_length"] = arr_max
|
|
73
|
+
|
|
74
|
+
# Numeric value bounds: validator.numeric @min/@max -> ge/le.
|
|
75
|
+
num_min = _first_attr(field, vc.VALIDATOR_SUBTYPE_NUMERIC, vc.VALIDATOR_ATTR_MIN)
|
|
76
|
+
num_max = _first_attr(field, vc.VALIDATOR_SUBTYPE_NUMERIC, vc.VALIDATOR_ATTR_MAX)
|
|
77
|
+
if num_min is not None:
|
|
78
|
+
kwargs["ge"] = num_min
|
|
79
|
+
if num_max is not None:
|
|
80
|
+
kwargs["le"] = num_max
|
|
81
|
+
|
|
82
|
+
# Regex: validator.regex @pattern -> pattern.
|
|
83
|
+
for v in _validators(field, vc.VALIDATOR_SUBTYPE_REGEX):
|
|
84
|
+
pattern = v.attr(vc.VALIDATOR_ATTR_PATTERN)
|
|
85
|
+
if isinstance(pattern, str):
|
|
86
|
+
kwargs["pattern"] = pattern
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
return kwargs
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _field_line(field: MetaField, imports: set[str]) -> tuple[str, bool]:
|
|
93
|
+
"""Return (source line, uses_field). Collects required imports into *imports*."""
|
|
94
|
+
pt = py_type_for(field)
|
|
95
|
+
imports.update(pt.imports)
|
|
96
|
+
if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
|
|
97
|
+
ref = field.attr(fc.FIELD_ATTR_OBJECT_REF)
|
|
98
|
+
if ref:
|
|
99
|
+
imports.add(f"from .{ref} import {ref}")
|
|
100
|
+
required = field.attr(fc.FIELD_ATTR_REQUIRED) is True
|
|
101
|
+
|
|
102
|
+
constraints = _validator_constraints(field)
|
|
103
|
+
# Emit kwargs in a stable order so generated output is deterministic.
|
|
104
|
+
_order = ["pattern", "ge", "le", "min_length", "max_length"]
|
|
105
|
+
parts = [f"{k}={constraints[k]!r}" for k in _order if k in constraints]
|
|
106
|
+
uses_field = bool(parts)
|
|
107
|
+
|
|
108
|
+
annotation = pt.expr if required else f"{pt.expr} | None"
|
|
109
|
+
if required and uses_field:
|
|
110
|
+
assignment = f" = Field({', '.join(parts)})"
|
|
111
|
+
elif required:
|
|
112
|
+
assignment = ""
|
|
113
|
+
elif uses_field:
|
|
114
|
+
assignment = f" = Field(default=None, {', '.join(parts)})"
|
|
115
|
+
else:
|
|
116
|
+
assignment = " = None"
|
|
117
|
+
|
|
118
|
+
return f" {field.name}: {annotation}{assignment}", uses_field
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _effective_fqn(entity: MetaObject) -> str:
|
|
122
|
+
"""`package::name`, resolving the package from the nearest ancestor that carries
|
|
123
|
+
one (objects inherit the file/root package). Falls back to the bare name."""
|
|
124
|
+
pkg = entity.package
|
|
125
|
+
parent = entity.parent
|
|
126
|
+
while pkg is None and parent is not None:
|
|
127
|
+
pkg = parent.package
|
|
128
|
+
parent = parent.parent
|
|
129
|
+
return f"{pkg}{PACKAGE_SEP}{entity.name}" if pkg else entity.name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class EntityModelGenerator:
|
|
133
|
+
"""``object.*`` → a Pydantic v2 model module per object.
|
|
134
|
+
|
|
135
|
+
EXTENSION SEAM (open-for-extension). Adopters subclass this and override one of
|
|
136
|
+
the protected ``_emit_*`` hooks (or ``render_entity_model``) to customize the
|
|
137
|
+
emitted model without forking the generator. The factory ``entity_model()`` and
|
|
138
|
+
the module-level ``render_entity_model()`` both delegate to a default instance,
|
|
139
|
+
so subclassing changes nothing for the default suite (output stays byte-identical).
|
|
140
|
+
|
|
141
|
+
Override points (in emission order):
|
|
142
|
+
|
|
143
|
+
* ``_emit_class_header(entity, base_class)`` — the ``class <Name>(<Base>):`` line.
|
|
144
|
+
* ``_emit_field_lines(entity, imports)`` — the body field lines (scalars + M:N
|
|
145
|
+
collections); collect any extra imports into the ``imports`` set.
|
|
146
|
+
* ``render_entity_model(entity, object_index)`` — the whole module (last resort).
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
name = "entity-model"
|
|
150
|
+
|
|
151
|
+
def _emit_class_header(self, entity: MetaObject, base_class: str) -> str:
|
|
152
|
+
"""The ``class <Name>(<Base>):`` declaration line. Override to inject a
|
|
153
|
+
decorator, a metaclass, or an alternate base."""
|
|
154
|
+
return f"class {entity.name}({base_class}):"
|
|
155
|
+
|
|
156
|
+
def _emit_field_lines(
|
|
157
|
+
self,
|
|
158
|
+
entity: MetaObject,
|
|
159
|
+
imports: set[str],
|
|
160
|
+
object_index: dict[str, MetaObject] | None,
|
|
161
|
+
) -> tuple[list[str], bool]:
|
|
162
|
+
"""The model body: one line per own field, then M:N nested collections when
|
|
163
|
+
*object_index* is supplied. Returns ``(lines, uses_field)`` where
|
|
164
|
+
``uses_field`` is True iff any line used a pydantic ``Field(...)`` (so the
|
|
165
|
+
caller knows to import ``Field``). Required imports are collected into
|
|
166
|
+
*imports*. Override to add/transform body lines."""
|
|
167
|
+
uses_field = False
|
|
168
|
+
lines: list[str] = []
|
|
169
|
+
for f in entity.own_fields():
|
|
170
|
+
line, used = _field_line(f, imports)
|
|
171
|
+
uses_field = uses_field or used
|
|
172
|
+
lines.append(line)
|
|
173
|
+
|
|
174
|
+
# M:N nested collections (FR-018). Element type is the target entity; a
|
|
175
|
+
# self-join element type is a forward-ref string so the model can name itself.
|
|
176
|
+
if object_index is not None:
|
|
177
|
+
for d in resolve_m2m_descriptors(entity, object_index):
|
|
178
|
+
if d.target_entity == entity.name:
|
|
179
|
+
element = f'"{entity.name}"'
|
|
180
|
+
else:
|
|
181
|
+
element = d.target_entity
|
|
182
|
+
imports.add(f"from .{d.target_entity} import {d.target_entity}")
|
|
183
|
+
lines.append(f" {d.relation_name}: list[{element}] = []")
|
|
184
|
+
return lines, uses_field
|
|
185
|
+
|
|
186
|
+
def render_entity_model(
|
|
187
|
+
self, entity: MetaObject, object_index: dict[str, MetaObject] | None = None
|
|
188
|
+
) -> str:
|
|
189
|
+
"""Render an entity as a Pydantic v2 model (pre-format; the generator runs ruff).
|
|
190
|
+
|
|
191
|
+
When *object_index* is supplied, M:N navigations (``relationship.*``
|
|
192
|
+
``@cardinality:"many" + @through``) are emitted as nested Pydantic
|
|
193
|
+
collections (``tags: list[Tag] = []``); a self-join uses a forward-ref string
|
|
194
|
+
(``following: list["Person"] = []``). Without an index, only scalar/object
|
|
195
|
+
fields are emitted (back-compat)."""
|
|
196
|
+
imports: set[str] = set()
|
|
197
|
+
base_class = "BaseModel"
|
|
198
|
+
if entity.super_data is not None:
|
|
199
|
+
base_class = entity.super_data.name
|
|
200
|
+
imports.add(f"from .{base_class} import {base_class}")
|
|
201
|
+
|
|
202
|
+
lines, uses_field = self._emit_field_lines(entity, imports, object_index)
|
|
203
|
+
|
|
204
|
+
# FR-017 TPH: a concrete subtype pins the inherited discriminator field to its
|
|
205
|
+
# own value (Literal) so the Pydantic model rejects a foreign-subtype tag —
|
|
206
|
+
# the type-layer parity with the TS `z.literal` / C# discriminator pin. (Python's
|
|
207
|
+
# runtime is dict-based, so the base's discriminated-UNION alias is intentionally
|
|
208
|
+
# deferred: it isn't consumed by the generated routes/ObjectManager and would
|
|
209
|
+
# force a base↔subtype circular import for an unused artifact.)
|
|
210
|
+
binding = tph_subtype_binding(entity)
|
|
211
|
+
if binding is not None:
|
|
212
|
+
disc_field, disc_value = binding
|
|
213
|
+
lines = [
|
|
214
|
+
f' {disc_field}: Literal["{disc_value}"] = "{disc_value}"',
|
|
215
|
+
*lines,
|
|
216
|
+
]
|
|
217
|
+
imports.add("from typing import Literal")
|
|
218
|
+
body = lines if lines else [" pass"]
|
|
219
|
+
|
|
220
|
+
# Import only the pydantic names actually referenced.
|
|
221
|
+
pyd_names: list[str] = []
|
|
222
|
+
if entity.super_data is None:
|
|
223
|
+
pyd_names.append("BaseModel")
|
|
224
|
+
if uses_field:
|
|
225
|
+
pyd_names.append("Field")
|
|
226
|
+
|
|
227
|
+
parts: list[str] = [
|
|
228
|
+
generated_header(entity.name, _effective_fqn(entity)),
|
|
229
|
+
"from __future__ import annotations",
|
|
230
|
+
"",
|
|
231
|
+
]
|
|
232
|
+
extra_imports = sorted(imports)
|
|
233
|
+
if extra_imports:
|
|
234
|
+
parts += [*extra_imports, ""]
|
|
235
|
+
if pyd_names:
|
|
236
|
+
parts += [f"from pydantic import {', '.join(pyd_names)}", ""]
|
|
237
|
+
parts += ["", self._emit_class_header(entity, base_class), *body, ""]
|
|
238
|
+
return "\n".join(parts)
|
|
239
|
+
|
|
240
|
+
def generate(self, ctx: GenContext) -> list[EmittedFile]:
|
|
241
|
+
index = build_object_index(ctx.entities)
|
|
242
|
+
return per_entity(
|
|
243
|
+
lambda e, _c: EmittedFile(
|
|
244
|
+
path=f"{e.name}.py",
|
|
245
|
+
content=ruff_format(self.render_entity_model(e, index)),
|
|
246
|
+
)
|
|
247
|
+
)(ctx)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def render_entity_model(
|
|
251
|
+
entity: MetaObject, object_index: dict[str, MetaObject] | None = None
|
|
252
|
+
) -> str:
|
|
253
|
+
"""Module-level back-compat wrapper. Delegates to a default
|
|
254
|
+
:class:`EntityModelGenerator` instance so existing callers (and the golden
|
|
255
|
+
tests) are unaffected. Subclass :class:`EntityModelGenerator` to customize."""
|
|
256
|
+
return EntityModelGenerator().render_entity_model(entity, object_index)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def entity_model() -> Generator:
|
|
260
|
+
"""Generator factory: object.* → a Pydantic model module per object.
|
|
261
|
+
|
|
262
|
+
Returns an :class:`EntityModelGenerator` (subclassable extension seam)."""
|
|
263
|
+
return EntityModelGenerator()
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Extractor codegen — one ``<template_name>_extractor.py`` per ``template.output``.
|
|
2
|
+
|
|
3
|
+
The ``extract`` tier (cross-port parity with the Java ``ExtractorCodeGenerator``, the
|
|
4
|
+
TS ``renderExtractor``, and the Kotlin / C# ports) sits OVER the existing tolerant
|
|
5
|
+
extract. It turns dirty LLM text into the STRICT typed payload graph (nested objects +
|
|
6
|
+
arrays-of-objects populated) in ONE call:
|
|
7
|
+
|
|
8
|
+
extract_<snake>(root, text, opts=None) -> <Template>Payload
|
|
9
|
+
r = extract_<snake>_with_loader(root, text, opts) # nested-capable extract
|
|
10
|
+
if r.report.has_lost_required(): raise ValueError(...)
|
|
11
|
+
return _to_strict_<RootVo>(r.data) # mirror -> strict mapper
|
|
12
|
+
|
|
13
|
+
Why the loaded ``root``: the SELF-CONTAINED ``extract_<snake>(text)`` leaves nested
|
|
14
|
+
objects ``None`` (the historical FR-010 gap — it only maps a flat dict). The
|
|
15
|
+
nested-capable path is ``extract_<snake>_with_loader(root, text, opts)`` (emitted by
|
|
16
|
+
``output_parser_generator``), which delegates to the metadata-driven runtime extract and
|
|
17
|
+
assembles the FULL nested graph reflection-free. So ``extract`` / the re-exposed
|
|
18
|
+
``extract`` are loader (``MetaRoot``)-driven, mirroring the Java ``extract(loader, text)``
|
|
19
|
+
and the TS ``extract<Name>(root, text)``.
|
|
20
|
+
|
|
21
|
+
The extract engine returns an all-nullable ``<Template>PayloadExtracted`` mirror (nested
|
|
22
|
+
VOs as ``<Vo>Extracted``, arrays as ``list[...]``). ``extract`` maps that onto the strict
|
|
23
|
+
``<Template>Payload`` Pydantic model (nested VOs as ``<Vo>Payload``, arrays as
|
|
24
|
+
``list[<Vo>Payload]``) via a generated recursive ``_to_strict_<vo>`` mapper — one per
|
|
25
|
+
value-object reachable through nested ``@objectRef`` fields (deduped, cycle-safe). The
|
|
26
|
+
mapper one-shot-constructs each Pydantic model (harmless for Pydantic's mutable models,
|
|
27
|
+
required-by-contract for the C#/Kotlin record ports).
|
|
28
|
+
|
|
29
|
+
NO registry / binding-provider / factory and NO new flavored object-class generation —
|
|
30
|
+
codegen walks the whole type graph statically (the same MetaObject walk the
|
|
31
|
+
extract-schema / payload emitters use). ``extract`` is re-exposed unchanged under its
|
|
32
|
+
public name.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from collections.abc import Callable
|
|
37
|
+
|
|
38
|
+
from metaobjects.codegen import fr010_field_mapping as fm
|
|
39
|
+
from metaobjects.codegen import extract_delegate_emitter as rde
|
|
40
|
+
from metaobjects.codegen.constants import generated_header
|
|
41
|
+
from metaobjects.codegen.format import ruff_format
|
|
42
|
+
from metaobjects.codegen.generator import EmittedFile, GenContext, Generator
|
|
43
|
+
from metaobjects.codegen.generators.payload_vo_generator import (
|
|
44
|
+
is_field_required,
|
|
45
|
+
payload_class_name,
|
|
46
|
+
payload_module_name,
|
|
47
|
+
resolve_payload_vo,
|
|
48
|
+
)
|
|
49
|
+
from metaobjects.meta.core.field import field_constants as fc
|
|
50
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
51
|
+
from metaobjects.meta.meta_data import MetaData
|
|
52
|
+
from metaobjects.meta.template import template_constants as tc
|
|
53
|
+
from metaobjects.shared.base_types import TYPE_TEMPLATE
|
|
54
|
+
|
|
55
|
+
_GENERATOR_NAME = "extractor-generator"
|
|
56
|
+
|
|
57
|
+
# The extract tier only exists where the tolerant extract API does — json/xml.
|
|
58
|
+
_EXTRACT_FORMATS = frozenset({tc.TEMPLATE_FORMAT_JSON, tc.TEMPLATE_FORMAT_XML})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _snake_case(name: str) -> str:
|
|
62
|
+
"""``OrderOut`` → ``order_out`` (matches the cross-generator convention)."""
|
|
63
|
+
out: list[str] = []
|
|
64
|
+
for i, ch in enumerate(name):
|
|
65
|
+
if ch.isupper() and i > 0:
|
|
66
|
+
out.append("_")
|
|
67
|
+
out.append(ch.lower())
|
|
68
|
+
return "".join(out)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _strict_class(vo: MetaData, root_vo: MetaData, template_name: str) -> str:
|
|
72
|
+
"""The strict Pydantic class name for a value-object. The ROOT payload VO maps to
|
|
73
|
+
the template-named ``<Template>Payload`` (payload_vo emits the primary class under
|
|
74
|
+
the template name); every nested VO maps to ``<Vo>Payload``."""
|
|
75
|
+
if vo.name == root_vo.name:
|
|
76
|
+
return payload_class_name(template_name)
|
|
77
|
+
return payload_class_name(vo.name)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _mapper_name(vo: MetaData) -> str:
|
|
81
|
+
"""``_to_strict_<vo_snake>`` — the recursive mirror→strict mapper for a VO."""
|
|
82
|
+
return f"_to_strict_{_snake_case(vo.name)}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _strict_arg(field: MetaData, root: MetaData) -> str:
|
|
86
|
+
"""The strict-payload initializer expression for one field, reading the mirror
|
|
87
|
+
member ``m.<name>`` and mapping it onto the strict payload's exact optionality
|
|
88
|
+
(``is_field_required`` — shared with payload_vo so there is no skew).
|
|
89
|
+
|
|
90
|
+
* required scalar/enum → ``m.f`` (extract guarantees presence when not lost)
|
|
91
|
+
* optional scalar/enum → ``m.f`` (the strict field is ``T | None``)
|
|
92
|
+
* scalar ARRAY → ``[x for x in (m.f or []) if x is not None]`` (drop the
|
|
93
|
+
mirror's possible-null elements; the strict type is
|
|
94
|
+
``list[T]`` / ``list[T] | None``)
|
|
95
|
+
* single nested object → ``_to_strict_<Vo>(m.f)`` (None-guarded when optional)
|
|
96
|
+
* array-of-objects → ``[_to_strict_<Vo>(e) for e in (m.f or [])]``
|
|
97
|
+
"""
|
|
98
|
+
name = field.name
|
|
99
|
+
required = is_field_required(field)
|
|
100
|
+
|
|
101
|
+
if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
|
|
102
|
+
target = rde.ref_vo(field, root)
|
|
103
|
+
if target is None:
|
|
104
|
+
return f"m.{name}" # unresolved @objectRef — pass the mirror value through
|
|
105
|
+
fn = _mapper_name(target)
|
|
106
|
+
if fm.is_array(field):
|
|
107
|
+
# Required or optional array-of-objects: map present elements (drop Nones).
|
|
108
|
+
return f"[{fn}(e) for e in (m.{name} or [])]" if required else (
|
|
109
|
+
f"([{fn}(e) for e in m.{name}] if m.{name} is not None else None)"
|
|
110
|
+
)
|
|
111
|
+
# Single nested object.
|
|
112
|
+
if required:
|
|
113
|
+
return f"{fn}(m.{name})"
|
|
114
|
+
return f"({fn}(m.{name}) if m.{name} is not None else None)"
|
|
115
|
+
|
|
116
|
+
# Scalar ARRAY: mirror is list[T | None] | None; strict is list[T] (/ | None).
|
|
117
|
+
if fm.is_array(field):
|
|
118
|
+
if required:
|
|
119
|
+
return f"[x for x in (m.{name} or []) if x is not None]"
|
|
120
|
+
return (
|
|
121
|
+
f"([x for x in m.{name} if x is not None] "
|
|
122
|
+
f"if m.{name} is not None else None)"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Scalar / enum (single): pass the mirror value straight through. The strict field
|
|
126
|
+
# is ``T`` when required (extract guarantees presence — lost-required already
|
|
127
|
+
# raised) and ``T | None`` when optional, so a bare ``m.f`` fits both.
|
|
128
|
+
return f"m.{name}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _emit_mapper(vo: MetaData, root: MetaData, root_vo: MetaData, template_name: str) -> list[str]:
|
|
132
|
+
"""One ``_to_strict_<vo>(m) -> <Strict>`` mapper, one-shot-constructing the strict
|
|
133
|
+
Pydantic model from the mirror ``m``."""
|
|
134
|
+
fn = _mapper_name(vo)
|
|
135
|
+
strict = _strict_class(vo, root_vo, template_name)
|
|
136
|
+
lines: list[str] = [
|
|
137
|
+
f"def {fn}(m) -> {strict}:",
|
|
138
|
+
f' """Map the all-nullable extracted mirror onto the strict ``{strict}``.',
|
|
139
|
+
' One-shot constructed; generated."""',
|
|
140
|
+
f" return {strict}(",
|
|
141
|
+
]
|
|
142
|
+
for f in fm.fields(vo):
|
|
143
|
+
lines.append(f" {f.name}={_strict_arg(f, root)},")
|
|
144
|
+
lines.append(" )")
|
|
145
|
+
return lines
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def render_extractor(
|
|
149
|
+
template: MetaData,
|
|
150
|
+
root: MetaData,
|
|
151
|
+
*,
|
|
152
|
+
generator: "ExtractorGenerator | None" = None,
|
|
153
|
+
) -> str | None:
|
|
154
|
+
"""Render one ``<snake>_extractor.py`` for a ``template.output`` node.
|
|
155
|
+
|
|
156
|
+
When *generator* is supplied, its ``_emit_mapper`` override is used for each
|
|
157
|
+
mirror→strict mapper (the extension seam); when ``None`` the module-level
|
|
158
|
+
:func:`_emit_mapper` is used (byte-identical back-compat path).
|
|
159
|
+
|
|
160
|
+
Returns ``None`` when the ``@payloadRef`` can't be resolved to an ``object.value``,
|
|
161
|
+
or when the target ``@format`` is not json/xml (the extract tier requires the
|
|
162
|
+
tolerant extract API, which only the json/xml output-parsers emit)."""
|
|
163
|
+
payload_ref = template.attr(tc.TEMPLATE_ATTR_PAYLOAD_REF)
|
|
164
|
+
if not isinstance(payload_ref, str) or not payload_ref:
|
|
165
|
+
return None
|
|
166
|
+
payload = resolve_payload_vo(root, payload_ref)
|
|
167
|
+
if payload is None:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
fmt = template.attr(tc.TEMPLATE_ATTR_FORMAT)
|
|
171
|
+
fmt_str = fmt if isinstance(fmt, str) else tc.TEMPLATE_FORMAT_DEFAULT
|
|
172
|
+
if fmt_str.lower() not in _EXTRACT_FORMATS:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
template_name = template.name
|
|
176
|
+
snake = _snake_case(template_name)
|
|
177
|
+
parser_module = f"{snake}_output_parser"
|
|
178
|
+
payload_module = payload_module_name(template_name)
|
|
179
|
+
extract_lenient_with_fn = f"extract_lenient_{snake}_with_loader"
|
|
180
|
+
extract_lenient_fn = f"extract_lenient_{snake}"
|
|
181
|
+
extract_fn = f"extract_{snake}"
|
|
182
|
+
root_strict = payload_class_name(template_name)
|
|
183
|
+
root_mapper = _mapper_name(payload)
|
|
184
|
+
|
|
185
|
+
fqn = f"{payload.package}::{template_name}" if payload.package else template_name
|
|
186
|
+
|
|
187
|
+
# The strict payload graph: root payload class (template-named) + every nested
|
|
188
|
+
# VO's ``<Vo>Payload`` (reachable through @objectRef, deduped/cycle-safe — the SAME
|
|
189
|
+
# walk payload_vo emits the nested classes for, so each import resolves).
|
|
190
|
+
vos = rde.reachable_vos(payload, root)
|
|
191
|
+
strict_imports = {root_strict}
|
|
192
|
+
for vo in vos:
|
|
193
|
+
if vo.name != payload.name:
|
|
194
|
+
strict_imports.add(payload_class_name(vo.name))
|
|
195
|
+
|
|
196
|
+
lines: list[str] = [
|
|
197
|
+
generated_header(template_name, fqn),
|
|
198
|
+
"from __future__ import annotations\n",
|
|
199
|
+
f"from .{parser_module} import {extract_lenient_with_fn}",
|
|
200
|
+
f"from .{payload_module} import (",
|
|
201
|
+
]
|
|
202
|
+
for cls in sorted(strict_imports):
|
|
203
|
+
lines.append(f" {cls},")
|
|
204
|
+
lines.append(")")
|
|
205
|
+
lines.append("")
|
|
206
|
+
lines.append("")
|
|
207
|
+
|
|
208
|
+
# extract — extract then map onto the strict payload, raising on lost-required.
|
|
209
|
+
lines.append(f"def {extract_fn}(root, text, opts=None) -> {root_strict}:")
|
|
210
|
+
lines.append(f' """Extract a fully-typed ``{root_strict}`` from dirty ``text`` using the')
|
|
211
|
+
lines.append(f" loaded ``root`` (which must declare the ``{payload.name}`` payload")
|
|
212
|
+
lines.append(" value-object). Runs the tolerant nested-capable extract, then maps the")
|
|
213
|
+
lines.append(" extracted mirror graph onto the strict Pydantic payload graph.")
|
|
214
|
+
lines.append("")
|
|
215
|
+
lines.append(" :raises ValueError: iff a ``@required`` field was lost (the strict gate).")
|
|
216
|
+
lines.append(' """')
|
|
217
|
+
lines.append(f" r = {extract_lenient_with_fn}(root, text, opts)")
|
|
218
|
+
lines.append(" if r.report.has_lost_required():")
|
|
219
|
+
lines.append(" raise ValueError(")
|
|
220
|
+
lines.append(
|
|
221
|
+
f' "{extract_fn}: lost required field(s): "'
|
|
222
|
+
)
|
|
223
|
+
lines.append(' + ", ".join(r.report.lost_required())')
|
|
224
|
+
lines.append(" )")
|
|
225
|
+
lines.append(f" return {root_mapper}(r.data)")
|
|
226
|
+
lines.append("")
|
|
227
|
+
lines.append("")
|
|
228
|
+
|
|
229
|
+
# extract — re-exposed under the public name, delegating to the nested-capable path.
|
|
230
|
+
lines.append(f"def {extract_lenient_fn}(root, text, opts=None):")
|
|
231
|
+
lines.append(f' """Extract a best-effort ``{root_strict}Extracted`` mirror from dirty')
|
|
232
|
+
lines.append(" ``text`` using the loaded ``root``; never raises. Re-exposes the")
|
|
233
|
+
lines.append(" nested-capable extract; inspect ``report`` for lost / defaulted fields.")
|
|
234
|
+
lines.append(' """')
|
|
235
|
+
lines.append(f" return {extract_lenient_with_fn}(root, text, opts)")
|
|
236
|
+
lines.append("")
|
|
237
|
+
lines.append("")
|
|
238
|
+
|
|
239
|
+
# One mirror→strict mapper per reachable VO (root + nested), in BFS order.
|
|
240
|
+
emit_mapper = generator._emit_mapper if generator is not None else _emit_mapper
|
|
241
|
+
for i, vo in enumerate(vos):
|
|
242
|
+
if i > 0:
|
|
243
|
+
lines.append("")
|
|
244
|
+
lines.append("")
|
|
245
|
+
lines.extend(emit_mapper(vo, root, payload, template_name))
|
|
246
|
+
|
|
247
|
+
lines.append("")
|
|
248
|
+
lines.append("")
|
|
249
|
+
lines.append(f'__all__ = ["{extract_fn}", "{extract_lenient_fn}"]')
|
|
250
|
+
lines.append("")
|
|
251
|
+
return "\n".join(lines)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ExtractorGenerator:
|
|
255
|
+
"""Generator wrapping ``render_extractor``. Emits one file per ``template.output``
|
|
256
|
+
declared at root level (mirrors ``OutputParserGenerator``)."""
|
|
257
|
+
|
|
258
|
+
name = _GENERATOR_NAME
|
|
259
|
+
|
|
260
|
+
def __init__(self, *, filter: Callable[[MetaObject], bool] | None = None) -> None:
|
|
261
|
+
self.filter = filter
|
|
262
|
+
|
|
263
|
+
def _emit_mapper(
|
|
264
|
+
self,
|
|
265
|
+
vo: MetaData,
|
|
266
|
+
root: MetaData,
|
|
267
|
+
root_vo: MetaData,
|
|
268
|
+
template_name: str,
|
|
269
|
+
) -> list[str]:
|
|
270
|
+
"""EXTENSION SEAM — one ``_to_strict_<vo>(m) -> <Strict>`` mirror→strict
|
|
271
|
+
mapper block. Defaults to the module-level :func:`_emit_mapper`; override to
|
|
272
|
+
customize how the extracted mirror graph is mapped onto the strict Pydantic
|
|
273
|
+
payload (e.g. coercion, post-validation, default-filling)."""
|
|
274
|
+
return _emit_mapper(vo, root, root_vo, template_name)
|
|
275
|
+
|
|
276
|
+
def _render_module(self, template: MetaData, root: MetaData) -> str | None:
|
|
277
|
+
"""EXTENSION SEAM — render the whole extractor module for one
|
|
278
|
+
``template.output``. Defaults to :func:`render_extractor` (passing this
|
|
279
|
+
instance so the ``_emit_mapper`` override is honored). Override to
|
|
280
|
+
pre/post-process the emitted source or replace the render path."""
|
|
281
|
+
return render_extractor(template, root, generator=self)
|
|
282
|
+
|
|
283
|
+
def generate(self, ctx: GenContext) -> list[EmittedFile]:
|
|
284
|
+
root = ctx.loaded_root
|
|
285
|
+
if root is None:
|
|
286
|
+
return []
|
|
287
|
+
files: list[EmittedFile] = []
|
|
288
|
+
outputs = sorted(
|
|
289
|
+
(
|
|
290
|
+
c
|
|
291
|
+
for c in root.own_children()
|
|
292
|
+
if c.type == TYPE_TEMPLATE and c.sub_type == tc.TEMPLATE_SUBTYPE_OUTPUT
|
|
293
|
+
),
|
|
294
|
+
key=lambda c: c.name,
|
|
295
|
+
)
|
|
296
|
+
for tmpl in outputs:
|
|
297
|
+
content = self._render_module(tmpl, root)
|
|
298
|
+
if content is None:
|
|
299
|
+
ctx.warn(
|
|
300
|
+
f"{_GENERATOR_NAME}: skipping template.output "
|
|
301
|
+
f"'{tmpl.name}' (no resolvable @payloadRef or non-json/xml format)."
|
|
302
|
+
)
|
|
303
|
+
continue
|
|
304
|
+
files.append(
|
|
305
|
+
EmittedFile(
|
|
306
|
+
path=f"{_snake_case(tmpl.name)}_extractor.py",
|
|
307
|
+
content=ruff_format(content),
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
return files
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def extractor_generator(
|
|
314
|
+
*, filter: Callable[[MetaObject], bool] | None = None
|
|
315
|
+
) -> Generator:
|
|
316
|
+
"""Factory mirroring the TS ``extractor()`` and the Java ``ExtractorCodeGenerator``."""
|
|
317
|
+
return ExtractorGenerator(filter=filter)
|