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/result.py ADDED
@@ -0,0 +1,162 @@
1
+ """Morphir.SDK.Result: a computation that may fail.
2
+
3
+ `Result[E, A]` is the union of two frozen dataclasses, `Ok[A]` and `Err[E]`.
4
+ The type parameter order is Elm's: the error type first, then the value type.
5
+ All functions take their arguments in Elm order, with the `Result` last.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING
10
+
11
+ from morphir.sdk.maybe import Just, Maybe, Nothing
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable
15
+
16
+ __all__ = [
17
+ "Err",
18
+ "Ok",
19
+ "Result",
20
+ "and_then",
21
+ "from_maybe",
22
+ "map",
23
+ "map2",
24
+ "map3",
25
+ "map4",
26
+ "map5",
27
+ "map_error",
28
+ "to_maybe",
29
+ "with_default",
30
+ ]
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class Ok[A]:
35
+ """A `Result` that holds a success value."""
36
+
37
+ value: A
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class Err[E]:
42
+ """A `Result` that holds an error."""
43
+
44
+ error: E
45
+
46
+
47
+ type Result[E, A] = Ok[A] | Err[E]
48
+ """Either `Ok[A]` or `Err[E]`."""
49
+
50
+
51
+ def with_default[E, A](default: A, result: Result[E, A]) -> A:
52
+ """Return the value of an `Ok`, or the default for an `Err`."""
53
+ match result:
54
+ case Ok(value):
55
+ return value
56
+ case Err():
57
+ return default
58
+
59
+
60
+ def map[E, A, B](f: Callable[[A], B], result: Result[E, A]) -> Result[E, B]:
61
+ """Apply a function to the value of an `Ok`."""
62
+ if isinstance(result, Ok):
63
+ return Ok(f(result.value))
64
+ return result
65
+
66
+
67
+ def map2[E, A, B, R](
68
+ f: Callable[[A, B], R], ra: Result[E, A], rb: Result[E, B]
69
+ ) -> Result[E, R]:
70
+ """Apply a function when both arguments are `Ok`; else yield the first `Err`."""
71
+ if isinstance(ra, Err):
72
+ return ra
73
+ if isinstance(rb, Err):
74
+ return rb
75
+ return Ok(f(ra.value, rb.value))
76
+
77
+
78
+ def map3[E, A, B, C, R](
79
+ f: Callable[[A, B, C], R],
80
+ ra: Result[E, A],
81
+ rb: Result[E, B],
82
+ rc: Result[E, C],
83
+ ) -> Result[E, R]:
84
+ """Apply a function when all arguments are `Ok`; else yield the first `Err`."""
85
+ if isinstance(ra, Err):
86
+ return ra
87
+ if isinstance(rb, Err):
88
+ return rb
89
+ if isinstance(rc, Err):
90
+ return rc
91
+ return Ok(f(ra.value, rb.value, rc.value))
92
+
93
+
94
+ def map4[E, A, B, C, D, R](
95
+ f: Callable[[A, B, C, D], R],
96
+ ra: Result[E, A],
97
+ rb: Result[E, B],
98
+ rc: Result[E, C],
99
+ rd: Result[E, D],
100
+ ) -> Result[E, R]:
101
+ """Apply a function when all arguments are `Ok`; else yield the first `Err`."""
102
+ if isinstance(ra, Err):
103
+ return ra
104
+ if isinstance(rb, Err):
105
+ return rb
106
+ if isinstance(rc, Err):
107
+ return rc
108
+ if isinstance(rd, Err):
109
+ return rd
110
+ return Ok(f(ra.value, rb.value, rc.value, rd.value))
111
+
112
+
113
+ def map5[E, A, B, C, D, F, R](
114
+ f: Callable[[A, B, C, D, F], R],
115
+ ra: Result[E, A],
116
+ rb: Result[E, B],
117
+ rc: Result[E, C],
118
+ rd: Result[E, D],
119
+ re: Result[E, F],
120
+ ) -> Result[E, R]:
121
+ """Apply a function when all arguments are `Ok`; else yield the first `Err`."""
122
+ if isinstance(ra, Err):
123
+ return ra
124
+ if isinstance(rb, Err):
125
+ return rb
126
+ if isinstance(rc, Err):
127
+ return rc
128
+ if isinstance(rd, Err):
129
+ return rd
130
+ if isinstance(re, Err):
131
+ return re
132
+ return Ok(f(ra.value, rb.value, rc.value, rd.value, re.value))
133
+
134
+
135
+ def and_then[E, A, B](
136
+ f: Callable[[A], Result[E, B]], result: Result[E, A]
137
+ ) -> Result[E, B]:
138
+ """Chain a computation that can fail onto a `Result`."""
139
+ if isinstance(result, Ok):
140
+ return f(result.value)
141
+ return result
142
+
143
+
144
+ def map_error[E, F, A](f: Callable[[E], F], result: Result[E, A]) -> Result[F, A]:
145
+ """Apply a function to the error of an `Err`."""
146
+ if isinstance(result, Err):
147
+ return Err(f(result.error))
148
+ return result
149
+
150
+
151
+ def to_maybe[E, A](result: Result[E, A]) -> Maybe[A]:
152
+ """Convert to a `Maybe`, which drops the error."""
153
+ if isinstance(result, Ok):
154
+ return Just(result.value)
155
+ return Nothing()
156
+
157
+
158
+ def from_maybe[E, A](error: E, maybe: Maybe[A]) -> Result[E, A]:
159
+ """Convert from a `Maybe`, with the given error for `Nothing`."""
160
+ if isinstance(maybe, Just):
161
+ return Ok(maybe.value)
162
+ return Err(error)
@@ -0,0 +1,169 @@
1
+ """Morphir.SDK.ResultList: operations on lists of results.
2
+
3
+ A `ResultList[E, A]` is a tuple of `Result[E, A]` values: a list that holds a
4
+ mix of failed and successful records. These operations model a processing
5
+ pipeline where an error can occur at any step, but must not stop the pipeline.
6
+ A record that failed at an earlier step stays in the list as it is, and each
7
+ step works on the successful records only.
8
+
9
+ All functions take their arguments in Elm order, with the result list last.
10
+ They accept any `Sequence` and always yield a tuple.
11
+
12
+ `filter` and `map` shadow Python built-ins. Import the module with an alias,
13
+ not the names:
14
+
15
+ >>> from morphir.sdk import result_list as ResultList
16
+ >>> from morphir.sdk.result import Err, Ok
17
+ >>> ResultList.keep_all_errors((Ok(1), Err("foo"), Ok(3), Err("bar")))
18
+ Err(error=('foo', 'bar'))
19
+ """
20
+
21
+ from typing import TYPE_CHECKING
22
+
23
+ from morphir.sdk.result import Err, Ok, Result
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable, Sequence
27
+
28
+ __all__ = [
29
+ "ResultList",
30
+ "errors",
31
+ "filter",
32
+ "filter_or_fail",
33
+ "from_list",
34
+ "keep_all_errors",
35
+ "keep_first_error",
36
+ "map",
37
+ "map_or_fail",
38
+ "partition",
39
+ "successes",
40
+ ]
41
+
42
+ type ResultList[E, A] = tuple[Result[E, A], ...]
43
+ """A list that holds a mix of failed (`Err`) and successful (`Ok`) records."""
44
+
45
+
46
+ # Create
47
+
48
+
49
+ def from_list[E, A](xs: Sequence[A]) -> ResultList[E, A]: # pyright: ignore[reportInvalidTypeVarUse]
50
+ """Create a result list from a list. Every element becomes an `Ok`."""
51
+ return tuple(Ok(x) for x in xs)
52
+
53
+
54
+ # Process
55
+
56
+
57
+ def filter[E, A](
58
+ f: Callable[[A], bool], result_list: Sequence[Result[E, A]]
59
+ ) -> ResultList[E, A]:
60
+ """Keep the successful records that pass the test, and all failed records."""
61
+ return tuple(
62
+ result for result in result_list if isinstance(result, Err) or f(result.value)
63
+ )
64
+
65
+
66
+ def filter_or_fail[E, A](
67
+ f: Callable[[A], Result[E, bool]], result_list: Sequence[Result[E, A]]
68
+ ) -> ResultList[E, A]:
69
+ """Filter with a test that can fail.
70
+
71
+ A successful record stays when the test yields `Ok(True)` and goes when the
72
+ test yields `Ok(False)`. When the test yields an `Err`, that error replaces
73
+ the record. All records that failed before stay.
74
+
75
+ Args:
76
+ f: The test.
77
+ result_list: The result list.
78
+
79
+ Returns:
80
+ The filtered result list.
81
+ """
82
+ kept: list[Result[E, A]] = []
83
+ for result in result_list:
84
+ if isinstance(result, Err):
85
+ kept.append(result)
86
+ continue
87
+ match f(result.value):
88
+ case Ok(passed):
89
+ if passed:
90
+ kept.append(result)
91
+ case Err() as failure:
92
+ kept.append(failure)
93
+ return tuple(kept)
94
+
95
+
96
+ def map[E, A, B](
97
+ f: Callable[[A], B], result_list: Sequence[Result[E, A]]
98
+ ) -> ResultList[E, B]:
99
+ """Apply a function to every successful record. Failed records stay."""
100
+ return tuple(
101
+ Ok(f(result.value)) if isinstance(result, Ok) else result
102
+ for result in result_list
103
+ )
104
+
105
+
106
+ def map_or_fail[E, A, B](
107
+ f: Callable[[A], Result[E, B]], result_list: Sequence[Result[E, A]]
108
+ ) -> ResultList[E, B]:
109
+ """Apply a function that can fail to every successful record.
110
+
111
+ An `Err` from the function replaces the record. All records that failed
112
+ before stay.
113
+ """
114
+ return tuple(
115
+ f(result.value) if isinstance(result, Ok) else result for result in result_list
116
+ )
117
+
118
+
119
+ # Decompose
120
+
121
+
122
+ def errors[E, A](result_list: Sequence[Result[E, A]]) -> tuple[E, ...]:
123
+ """Extract the errors of all failed records."""
124
+ return tuple(result.error for result in result_list if isinstance(result, Err))
125
+
126
+
127
+ def successes[E, A](result_list: Sequence[Result[E, A]]) -> tuple[A, ...]:
128
+ """Extract the values of all successful records."""
129
+ return tuple(result.value for result in result_list if isinstance(result, Ok))
130
+
131
+
132
+ def partition[E, A](
133
+ result_list: Sequence[Result[E, A]],
134
+ ) -> tuple[tuple[E, ...], tuple[A, ...]]:
135
+ """Divide a result list into its errors and its successful values."""
136
+ return (errors(result_list), successes(result_list))
137
+
138
+
139
+ # Map to a single result
140
+
141
+
142
+ def keep_all_errors[E, A](
143
+ result_list: Sequence[Result[E, A]],
144
+ ) -> Result[tuple[E, ...], tuple[A, ...]]:
145
+ """Turn a result list into a single result that keeps all errors.
146
+
147
+ Returns:
148
+ `Ok` with all values when no record failed (also for an empty list),
149
+ else `Err` with all errors in list order.
150
+ """
151
+ errs = errors(result_list)
152
+ if errs:
153
+ return Err(errs)
154
+ return Ok(successes(result_list))
155
+
156
+
157
+ def keep_first_error[E, A](
158
+ result_list: Sequence[Result[E, A]],
159
+ ) -> Result[E, tuple[A, ...]]:
160
+ """Turn a result list into a single result that keeps only the first error.
161
+
162
+ Returns:
163
+ `Ok` with all values when no record failed (also for an empty list),
164
+ else `Err` with the error of the first failed record.
165
+ """
166
+ errs = errors(result_list)
167
+ if errs:
168
+ return Err(errs[0])
169
+ return Ok(successes(result_list))
morphir/sdk/rule.py ADDED
@@ -0,0 +1,103 @@
1
+ """Morphir.SDK.Rule: business logic as a sequence of rules.
2
+
3
+ A `Rule[A, B]` is a partial function: a function from `A` to `Maybe[B]` that
4
+ yields `Just` a result when it applies to the input, and `Nothing` when it does
5
+ not. `chain` composes rules into one rule that tries them in sequence. Morphir
6
+ supports only sequential evaluation, so the result is always predictable.
7
+
8
+ `any`, `is_`, `any_of` and `none_of` are the matchers for the cells of a
9
+ decision table:
10
+
11
+ >>> from morphir.sdk import rule as Rule
12
+ >>> from morphir.sdk.maybe import Just, Maybe, Nothing
13
+ >>> def discount(order: tuple[str, int]) -> Maybe[float]:
14
+ ... kind, quantity = order
15
+ ... if Rule.is_("gold", kind) and Rule.any(quantity):
16
+ ... return Just(0.2)
17
+ ... if Rule.any_of(("silver", "bronze"), kind) and Rule.none_of((0,), quantity):
18
+ ... return Just(0.1)
19
+ ... return Nothing()
20
+ >>> Rule.chain((discount, lambda order: Just(0.0)))(("bronze", 3))
21
+ Just(value=0.1)
22
+
23
+ `any` shadows the Python built-in, and `is` is a Python keyword, so it is `is_`.
24
+ Import the module with an alias, not the names.
25
+ """
26
+
27
+ from typing import TYPE_CHECKING
28
+
29
+ from morphir.sdk import _compare
30
+ from morphir.sdk.maybe import Just, Maybe, Nothing
31
+
32
+ if TYPE_CHECKING:
33
+ from collections.abc import Callable, Sequence
34
+
35
+ __all__ = [
36
+ "Rule",
37
+ "any",
38
+ "any_of",
39
+ "chain",
40
+ "is_",
41
+ "none_of",
42
+ ]
43
+
44
+ type Rule[A, B] = Callable[[A], Maybe[B]]
45
+ """A function that yields `Just` a result when it applies, else `Nothing`."""
46
+
47
+
48
+ def chain[A, B](rules: Sequence[Rule[A, B]]) -> Rule[A, B]:
49
+ """Chain a list of rules into a single rule.
50
+
51
+ The rules are evaluated in the sequence in which they were supplied. The
52
+ first rule that yields a `Just` gives the result, and the rules after it do
53
+ not run.
54
+
55
+ Args:
56
+ rules: The rules, in the sequence of evaluation.
57
+
58
+ Returns:
59
+ A rule that yields the result of the first rule that matches, or
60
+ `Nothing` when no rule matches.
61
+ """
62
+ fixed = tuple(rules)
63
+
64
+ def chained(value: A) -> Maybe[B]:
65
+ for rule in fixed:
66
+ result = rule(value)
67
+ if isinstance(result, Just):
68
+ return result
69
+ return Nothing()
70
+
71
+ return chained
72
+
73
+
74
+ def any(value: object) -> bool:
75
+ """Return `True` for any input. Use as a wildcard in a decision table."""
76
+ return True
77
+
78
+
79
+ def is_[A](ref: A, value: A) -> bool:
80
+ """Check that the value is structurally equal to the reference (Elm `is`).
81
+
82
+ Raises:
83
+ TypeError: If a value is a function or holds a function.
84
+ """
85
+ return _compare.equal(ref, value)
86
+
87
+
88
+ def any_of[A](ref: Sequence[A], value: A) -> bool:
89
+ """Check that the value is in the list of references, by structural equality.
90
+
91
+ Raises:
92
+ TypeError: If a value is a function or holds a function.
93
+ """
94
+ return next((True for item in ref if _compare.equal(value, item)), False)
95
+
96
+
97
+ def none_of[A](ref: Sequence[A], value: A) -> bool:
98
+ """Check that the value is not in the list of references.
99
+
100
+ Raises:
101
+ TypeError: If a value is a function or holds a function.
102
+ """
103
+ return not any_of(ref, value)
morphir/sdk/set.py ADDED
@@ -0,0 +1,214 @@
1
+ """Morphir.SDK.Set: a collection of unique comparable values.
2
+
3
+ This module follows elm/core 1.0.5. A `Set[A]` is a frozen dataclass that holds
4
+ its members as a tuple, sorted in ascending order by `basics.compare`, with no
5
+ member twice. Thus `to_list` yields the members in order, and two sets with the
6
+ same members are equal (`==`) and have the same hash, no matter in which order
7
+ the members were inserted.
8
+
9
+ Members can be any comparable value: numbers, strings, and tuples or lists of
10
+ comparables. A member that is not comparable raises `TypeError`.
11
+
12
+ Build a `Set` with `empty`, `singleton`, `from_list` and `insert`. Do not pass
13
+ members to the `Set` constructor, because it does not sort them.
14
+
15
+ Some names in this module shadow Python built-ins (`filter`, `map`). Import the
16
+ module with an alias, not the names:
17
+
18
+ >>> from morphir.sdk import set as Set
19
+ >>> Set.to_list(Set.from_list((3, 1, 3, 2)))
20
+ (1, 2, 3)
21
+ """
22
+
23
+ from dataclasses import dataclass
24
+ from typing import TYPE_CHECKING
25
+
26
+ from morphir.sdk import _compare
27
+ from morphir.sdk._compare import Comparable, Order
28
+
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Callable, Sequence
31
+
32
+ __all__ = [
33
+ "Set",
34
+ "diff",
35
+ "empty",
36
+ "filter",
37
+ "foldl",
38
+ "foldr",
39
+ "from_list",
40
+ "insert",
41
+ "intersect",
42
+ "is_empty",
43
+ "map",
44
+ "member",
45
+ "partition",
46
+ "remove",
47
+ "singleton",
48
+ "size",
49
+ "to_list",
50
+ "union",
51
+ ]
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class Set[A: Comparable]:
56
+ """An immutable set with its members sorted.
57
+
58
+ Use `from_list`, `singleton` or `insert` to build one. The constructor
59
+ accepts only members that are already sorted with no member twice, and
60
+ stores every list inside a member as a tuple.
61
+
62
+ Raises:
63
+ ValueError: If the members are not sorted, or a member repeats.
64
+ TypeError: If two members are not comparable with each other.
65
+ """
66
+
67
+ entries: tuple[A, ...] = ()
68
+
69
+ def __post_init__(self) -> None:
70
+ entries = tuple(_compare.freeze(member) for member in self.entries)
71
+ _compare.check_strictly_ascending(entries, "Set")
72
+ object.__setattr__(self, "entries", entries)
73
+
74
+
75
+ def _identity[A](value: A) -> A:
76
+ return value
77
+
78
+
79
+ def _search[A: Comparable](value: A, set_: Set[A]) -> tuple[bool, int]:
80
+ return _compare.search(value, set_.entries, _identity)
81
+
82
+
83
+ # Build
84
+
85
+
86
+ def empty[A: Comparable]() -> Set[A]: # pyright: ignore[reportInvalidTypeVarUse]
87
+ """Create an empty set."""
88
+ return Set()
89
+
90
+
91
+ def singleton[A: Comparable](value: A) -> Set[A]:
92
+ """Create a set with one member."""
93
+ return Set((value,))
94
+
95
+
96
+ def insert[A: Comparable](value: A, set_: Set[A]) -> Set[A]:
97
+ """Insert a value into a set.
98
+
99
+ Raises:
100
+ TypeError: If the value is not comparable with the members of the set.
101
+ """
102
+ found, index = _search(value, set_)
103
+ if found:
104
+ return set_
105
+ return Set((*set_.entries[:index], value, *set_.entries[index:]))
106
+
107
+
108
+ def remove[A: Comparable](value: A, set_: Set[A]) -> Set[A]:
109
+ """Remove a value from a set. A value that is not there changes nothing."""
110
+ found, index = _search(value, set_)
111
+ if not found:
112
+ return set_
113
+ return Set((*set_.entries[:index], *set_.entries[index + 1 :]))
114
+
115
+
116
+ # Query
117
+
118
+
119
+ def is_empty[A: Comparable](set_: Set[A]) -> bool:
120
+ """Check that a set is empty."""
121
+ return not set_.entries
122
+
123
+
124
+ def member[A: Comparable](value: A, set_: Set[A]) -> bool:
125
+ """Check that a value is in a set."""
126
+ return _search(value, set_)[0]
127
+
128
+
129
+ def size[A: Comparable](set_: Set[A]) -> int:
130
+ """Return the number of members in a set."""
131
+ return len(set_.entries)
132
+
133
+
134
+ # Lists
135
+
136
+
137
+ def to_list[A: Comparable](set_: Set[A]) -> tuple[A, ...]:
138
+ """Return the members, from lowest to highest."""
139
+ return set_.entries
140
+
141
+
142
+ def from_list[A: Comparable](values: Sequence[A]) -> Set[A]:
143
+ """Create a set from a list of values.
144
+
145
+ Raises:
146
+ TypeError: If the values are not comparable with each other.
147
+ """
148
+ entries: list[A] = []
149
+ for value in sorted(values, key=_compare.sort_key(_compare.compare)):
150
+ if not entries or _compare.compare(entries[-1], value) is not Order.EQ:
151
+ entries.append(value)
152
+ return Set(tuple(entries))
153
+
154
+
155
+ # Transform
156
+
157
+
158
+ def map[A: Comparable, B: Comparable](f: Callable[[A], B], set_: Set[A]) -> Set[B]:
159
+ """Apply a function to every member.
160
+
161
+ The result is sorted again and has no duplicates, so it can be smaller than
162
+ the input.
163
+ """
164
+ return from_list(tuple(f(value) for value in set_.entries))
165
+
166
+
167
+ def foldl[A: Comparable, R](f: Callable[[A, R], R], initial: R, set_: Set[A]) -> R:
168
+ """Reduce the members from lowest to highest."""
169
+ accumulator = initial
170
+ for value in set_.entries:
171
+ accumulator = f(value, accumulator)
172
+ return accumulator
173
+
174
+
175
+ def foldr[A: Comparable, R](f: Callable[[A, R], R], initial: R, set_: Set[A]) -> R:
176
+ """Reduce the members from highest to lowest."""
177
+ accumulator = initial
178
+ for value in reversed(set_.entries):
179
+ accumulator = f(value, accumulator)
180
+ return accumulator
181
+
182
+
183
+ def filter[A: Comparable](predicate: Callable[[A], bool], set_: Set[A]) -> Set[A]:
184
+ """Keep the members that pass the test."""
185
+ return Set(tuple(value for value in set_.entries if predicate(value)))
186
+
187
+
188
+ def partition[A: Comparable](
189
+ predicate: Callable[[A], bool], set_: Set[A]
190
+ ) -> tuple[Set[A], Set[A]]:
191
+ """Split a set into the members that pass the test and those that fail."""
192
+ passed: list[A] = []
193
+ failed: list[A] = []
194
+ for value in set_.entries:
195
+ (passed if predicate(value) else failed).append(value)
196
+ return (Set(tuple(passed)), Set(tuple(failed)))
197
+
198
+
199
+ # Combine
200
+
201
+
202
+ def union[A: Comparable](left: Set[A], right: Set[A]) -> Set[A]:
203
+ """Return the values that are in one set or the other."""
204
+ return from_list((*left.entries, *right.entries))
205
+
206
+
207
+ def intersect[A: Comparable](left: Set[A], right: Set[A]) -> Set[A]:
208
+ """Return the values that are in both sets."""
209
+ return Set(tuple(value for value in left.entries if member(value, right)))
210
+
211
+
212
+ def diff[A: Comparable](left: Set[A], right: Set[A]) -> Set[A]:
213
+ """Return the values of the first set that are not in the second."""
214
+ return Set(tuple(value for value in left.entries if not member(value, right)))
@@ -0,0 +1,49 @@
1
+ """Morphir.SDK.StatefulApp: the model of a stateful application.
2
+
3
+ A `StatefulApp` holds one function, the business logic. The function gets the
4
+ current state (`Nothing` when there is no state yet) and a command. It yields
5
+ the new state (`Nothing` to delete the state) and an event:
6
+
7
+ >>> from morphir.sdk.maybe import Just, Maybe, Nothing
8
+ >>> from morphir.sdk.stateful_app import StatefulApp
9
+ >>> def logic(state: Maybe[int], command: str) -> tuple[Maybe[int], str]:
10
+ ... total = (state.value if isinstance(state, Just) else 0) + 1
11
+ ... return (Just(total), f"{command}: {total}")
12
+ >>> app: StatefulApp[str, str, int, str] = StatefulApp(logic)
13
+ >>> app.logic(Nothing(), "open")
14
+ (Just(value=1), 'open: 1')
15
+
16
+ The module has a type and no functions, as in Elm.
17
+ """
18
+
19
+ from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING
21
+
22
+ from morphir.sdk.maybe import (
23
+ Maybe, # noqa: TC001 - the dataclass field annotation needs it at run time
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Callable
28
+
29
+ __all__ = ["StatefulApp"]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class StatefulApp[K, C, S, E]:
34
+ """A stateful application (Elm `StatefulApp (Maybe s -> c -> ( Maybe s, e ))`).
35
+
36
+ The type parameters fit the application to a use case:
37
+
38
+ * `K`: the key that partitions commands, events and state. No field uses
39
+ it, as in Elm; it is there for the type checker only.
40
+ * `C`: the commands that the application accepts.
41
+ * `S`: the state that the application manages.
42
+ * `E`: the events that the application publishes.
43
+
44
+ Attributes:
45
+ logic: The business logic. It gets the current state and a command, and
46
+ yields the new state and an event.
47
+ """
48
+
49
+ logic: Callable[[Maybe[S], C], tuple[Maybe[S], E]]