fluidattacks_integrates_dal 2.2.1__tar.gz → 2.3.1__tar.gz
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.
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/PKG-INFO +2 -1
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/__init__.py +1 -1
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/_cli.py +15 -11
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/client.py +7 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/clients/__init__.py +8 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/clients/group/__init__.py +10 -8
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/clients/organization/__init__.py +3 -3
- fluidattacks_integrates_dal-2.3.1/fluidattacks_integrates_dal/clients/out_of_scope/__init__.py +126 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/core.py +14 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/utils.py +1 -1
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/pyproject.toml +6 -1
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/arch/arch.py +1 -1
- fluidattacks_integrates_dal-2.3.1/tests/test_core.py +30 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/test_list_groups.py +3 -3
- fluidattacks_integrates_dal-2.3.1/uv.lock +1082 -0
- fluidattacks_integrates_dal-2.2.1/tests/test_core.py +0 -12
- fluidattacks_integrates_dal-2.2.1/uv.lock +0 -1312
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/.envrc +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/build/default.nix +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/build/filter.nix +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/flake.lock +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/flake.nix +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/_logger.py +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/_typing.py +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/list_groups.py +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/py.typed +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/mypy.ini +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/ruff.toml +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/__init__.py +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/arch/__init__.py +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/arch/test_arch.py +0 -0
- {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fluidattacks_integrates_dal
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: ASM Synchronous Data Access Layer
|
|
5
5
|
Author-email: Engineering Team <development@fluidattacks.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -8,4 +8,5 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
8
8
|
Requires-Dist: boto3 >=1.40.53, <2.0.0
|
|
9
9
|
Requires-Dist: click >=8.1.7, <9.0.0
|
|
10
10
|
Requires-Dist: fa-purity >=2.5.2, <3.0.0
|
|
11
|
+
Requires-Dist: fluidattacks-etl-utils >=5.1.0, <6.0.0
|
|
11
12
|
Requires-Dist: fluidattacks-utils-logger >=3.0.0, <4.0.0
|
|
@@ -31,19 +31,23 @@ LOG = logging.getLogger(__name__)
|
|
|
31
31
|
@click.command(help="Requires AWS authentication (retrieved from the environment)")
|
|
32
32
|
def list_all_groups() -> NoReturn:
|
|
33
33
|
clients = new_session().bind(
|
|
34
|
-
lambda s:
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
lambda s: (
|
|
35
|
+
new_client(s)
|
|
36
|
+
.map(new_org_client)
|
|
37
|
+
.bind(lambda o: new_resource(s).map(new_group_client).map(lambda g: (o, g)))
|
|
38
|
+
),
|
|
37
39
|
)
|
|
38
40
|
cmd: Cmd[None] = clients.bind(
|
|
39
|
-
lambda t:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
lambda:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
lambda t: (
|
|
42
|
+
list_groups.list_all_groups(t[0], t[1])
|
|
43
|
+
.map(lambda gs: "\n".join(g.name for g in gs))
|
|
44
|
+
.bind(
|
|
45
|
+
lambda s: Cmd.wrap_impure(
|
|
46
|
+
lambda: sys.stdout.write(s + "\n"), # type: ignore[misc]
|
|
47
|
+
).map(
|
|
48
|
+
lambda _: None, # type: ignore[misc]
|
|
49
|
+
),
|
|
50
|
+
)
|
|
47
51
|
),
|
|
48
52
|
)
|
|
49
53
|
cmd.compute()
|
|
@@ -6,12 +6,14 @@ from dataclasses import (
|
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
from fa_purity import (
|
|
9
|
+
FrozenList,
|
|
9
10
|
Stream,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
from .core import (
|
|
13
14
|
GroupId,
|
|
14
15
|
OrganizationId,
|
|
16
|
+
OutOfScopeRoot,
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
|
|
@@ -23,3 +25,8 @@ class OrgsClient:
|
|
|
23
25
|
@dataclass(frozen=True)
|
|
24
26
|
class GroupsClient:
|
|
25
27
|
get_groups: Callable[[OrganizationId], Stream[GroupId]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class OutOfScopeRootsClient:
|
|
32
|
+
get_roots: Callable[[OrganizationId], Stream[FrozenList[OutOfScopeRoot]]]
|
|
@@ -5,6 +5,7 @@ from fa_purity import (
|
|
|
5
5
|
from fluidattacks_integrates_dal.client import (
|
|
6
6
|
GroupsClient,
|
|
7
7
|
OrgsClient,
|
|
8
|
+
OutOfScopeRootsClient,
|
|
8
9
|
)
|
|
9
10
|
from fluidattacks_integrates_dal.utils import (
|
|
10
11
|
new_client,
|
|
@@ -18,6 +19,9 @@ from .group import (
|
|
|
18
19
|
from .organization import (
|
|
19
20
|
new_client as _new_org_client,
|
|
20
21
|
)
|
|
22
|
+
from .out_of_scope import (
|
|
23
|
+
new_client as _new_out_of_scope_client,
|
|
24
|
+
)
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
def new_group_client() -> Cmd[GroupsClient]:
|
|
@@ -26,3 +30,7 @@ def new_group_client() -> Cmd[GroupsClient]:
|
|
|
26
30
|
|
|
27
31
|
def new_org_client() -> Cmd[OrgsClient]:
|
|
28
32
|
return new_session().bind(new_client).map(_new_org_client)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def new_out_of_scope_client() -> Cmd[OutOfScopeRootsClient]:
|
|
36
|
+
return new_session().bind(new_resource).map(_new_out_of_scope_client)
|
|
@@ -134,17 +134,19 @@ def _get_groups(table: Table, org: OrganizationId) -> Stream[GroupId]:
|
|
|
134
134
|
return (
|
|
135
135
|
PureIterFactory.infinite_gen(
|
|
136
136
|
lambda wp: wp.bind(
|
|
137
|
-
lambda p:
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
lambda p: (
|
|
138
|
+
_get_groups_page(table, org, p.last_index).map(_union.inl)
|
|
139
|
+
if p and p.last_index
|
|
140
|
+
else Cmd.wrap_value(None).map(_union.inr)
|
|
141
|
+
),
|
|
140
142
|
),
|
|
141
143
|
init.map(_union.inl),
|
|
142
144
|
)
|
|
143
|
-
.transform(
|
|
144
|
-
.transform(
|
|
145
|
-
.map(
|
|
146
|
-
.map(
|
|
147
|
-
.transform(
|
|
145
|
+
.transform(StreamFactory.from_commands)
|
|
146
|
+
.transform(StreamTransform.until_none)
|
|
147
|
+
.map(_to_group)
|
|
148
|
+
.map(PureIterFactory.from_list)
|
|
149
|
+
.transform(StreamTransform.chain)
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
|
|
@@ -106,9 +106,9 @@ def _all_orgs(client: DynamoDBClient) -> Stream[OrganizationId]:
|
|
|
106
106
|
Unsafe.pure_iter_from_cmd(Cmd.wrap_impure(_new_iter)), # type: ignore[misc]
|
|
107
107
|
)
|
|
108
108
|
return (
|
|
109
|
-
data.map(
|
|
110
|
-
.map(
|
|
111
|
-
.transform(
|
|
109
|
+
data.map(_to_items) # type: ignore[misc]
|
|
110
|
+
.map(PureIterFactory.from_list)
|
|
111
|
+
.transform(StreamTransform.chain)
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
|
fluidattacks_integrates_dal-2.3.1/fluidattacks_integrates_dal/clients/out_of_scope/__init__.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import (
|
|
2
|
+
annotations,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import (
|
|
7
|
+
Mapping,
|
|
8
|
+
Sequence,
|
|
9
|
+
)
|
|
10
|
+
from decimal import (
|
|
11
|
+
Decimal,
|
|
12
|
+
)
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from boto3.dynamodb.conditions import (
|
|
16
|
+
Key,
|
|
17
|
+
)
|
|
18
|
+
from fa_purity import (
|
|
19
|
+
Cmd,
|
|
20
|
+
FrozenList,
|
|
21
|
+
Maybe,
|
|
22
|
+
PureIterFactory,
|
|
23
|
+
PureIterTransform,
|
|
24
|
+
ResultE,
|
|
25
|
+
Stream,
|
|
26
|
+
)
|
|
27
|
+
from fa_purity.json import (
|
|
28
|
+
JsonPrimitiveFactory,
|
|
29
|
+
JsonPrimitiveUnfolder,
|
|
30
|
+
)
|
|
31
|
+
from fluidattacks_etl_utils.paginate import (
|
|
32
|
+
cursor_pagination,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from fluidattacks_integrates_dal.client import (
|
|
36
|
+
OutOfScopeRootsClient,
|
|
37
|
+
)
|
|
38
|
+
from fluidattacks_integrates_dal.core import (
|
|
39
|
+
OrganizationId,
|
|
40
|
+
OutOfScopeRoot,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from mypy_boto3_dynamodb import (
|
|
45
|
+
DynamoDBServiceResource,
|
|
46
|
+
)
|
|
47
|
+
from mypy_boto3_dynamodb.service_resource import (
|
|
48
|
+
Table,
|
|
49
|
+
)
|
|
50
|
+
LOG = logging.getLogger(__name__)
|
|
51
|
+
_ORGS_TABLE = "integrates_vms"
|
|
52
|
+
_Cursor = Mapping[
|
|
53
|
+
str,
|
|
54
|
+
bytes
|
|
55
|
+
| bytearray
|
|
56
|
+
| str
|
|
57
|
+
| int
|
|
58
|
+
| Decimal
|
|
59
|
+
| bool
|
|
60
|
+
| set[int]
|
|
61
|
+
| set[Decimal]
|
|
62
|
+
| set[str]
|
|
63
|
+
| set[bytes]
|
|
64
|
+
| set[bytearray]
|
|
65
|
+
| Sequence[object]
|
|
66
|
+
| Mapping[str, object]
|
|
67
|
+
| None,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _decode_str(item: Mapping[str, object], key: str) -> ResultE[str]:
|
|
72
|
+
return JsonPrimitiveFactory.from_any(item.get(key)).bind(JsonPrimitiveUnfolder.to_str)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _decode_root(item: Mapping[str, object]) -> ResultE[OutOfScopeRoot]:
|
|
76
|
+
return _decode_str(item, "nickname").bind(
|
|
77
|
+
lambda nickname: _decode_str(item, "status").bind(
|
|
78
|
+
lambda status: OutOfScopeRoot.new(nickname, status),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _warn_skip(error: Exception) -> Maybe[OutOfScopeRoot]:
|
|
84
|
+
LOG.warning("Skipping malformed out-of-scope root: %s", error)
|
|
85
|
+
return Maybe[OutOfScopeRoot].empty()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _ignore_errors(result: ResultE[OutOfScopeRoot]) -> Maybe[OutOfScopeRoot]:
|
|
89
|
+
# A malformed cloning record (e.g. still in progress with no status) must not
|
|
90
|
+
# abort the enumeration: log it and drop it. The log is a negligible impurity.
|
|
91
|
+
return result.map(Maybe[OutOfScopeRoot].some).alt(_warn_skip).to_union()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_page(
|
|
95
|
+
table: Table,
|
|
96
|
+
org: OrganizationId,
|
|
97
|
+
cursor: Maybe[_Cursor],
|
|
98
|
+
) -> Cmd[tuple[FrozenList[OutOfScopeRoot], Maybe[_Cursor]]]:
|
|
99
|
+
def _action() -> tuple[FrozenList[OutOfScopeRoot], Maybe[_Cursor]]:
|
|
100
|
+
# pk holds the org, sk starts with CLONING# per the
|
|
101
|
+
# organization_out_of_scope_root facet; queried on the main table.
|
|
102
|
+
condition = Key("pk").eq(f"ORG#{org.uuid}") & Key("sk").begins_with("CLONING#")
|
|
103
|
+
response = cursor.map(
|
|
104
|
+
lambda start_key: table.query(
|
|
105
|
+
KeyConditionExpression=condition,
|
|
106
|
+
ExclusiveStartKey=start_key,
|
|
107
|
+
),
|
|
108
|
+
).or_else_call(lambda: table.query(KeyConditionExpression=condition))
|
|
109
|
+
roots = (
|
|
110
|
+
PureIterFactory.from_list(tuple(response["Items"]))
|
|
111
|
+
.map(_decode_root)
|
|
112
|
+
.map(_ignore_errors)
|
|
113
|
+
.transform(PureIterTransform.filter_maybe)
|
|
114
|
+
.to_list()
|
|
115
|
+
)
|
|
116
|
+
return (roots, Maybe.from_optional(response.get("LastEvaluatedKey")))
|
|
117
|
+
|
|
118
|
+
return Cmd.wrap_impure(_action)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_roots(table: Table, org: OrganizationId) -> Stream[FrozenList[OutOfScopeRoot]]:
|
|
122
|
+
return cursor_pagination(lambda cursor: _get_page(table, org, cursor))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def new_client(resource: DynamoDBServiceResource) -> OutOfScopeRootsClient:
|
|
126
|
+
return OutOfScopeRootsClient(lambda o: _get_roots(resource.Table(_ORGS_TABLE), o))
|
|
@@ -54,3 +54,17 @@ class OrganizationId:
|
|
|
54
54
|
return Result.failure(err, OrganizationId).alt(cast_exception)
|
|
55
55
|
|
|
56
56
|
return Result.success(OrganizationId(_Private(), _name, _uuid), Exception)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class OutOfScopeRoot:
|
|
61
|
+
_private: _Private = field(repr=False, hash=False, compare=False)
|
|
62
|
+
nickname: str
|
|
63
|
+
status: str
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def new(nickname: str, status: str) -> ResultE[OutOfScopeRoot]:
|
|
67
|
+
if not nickname:
|
|
68
|
+
err = ValueError("Out-of-scope root nickname is empty")
|
|
69
|
+
return Result.failure(err, OutOfScopeRoot).alt(cast_exception)
|
|
70
|
+
return Result.success(OutOfScopeRoot(_Private(), nickname, status), Exception)
|
|
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
|
|
19
19
|
def new_session() -> Cmd[Session]:
|
|
20
20
|
# This impure procedure gets inputs (credentials) through the environment
|
|
21
21
|
# e.g. AWS_DEFAULT_REGION
|
|
22
|
-
return Cmd.wrap_impure(
|
|
22
|
+
return Cmd.wrap_impure(Session)
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def new_resource(session: Session) -> Cmd[DynamoDBServiceResource]:
|
|
@@ -11,6 +11,7 @@ dependencies = [
|
|
|
11
11
|
"boto3 >=1.40.53, <2.0.0",
|
|
12
12
|
"click >=8.1.7, <9.0.0",
|
|
13
13
|
"fa-purity >=2.5.2, <3.0.0",
|
|
14
|
+
"fluidattacks-etl-utils >=5.1.0, <6.0.0",
|
|
14
15
|
"fluidattacks-utils-logger >=3.0.0, <4.0.0",
|
|
15
16
|
]
|
|
16
17
|
description = "ASM Synchronous Data Access Layer"
|
|
@@ -27,10 +28,14 @@ build-backend = "flit_core.buildapi"
|
|
|
27
28
|
dev = [
|
|
28
29
|
"arch-lint >=5.0.0, <6.0.0",
|
|
29
30
|
"mypy >=1.18.2, <2.0.0",
|
|
30
|
-
"pytest >=
|
|
31
|
+
"pytest >=9.0.3, <10.0.0",
|
|
31
32
|
"pytest-cov >=7.0.0, <8.0.0",
|
|
32
33
|
"ruff >=0.14.1, <1.0.0",
|
|
33
34
|
"botocore-stubs >=1.40.53, <2.0.0",
|
|
34
35
|
"boto3-stubs >=1.40.53, <2.0.0",
|
|
35
36
|
"mypy-boto3-dynamodb >=1.40.40, <2.0.0",
|
|
36
37
|
]
|
|
38
|
+
|
|
39
|
+
[tool.uv]
|
|
40
|
+
exclude-newer = "7 days"
|
|
41
|
+
exclude-newer-package = { fluidattacks-core = "2099-01-01", fluid-blends = "2099-01-01" , fluidattacks-utils-logger = "2099-01-01" }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from fa_purity import (
|
|
2
|
+
Unsafe,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from fluidattacks_integrates_dal.core import (
|
|
6
|
+
GroupId,
|
|
7
|
+
OrganizationId,
|
|
8
|
+
OutOfScopeRoot,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_group() -> None:
|
|
13
|
+
assert GroupId.new("foo")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_org() -> None:
|
|
17
|
+
assert OrganizationId.new("aaaaaaaa-aaaa-4aaa-baaa-aaaaaaaaaaaa", "foo_org")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_out_of_scope_root() -> None:
|
|
21
|
+
root = OutOfScopeRoot.new("my-repo", "OK").alt(Unsafe.raise_exception).to_union()
|
|
22
|
+
assert root.nickname == "my-repo"
|
|
23
|
+
assert root.status == "OK"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_out_of_scope_root_empty_nickname_fails() -> None:
|
|
27
|
+
outcome = (
|
|
28
|
+
OutOfScopeRoot.new("", "OK").map(lambda _: "created").alt(lambda _: "rejected").to_union()
|
|
29
|
+
)
|
|
30
|
+
assert outcome == "rejected"
|
{fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/test_list_groups.py
RENAMED
|
@@ -32,13 +32,13 @@ def test_list_groups() -> None:
|
|
|
32
32
|
)
|
|
33
33
|
mock_group = GroupId.new("foo_group").alt(Unsafe.raise_exception).to_union()
|
|
34
34
|
groups = PureIterFactory.from_list((mock_group,))
|
|
35
|
-
mock = OrgsClient(StreamFactory.from_commands(orgs.map(
|
|
35
|
+
mock = OrgsClient(StreamFactory.from_commands(orgs.map(Cmd.wrap_value)))
|
|
36
36
|
mock_2 = GroupsClient(
|
|
37
|
-
lambda _: StreamFactory.from_commands(groups.map(
|
|
37
|
+
lambda _: StreamFactory.from_commands(groups.map(Cmd.wrap_value)),
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
def check(items: FrozenList[GroupId]) -> None:
|
|
41
41
|
assert items == (mock_group,)
|
|
42
42
|
|
|
43
43
|
with pytest.raises(SystemExit):
|
|
44
|
-
list_all_groups(mock, mock_2).map(
|
|
44
|
+
list_all_groups(mock, mock_2).map(check).compute()
|