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.
Files changed (181) hide show
  1. metaobjects/__init__.py +75 -0
  2. metaobjects/agent_context/__init__.py +55 -0
  3. metaobjects/agent_context/_content/README.md +14 -0
  4. metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
  5. metaobjects/agent_context/_content/servers/java.meta.json +5 -0
  6. metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
  7. metaobjects/agent_context/_content/servers/python.meta.json +5 -0
  8. metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
  9. metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
  10. metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
  11. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
  12. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
  13. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
  14. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
  15. metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
  16. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
  17. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
  18. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
  19. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
  20. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
  21. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
  22. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
  23. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
  24. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
  25. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
  26. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
  27. metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
  28. metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
  29. metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
  30. metaobjects/agent_context/assemble.py +133 -0
  31. metaobjects/agent_context/content_root.py +54 -0
  32. metaobjects/agent_context/scaffold.py +191 -0
  33. metaobjects/agent_context/types.py +44 -0
  34. metaobjects/attr_class_map.py +23 -0
  35. metaobjects/cli.py +696 -0
  36. metaobjects/codegen/__init__.py +0 -0
  37. metaobjects/codegen/config.py +11 -0
  38. metaobjects/codegen/constants.py +13 -0
  39. metaobjects/codegen/extract_delegate_emitter.py +384 -0
  40. metaobjects/codegen/extract_schema_emitter.py +139 -0
  41. metaobjects/codegen/format.py +31 -0
  42. metaobjects/codegen/fr010_field_mapping.py +220 -0
  43. metaobjects/codegen/generator.py +62 -0
  44. metaobjects/codegen/generator_registry.py +163 -0
  45. metaobjects/codegen/generators/__init__.py +0 -0
  46. metaobjects/codegen/generators/entity_model.py +263 -0
  47. metaobjects/codegen/generators/extractor_generator.py +317 -0
  48. metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
  49. metaobjects/codegen/generators/m2m_codegen.py +192 -0
  50. metaobjects/codegen/generators/output_parser_generator.py +272 -0
  51. metaobjects/codegen/generators/output_prompt_generator.py +192 -0
  52. metaobjects/codegen/generators/payload_vo_generator.py +672 -0
  53. metaobjects/codegen/generators/render_helper_generator.py +451 -0
  54. metaobjects/codegen/generators/router_generator.py +635 -0
  55. metaobjects/codegen/generators/template_generator.py +70 -0
  56. metaobjects/codegen/generators/tph_plan.py +120 -0
  57. metaobjects/codegen/generators/trace_helper_generator.py +336 -0
  58. metaobjects/codegen/instance_artifacts.py +15 -0
  59. metaobjects/codegen/output_format_spec_emitter.py +79 -0
  60. metaobjects/codegen/overwrite_policy.py +27 -0
  61. metaobjects/codegen/runner.py +110 -0
  62. metaobjects/codegen/runtime/__init__.py +6 -0
  63. metaobjects/codegen/runtime/filter_parser.py +193 -0
  64. metaobjects/codegen/type_map.py +84 -0
  65. metaobjects/core_types.py +809 -0
  66. metaobjects/datatype.py +19 -0
  67. metaobjects/documentation/__init__.py +28 -0
  68. metaobjects/documentation/doc_constants.py +20 -0
  69. metaobjects/documentation/doc_provider.py +20 -0
  70. metaobjects/documentation/doc_schema.py +24 -0
  71. metaobjects/errors.py +124 -0
  72. metaobjects/loader/__init__.py +0 -0
  73. metaobjects/loader/merge.py +287 -0
  74. metaobjects/loader/meta_data_loader.py +245 -0
  75. metaobjects/loader/sources/__init__.py +24 -0
  76. metaobjects/loader/sources/directory_source.py +50 -0
  77. metaobjects/loader/sources/file_source.py +41 -0
  78. metaobjects/loader/sources/meta_data_source.py +67 -0
  79. metaobjects/loader/sources/uri_source.py +56 -0
  80. metaobjects/loader/validate_discriminator.py +181 -0
  81. metaobjects/loader/validate_field_readonly.py +146 -0
  82. metaobjects/loader/validate_source_parameter_ref.py +159 -0
  83. metaobjects/loader/validate_source_physical_names.py +140 -0
  84. metaobjects/loader/validation_passes.py +1513 -0
  85. metaobjects/meta/__init__.py +1 -0
  86. metaobjects/meta/core/__init__.py +0 -0
  87. metaobjects/meta/core/attr/__init__.py +0 -0
  88. metaobjects/meta/core/attr/attr_constants.py +31 -0
  89. metaobjects/meta/core/attr/meta_attr.py +136 -0
  90. metaobjects/meta/core/field/__init__.py +0 -0
  91. metaobjects/meta/core/field/field_constants.py +105 -0
  92. metaobjects/meta/core/field/meta_field.py +76 -0
  93. metaobjects/meta/core/identity/__init__.py +0 -0
  94. metaobjects/meta/core/identity/identity_constants.py +19 -0
  95. metaobjects/meta/core/identity/meta_identity.py +8 -0
  96. metaobjects/meta/core/object/__init__.py +0 -0
  97. metaobjects/meta/core/object/meta_object.py +65 -0
  98. metaobjects/meta/core/object/meta_object_aware.py +43 -0
  99. metaobjects/meta/core/object/object_class_registry.py +56 -0
  100. metaobjects/meta/core/object/object_constants.py +13 -0
  101. metaobjects/meta/core/object/object_extract.py +400 -0
  102. metaobjects/meta/core/object/value_object.py +70 -0
  103. metaobjects/meta/core/relationship/__init__.py +0 -0
  104. metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
  105. metaobjects/meta/core/relationship/meta_relationship.py +54 -0
  106. metaobjects/meta/core/relationship/relationship_constants.py +51 -0
  107. metaobjects/meta/core/validator/__init__.py +0 -0
  108. metaobjects/meta/core/validator/validator_constants.py +18 -0
  109. metaobjects/meta/meta_data.py +206 -0
  110. metaobjects/meta/meta_root.py +8 -0
  111. metaobjects/meta/persistence/__init__.py +0 -0
  112. metaobjects/meta/persistence/db/__init__.py +1 -0
  113. metaobjects/meta/persistence/db/db_constants.py +41 -0
  114. metaobjects/meta/persistence/db/db_provider.py +60 -0
  115. metaobjects/meta/persistence/origin/__init__.py +0 -0
  116. metaobjects/meta/persistence/origin/meta_origin.py +8 -0
  117. metaobjects/meta/persistence/origin/origin_constants.py +20 -0
  118. metaobjects/meta/persistence/source/__init__.py +0 -0
  119. metaobjects/meta/persistence/source/meta_source.py +137 -0
  120. metaobjects/meta/persistence/source/source_constants.py +115 -0
  121. metaobjects/meta/presentation/__init__.py +0 -0
  122. metaobjects/meta/presentation/layout/__init__.py +0 -0
  123. metaobjects/meta/presentation/layout/layout_constants.py +13 -0
  124. metaobjects/meta/presentation/layout/meta_layout.py +8 -0
  125. metaobjects/meta/presentation/view/__init__.py +0 -0
  126. metaobjects/meta/presentation/view/meta_view.py +8 -0
  127. metaobjects/meta/presentation/view/view_constants.py +22 -0
  128. metaobjects/meta/template/__init__.py +0 -0
  129. metaobjects/meta/template/meta_template.py +46 -0
  130. metaobjects/meta/template/template_constants.py +112 -0
  131. metaobjects/meta/template/template_provider.py +43 -0
  132. metaobjects/parser.py +380 -0
  133. metaobjects/parser_yaml.py +82 -0
  134. metaobjects/provider.py +111 -0
  135. metaobjects/py.typed +0 -0
  136. metaobjects/registry.py +210 -0
  137. metaobjects/registry_manifest.py +223 -0
  138. metaobjects/render/__init__.py +74 -0
  139. metaobjects/render/email_document.py +14 -0
  140. metaobjects/render/escapers.py +109 -0
  141. metaobjects/render/extract/__init__.py +59 -0
  142. metaobjects/render/extract/coerce.py +279 -0
  143. metaobjects/render/extract/extract.py +211 -0
  144. metaobjects/render/extract/extract_map.py +61 -0
  145. metaobjects/render/extract/json_forgiving_reader.py +203 -0
  146. metaobjects/render/extract/locate.py +65 -0
  147. metaobjects/render/extract/normalize.py +96 -0
  148. metaobjects/render/extract/strip.py +20 -0
  149. metaobjects/render/extract/types.py +332 -0
  150. metaobjects/render/extract/xml_forgiving_reader.py +162 -0
  151. metaobjects/render/filesystem_provider.py +51 -0
  152. metaobjects/render/prompt/__init__.py +32 -0
  153. metaobjects/render/prompt/output_format_renderer.py +340 -0
  154. metaobjects/render/prompt/output_format_spec.py +28 -0
  155. metaobjects/render/prompt/prompt_field.py +29 -0
  156. metaobjects/render/prompt/prompt_overrides.py +29 -0
  157. metaobjects/render/prompt/prompt_style.py +38 -0
  158. metaobjects/render/renderer.py +358 -0
  159. metaobjects/render/verify.py +266 -0
  160. metaobjects/runtime/__init__.py +39 -0
  161. metaobjects/runtime/llm_recorder.py +210 -0
  162. metaobjects/runtime/n2m_resolver.py +155 -0
  163. metaobjects/runtime/object_manager.py +715 -0
  164. metaobjects/runtime/tph.py +50 -0
  165. metaobjects/serializer_json.py +172 -0
  166. metaobjects/shared/__init__.py +0 -0
  167. metaobjects/shared/base_types.py +16 -0
  168. metaobjects/shared/separators.py +4 -0
  169. metaobjects/shared/structural.py +9 -0
  170. metaobjects/source/__init__.py +79 -0
  171. metaobjects/source/error_source.py +266 -0
  172. metaobjects/source/json_path.py +106 -0
  173. metaobjects/source/semantic_diff.py +98 -0
  174. metaobjects/source/yaml_positions.py +174 -0
  175. metaobjects/super_resolve.py +128 -0
  176. metaobjects/yaml_desugar.py +481 -0
  177. metaobjects-0.9.0.dist-info/METADATA +97 -0
  178. metaobjects-0.9.0.dist-info/RECORD +181 -0
  179. metaobjects-0.9.0.dist-info/WHEEL +4 -0
  180. metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
  181. 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)