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.
Files changed (99) hide show
  1. morphir/__init__.py +14 -0
  2. morphir/ir/__init__.py +110 -0
  3. morphir/ir/access_controlled.py +15 -0
  4. morphir/ir/decorations.py +19 -0
  5. morphir/ir/diagnostics.py +105 -0
  6. morphir/ir/distribution.py +21 -0
  7. morphir/ir/document.py +37 -0
  8. morphir/ir/documented.py +11 -0
  9. morphir/ir/format_version.py +483 -0
  10. morphir/ir/fqname.py +9 -0
  11. morphir/ir/json.py +11 -0
  12. morphir/ir/literal.py +25 -0
  13. morphir/ir/meta.py +11 -0
  14. morphir/ir/migrate.py +1267 -0
  15. morphir/ir/module.py +17 -0
  16. morphir/ir/name.py +27 -0
  17. morphir/ir/package.py +15 -0
  18. morphir/ir/path.py +25 -0
  19. morphir/ir/qname.py +9 -0
  20. morphir/ir/ref.py +17 -0
  21. morphir/ir/type.py +37 -0
  22. morphir/ir/type_constraints.py +33 -0
  23. morphir/ir/type_def.py +19 -0
  24. morphir/ir/type_spec.py +25 -0
  25. morphir/ir/v3/__init__.py +64 -0
  26. morphir/ir/v3/_legacy_value.py +201 -0
  27. morphir/ir/v3/access_controlled.py +31 -0
  28. morphir/ir/v3/codec.py +1275 -0
  29. morphir/ir/v3/codec_v1.py +491 -0
  30. morphir/ir/v3/codec_v2.py +187 -0
  31. morphir/ir/v3/distribution.py +42 -0
  32. morphir/ir/v3/documented.py +13 -0
  33. morphir/ir/v3/fqname.py +30 -0
  34. morphir/ir/v3/literal.py +46 -0
  35. morphir/ir/v3/module.py +32 -0
  36. morphir/ir/v3/name.py +42 -0
  37. morphir/ir/v3/package.py +23 -0
  38. morphir/ir/v3/path.py +29 -0
  39. morphir/ir/v3/qname.py +16 -0
  40. morphir/ir/v3/type.py +126 -0
  41. morphir/ir/v3/value.py +235 -0
  42. morphir/ir/v4/__init__.py +160 -0
  43. morphir/ir/v4/access_controlled.py +17 -0
  44. morphir/ir/v4/annotation.py +75 -0
  45. morphir/ir/v4/codec.py +2161 -0
  46. morphir/ir/v4/decorations.py +88 -0
  47. morphir/ir/v4/distribution.py +56 -0
  48. morphir/ir/v4/document.py +69 -0
  49. morphir/ir/v4/documented.py +10 -0
  50. morphir/ir/v4/fqname.py +69 -0
  51. morphir/ir/v4/incompleteness.py +78 -0
  52. morphir/ir/v4/json.py +103 -0
  53. morphir/ir/v4/literal.py +51 -0
  54. morphir/ir/v4/meta.py +27 -0
  55. morphir/ir/v4/module.py +30 -0
  56. morphir/ir/v4/name.py +69 -0
  57. morphir/ir/v4/package.py +19 -0
  58. morphir/ir/v4/path.py +50 -0
  59. morphir/ir/v4/qname.py +34 -0
  60. morphir/ir/v4/ref.py +35 -0
  61. morphir/ir/v4/type.py +140 -0
  62. morphir/ir/v4/type_constraints.py +91 -0
  63. morphir/ir/v4/type_def.py +38 -0
  64. morphir/ir/v4/type_spec.py +64 -0
  65. morphir/ir/v4/value.py +480 -0
  66. morphir/ir/value.py +81 -0
  67. morphir/py.typed +0 -0
  68. morphir/sdk/__init__.py +110 -0
  69. morphir/sdk/_compare.py +187 -0
  70. morphir/sdk/aggregate.py +421 -0
  71. morphir/sdk/basics.py +504 -0
  72. morphir/sdk/char.py +110 -0
  73. morphir/sdk/decimal.py +311 -0
  74. morphir/sdk/dict.py +336 -0
  75. morphir/sdk/instant.py +118 -0
  76. morphir/sdk/int.py +86 -0
  77. morphir/sdk/key.py +522 -0
  78. morphir/sdk/list.py +356 -0
  79. morphir/sdk/local_date.py +436 -0
  80. morphir/sdk/local_time.py +267 -0
  81. morphir/sdk/maybe.py +135 -0
  82. morphir/sdk/number.py +237 -0
  83. morphir/sdk/regex.py +287 -0
  84. morphir/sdk/result.py +162 -0
  85. morphir/sdk/result_list.py +169 -0
  86. morphir/sdk/rule.py +103 -0
  87. morphir/sdk/set.py +214 -0
  88. morphir/sdk/stateful_app.py +49 -0
  89. morphir/sdk/string.py +427 -0
  90. morphir/sdk/tuple.py +43 -0
  91. morphir/sdk/uuid.py +209 -0
  92. morphir_python-0.0.1.dist-info/METADATA +269 -0
  93. morphir_python-0.0.1.dist-info/RECORD +99 -0
  94. morphir_python-0.0.1.dist-info/WHEEL +4 -0
  95. morphir_python-0.0.1.dist-info/entry_points.txt +2 -0
  96. morphir_python-0.0.1.dist-info/licenses/LICENSE +201 -0
  97. morphir_tools/__init__.py +13 -0
  98. morphir_tools/cli.py +223 -0
  99. 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)