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,481 @@
|
|
|
1
|
+
"""YAML authoring -> canonical desugar (ADR-0006).
|
|
2
|
+
|
|
3
|
+
Mirrors the TS reference at server/typescript/packages/metadata/src/core/yaml-desugar.ts.
|
|
4
|
+
|
|
5
|
+
desugar() turns the sugared authoring object (from yaml.safe_load) into the
|
|
6
|
+
canonical-shaped object that parse_document (parser.py) consumes. It applies
|
|
7
|
+
the five format-spec sugar rules:
|
|
8
|
+
|
|
9
|
+
1. Fused key, subType omittable - a bare `type` key resolves to the type's
|
|
10
|
+
registry default subType.
|
|
11
|
+
2. Scalar-or-map body - a scalar body becomes { name: <scalar> }.
|
|
12
|
+
3. Omit empties - absent keys stay absent; the desugar invents nothing.
|
|
13
|
+
4. `[]` arrays - a trailing `[]` on the key strips to isArray: true.
|
|
14
|
+
5. Sigil-free attributes (ADR-0006 D1) - every body key not in
|
|
15
|
+
RESERVED_KEYS is treated as an inline attribute and re-prefixed with `@`
|
|
16
|
+
when lowering to canonical JSON. Keys already prefixed with `@` are
|
|
17
|
+
left as-is (backward-compat). Reserved structural keywords (name,
|
|
18
|
+
package, extends, abstract, overlay, isArray, children, value) stay
|
|
19
|
+
bare. Note: an already-`@`-prefixed reserved word remains an error
|
|
20
|
+
downstream - parser.py's ERR_RESERVED_ATTR check fires.
|
|
21
|
+
|
|
22
|
+
In addition, the desugar runs the ADR-0006 D2 type-coercion guard: for every
|
|
23
|
+
inline attr whose owning (type, subType) has a declared schema, if the raw
|
|
24
|
+
Python value's type does not match the declared valueType AND the value is
|
|
25
|
+
one of YAML 1.2's silently coerced shapes (bool/int/float/None), the desugar
|
|
26
|
+
collects an ERR_YAML_COERCION error telling the author to quote the value.
|
|
27
|
+
|
|
28
|
+
Pure and total: it never throws. Malformed fragments are collected as
|
|
29
|
+
CollectedError entries (a string message + optional stable error code) and a
|
|
30
|
+
safe placeholder is substituted so parse_document does not double-report.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from .errors import ErrorCode
|
|
38
|
+
from .meta.core.attr.attr_constants import (
|
|
39
|
+
ATTR_SUBTYPE_BOOLEAN,
|
|
40
|
+
ATTR_SUBTYPE_CLASS,
|
|
41
|
+
ATTR_SUBTYPE_DOUBLE,
|
|
42
|
+
ATTR_SUBTYPE_INT,
|
|
43
|
+
ATTR_SUBTYPE_LONG,
|
|
44
|
+
ATTR_SUBTYPE_STRING,
|
|
45
|
+
ATTR_SUBTYPE_STRINGARRAY,
|
|
46
|
+
)
|
|
47
|
+
from .registry import AttrSchema, TypeRegistry
|
|
48
|
+
from .shared.separators import ATTR_PREFIX, FUSED_KEY_SEP
|
|
49
|
+
from .shared.structural import (
|
|
50
|
+
KEY_ABSTRACT,
|
|
51
|
+
KEY_CHILDREN,
|
|
52
|
+
KEY_EXTENDS,
|
|
53
|
+
KEY_IS_ARRAY,
|
|
54
|
+
KEY_NAME,
|
|
55
|
+
KEY_OVERLAY,
|
|
56
|
+
KEY_PACKAGE,
|
|
57
|
+
KEY_VALUE,
|
|
58
|
+
)
|
|
59
|
+
from .source import YamlPosition
|
|
60
|
+
from .source.yaml_positions import (
|
|
61
|
+
YamlPositionMap,
|
|
62
|
+
get_position_map,
|
|
63
|
+
set_position_map,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
ARRAY_SUFFIX = "[]"
|
|
67
|
+
|
|
68
|
+
RESERVED_KEYS: frozenset[str] = frozenset({
|
|
69
|
+
KEY_NAME,
|
|
70
|
+
KEY_PACKAGE,
|
|
71
|
+
KEY_EXTENDS,
|
|
72
|
+
KEY_ABSTRACT,
|
|
73
|
+
KEY_OVERLAY,
|
|
74
|
+
KEY_IS_ARRAY,
|
|
75
|
+
KEY_CHILDREN,
|
|
76
|
+
KEY_VALUE,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class CollectedError:
|
|
82
|
+
"""A collected desugar problem - message plus optional stable error code.
|
|
83
|
+
|
|
84
|
+
Absent codes map to ERR_MALFORMED_YAML in parser_yaml.py (matches TS).
|
|
85
|
+
"""
|
|
86
|
+
message: str
|
|
87
|
+
code: ErrorCode | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class DesugarResult:
|
|
92
|
+
"""Outcome of desugaring a parsed-YAML document."""
|
|
93
|
+
canonical: dict[str, Any] = field(default_factory=dict)
|
|
94
|
+
errors: list[CollectedError] = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def desugar(input_obj: object, registry: TypeRegistry) -> DesugarResult:
|
|
98
|
+
"""Desugar a parsed-YAML authoring document into a canonical-shaped object."""
|
|
99
|
+
errors: list[CollectedError] = []
|
|
100
|
+
node = _desugar_node(input_obj, registry, errors, "<root>")
|
|
101
|
+
return DesugarResult(canonical=node if node is not None else {}, errors=errors)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _desugar_node(
|
|
105
|
+
input_obj: object,
|
|
106
|
+
registry: TypeRegistry,
|
|
107
|
+
errors: list[CollectedError],
|
|
108
|
+
path: str,
|
|
109
|
+
) -> dict[str, Any] | None:
|
|
110
|
+
"""Desugar one node - a single-key mapping { "type.subType": body }.
|
|
111
|
+
|
|
112
|
+
Returns the canonical node object, or None if `input_obj` is not a usable
|
|
113
|
+
node (the caller substitutes a placeholder).
|
|
114
|
+
"""
|
|
115
|
+
if not isinstance(input_obj, dict):
|
|
116
|
+
errors.append(CollectedError(
|
|
117
|
+
message=f"Node at {path} must be a mapping with one type key",
|
|
118
|
+
))
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
keys = list(input_obj.keys())
|
|
122
|
+
if len(keys) != 1:
|
|
123
|
+
found = "none" if not keys else ", ".join(str(k) for k in keys)
|
|
124
|
+
errors.append(CollectedError(
|
|
125
|
+
message=(
|
|
126
|
+
f"Node at {path} must have exactly one type key "
|
|
127
|
+
f"(found: {found})"
|
|
128
|
+
),
|
|
129
|
+
))
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
raw_key = keys[0]
|
|
133
|
+
if not isinstance(raw_key, str):
|
|
134
|
+
errors.append(CollectedError(
|
|
135
|
+
message=f"Node key at {path} must be a string, got {type(raw_key).__name__}",
|
|
136
|
+
))
|
|
137
|
+
return None
|
|
138
|
+
raw_body = input_obj[raw_key]
|
|
139
|
+
|
|
140
|
+
# FR5b - capture the wrapper-level position-by-key map BEFORE re-keying.
|
|
141
|
+
# The author's raw key (with `[]` suffix and possibly omitted subType) is
|
|
142
|
+
# the lookup key; the desugar's canonical key is what we emit.
|
|
143
|
+
wrapper_positions = get_position_map(input_obj)
|
|
144
|
+
|
|
145
|
+
# Rule 4: a trailing "[]" on the key -> isArray.
|
|
146
|
+
key = raw_key
|
|
147
|
+
is_array = False
|
|
148
|
+
if key.endswith(ARRAY_SUFFIX):
|
|
149
|
+
key = key[: -len(ARRAY_SUFFIX)]
|
|
150
|
+
is_array = True
|
|
151
|
+
|
|
152
|
+
# Rule 1: a bare `type` key -> the type's registry default subType.
|
|
153
|
+
canonical_key = _resolve_key(key, registry, errors, path)
|
|
154
|
+
|
|
155
|
+
# FR5b - position of the wrapper key (the `field.string:` line). Used to
|
|
156
|
+
# back-fill `yaml_position` on synthesized bodies (Rule 2 scalar lift) and
|
|
157
|
+
# on the canonical wrapper after Rule 1 / Rule 4 rewrites.
|
|
158
|
+
wrapper_key_pos = (
|
|
159
|
+
wrapper_positions.get(raw_key) if wrapper_positions is not None else None
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Rule 2: a scalar body -> { name: <scalar> }.
|
|
163
|
+
body = _desugar_body(
|
|
164
|
+
raw_body, registry, canonical_key, errors, path, wrapper_key_pos,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Rule 4 (cont.): stamp isArray onto the canonical body.
|
|
168
|
+
if is_array:
|
|
169
|
+
body[KEY_IS_ARRAY] = True
|
|
170
|
+
|
|
171
|
+
# Recurse into children.
|
|
172
|
+
raw_children = body.get(KEY_CHILDREN)
|
|
173
|
+
if isinstance(raw_children, list):
|
|
174
|
+
children: list[Any] = []
|
|
175
|
+
for i, raw_child in enumerate(raw_children):
|
|
176
|
+
child_path = f"{path}.{KEY_CHILDREN}[{i}]"
|
|
177
|
+
child = _desugar_node(raw_child, registry, errors, child_path)
|
|
178
|
+
# On a bad child keep an empty-object placeholder so sibling
|
|
179
|
+
# indices stay stable; the error is already collected.
|
|
180
|
+
children.append(child if child is not None else {})
|
|
181
|
+
body[KEY_CHILDREN] = children
|
|
182
|
+
# A non-list `children` value is left untouched - parse_document reports it.
|
|
183
|
+
|
|
184
|
+
# FR5b - emit a wrapper-level position-by-key map for the canonical wrapper
|
|
185
|
+
# so parse_document's per-child iteration can read the position via the
|
|
186
|
+
# same lookup it uses for JSON input. The single key transformation is
|
|
187
|
+
# raw_key -> canonical_key (Rule 1 fuses the subType, Rule 4 strips `[]`).
|
|
188
|
+
out_wrapper = YamlPositionMap({canonical_key: body})
|
|
189
|
+
if wrapper_key_pos is not None:
|
|
190
|
+
set_position_map(out_wrapper, {canonical_key: wrapper_key_pos})
|
|
191
|
+
return out_wrapper
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _resolve_key(
|
|
195
|
+
key: str,
|
|
196
|
+
registry: TypeRegistry,
|
|
197
|
+
errors: list[CollectedError],
|
|
198
|
+
path: str,
|
|
199
|
+
) -> str:
|
|
200
|
+
"""Rule 1 - resolve a possibly-bare key to a fused `type.subType` token."""
|
|
201
|
+
if FUSED_KEY_SEP in key:
|
|
202
|
+
return key # already fused
|
|
203
|
+
sub_type = registry.default_sub_type_of(key)
|
|
204
|
+
if sub_type is None:
|
|
205
|
+
errors.append(CollectedError(
|
|
206
|
+
message=(
|
|
207
|
+
f"Cannot resolve subType for bare type key '{key}' at {path} - "
|
|
208
|
+
f"type '{key}' has no default subType; write the full 'type.subType'"
|
|
209
|
+
),
|
|
210
|
+
))
|
|
211
|
+
return key # pass through; parse_document reports the unknown type
|
|
212
|
+
return f"{key}{FUSED_KEY_SEP}{sub_type}"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _desugar_body(
|
|
216
|
+
raw_body: object,
|
|
217
|
+
registry: TypeRegistry,
|
|
218
|
+
canonical_key: str,
|
|
219
|
+
errors: list[CollectedError],
|
|
220
|
+
path: str,
|
|
221
|
+
wrapper_key_pos: YamlPosition | None = None,
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
"""Rule 2 + 5 - normalize a node body into a canonical mapping.
|
|
224
|
+
|
|
225
|
+
Reserved structural keys stay bare; every other key is treated as an inline
|
|
226
|
+
attribute and `@`-prefixed (Rule 5 / ADR-0006 D1). Keys already starting
|
|
227
|
+
with `@` are kept as-authored so the awkward "@column: foo" form remains
|
|
228
|
+
accepted.
|
|
229
|
+
|
|
230
|
+
Also runs the D2 type-coercion guard: for each inline attr that the owning
|
|
231
|
+
(type, subType) declares with a typed `value_type`, if the raw Python
|
|
232
|
+
value's type was silently coerced by YAML 1.2 to something incompatible
|
|
233
|
+
(e.g. a `bool` for a `string`-declared attr), an ERR_YAML_COERCION is
|
|
234
|
+
collected.
|
|
235
|
+
|
|
236
|
+
FR5b — *wrapper_key_pos* is the YAML position of the wrapper key (the
|
|
237
|
+
``field.string:`` line) used to back-fill ``yaml_position`` on synthesized
|
|
238
|
+
bodies (Rule 2 scalar lift). Reserved structural keys keep their bare
|
|
239
|
+
form and carry their own positions from the source body's position map;
|
|
240
|
+
sigil-free attrs (Rule 5) re-key the position map across the ``@``-prefix
|
|
241
|
+
rewrite.
|
|
242
|
+
"""
|
|
243
|
+
if isinstance(raw_body, str) or isinstance(raw_body, bool) or (
|
|
244
|
+
isinstance(raw_body, (int, float)) and not isinstance(raw_body, bool)
|
|
245
|
+
):
|
|
246
|
+
# FR5b - the synthesized `{ name: rawBody }` has no YAML-side
|
|
247
|
+
# counterpart; we attribute the `name` slot to the wrapper-key's
|
|
248
|
+
# position (the only YAML position that meaningfully belongs to
|
|
249
|
+
# this synthesis).
|
|
250
|
+
out_scalar = YamlPositionMap({KEY_NAME: raw_body})
|
|
251
|
+
if wrapper_key_pos is not None:
|
|
252
|
+
set_position_map(out_scalar, {KEY_NAME: wrapper_key_pos})
|
|
253
|
+
return out_scalar
|
|
254
|
+
if raw_body is None:
|
|
255
|
+
# An empty body (`field.string:` with nothing after) -> an empty node.
|
|
256
|
+
# No body keys to position; the wrapper carries the node's position.
|
|
257
|
+
return YamlPositionMap()
|
|
258
|
+
if isinstance(raw_body, list):
|
|
259
|
+
errors.append(CollectedError(
|
|
260
|
+
message=f"Node body at {path} must be a scalar or mapping, not a list",
|
|
261
|
+
))
|
|
262
|
+
return YamlPositionMap()
|
|
263
|
+
if not isinstance(raw_body, dict):
|
|
264
|
+
# Catch-all for non-dict, non-scalar shapes (e.g. tuples).
|
|
265
|
+
errors.append(CollectedError(
|
|
266
|
+
message=f"Node body at {path} must be a scalar or mapping",
|
|
267
|
+
))
|
|
268
|
+
return YamlPositionMap()
|
|
269
|
+
|
|
270
|
+
# A mapping - shallow-copy so isArray / children replacement do not mutate
|
|
271
|
+
# the caller's parsed-YAML object, AND apply Rule 5 (sigil-free attrs) +
|
|
272
|
+
# Rule D2 (type-coercion guard).
|
|
273
|
+
out = YamlPositionMap()
|
|
274
|
+
schema_index = _attr_schema_index(registry, canonical_key)
|
|
275
|
+
# FR5b - translate the body's position-by-key map across the sigil-free
|
|
276
|
+
# rewrite. A bare `filterable` key in the source maps to `@filterable`
|
|
277
|
+
# in the canonical body; the YAML position belongs to BOTH names (the
|
|
278
|
+
# YAML author only wrote one). We re-key the position map to match
|
|
279
|
+
# the canonical body's keys so parse_document's per-attr inspection
|
|
280
|
+
# can find the position via the canonical key.
|
|
281
|
+
src_positions = get_position_map(raw_body)
|
|
282
|
+
out_positions: dict[str, YamlPosition] = {}
|
|
283
|
+
for key, value in raw_body.items():
|
|
284
|
+
if not isinstance(key, str):
|
|
285
|
+
errors.append(CollectedError(
|
|
286
|
+
message=(
|
|
287
|
+
f"Body key at {path} must be a string, got "
|
|
288
|
+
f"{type(key).__name__}"
|
|
289
|
+
),
|
|
290
|
+
))
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
if key in RESERVED_KEYS or key.startswith(ATTR_PREFIX):
|
|
294
|
+
out[key] = value
|
|
295
|
+
out_key = key
|
|
296
|
+
# D2 also applies to author-written @-keys (the awkward form).
|
|
297
|
+
if key.startswith(ATTR_PREFIX):
|
|
298
|
+
attr_name = key[len(ATTR_PREFIX):]
|
|
299
|
+
if attr_name and attr_name not in RESERVED_KEYS:
|
|
300
|
+
_check_coercion(attr_name, value, schema_index, errors, path)
|
|
301
|
+
else:
|
|
302
|
+
out_key = f"{ATTR_PREFIX}{key}"
|
|
303
|
+
out[out_key] = value
|
|
304
|
+
_check_coercion(key, value, schema_index, errors, path)
|
|
305
|
+
|
|
306
|
+
if src_positions is not None:
|
|
307
|
+
pos = src_positions.get(key)
|
|
308
|
+
if pos is not None:
|
|
309
|
+
out_positions[out_key] = pos
|
|
310
|
+
|
|
311
|
+
if out_positions:
|
|
312
|
+
set_position_map(out, out_positions)
|
|
313
|
+
return out
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# D2 - YAML type-coercion guard
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _attr_schema_index(
|
|
322
|
+
registry: TypeRegistry,
|
|
323
|
+
canonical_key: str,
|
|
324
|
+
) -> dict[str, AttrSchema] | None:
|
|
325
|
+
"""Build a name -> AttrSchema map for the given canonical key (type.subType).
|
|
326
|
+
|
|
327
|
+
Returns None when the key has no declared attrs (open schema).
|
|
328
|
+
"""
|
|
329
|
+
# canonical_key is "type.subType" - split on the FIRST dot only so a subType
|
|
330
|
+
# that happens to contain a dot still works.
|
|
331
|
+
dot = canonical_key.find(FUSED_KEY_SEP)
|
|
332
|
+
if dot < 0:
|
|
333
|
+
return None
|
|
334
|
+
type_ = canonical_key[:dot]
|
|
335
|
+
sub_type = canonical_key[dot + 1:]
|
|
336
|
+
schemas = registry.attrs_of(type_, sub_type)
|
|
337
|
+
if not schemas:
|
|
338
|
+
return None
|
|
339
|
+
return {spec.name: spec for spec in schemas}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _check_coercion(
|
|
343
|
+
attr_name: str,
|
|
344
|
+
raw: object,
|
|
345
|
+
schema_index: dict[str, AttrSchema] | None,
|
|
346
|
+
errors: list[CollectedError],
|
|
347
|
+
path: str,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Check a single attr value against its declared schema's `value_type`.
|
|
350
|
+
|
|
351
|
+
Emits ERR_YAML_COERCION when YAML 1.2's core schema silently changed the
|
|
352
|
+
Python type (bool/int/float/None where a string/stringArray was declared,
|
|
353
|
+
or vice versa for booleans/numbers).
|
|
354
|
+
"""
|
|
355
|
+
if schema_index is None:
|
|
356
|
+
return
|
|
357
|
+
spec = schema_index.get(attr_name)
|
|
358
|
+
if spec is None or spec.value_type is None:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# Array-valued attrs (the `string` + is_array model that replaced the
|
|
362
|
+
# `stringarray` subtype): validate as a string array regardless of the scalar
|
|
363
|
+
# value_type token. Also tolerate a legacy stringarray token.
|
|
364
|
+
if spec.is_array or spec.value_type == ATTR_SUBTYPE_STRINGARRAY:
|
|
365
|
+
if isinstance(raw, str):
|
|
366
|
+
return
|
|
367
|
+
if not isinstance(raw, list):
|
|
368
|
+
_emit_coercion(
|
|
369
|
+
attr_name, raw, "string-array (or single string)", errors, path,
|
|
370
|
+
)
|
|
371
|
+
return
|
|
372
|
+
for i, elem in enumerate(raw):
|
|
373
|
+
if not isinstance(elem, str):
|
|
374
|
+
_emit_coercion(
|
|
375
|
+
f"{attr_name}[{i}]", elem, "string (in string-array)", errors, path,
|
|
376
|
+
)
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
if spec.value_type in (ATTR_SUBTYPE_STRING, ATTR_SUBTYPE_CLASS):
|
|
380
|
+
if not isinstance(raw, str):
|
|
381
|
+
_emit_coercion(attr_name, raw, "string", errors, path)
|
|
382
|
+
return
|
|
383
|
+
if spec.value_type == ATTR_SUBTYPE_BOOLEAN:
|
|
384
|
+
if not isinstance(raw, bool):
|
|
385
|
+
_emit_coercion(attr_name, raw, "boolean", errors, path)
|
|
386
|
+
return
|
|
387
|
+
if spec.value_type in (ATTR_SUBTYPE_INT, ATTR_SUBTYPE_LONG, ATTR_SUBTYPE_DOUBLE):
|
|
388
|
+
# In Python, bool is a subclass of int; YAML 1.2 boolean literals
|
|
389
|
+
# (true/false/TRUE/FALSE) should not satisfy a numeric attr.
|
|
390
|
+
if isinstance(raw, bool) or not isinstance(raw, (int, float)):
|
|
391
|
+
_emit_coercion(attr_name, raw, "number", errors, path)
|
|
392
|
+
return
|
|
393
|
+
if spec.value_type == ATTR_SUBTYPE_STRINGARRAY:
|
|
394
|
+
# A bare string at the value position is the legitimate one-element
|
|
395
|
+
# authoring shorthand for a string-array attr (StringArrayAttr.coerce
|
|
396
|
+
# wraps it into a one-element array). It is NOT a coercion. A
|
|
397
|
+
# non-string non-array scalar (boolean/number/null), however, is.
|
|
398
|
+
if isinstance(raw, str):
|
|
399
|
+
return
|
|
400
|
+
if not isinstance(raw, list):
|
|
401
|
+
_emit_coercion(
|
|
402
|
+
attr_name, raw, "string-array (or single string)", errors, path,
|
|
403
|
+
)
|
|
404
|
+
return
|
|
405
|
+
# For a list, check every element. A non-string element is a YAML
|
|
406
|
+
# coercion (e.g. unquoted `true` in a string-array list).
|
|
407
|
+
for i, elem in enumerate(raw):
|
|
408
|
+
if not isinstance(elem, str):
|
|
409
|
+
_emit_coercion(
|
|
410
|
+
f"{attr_name}[{i}]",
|
|
411
|
+
elem,
|
|
412
|
+
"string (in string-array)",
|
|
413
|
+
errors,
|
|
414
|
+
path,
|
|
415
|
+
)
|
|
416
|
+
return
|
|
417
|
+
# Object-shaped attrs (properties, filter) - accept any object/array,
|
|
418
|
+
# no YAML coercion path applies.
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _emit_coercion(
|
|
423
|
+
attr_name: str,
|
|
424
|
+
raw: object,
|
|
425
|
+
expected: str,
|
|
426
|
+
errors: list[CollectedError],
|
|
427
|
+
path: str,
|
|
428
|
+
) -> None:
|
|
429
|
+
"""Build the "quote this value" error.
|
|
430
|
+
|
|
431
|
+
The shape is intentionally explicit so AI authors can act on it: it
|
|
432
|
+
identifies the attr, the (coerced) value, its Python type, the declared
|
|
433
|
+
expected type, and a one-line fix hint.
|
|
434
|
+
"""
|
|
435
|
+
actual_type = _coerced_type_name(raw)
|
|
436
|
+
literal = _literal_repr(raw)
|
|
437
|
+
errors.append(CollectedError(
|
|
438
|
+
message=(
|
|
439
|
+
f"Attribute '@{attr_name}' at {path}: expected {expected} but got "
|
|
440
|
+
f"{actual_type} ({literal}). YAML 1.2 silently coerced an unquoted "
|
|
441
|
+
f"value - quote it in YAML: "
|
|
442
|
+
f"'@{attr_name}: \"{literal}\"' not '@{attr_name}: {literal}'."
|
|
443
|
+
),
|
|
444
|
+
code=ErrorCode.ERR_YAML_COERCION,
|
|
445
|
+
))
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _coerced_type_name(raw: object) -> str:
|
|
449
|
+
if raw is None:
|
|
450
|
+
return "null"
|
|
451
|
+
if isinstance(raw, bool):
|
|
452
|
+
# bool must be checked before int (Python: bool is subclass of int).
|
|
453
|
+
return "boolean"
|
|
454
|
+
if isinstance(raw, int):
|
|
455
|
+
return "number"
|
|
456
|
+
if isinstance(raw, float):
|
|
457
|
+
return "number"
|
|
458
|
+
if isinstance(raw, str):
|
|
459
|
+
return "string"
|
|
460
|
+
if isinstance(raw, list):
|
|
461
|
+
return "array"
|
|
462
|
+
if isinstance(raw, dict):
|
|
463
|
+
return "object"
|
|
464
|
+
return type(raw).__name__
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _literal_repr(raw: object) -> str:
|
|
468
|
+
if raw is None:
|
|
469
|
+
return "null"
|
|
470
|
+
if isinstance(raw, bool):
|
|
471
|
+
return "true" if raw else "false"
|
|
472
|
+
if isinstance(raw, (int, float)):
|
|
473
|
+
return str(raw)
|
|
474
|
+
if isinstance(raw, str):
|
|
475
|
+
return raw
|
|
476
|
+
# Best-effort JSON-style for non-scalar (for the error message only).
|
|
477
|
+
import json
|
|
478
|
+
try:
|
|
479
|
+
return json.dumps(raw)
|
|
480
|
+
except (TypeError, ValueError):
|
|
481
|
+
return repr(raw)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metaobjects
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Cross-language metadata standard: declare typed entities once, generate idiomatic drift-checked code across languages — Python port.
|
|
5
|
+
Project-URL: Homepage, https://metaobjects.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/metaobjectsdev/metaobjects
|
|
7
|
+
Project-URL: Documentation, https://github.com/metaobjectsdev/metaobjects/tree/main/docs
|
|
8
|
+
Project-URL: Issues, https://github.com/metaobjectsdev/metaobjects/issues
|
|
9
|
+
Author-email: Doug Mealing <doug@metaobjects.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: code-generation,codegen,cross-language,drift-detection,fastapi,metadata,orm,pydantic,schema,sqlalchemy
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pydantic>=2; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Provides-Extra: integration
|
|
31
|
+
Requires-Dist: fastapi>=0.110; extra == 'integration'
|
|
32
|
+
Requires-Dist: httpx>=0.27; extra == 'integration'
|
|
33
|
+
Requires-Dist: pg8000>=1.31; extra == 'integration'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# MetaObjects (Python)
|
|
37
|
+
|
|
38
|
+
The Python port of the [MetaObjects](https://metaobjects.dev) cross-language metadata
|
|
39
|
+
standard: declare your typed entity model once, then generate idiomatic, drift-checked
|
|
40
|
+
code across TypeScript, Java, C#, Python, and Kotlin. The metamodel is the durable spine;
|
|
41
|
+
generated code is the disposable artifact.
|
|
42
|
+
|
|
43
|
+
Behavior is verified byte-for-byte against the same shared conformance corpora as every
|
|
44
|
+
other language port.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install metaobjects
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.11+. The only runtime dependency is PyYAML.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
Load a directory of metadata (`*.json` canonical or sigil-free `*.yaml`):
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from metaobjects import load_directory
|
|
60
|
+
|
|
61
|
+
result = load_directory("metaobjects/") # your *.json / *.yaml metadata files
|
|
62
|
+
|
|
63
|
+
if result.errors:
|
|
64
|
+
for err in result.errors:
|
|
65
|
+
print(err) # structured MetaError with a stable ErrorCode
|
|
66
|
+
else:
|
|
67
|
+
root = result.root # the merged metadata tree (a MetaData node)
|
|
68
|
+
print(root)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`load_directory`, `load_uris`, and `load_string` are module-level shortcuts over
|
|
72
|
+
`MetaDataLoader`; all return a `LoadResult` with the same field shape as the other ports.
|
|
73
|
+
|
|
74
|
+
## What's in the package
|
|
75
|
+
|
|
76
|
+
The primary public API is the **loader** (`load_directory` / `load_uris` /
|
|
77
|
+
`load_string`, `MetaDataLoader`, `LoadResult`, `ErrorCode`, `MetaError`). The
|
|
78
|
+
distribution also ships the Python implementations of the other pillars used by the CLI
|
|
79
|
+
and tooling: `codegen` (Pydantic + FastAPI emit), `render` (Mustache + payload-VO +
|
|
80
|
+
verify), `runtime` (SQLAlchemy-Core object manager), and `migrate`.
|
|
81
|
+
|
|
82
|
+
## Authoring formats
|
|
83
|
+
|
|
84
|
+
- **Canonical JSON** (`*.json`) — the cross-language interchange shape.
|
|
85
|
+
- **Sigil-free YAML** (`*.yaml` / `*.yml`) — the AI-first authoring front-end
|
|
86
|
+
([ADR-0006](https://github.com/metaobjectsdev/metaobjects/blob/main/spec/decisions/ADR-0006-ai-first-yaml-authoring.md)).
|
|
87
|
+
Desugared to canonical JSON at load time. A directory may mix both freely.
|
|
88
|
+
|
|
89
|
+
## Links
|
|
90
|
+
|
|
91
|
+
- Standard, docs, and the other four ports: <https://metaobjects.dev>
|
|
92
|
+
- Source & issues: <https://github.com/metaobjectsdev/metaobjects>
|
|
93
|
+
- Full docs: <https://github.com/metaobjectsdev/metaobjects/tree/main/docs>
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
Apache-2.0. See [LICENSE](https://github.com/metaobjectsdev/metaobjects/blob/main/LICENSE).
|