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/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]]
|