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.
Files changed (32) hide show
  1. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/PKG-INFO +2 -1
  2. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/__init__.py +1 -1
  3. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/_cli.py +15 -11
  4. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/client.py +7 -0
  5. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/clients/__init__.py +8 -0
  6. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/clients/group/__init__.py +10 -8
  7. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/clients/organization/__init__.py +3 -3
  8. fluidattacks_integrates_dal-2.3.1/fluidattacks_integrates_dal/clients/out_of_scope/__init__.py +126 -0
  9. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/core.py +14 -0
  10. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/utils.py +1 -1
  11. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/pyproject.toml +6 -1
  12. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/arch/arch.py +1 -1
  13. fluidattacks_integrates_dal-2.3.1/tests/test_core.py +30 -0
  14. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/test_list_groups.py +3 -3
  15. fluidattacks_integrates_dal-2.3.1/uv.lock +1082 -0
  16. fluidattacks_integrates_dal-2.2.1/tests/test_core.py +0 -12
  17. fluidattacks_integrates_dal-2.2.1/uv.lock +0 -1312
  18. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/.envrc +0 -0
  19. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/build/default.nix +0 -0
  20. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/build/filter.nix +0 -0
  21. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/flake.lock +0 -0
  22. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/flake.nix +0 -0
  23. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/_logger.py +0 -0
  24. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/_typing.py +0 -0
  25. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/list_groups.py +0 -0
  26. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/fluidattacks_integrates_dal/py.typed +0 -0
  27. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/mypy.ini +0 -0
  28. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/ruff.toml +0 -0
  29. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/__init__.py +0 -0
  30. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/arch/__init__.py +0 -0
  31. {fluidattacks_integrates_dal-2.2.1 → fluidattacks_integrates_dal-2.3.1}/tests/arch/test_arch.py +0 -0
  32. {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.2.1
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
@@ -6,6 +6,6 @@ from ._logger import (
6
6
  set_logger,
7
7
  )
8
8
 
9
- __version__ = "2.2.1"
9
+ __version__ = "2.3.1"
10
10
 
11
11
  Unsafe.compute(set_logger(__name__, __version__))
@@ -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: new_client(s)
35
- .map(new_org_client)
36
- .bind(lambda o: new_resource(s).map(new_group_client).map(lambda g: (o, g))),
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: list_groups.list_all_groups(t[0], t[1])
40
- .map(lambda gs: "\n".join(g.name for g in gs))
41
- .bind(
42
- lambda s: Cmd.wrap_impure(
43
- lambda: sys.stdout.write(s + "\n"), # type: ignore[misc]
44
- ).map(
45
- lambda _: None, # type: ignore[misc]
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: _get_groups_page(table, org, p.last_index).map(_union.inl)
138
- if p and p.last_index
139
- else Cmd.wrap_value(None).map(_union.inr),
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(lambda s: StreamFactory.from_commands(s))
144
- .transform(lambda s: StreamTransform.until_none(s))
145
- .map(lambda x: _to_group(x))
146
- .map(lambda x: PureIterFactory.from_list(x))
147
- .transform(lambda s: StreamTransform.chain(s))
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(lambda x: _to_items(x)) # type: ignore[misc]
110
- .map(lambda x: PureIterFactory.from_list(x))
111
- .transform(lambda s: StreamTransform.chain(s))
109
+ data.map(_to_items) # type: ignore[misc]
110
+ .map(PureIterFactory.from_list)
111
+ .transform(StreamTransform.chain)
112
112
  )
113
113
 
114
114
 
@@ -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(lambda: Session())
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 >=8.4.2, <9.0.0",
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" }
@@ -24,7 +24,7 @@ _dag: Dict[str, FrozenList[FrozenList[str] | str]] = {
24
24
  "_logger",
25
25
  "_typing",
26
26
  ),
27
- "fluidattacks_integrates_dal.clients": (("group", "organization"),),
27
+ "fluidattacks_integrates_dal.clients": (("group", "organization", "out_of_scope"),),
28
28
  }
29
29
 
30
30
 
@@ -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"
@@ -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(lambda o: Cmd.wrap_value(o))))
35
+ mock = OrgsClient(StreamFactory.from_commands(orgs.map(Cmd.wrap_value)))
36
36
  mock_2 = GroupsClient(
37
- lambda _: StreamFactory.from_commands(groups.map(lambda i: Cmd.wrap_value(i))),
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(lambda f: check(f)).compute()
44
+ list_all_groups(mock, mock_2).map(check).compute()