morphir-python 0.0.1__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.
- morphir/__init__.py +14 -0
- morphir/ir/__init__.py +110 -0
- morphir/ir/access_controlled.py +15 -0
- morphir/ir/decorations.py +19 -0
- morphir/ir/diagnostics.py +105 -0
- morphir/ir/distribution.py +21 -0
- morphir/ir/document.py +37 -0
- morphir/ir/documented.py +11 -0
- morphir/ir/format_version.py +483 -0
- morphir/ir/fqname.py +9 -0
- morphir/ir/json.py +11 -0
- morphir/ir/literal.py +25 -0
- morphir/ir/meta.py +11 -0
- morphir/ir/migrate.py +1267 -0
- morphir/ir/module.py +17 -0
- morphir/ir/name.py +27 -0
- morphir/ir/package.py +15 -0
- morphir/ir/path.py +25 -0
- morphir/ir/qname.py +9 -0
- morphir/ir/ref.py +17 -0
- morphir/ir/type.py +37 -0
- morphir/ir/type_constraints.py +33 -0
- morphir/ir/type_def.py +19 -0
- morphir/ir/type_spec.py +25 -0
- morphir/ir/v3/__init__.py +64 -0
- morphir/ir/v3/_legacy_value.py +201 -0
- morphir/ir/v3/access_controlled.py +31 -0
- morphir/ir/v3/codec.py +1275 -0
- morphir/ir/v3/codec_v1.py +491 -0
- morphir/ir/v3/codec_v2.py +187 -0
- morphir/ir/v3/distribution.py +42 -0
- morphir/ir/v3/documented.py +13 -0
- morphir/ir/v3/fqname.py +30 -0
- morphir/ir/v3/literal.py +46 -0
- morphir/ir/v3/module.py +32 -0
- morphir/ir/v3/name.py +42 -0
- morphir/ir/v3/package.py +23 -0
- morphir/ir/v3/path.py +29 -0
- morphir/ir/v3/qname.py +16 -0
- morphir/ir/v3/type.py +126 -0
- morphir/ir/v3/value.py +235 -0
- morphir/ir/v4/__init__.py +160 -0
- morphir/ir/v4/access_controlled.py +17 -0
- morphir/ir/v4/annotation.py +75 -0
- morphir/ir/v4/codec.py +2161 -0
- morphir/ir/v4/decorations.py +88 -0
- morphir/ir/v4/distribution.py +56 -0
- morphir/ir/v4/document.py +69 -0
- morphir/ir/v4/documented.py +10 -0
- morphir/ir/v4/fqname.py +69 -0
- morphir/ir/v4/incompleteness.py +78 -0
- morphir/ir/v4/json.py +103 -0
- morphir/ir/v4/literal.py +51 -0
- morphir/ir/v4/meta.py +27 -0
- morphir/ir/v4/module.py +30 -0
- morphir/ir/v4/name.py +69 -0
- morphir/ir/v4/package.py +19 -0
- morphir/ir/v4/path.py +50 -0
- morphir/ir/v4/qname.py +34 -0
- morphir/ir/v4/ref.py +35 -0
- morphir/ir/v4/type.py +140 -0
- morphir/ir/v4/type_constraints.py +91 -0
- morphir/ir/v4/type_def.py +38 -0
- morphir/ir/v4/type_spec.py +64 -0
- morphir/ir/v4/value.py +480 -0
- morphir/ir/value.py +81 -0
- morphir/py.typed +0 -0
- morphir/sdk/__init__.py +110 -0
- morphir/sdk/_compare.py +187 -0
- morphir/sdk/aggregate.py +421 -0
- morphir/sdk/basics.py +504 -0
- morphir/sdk/char.py +110 -0
- morphir/sdk/decimal.py +311 -0
- morphir/sdk/dict.py +336 -0
- morphir/sdk/instant.py +118 -0
- morphir/sdk/int.py +86 -0
- morphir/sdk/key.py +522 -0
- morphir/sdk/list.py +356 -0
- morphir/sdk/local_date.py +436 -0
- morphir/sdk/local_time.py +267 -0
- morphir/sdk/maybe.py +135 -0
- morphir/sdk/number.py +237 -0
- morphir/sdk/regex.py +287 -0
- morphir/sdk/result.py +162 -0
- morphir/sdk/result_list.py +169 -0
- morphir/sdk/rule.py +103 -0
- morphir/sdk/set.py +214 -0
- morphir/sdk/stateful_app.py +49 -0
- morphir/sdk/string.py +427 -0
- morphir/sdk/tuple.py +43 -0
- morphir/sdk/uuid.py +209 -0
- morphir_python-0.0.1.dist-info/METADATA +269 -0
- morphir_python-0.0.1.dist-info/RECORD +99 -0
- morphir_python-0.0.1.dist-info/WHEEL +4 -0
- morphir_python-0.0.1.dist-info/entry_points.txt +2 -0
- morphir_python-0.0.1.dist-info/licenses/LICENSE +201 -0
- morphir_tools/__init__.py +13 -0
- morphir_tools/cli.py +223 -0
- morphir_tools/py.typed +0 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""Reading, checking, and writing the ``formatVersion`` member of Morphir IR JSON.
|
|
2
|
+
|
|
3
|
+
The reader is deliberately more permissive than its sources:
|
|
4
|
+
|
|
5
|
+
- morphir-elm decodes ``formatVersion`` with ``Decode.int``, so it rejects the
|
|
6
|
+
release string ``"3.0.0"``;
|
|
7
|
+
- the finos/morphir format-version contract accepts integer ``N`` and
|
|
8
|
+
``"N.minor.patch"`` but rejects the bare string ``"N"``.
|
|
9
|
+
|
|
10
|
+
This reader accepts all three spellings (``3``, ``"3"``, ``"3.0.0"``) so migration
|
|
11
|
+
tooling can read files written by older and newer tools. Writers stay canonical:
|
|
12
|
+
integer ``N`` for ``N.0.0`` and the exact release string otherwise, so this package
|
|
13
|
+
never produces a file that either source would reject.
|
|
14
|
+
|
|
15
|
+
The support table is ``[3.0.0,3.2.0),[4.0.0,4.1.0)``, the finos reference table
|
|
16
|
+
(finos/morphir decision record 0018). IR 3.1.0 added the v3 ``Specs``
|
|
17
|
+
distribution; `V3_1` names that release. v4 support is draft.
|
|
18
|
+
|
|
19
|
+
Formats 1 and 2 are legacy: the finos release grammar starts at major 3, so they
|
|
20
|
+
are in no support table, and `read_format_version` refuses them with
|
|
21
|
+
``unsupported_format_version_major``. `read_document_with_legacy` and
|
|
22
|
+
`read_legacy_document` recognize them, only as the integers morphir-elm wrote, for
|
|
23
|
+
the read-only decoders in `morphir.ir.v3.codec_v1` and `morphir.ir.v3.codec_v2`.
|
|
24
|
+
`write_format_version` never writes them.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> read_format_version(3)
|
|
28
|
+
FormatVersion(major=3, minor=0, patch=0)
|
|
29
|
+
>>> read_format_version("3.0.0") == read_format_version("3")
|
|
30
|
+
True
|
|
31
|
+
>>> write_format_version(FormatVersion(3, 0, 0))
|
|
32
|
+
3
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import re
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
from enum import Enum
|
|
41
|
+
|
|
42
|
+
from . import diagnostics as d
|
|
43
|
+
from .diagnostics import DiagnosticError, fail
|
|
44
|
+
|
|
45
|
+
_U32_MAX = 4294967295
|
|
46
|
+
_COMPONENT = r"(0|[1-9][0-9]*)"
|
|
47
|
+
_RELEASE_RE = re.compile(rf"^{_COMPONENT}\.{_COMPONENT}\.{_COMPONENT}$")
|
|
48
|
+
_MAJOR_RE = re.compile(rf"^{_COMPONENT}$")
|
|
49
|
+
_CONSTRUCT = "formatVersion"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, order=True)
|
|
53
|
+
class FormatVersion:
|
|
54
|
+
"""An exact, normalized format release such as 3.0.0."""
|
|
55
|
+
|
|
56
|
+
major: int
|
|
57
|
+
minor: int
|
|
58
|
+
patch: int
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Interval:
|
|
66
|
+
"""A half-open release interval ``[lower, upper)``."""
|
|
67
|
+
|
|
68
|
+
lower: FormatVersion
|
|
69
|
+
upper: FormatVersion
|
|
70
|
+
|
|
71
|
+
def __contains__(self, version: object) -> bool:
|
|
72
|
+
return isinstance(version, FormatVersion) and (
|
|
73
|
+
self.lower <= version < self.upper
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return f"[{self.lower},{self.upper})"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
SupportTable = tuple[Interval, ...]
|
|
81
|
+
|
|
82
|
+
V3_INTERVAL = Interval(FormatVersion(3, 0, 0), FormatVersion(3, 2, 0))
|
|
83
|
+
V4_INTERVAL = Interval(FormatVersion(4, 0, 0), FormatVersion(4, 1, 0))
|
|
84
|
+
SUPPORT_TABLE: SupportTable = (V3_INTERVAL, V4_INTERVAL)
|
|
85
|
+
V3_TABLE: SupportTable = (V3_INTERVAL,)
|
|
86
|
+
V4_TABLE: SupportTable = (V4_INTERVAL,)
|
|
87
|
+
|
|
88
|
+
V3 = FormatVersion(3, 0, 0)
|
|
89
|
+
V3_1 = FormatVersion(3, 1, 0)
|
|
90
|
+
"""The release that introduced the v3 ``Specs`` distribution."""
|
|
91
|
+
V4 = FormatVersion(4, 0, 0)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def table_to_string(table: SupportTable) -> str:
|
|
95
|
+
"""Render a support table in canonical interval notation."""
|
|
96
|
+
return ",".join(str(interval) for interval in table)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _component(text: str, value: object) -> int:
|
|
100
|
+
number = int(text)
|
|
101
|
+
if number > _U32_MAX:
|
|
102
|
+
raise fail(
|
|
103
|
+
d.FORMAT_VERSION_OUT_OF_RANGE,
|
|
104
|
+
_CONSTRUCT,
|
|
105
|
+
(_CONSTRUCT,),
|
|
106
|
+
f"formatVersion {value!r} has a component above {_U32_MAX}",
|
|
107
|
+
)
|
|
108
|
+
return number
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def recognize_format_version(value: object) -> FormatVersion:
|
|
112
|
+
"""Recognize and normalize a ``formatVersion`` value without a support check.
|
|
113
|
+
|
|
114
|
+
Historical integers 1 and 2 normalize to ``1.0.0`` and ``2.0.0`` so that the
|
|
115
|
+
support check can report them as unsupported majors.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
value: The decoded JSON value of the ``formatVersion`` member.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The normalized release.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
DiagnosticError: With ``invalid_format_version_type``,
|
|
125
|
+
``invalid_format_version_syntax``, or ``format_version_out_of_range``.
|
|
126
|
+
"""
|
|
127
|
+
loc: d.Location = (_CONSTRUCT,)
|
|
128
|
+
if isinstance(value, bool) or not isinstance(value, (int, str)):
|
|
129
|
+
raise fail(
|
|
130
|
+
d.INVALID_FORMAT_VERSION_TYPE,
|
|
131
|
+
_CONSTRUCT,
|
|
132
|
+
loc,
|
|
133
|
+
f"formatVersion must be an integer or a release string, "
|
|
134
|
+
f"not {type(value).__name__} {value!r}",
|
|
135
|
+
)
|
|
136
|
+
if isinstance(value, int):
|
|
137
|
+
if value < 0:
|
|
138
|
+
raise fail(
|
|
139
|
+
d.INVALID_FORMAT_VERSION_TYPE,
|
|
140
|
+
_CONSTRUCT,
|
|
141
|
+
loc,
|
|
142
|
+
f"formatVersion must be an unsigned integer, not {value}",
|
|
143
|
+
)
|
|
144
|
+
if value > _U32_MAX:
|
|
145
|
+
raise fail(
|
|
146
|
+
d.FORMAT_VERSION_OUT_OF_RANGE,
|
|
147
|
+
_CONSTRUCT,
|
|
148
|
+
loc,
|
|
149
|
+
f"formatVersion {value} is above {_U32_MAX}",
|
|
150
|
+
)
|
|
151
|
+
if value == 0:
|
|
152
|
+
raise fail(
|
|
153
|
+
d.INVALID_FORMAT_VERSION_SYNTAX,
|
|
154
|
+
_CONSTRUCT,
|
|
155
|
+
loc,
|
|
156
|
+
"formatVersion 0 is not a format family",
|
|
157
|
+
)
|
|
158
|
+
return FormatVersion(value, 0, 0)
|
|
159
|
+
release = _RELEASE_RE.match(value)
|
|
160
|
+
if release is not None:
|
|
161
|
+
major, minor, patch = (_component(part, value) for part in release.groups())
|
|
162
|
+
version = FormatVersion(major, minor, patch)
|
|
163
|
+
else:
|
|
164
|
+
bare = _MAJOR_RE.match(value)
|
|
165
|
+
if bare is None:
|
|
166
|
+
raise fail(
|
|
167
|
+
d.INVALID_FORMAT_VERSION_SYNTAX,
|
|
168
|
+
_CONSTRUCT,
|
|
169
|
+
loc,
|
|
170
|
+
f"formatVersion {value!r} is not N or N.minor.patch",
|
|
171
|
+
)
|
|
172
|
+
version = FormatVersion(_component(bare.group(1), value), 0, 0)
|
|
173
|
+
if version.major < 3:
|
|
174
|
+
raise fail(
|
|
175
|
+
d.INVALID_FORMAT_VERSION_SYNTAX,
|
|
176
|
+
_CONSTRUCT,
|
|
177
|
+
loc,
|
|
178
|
+
f"formatVersion {value!r} names a major below 3; "
|
|
179
|
+
"formats 1 and 2 are written only as integers",
|
|
180
|
+
)
|
|
181
|
+
return version
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def check_supported(
|
|
185
|
+
version: FormatVersion, table: SupportTable = SUPPORT_TABLE
|
|
186
|
+
) -> FormatVersion:
|
|
187
|
+
"""Check a normalized release against a support table.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
version: A release returned by `recognize_format_version`.
|
|
191
|
+
table: The intervals the caller can process.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The same release, when it is supported.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
DiagnosticError: With ``unsupported_format_version_major`` or
|
|
198
|
+
``unsupported_format_version_minor``.
|
|
199
|
+
"""
|
|
200
|
+
if any(version in interval for interval in table):
|
|
201
|
+
return version
|
|
202
|
+
accepted = table_to_string(table)
|
|
203
|
+
loc: d.Location = (_CONSTRUCT,)
|
|
204
|
+
if not any(interval.lower.major == version.major for interval in table):
|
|
205
|
+
if version.major in (1, 2):
|
|
206
|
+
detail = _LEGACY_DETAIL
|
|
207
|
+
else:
|
|
208
|
+
detail = f"format family {version.major} is not supported"
|
|
209
|
+
raise fail(
|
|
210
|
+
d.UNSUPPORTED_FORMAT_VERSION_MAJOR,
|
|
211
|
+
_CONSTRUCT,
|
|
212
|
+
loc,
|
|
213
|
+
f"format version {version} is not supported ({detail}); "
|
|
214
|
+
f"this reader accepts only {_describe(table)} ({accepted})",
|
|
215
|
+
)
|
|
216
|
+
raise fail(
|
|
217
|
+
d.UNSUPPORTED_FORMAT_VERSION_MINOR,
|
|
218
|
+
_CONSTRUCT,
|
|
219
|
+
loc,
|
|
220
|
+
f"format version {version} is not supported; "
|
|
221
|
+
f"this reader accepts only {_describe(table)} ({accepted})",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _families(interval: Interval) -> list[str]:
|
|
226
|
+
lower, upper = interval.lower, interval.upper
|
|
227
|
+
if upper.major == lower.major and upper.patch == 0:
|
|
228
|
+
minors = range(lower.minor, max(upper.minor, lower.minor + 1))
|
|
229
|
+
else:
|
|
230
|
+
minors = range(lower.minor, lower.minor + 1)
|
|
231
|
+
draft = "draft " if lower.major == 4 else ""
|
|
232
|
+
return [f"{draft}{lower.major}.{minor}.x" for minor in minors]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _describe(table: SupportTable) -> str:
|
|
236
|
+
names = [name for interval in table for name in _families(interval)]
|
|
237
|
+
if len(names) == 1:
|
|
238
|
+
return names[0]
|
|
239
|
+
return f"{', '.join(names[:-1])} and {names[-1]}"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def read_format_version(
|
|
243
|
+
value: object, table: SupportTable = SUPPORT_TABLE
|
|
244
|
+
) -> FormatVersion:
|
|
245
|
+
"""Recognize, normalize, and support-check a ``formatVersion`` value.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
value: The decoded JSON value of the ``formatVersion`` member.
|
|
249
|
+
table: The intervals the caller can process.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
The normalized, supported release.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
DiagnosticError: With one of the finos format-version codes.
|
|
256
|
+
"""
|
|
257
|
+
return check_supported(recognize_format_version(value), table)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def write_format_version(version: FormatVersion) -> int | str:
|
|
261
|
+
"""Return the canonical spelling: integer N for N.0.0, else the release string.
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ValueError: For a legacy major (1 or 2); legacy formats are never written.
|
|
265
|
+
"""
|
|
266
|
+
if version.major < 3:
|
|
267
|
+
raise ValueError(f"format {version.major} is legacy and is never written")
|
|
268
|
+
if version.minor == 0 and version.patch == 0:
|
|
269
|
+
return version.major
|
|
270
|
+
return str(version)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class _Object(dict[str, object]):
|
|
274
|
+
duplicates: frozenset[str] = frozenset()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _object_pairs(pairs: list[tuple[str, object]], seen: list[str]) -> _Object:
|
|
278
|
+
obj = _Object()
|
|
279
|
+
dups: set[str] = set()
|
|
280
|
+
for key, value in pairs:
|
|
281
|
+
if key in obj:
|
|
282
|
+
dups.add(key)
|
|
283
|
+
obj[key] = value
|
|
284
|
+
if dups:
|
|
285
|
+
obj.duplicates = frozenset(dups)
|
|
286
|
+
seen.extend(sorted(dups))
|
|
287
|
+
return obj
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def parse_json(text: str) -> object:
|
|
291
|
+
"""Parse JSON text, rejecting duplicate object members.
|
|
292
|
+
|
|
293
|
+
``json.loads`` silently keeps the last duplicate member, which the v4 JSON
|
|
294
|
+
profile and the format-version contract both forbid.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
DiagnosticError: With ``invalid_json``, ``duplicate_format_version`` (root
|
|
298
|
+
only), or ``duplicate_member``.
|
|
299
|
+
"""
|
|
300
|
+
seen: list[str] = []
|
|
301
|
+
try:
|
|
302
|
+
root: object = json.loads(
|
|
303
|
+
text, object_pairs_hook=lambda pairs: _object_pairs(pairs, seen)
|
|
304
|
+
)
|
|
305
|
+
except json.JSONDecodeError as error:
|
|
306
|
+
raise fail(
|
|
307
|
+
d.INVALID_JSON, "document", (), f"input is not valid JSON: {error}"
|
|
308
|
+
) from None
|
|
309
|
+
if isinstance(root, _Object) and _CONSTRUCT in root.duplicates:
|
|
310
|
+
raise fail(
|
|
311
|
+
d.DUPLICATE_FORMAT_VERSION,
|
|
312
|
+
_CONSTRUCT,
|
|
313
|
+
(),
|
|
314
|
+
"the document root contains formatVersion more than once",
|
|
315
|
+
)
|
|
316
|
+
if seen:
|
|
317
|
+
raise fail(
|
|
318
|
+
d.DUPLICATE_MEMBER,
|
|
319
|
+
"object",
|
|
320
|
+
(),
|
|
321
|
+
f"an object contains member {seen[0]!r} more than once",
|
|
322
|
+
)
|
|
323
|
+
return root
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def read_document(
|
|
327
|
+
document: str | object, table: SupportTable = SUPPORT_TABLE
|
|
328
|
+
) -> tuple[FormatVersion, dict[str, object]]:
|
|
329
|
+
"""Read the root of a versioned IR document and check its format version.
|
|
330
|
+
|
|
331
|
+
Recognition and the support check finish before any distribution decoder runs.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
document: JSON text, or an already-decoded JSON value. Only text input can
|
|
335
|
+
be checked for duplicate members.
|
|
336
|
+
table: The intervals the caller can process.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The supported release and the root object.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
DiagnosticError: With a format-version or JSON diagnostic.
|
|
343
|
+
"""
|
|
344
|
+
obj = _versioned_root(document)
|
|
345
|
+
return read_format_version(obj[_CONSTRUCT], table), obj
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _versioned_root(document: str | object) -> dict[str, object]:
|
|
349
|
+
root = parse_json(document) if isinstance(document, str) else document
|
|
350
|
+
if not isinstance(root, dict):
|
|
351
|
+
raise fail(
|
|
352
|
+
d.INVALID_JSON,
|
|
353
|
+
"document",
|
|
354
|
+
(),
|
|
355
|
+
"the document root must be a JSON object",
|
|
356
|
+
)
|
|
357
|
+
if _CONSTRUCT not in root:
|
|
358
|
+
raise fail(
|
|
359
|
+
d.MISSING_FORMAT_VERSION,
|
|
360
|
+
_CONSTRUCT,
|
|
361
|
+
(),
|
|
362
|
+
"the IR has no formatVersion member; it is in an old format. "
|
|
363
|
+
"Please regenerate it!",
|
|
364
|
+
)
|
|
365
|
+
return dict(root)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def dump_json(value: object) -> str:
|
|
369
|
+
"""Serialize JSON compactly with members in insertion order."""
|
|
370
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# --- Legacy formats 1 and 2 (read-only) ---
|
|
374
|
+
|
|
375
|
+
_LEGACY_DETAIL = (
|
|
376
|
+
"formats 1 and 2 are legacy formats outside the support table; "
|
|
377
|
+
"morphir.ir.v3.codec_v1 and codec_v2 read them as format 3.0.0"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class LegacyFormat(Enum):
|
|
382
|
+
"""A morphir-elm format from before the release grammar, which starts at 3.
|
|
383
|
+
|
|
384
|
+
morphir-elm wrote formats 1 and 2 only as the JSON integers ``1`` and ``2``.
|
|
385
|
+
They are in no support table: the legacy readers decode them into the
|
|
386
|
+
format 3.0.0 model, and no writer emits them.
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
V1 = 1
|
|
390
|
+
V2 = 2
|
|
391
|
+
|
|
392
|
+
def __str__(self) -> str:
|
|
393
|
+
return str(self.value)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def recognize_legacy_format(value: object) -> LegacyFormat | None:
|
|
397
|
+
"""Return the legacy format for the JSON integer ``1`` or ``2``, else ``None``.
|
|
398
|
+
|
|
399
|
+
Example:
|
|
400
|
+
>>> recognize_legacy_format(1)
|
|
401
|
+
<LegacyFormat.V1: 1>
|
|
402
|
+
>>> recognize_legacy_format("1") is None
|
|
403
|
+
True
|
|
404
|
+
"""
|
|
405
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
406
|
+
return None
|
|
407
|
+
return {1: LegacyFormat.V1, 2: LegacyFormat.V2}.get(value)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def read_document_with_legacy(
|
|
411
|
+
document: str | object, table: SupportTable = SUPPORT_TABLE
|
|
412
|
+
) -> tuple[FormatVersion | LegacyFormat, dict[str, object]]:
|
|
413
|
+
"""Read a document root like `read_document`, but report legacy formats.
|
|
414
|
+
|
|
415
|
+
The integers ``1`` and ``2`` give a `LegacyFormat`; every other value goes
|
|
416
|
+
through `read_format_version` with ``table`` and fails as it does there.
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
DiagnosticError: With a format-version or JSON diagnostic.
|
|
420
|
+
"""
|
|
421
|
+
obj = _versioned_root(document)
|
|
422
|
+
legacy = recognize_legacy_format(obj[_CONSTRUCT])
|
|
423
|
+
if legacy is not None:
|
|
424
|
+
return legacy, obj
|
|
425
|
+
return read_format_version(obj[_CONSTRUCT], table), obj
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def read_legacy_document(
|
|
429
|
+
document: str | object, accepted: LegacyFormat
|
|
430
|
+
) -> dict[str, object]:
|
|
431
|
+
"""Read the root of a legacy document and check that it is ``accepted``.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
document: JSON text, or an already-decoded JSON value.
|
|
435
|
+
accepted: The one legacy format the caller decodes.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
The root object.
|
|
439
|
+
|
|
440
|
+
Raises:
|
|
441
|
+
DiagnosticError: With ``unsupported_format_version_major`` for any other
|
|
442
|
+
format, or with the recognition diagnostic for a malformed version.
|
|
443
|
+
"""
|
|
444
|
+
obj = _versioned_root(document)
|
|
445
|
+
value = obj[_CONSTRUCT]
|
|
446
|
+
found: FormatVersion | LegacyFormat | None = recognize_legacy_format(value)
|
|
447
|
+
if found is accepted:
|
|
448
|
+
return obj
|
|
449
|
+
if found is None:
|
|
450
|
+
found = recognize_format_version(value)
|
|
451
|
+
raise fail(
|
|
452
|
+
d.UNSUPPORTED_FORMAT_VERSION_MAJOR,
|
|
453
|
+
_CONSTRUCT,
|
|
454
|
+
(_CONSTRUCT,),
|
|
455
|
+
f"format version {found} is not supported; "
|
|
456
|
+
f"this reader accepts only legacy format {accepted}",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
__all__ = [
|
|
461
|
+
"SUPPORT_TABLE",
|
|
462
|
+
"V3",
|
|
463
|
+
"V3_1",
|
|
464
|
+
"V3_TABLE",
|
|
465
|
+
"V4",
|
|
466
|
+
"V4_TABLE",
|
|
467
|
+
"DiagnosticError",
|
|
468
|
+
"FormatVersion",
|
|
469
|
+
"Interval",
|
|
470
|
+
"LegacyFormat",
|
|
471
|
+
"SupportTable",
|
|
472
|
+
"check_supported",
|
|
473
|
+
"dump_json",
|
|
474
|
+
"parse_json",
|
|
475
|
+
"read_document",
|
|
476
|
+
"read_document_with_legacy",
|
|
477
|
+
"read_format_version",
|
|
478
|
+
"read_legacy_document",
|
|
479
|
+
"recognize_format_version",
|
|
480
|
+
"recognize_legacy_format",
|
|
481
|
+
"table_to_string",
|
|
482
|
+
"write_format_version",
|
|
483
|
+
]
|
morphir/ir/fqname.py
ADDED
morphir/ir/json.py
ADDED
morphir/ir/literal.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Compatibility shim: re-exports `morphir.ir.v4.literal`."""
|
|
2
|
+
|
|
3
|
+
from morphir.ir.v4.document import Document
|
|
4
|
+
from morphir.ir.v4.literal import (
|
|
5
|
+
BoolLiteral,
|
|
6
|
+
CharLiteral,
|
|
7
|
+
DecimalLiteral,
|
|
8
|
+
DocumentLiteral,
|
|
9
|
+
FloatLiteral,
|
|
10
|
+
IntegerLiteral,
|
|
11
|
+
Literal,
|
|
12
|
+
StringLiteral,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BoolLiteral",
|
|
17
|
+
"CharLiteral",
|
|
18
|
+
"DecimalLiteral",
|
|
19
|
+
"Document",
|
|
20
|
+
"DocumentLiteral",
|
|
21
|
+
"FloatLiteral",
|
|
22
|
+
"IntegerLiteral",
|
|
23
|
+
"Literal",
|
|
24
|
+
"StringLiteral",
|
|
25
|
+
]
|