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
morphir/sdk/string.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Morphir.SDK.String: Elm's String module.
|
|
2
|
+
|
|
3
|
+
A `String` is a Python `str` and a `Char` is a `str` that holds one code point.
|
|
4
|
+
All functions take their arguments in Elm order, with the string last. Functions
|
|
5
|
+
that yield a list yield a tuple.
|
|
6
|
+
|
|
7
|
+
Some names in this module shadow Python built-ins (`all`, `any`, `filter`,
|
|
8
|
+
`map`, `slice`). Import the module, not the names:
|
|
9
|
+
|
|
10
|
+
>>> from morphir.sdk import string
|
|
11
|
+
>>> string.pad(5, ".", "11")
|
|
12
|
+
'..11.'
|
|
13
|
+
|
|
14
|
+
Departure from Elm: a Python `str` is a sequence of code points, while an Elm
|
|
15
|
+
string is a sequence of UTF-16 code units. `length`, `slice`, `left`, `right`,
|
|
16
|
+
`drop_left`, `drop_right`, `indexes` and the `pad` functions count code points,
|
|
17
|
+
so a character above U+FFFF counts as 1 here and as 2 in Elm. Python never
|
|
18
|
+
splits such a character in two.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
import re
|
|
23
|
+
from decimal import Decimal
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from morphir.sdk.maybe import Just, Maybe, Nothing
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from collections.abc import Callable, Sequence
|
|
30
|
+
|
|
31
|
+
from morphir.sdk.char import Char
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"all",
|
|
35
|
+
"any",
|
|
36
|
+
"append",
|
|
37
|
+
"concat",
|
|
38
|
+
"cons",
|
|
39
|
+
"contains",
|
|
40
|
+
"drop_left",
|
|
41
|
+
"drop_right",
|
|
42
|
+
"ends_with",
|
|
43
|
+
"filter",
|
|
44
|
+
"foldl",
|
|
45
|
+
"foldr",
|
|
46
|
+
"from_char",
|
|
47
|
+
"from_float",
|
|
48
|
+
"from_int",
|
|
49
|
+
"from_list",
|
|
50
|
+
"indexes",
|
|
51
|
+
"indices",
|
|
52
|
+
"is_empty",
|
|
53
|
+
"join",
|
|
54
|
+
"left",
|
|
55
|
+
"length",
|
|
56
|
+
"lines",
|
|
57
|
+
"map",
|
|
58
|
+
"pad",
|
|
59
|
+
"pad_left",
|
|
60
|
+
"pad_right",
|
|
61
|
+
"repeat",
|
|
62
|
+
"replace",
|
|
63
|
+
"reverse",
|
|
64
|
+
"right",
|
|
65
|
+
"slice",
|
|
66
|
+
"split",
|
|
67
|
+
"starts_with",
|
|
68
|
+
"to_float",
|
|
69
|
+
"to_int",
|
|
70
|
+
"to_list",
|
|
71
|
+
"to_lower",
|
|
72
|
+
"to_upper",
|
|
73
|
+
"trim",
|
|
74
|
+
"trim_left",
|
|
75
|
+
"trim_right",
|
|
76
|
+
"uncons",
|
|
77
|
+
"words",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# The characters that JavaScript `trim` and the regex class `\s` treat as white
|
|
81
|
+
# space. Python's `str.strip` and `\s` use a different set.
|
|
82
|
+
_JS_SPACE = (
|
|
83
|
+
"\t\n\v\f\r "
|
|
84
|
+
"\N{NO-BREAK SPACE}"
|
|
85
|
+
"\N{OGHAM SPACE MARK}"
|
|
86
|
+
"\N{EN QUAD}\N{EM QUAD}\N{EN SPACE}\N{EM SPACE}"
|
|
87
|
+
"\N{THREE-PER-EM SPACE}\N{FOUR-PER-EM SPACE}\N{SIX-PER-EM SPACE}"
|
|
88
|
+
"\N{FIGURE SPACE}\N{PUNCTUATION SPACE}\N{THIN SPACE}\N{HAIR SPACE}"
|
|
89
|
+
"\N{LINE SEPARATOR}\N{PARAGRAPH SEPARATOR}"
|
|
90
|
+
"\N{NARROW NO-BREAK SPACE}"
|
|
91
|
+
"\N{MEDIUM MATHEMATICAL SPACE}"
|
|
92
|
+
"\N{IDEOGRAPHIC SPACE}"
|
|
93
|
+
"\N{ZERO WIDTH NO-BREAK SPACE}"
|
|
94
|
+
)
|
|
95
|
+
_JS_SPACE_RUN = re.compile(f"[{re.escape(_JS_SPACE)}]+")
|
|
96
|
+
_LINE_BREAK = re.compile(r"\r\n|\r|\n")
|
|
97
|
+
_INT = re.compile(r"[+-]?[0-9]+")
|
|
98
|
+
_FLOAT_REJECT = re.compile(f"[{re.escape(_JS_SPACE)}xbo]")
|
|
99
|
+
_JS_NUMBER = re.compile(r"[+-]?(Infinity|([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?)")
|
|
100
|
+
# JavaScript prints a number in positional notation when its decimal exponent is
|
|
101
|
+
# in this range, and in exponent notation otherwise.
|
|
102
|
+
_JS_MIN_POSITIONAL_EXPONENT = -6
|
|
103
|
+
_JS_MAX_POSITIONAL_EXPONENT = 21
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Basics
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_empty(s: str) -> bool:
|
|
110
|
+
"""Check that a string is empty."""
|
|
111
|
+
return s == ""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def length(s: str) -> int:
|
|
115
|
+
"""Return the number of code points in a string."""
|
|
116
|
+
return len(s)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def reverse(s: str) -> str:
|
|
120
|
+
"""Reverse a string."""
|
|
121
|
+
return s[::-1]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def repeat(n: int, s: str) -> str:
|
|
125
|
+
"""Repeat a string `n` times. A count below 1 yields the empty string."""
|
|
126
|
+
return s * n if n > 0 else ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def replace(match: str, replacement: str, s: str) -> str:
|
|
130
|
+
"""Replace every occurrence of `match`.
|
|
131
|
+
|
|
132
|
+
An empty `match` puts the replacement between the characters, as in Elm.
|
|
133
|
+
"""
|
|
134
|
+
if match == "":
|
|
135
|
+
return replacement.join(s)
|
|
136
|
+
return s.replace(match, replacement)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Building and splitting
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def append(s1: str, s2: str) -> str:
|
|
143
|
+
"""Append two strings."""
|
|
144
|
+
return s1 + s2
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def concat(strings: Sequence[str]) -> str:
|
|
148
|
+
"""Concatenate many strings into one."""
|
|
149
|
+
return "".join(strings)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def split(separator: str, s: str) -> tuple[str, ...]:
|
|
153
|
+
"""Split a string on a separator.
|
|
154
|
+
|
|
155
|
+
An empty separator splits the string into its characters, as in Elm.
|
|
156
|
+
"""
|
|
157
|
+
if separator == "":
|
|
158
|
+
return tuple(s)
|
|
159
|
+
return tuple(s.split(separator))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def join(separator: str, strings: Sequence[str]) -> str:
|
|
163
|
+
"""Put many strings together with a separator."""
|
|
164
|
+
return separator.join(strings)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def words(s: str) -> tuple[str, ...]:
|
|
168
|
+
"""Break a string into words, split on runs of white space.
|
|
169
|
+
|
|
170
|
+
As in elm/core, the string is trimmed first, so an empty or all-white-space
|
|
171
|
+
string yields `("",)`.
|
|
172
|
+
"""
|
|
173
|
+
return tuple(_JS_SPACE_RUN.split(trim(s)))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def lines(s: str) -> tuple[str, ...]:
|
|
177
|
+
r"""Break a string into lines, split on `\n`, `\r\n` and `\r`."""
|
|
178
|
+
return tuple(_LINE_BREAK.split(s))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Substrings
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def slice(start: int, end: int, s: str) -> str:
|
|
185
|
+
"""Take a substring from `start` up to, but not including, `end`.
|
|
186
|
+
|
|
187
|
+
A negative index counts from the end of the string, as in Elm.
|
|
188
|
+
"""
|
|
189
|
+
return s[start:end]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def left(n: int, s: str) -> str:
|
|
193
|
+
"""Take `n` characters from the left side of a string."""
|
|
194
|
+
return "" if n < 1 else s[:n]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def right(n: int, s: str) -> str:
|
|
198
|
+
"""Take `n` characters from the right side of a string."""
|
|
199
|
+
return "" if n < 1 else s[-n:]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def drop_left(n: int, s: str) -> str:
|
|
203
|
+
"""Drop `n` characters from the left side of a string."""
|
|
204
|
+
return s if n < 1 else s[n:]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def drop_right(n: int, s: str) -> str:
|
|
208
|
+
"""Drop `n` characters from the right side of a string."""
|
|
209
|
+
return s if n < 1 else s[:-n]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Checks
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def contains(sub: str, s: str) -> bool:
|
|
216
|
+
"""Check that the second string contains the first one."""
|
|
217
|
+
return sub in s
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def starts_with(prefix: str, s: str) -> bool:
|
|
221
|
+
"""Check that the second string starts with the first one."""
|
|
222
|
+
return s.startswith(prefix)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def ends_with(suffix: str, s: str) -> bool:
|
|
226
|
+
"""Check that the second string ends with the first one."""
|
|
227
|
+
return s.endswith(suffix)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def indexes(sub: str, s: str) -> tuple[int, ...]:
|
|
231
|
+
"""Return the start positions of a substring in a string.
|
|
232
|
+
|
|
233
|
+
Matches do not overlap: `indexes("aa", "aaaa")` is `(0, 2)`, as in elm/core.
|
|
234
|
+
An empty substring yields no positions.
|
|
235
|
+
"""
|
|
236
|
+
if sub == "":
|
|
237
|
+
return ()
|
|
238
|
+
found: list[int] = []
|
|
239
|
+
index = s.find(sub)
|
|
240
|
+
while index > -1:
|
|
241
|
+
found.append(index)
|
|
242
|
+
index = s.find(sub, index + len(sub))
|
|
243
|
+
return tuple(found)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
indices = indexes
|
|
247
|
+
"""Alias of `indexes`."""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# Int conversions
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def to_int(s: str) -> Maybe[int]:
|
|
254
|
+
"""Parse an integer.
|
|
255
|
+
|
|
256
|
+
Elm accepts an optional sign and then ASCII digits only: no white space, no
|
|
257
|
+
decimal point, no exponent, no radix prefix and no underscores.
|
|
258
|
+
"""
|
|
259
|
+
return Just(int(s)) if _INT.fullmatch(s) else Nothing()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def from_int(n: int) -> str:
|
|
263
|
+
"""Convert an integer to a string."""
|
|
264
|
+
return str(n)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# Float conversions
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def to_float(s: str) -> Maybe[float]:
|
|
271
|
+
"""Parse a float, like elm/core.
|
|
272
|
+
|
|
273
|
+
elm/core rejects the empty string and any string that contains white space
|
|
274
|
+
or one of the letters `x`, `b` and `o`. It then converts the string with
|
|
275
|
+
JavaScript number rules and rejects `NaN`. Thus ".5", "1." and "Infinity"
|
|
276
|
+
are accepted, and " 1", "0x10", "1_000", "inf" and "nan" are not.
|
|
277
|
+
"""
|
|
278
|
+
if s == "" or _FLOAT_REJECT.search(s) or not _JS_NUMBER.fullmatch(s):
|
|
279
|
+
return Nothing()
|
|
280
|
+
return Just(float(s))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def from_float(f: float) -> str:
|
|
284
|
+
"""Convert a float to a string, like Elm.
|
|
285
|
+
|
|
286
|
+
Elm prints a float as JavaScript does: `1.0` prints as "1", `1e21` as
|
|
287
|
+
"1e+21" and `1.5e-7` as "1.5e-7". `NaN` and the infinities print as "NaN",
|
|
288
|
+
"Infinity" and "-Infinity".
|
|
289
|
+
"""
|
|
290
|
+
if math.isnan(f):
|
|
291
|
+
return "NaN"
|
|
292
|
+
if math.isinf(f):
|
|
293
|
+
return "Infinity" if f > 0 else "-Infinity"
|
|
294
|
+
if f == 0:
|
|
295
|
+
return "0"
|
|
296
|
+
# `repr` gives the shortest digits that round-trip, as JavaScript does.
|
|
297
|
+
sign, digit_tuple, exponent = Decimal(repr(f)).as_tuple()
|
|
298
|
+
digits = "".join(str(d) for d in digit_tuple).rstrip("0")
|
|
299
|
+
assert isinstance(exponent, int)
|
|
300
|
+
# The decimal point goes after `point` digits: 0.d1d2... * 10 ** point.
|
|
301
|
+
point = len(digit_tuple) + exponent
|
|
302
|
+
count = len(digits)
|
|
303
|
+
if count <= point <= _JS_MAX_POSITIONAL_EXPONENT:
|
|
304
|
+
body = digits + "0" * (point - count)
|
|
305
|
+
elif 0 < point <= _JS_MAX_POSITIONAL_EXPONENT:
|
|
306
|
+
body = f"{digits[:point]}.{digits[point:]}"
|
|
307
|
+
elif _JS_MIN_POSITIONAL_EXPONENT < point <= 0:
|
|
308
|
+
body = "0." + "0" * -point + digits
|
|
309
|
+
else:
|
|
310
|
+
mantissa = digits if count == 1 else f"{digits[0]}.{digits[1:]}"
|
|
311
|
+
body = f"{mantissa}e{'+' if point > 0 else '-'}{abs(point - 1)}"
|
|
312
|
+
return f"-{body}" if sign else body
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Char conversions
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def from_char(c: Char) -> str:
|
|
319
|
+
"""Create a string from a character."""
|
|
320
|
+
return c
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def cons(c: Char, s: str) -> str:
|
|
324
|
+
"""Add a character to the start of a string."""
|
|
325
|
+
return c + s
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def uncons(s: str) -> Maybe[tuple[Char, str]]:
|
|
329
|
+
"""Split a non-empty string into its first character and the remainder."""
|
|
330
|
+
return Just((s[0], s[1:])) if s else Nothing()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def to_list(s: str) -> tuple[Char, ...]:
|
|
334
|
+
"""Convert a string to a list of characters."""
|
|
335
|
+
return tuple(s)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def from_list(chars: Sequence[Char]) -> str:
|
|
339
|
+
"""Convert a list of characters to a string."""
|
|
340
|
+
return "".join(chars)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Formatting
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def to_upper(s: str) -> str:
|
|
347
|
+
"""Convert a string to upper case."""
|
|
348
|
+
return s.upper()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def to_lower(s: str) -> str:
|
|
352
|
+
"""Convert a string to lower case."""
|
|
353
|
+
return s.lower()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def pad(n: int, c: Char, s: str) -> str:
|
|
357
|
+
"""Pad a string on both sides until it has `n` characters.
|
|
358
|
+
|
|
359
|
+
When the padding is odd, the extra character goes on the left, as in
|
|
360
|
+
elm/core: `pad(5, ".", "11")` is "..11.".
|
|
361
|
+
"""
|
|
362
|
+
total = n - len(s)
|
|
363
|
+
return repeat(total - total // 2, c) + s + repeat(total // 2, c)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def pad_left(n: int, c: Char, s: str) -> str:
|
|
367
|
+
"""Pad a string on the left until it has `n` characters."""
|
|
368
|
+
return repeat(n - len(s), c) + s
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def pad_right(n: int, c: Char, s: str) -> str:
|
|
372
|
+
"""Pad a string on the right until it has `n` characters."""
|
|
373
|
+
return s + repeat(n - len(s), c)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def trim(s: str) -> str:
|
|
377
|
+
"""Remove white space on both sides of a string."""
|
|
378
|
+
return s.strip(_JS_SPACE)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def trim_left(s: str) -> str:
|
|
382
|
+
"""Remove white space on the left of a string."""
|
|
383
|
+
return s.lstrip(_JS_SPACE)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def trim_right(s: str) -> str:
|
|
387
|
+
"""Remove white space on the right of a string."""
|
|
388
|
+
return s.rstrip(_JS_SPACE)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# Higher-order functions
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def map(f: Callable[[Char], Char], s: str) -> str:
|
|
395
|
+
"""Transform every character in a string."""
|
|
396
|
+
return "".join(f(c) for c in s)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def filter(predicate: Callable[[Char], bool], s: str) -> str:
|
|
400
|
+
"""Keep only the characters that pass the test."""
|
|
401
|
+
return "".join(c for c in s if predicate(c))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def foldl[B](f: Callable[[Char, B], B], initial: B, s: str) -> B:
|
|
405
|
+
"""Reduce a string from the left. The function gets the character first."""
|
|
406
|
+
accumulator = initial
|
|
407
|
+
for c in s:
|
|
408
|
+
accumulator = f(c, accumulator)
|
|
409
|
+
return accumulator
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def foldr[B](f: Callable[[Char, B], B], initial: B, s: str) -> B:
|
|
413
|
+
"""Reduce a string from the right. The function gets the character first."""
|
|
414
|
+
accumulator = initial
|
|
415
|
+
for c in reversed(s):
|
|
416
|
+
accumulator = f(c, accumulator)
|
|
417
|
+
return accumulator
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def any(predicate: Callable[[Char], bool], s: str) -> bool:
|
|
421
|
+
"""Check that at least one character passes the test."""
|
|
422
|
+
return next((True for c in s if predicate(c)), False)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def all(predicate: Callable[[Char], bool], s: str) -> bool:
|
|
426
|
+
"""Check that every character passes the test."""
|
|
427
|
+
return next((False for c in s if not predicate(c)), True)
|
morphir/sdk/tuple.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Morphir.SDK.Tuple: helpers for pairs.
|
|
2
|
+
|
|
3
|
+
An Elm tuple `( a, b )` is a Python `tuple[A, B]`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
__all__ = ["first", "map_both", "map_first", "map_second", "pair", "second"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pair[A, B](a: A, b: B) -> tuple[A, B]:
|
|
15
|
+
"""Create a pair."""
|
|
16
|
+
return (a, b)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def first[A, B](pair: tuple[A, B]) -> A:
|
|
20
|
+
"""Return the first value of a pair."""
|
|
21
|
+
return pair[0]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def second[A, B](pair: tuple[A, B]) -> B:
|
|
25
|
+
"""Return the second value of a pair."""
|
|
26
|
+
return pair[1]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def map_first[A, B, X](f: Callable[[A], X], pair: tuple[A, B]) -> tuple[X, B]:
|
|
30
|
+
"""Apply a function to the first value of a pair."""
|
|
31
|
+
return (f(pair[0]), pair[1])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def map_second[A, B, Y](f: Callable[[B], Y], pair: tuple[A, B]) -> tuple[A, Y]:
|
|
35
|
+
"""Apply a function to the second value of a pair."""
|
|
36
|
+
return (pair[0], f(pair[1]))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def map_both[A, B, X, Y](
|
|
40
|
+
f: Callable[[A], X], g: Callable[[B], Y], pair: tuple[A, B]
|
|
41
|
+
) -> tuple[X, Y]:
|
|
42
|
+
"""Apply one function to each value of a pair."""
|
|
43
|
+
return (f(pair[0]), g(pair[1]))
|
morphir/sdk/uuid.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Morphir.SDK.UUID: universally unique identifiers.
|
|
2
|
+
|
|
3
|
+
A `UUID` is the standard library `uuid.UUID`. The reference behaviour is the Elm
|
|
4
|
+
runtime, which uses TSFoster/elm-uuid.
|
|
5
|
+
|
|
6
|
+
The name `compare` and the module name are also names in Python. Import the
|
|
7
|
+
module with an alias:
|
|
8
|
+
|
|
9
|
+
>>> from morphir.sdk import uuid as UUID
|
|
10
|
+
>>> UUID.parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") == Ok(UUID.dns_namespace)
|
|
11
|
+
True
|
|
12
|
+
>>> UUID.to_string(UUID.for_name("python.org", UUID.dns_namespace))
|
|
13
|
+
'886313e1-3b8a-5372-9b90-0c9aee199e5d'
|
|
14
|
+
>>> UUID.parse("not a uuid")
|
|
15
|
+
Err(error=<Error.WrongLength: 'WrongLength'>)
|
|
16
|
+
|
|
17
|
+
`parse` and `from_string` read the same text forms as the Elm runtime. They
|
|
18
|
+
remove all spaces, tabs, line feeds and hyphens, and ignore the case. They then
|
|
19
|
+
accept 32 hexadecimal digits, optionally after the prefix `urn:uuid:` or
|
|
20
|
+
between `{` and `}`. They do not use the parser of `uuid.UUID`, which accepts
|
|
21
|
+
other forms (for example `{` with no `}`, or `urn:uuid:` with braces).
|
|
22
|
+
|
|
23
|
+
A UUID from `parse` is never the nil UUID, has the RFC 4122 variant and has a
|
|
24
|
+
version from 0 to 8. A `uuid.UUID` from a different source can break these
|
|
25
|
+
rules. All functions in this module accept such a value.
|
|
26
|
+
|
|
27
|
+
Departure from the Elm runtime: `parse` gives `NoVersion` for a version above
|
|
28
|
+
8, as TSFoster/elm-uuid 4.3.1 does. Release 4.2.0 of that package, which
|
|
29
|
+
morphir-elm also permits, gives `NoVersion` for a version above 5.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import uuid as _uuid
|
|
33
|
+
from enum import Enum
|
|
34
|
+
|
|
35
|
+
from morphir.sdk._compare import Order
|
|
36
|
+
from morphir.sdk.maybe import Just, Maybe, Nothing
|
|
37
|
+
from morphir.sdk.result import Err, Ok, Result
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"UUID",
|
|
41
|
+
"Error",
|
|
42
|
+
"compare",
|
|
43
|
+
"dns_namespace",
|
|
44
|
+
"for_name",
|
|
45
|
+
"from_string",
|
|
46
|
+
"is_nil_string",
|
|
47
|
+
"nil_string",
|
|
48
|
+
"oid_namespace",
|
|
49
|
+
"parse",
|
|
50
|
+
"to_string",
|
|
51
|
+
"url_namespace",
|
|
52
|
+
"version",
|
|
53
|
+
"x500_namespace",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
UUID = _uuid.UUID
|
|
57
|
+
"""The standard library UUID type."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Error(Enum):
|
|
61
|
+
"""The reason why `parse` did not accept a text."""
|
|
62
|
+
|
|
63
|
+
WrongFormat = "WrongFormat"
|
|
64
|
+
"""The text has a character that is not a hexadecimal digit."""
|
|
65
|
+
|
|
66
|
+
WrongLength = "WrongLength"
|
|
67
|
+
"""The text does not have 32 hexadecimal digits."""
|
|
68
|
+
|
|
69
|
+
UnsupportedVariant = "UnsupportedVariant"
|
|
70
|
+
"""The UUID does not have the RFC 4122 variant."""
|
|
71
|
+
|
|
72
|
+
IsNil = "IsNil"
|
|
73
|
+
"""The UUID is the nil UUID, which has only zeros."""
|
|
74
|
+
|
|
75
|
+
NoVersion = "NoVersion"
|
|
76
|
+
"""The version of the UUID is not in the range 0 to 8."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_HEX_DIGITS = frozenset("0123456789abcdef")
|
|
80
|
+
_MAX_VERSION = 8
|
|
81
|
+
_URN_PREFIX = "urn:uuid:"
|
|
82
|
+
|
|
83
|
+
nil_string: str = "00000000-0000-0000-0000-000000000000"
|
|
84
|
+
"""The text of the nil UUID. `parse` does not accept it."""
|
|
85
|
+
|
|
86
|
+
dns_namespace: UUID = _uuid.NAMESPACE_DNS
|
|
87
|
+
"""The namespace for domain names, `6ba7b810-9dad-11d1-80b4-00c04fd430c8`."""
|
|
88
|
+
|
|
89
|
+
url_namespace: UUID = _uuid.NAMESPACE_URL
|
|
90
|
+
"""The namespace for URLs, `6ba7b811-9dad-11d1-80b4-00c04fd430c8`."""
|
|
91
|
+
|
|
92
|
+
oid_namespace: UUID = _uuid.NAMESPACE_OID
|
|
93
|
+
"""The namespace for ISO object IDs, `6ba7b812-9dad-11d1-80b4-00c04fd430c8`."""
|
|
94
|
+
|
|
95
|
+
x500_namespace: UUID = _uuid.NAMESPACE_X500
|
|
96
|
+
"""The namespace for X.500 names, `6ba7b814-9dad-11d1-80b4-00c04fd430c8`."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _utf16_length(text: str) -> int:
|
|
100
|
+
# Elm measures a string in UTF-16 code units.
|
|
101
|
+
return sum(2 if ord(char) > 0xFFFF else 1 for char in text)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _normalize(s: str) -> str:
|
|
105
|
+
text = s
|
|
106
|
+
for removed in ("\n", "\t", " ", "-"):
|
|
107
|
+
text = text.replace(removed, "")
|
|
108
|
+
text = text.lower()
|
|
109
|
+
if text.startswith(_URN_PREFIX):
|
|
110
|
+
return text[len(_URN_PREFIX) :]
|
|
111
|
+
if text.startswith("{") and text.endswith("}"):
|
|
112
|
+
return text[1:-1]
|
|
113
|
+
return text
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse(s: str) -> Result[Error, UUID]:
|
|
117
|
+
"""Read a UUID from text.
|
|
118
|
+
|
|
119
|
+
The module docstring gives the accepted text forms.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
s: The text to read.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
`Ok` with the UUID, or `Err` with the reason. The checks are in this
|
|
126
|
+
order: `WrongLength`, `WrongFormat`, `IsNil`, `NoVersion`,
|
|
127
|
+
`UnsupportedVariant`.
|
|
128
|
+
"""
|
|
129
|
+
normalized = _normalize(s)
|
|
130
|
+
if _utf16_length(normalized) != 32:
|
|
131
|
+
return Err(Error.WrongLength)
|
|
132
|
+
if not _HEX_DIGITS.issuperset(normalized):
|
|
133
|
+
return Err(Error.WrongFormat)
|
|
134
|
+
value = _uuid.UUID(int=int(normalized, 16))
|
|
135
|
+
if value.int == 0:
|
|
136
|
+
return Err(Error.IsNil)
|
|
137
|
+
if version(value) > _MAX_VERSION:
|
|
138
|
+
return Err(Error.NoVersion)
|
|
139
|
+
if (value.int >> 62) & 0b11 != 0b10:
|
|
140
|
+
return Err(Error.UnsupportedVariant)
|
|
141
|
+
return Ok(value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def from_string(s: str) -> Maybe[UUID]:
|
|
145
|
+
"""Read a UUID from text, as `parse` does, with no reason for a failure.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
s: The text to read.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
`Just` the UUID, or `Nothing` when `parse` gives an error.
|
|
152
|
+
"""
|
|
153
|
+
match parse(s):
|
|
154
|
+
case Ok(value):
|
|
155
|
+
return Just(value)
|
|
156
|
+
case Err():
|
|
157
|
+
return Nothing()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def for_name(s: str, uuid: UUID) -> UUID:
|
|
161
|
+
"""Make a version 5 UUID from a name and a namespace.
|
|
162
|
+
|
|
163
|
+
The same name and namespace always give the same UUID. The result can be the
|
|
164
|
+
namespace of other UUIDs.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
s: The name. It is encoded as UTF-8.
|
|
168
|
+
uuid: The namespace.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The version 5 (SHA-1) UUID of RFC 4122.
|
|
172
|
+
"""
|
|
173
|
+
return _uuid.uuid5(uuid, s)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def to_string(uuid: UUID) -> str:
|
|
177
|
+
"""Convert a UUID to the canonical text: lower case, with four hyphens."""
|
|
178
|
+
return str(uuid)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def version(uuid: UUID) -> int:
|
|
182
|
+
"""Return the version number of a UUID.
|
|
183
|
+
|
|
184
|
+
The result is the value of the four version bits. Unlike `uuid.UUID.version`,
|
|
185
|
+
it is a number for a UUID of any variant.
|
|
186
|
+
"""
|
|
187
|
+
return (uuid.int >> 76) & 0xF
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def compare(uuid1: UUID, uuid2: UUID) -> Order:
|
|
191
|
+
"""Compare two UUIDs as unsigned 128-bit numbers.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
uuid1: The first UUID.
|
|
195
|
+
uuid2: The second UUID.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
`Order.LT`, `Order.EQ` or `Order.GT`.
|
|
199
|
+
"""
|
|
200
|
+
if uuid1.int < uuid2.int:
|
|
201
|
+
return Order.LT
|
|
202
|
+
if uuid1.int > uuid2.int:
|
|
203
|
+
return Order.GT
|
|
204
|
+
return Order.EQ
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def is_nil_string(s: str) -> bool:
|
|
208
|
+
"""Check that a text is the nil UUID in one of the forms that `parse` reads."""
|
|
209
|
+
return parse(s) == Err(Error.IsNil)
|