fluidattacks_gitlab_sdk 3.0.2__tar.gz → 4.0.0__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 (96) hide show
  1. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/PKG-INFO +1 -1
  2. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/__init__.py +1 -1
  3. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/issues/_client/_updated_by.py +14 -2
  4. fluidattacks_gitlab_sdk-4.0.0/fluidattacks_gitlab_sdk/mr_notes/__init__.py +4 -0
  5. fluidattacks_gitlab_sdk-4.0.0/fluidattacks_gitlab_sdk/mr_notes/_client.py +113 -0
  6. fluidattacks_gitlab_sdk-4.0.0/fluidattacks_gitlab_sdk/mr_notes/_core.py +23 -0
  7. fluidattacks_gitlab_sdk-4.0.0/fluidattacks_gitlab_sdk/mr_notes/_decode.py +24 -0
  8. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/users/core.py +3 -0
  9. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/users/decode.py +13 -2
  10. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/pyproject.toml +1 -1
  11. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/arch/arch.py +6 -1
  12. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/issues/test_decode.py +7 -1
  13. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/merge_request/test_decode.py +4 -0
  14. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/mr_approvals/decoder_test.py +3 -1
  15. fluidattacks_gitlab_sdk-4.0.0/tests/mr_notes/data.json +62 -0
  16. fluidattacks_gitlab_sdk-4.0.0/tests/mr_notes/decoder_test.py +65 -0
  17. fluidattacks_gitlab_sdk-4.0.0/tests_fx/py.typed +0 -0
  18. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/uv.lock +22 -13
  19. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/.envrc +0 -0
  20. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/build/default.nix +0 -0
  21. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/build/filter.nix +0 -0
  22. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/flake.lock +0 -0
  23. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/flake.nix +0 -0
  24. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_decoders.py +0 -0
  25. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_gql_client/__init__.py +0 -0
  26. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_gql_client/_client.py +0 -0
  27. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_gql_client/_error.py +0 -0
  28. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_gql_client/_handlers.py +0 -0
  29. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_handlers.py +0 -0
  30. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_http_client/__init__.py +0 -0
  31. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_http_client/_client_1.py +0 -0
  32. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_http_client/_core.py +0 -0
  33. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/_logger.py +0 -0
  34. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/ids.py +0 -0
  35. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/issues/__init__.py +0 -0
  36. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/issues/_client/__init__.py +0 -0
  37. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/issues/_client/_decode.py +0 -0
  38. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/issues/_client/_get_issue.py +0 -0
  39. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/issues/_client/_most_recent.py +0 -0
  40. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/issues/core.py +0 -0
  41. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/jobs/__init__.py +0 -0
  42. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/jobs/_client.py +0 -0
  43. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/jobs/_decode.py +0 -0
  44. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/jobs/core.py +0 -0
  45. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/members/__init__.py +0 -0
  46. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/members/_client.py +0 -0
  47. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/members/_decode.py +0 -0
  48. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/members/core.py +0 -0
  49. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/merge_requests/__init__.py +0 -0
  50. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/merge_requests/_client.py +0 -0
  51. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/merge_requests/_decode.py +0 -0
  52. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/merge_requests/core.py +0 -0
  53. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/milestones/__init__.py +0 -0
  54. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/milestones/_client.py +0 -0
  55. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/milestones/_decode.py +0 -0
  56. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/milestones/core.py +0 -0
  57. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/mr_approvals/__init__.py +0 -0
  58. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/mr_approvals/_client.py +0 -0
  59. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/mr_approvals/_decode.py +0 -0
  60. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/mr_approvals/core.py +0 -0
  61. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/py.typed +0 -0
  62. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/fluidattacks_gitlab_sdk/users/__init__.py +0 -0
  63. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/mypy.ini +0 -0
  64. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/ruff.toml +0 -0
  65. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/__init__.py +0 -0
  66. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/arch/__init__.py +0 -0
  67. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/arch/test_arch.py +0 -0
  68. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/issues/__init__.py +0 -0
  69. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/issues/data.json +0 -0
  70. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/jobs/__init__.py +0 -0
  71. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/jobs/data.json +0 -0
  72. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/jobs/test_decode_job.py +0 -0
  73. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/members/__init__.py +0 -0
  74. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/members/data.json +0 -0
  75. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/members/test_decode_members.py +0 -0
  76. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/merge_request/__init__.py +0 -0
  77. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/merge_request/data.json +0 -0
  78. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/merge_request/data_mr_updates.json +0 -0
  79. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/mr_approvals/__init__.py +0 -0
  80. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/mr_approvals/data.json +0 -0
  81. {fluidattacks_gitlab_sdk-3.0.2/tests_fx → fluidattacks_gitlab_sdk-4.0.0/tests/mr_notes}/__init__.py +0 -0
  82. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests/py.typed +0 -0
  83. {fluidattacks_gitlab_sdk-3.0.2/tests_fx/issues → fluidattacks_gitlab_sdk-4.0.0/tests_fx}/__init__.py +0 -0
  84. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/_utils.py +0 -0
  85. {fluidattacks_gitlab_sdk-3.0.2/tests_fx/jobs → fluidattacks_gitlab_sdk-4.0.0/tests_fx/issues}/__init__.py +0 -0
  86. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/issues/test_issue_client.py +0 -0
  87. {fluidattacks_gitlab_sdk-3.0.2/tests_fx/members → fluidattacks_gitlab_sdk-4.0.0/tests_fx/jobs}/__init__.py +0 -0
  88. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/jobs/client_test.py +0 -0
  89. {fluidattacks_gitlab_sdk-3.0.2/tests_fx/milestones → fluidattacks_gitlab_sdk-4.0.0/tests_fx/members}/__init__.py +0 -0
  90. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/members/test_client_members.py +0 -0
  91. {fluidattacks_gitlab_sdk-3.0.2/tests_fx/mr_approvals → fluidattacks_gitlab_sdk-4.0.0/tests_fx/milestones}/__init__.py +0 -0
  92. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/milestones/client_test.py +0 -0
  93. /fluidattacks_gitlab_sdk-3.0.2/tests_fx/py.typed → /fluidattacks_gitlab_sdk-4.0.0/tests_fx/mr_approvals/__init__.py +0 -0
  94. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/mr_approvals/client_test.py +0 -0
  95. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/test_gql_client.py +0 -0
  96. {fluidattacks_gitlab_sdk-3.0.2 → fluidattacks_gitlab_sdk-4.0.0}/tests_fx/test_mr.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluidattacks_gitlab_sdk
3
- Version: 3.0.2
3
+ Version: 4.0.0
4
4
  Summary: gitlab SDK
5
5
  Author-email: Product Team <development@fluidattacks.com>
6
6
  Requires-Python: >=3.11
@@ -11,7 +11,7 @@ from ._logger import (
11
11
  set_logger,
12
12
  )
13
13
 
14
- __version__ = "3.0.2"
14
+ __version__ = "4.0.0"
15
15
 
16
16
  Unsafe.compute(set_logger(__name__, __version__))
17
17
  LOG = logging.getLogger(__name__)
@@ -55,8 +55,19 @@ def decode_user_obj(raw: JsonObj) -> ResultE[UserObj]:
55
55
  lambda user: (
56
56
  JsonUnfolder.require(raw, "name", DecodeUtils.to_str)
57
57
  .map(UserName)
58
- .map(
59
- lambda name: UserObj(user_id, user, name),
58
+ .bind(
59
+ lambda name: JsonUnfolder.optional(
60
+ raw,
61
+ "publicEmail",
62
+ DecodeUtils.to_opt_str,
63
+ ).map(
64
+ lambda email: UserObj(
65
+ user_id,
66
+ user,
67
+ name,
68
+ email.bind(lambda m: m),
69
+ ),
70
+ ),
60
71
  )
61
72
  ),
62
73
  )
@@ -135,6 +146,7 @@ def get_updated_by(
135
146
  id
136
147
  name
137
148
  username
149
+ publicEmail
138
150
  }
139
151
  }
140
152
  }
@@ -0,0 +1,4 @@
1
+ from ._client import MrNotesFactory
2
+ from ._core import MrNote, NotesClient
3
+
4
+ __all__ = ["MrNote", "MrNotesFactory", "NotesClient"]
@@ -0,0 +1,113 @@
1
+ from __future__ import (
2
+ annotations,
3
+ )
4
+
5
+ import inspect
6
+ from dataclasses import dataclass
7
+
8
+ from fa_purity import (
9
+ Cmd,
10
+ FrozenList,
11
+ Maybe,
12
+ ResultE,
13
+ Stream,
14
+ cast_exception,
15
+ )
16
+ from fa_purity._core.utils import raise_exception
17
+ from fa_purity.json import Primitive, UnfoldedFactory
18
+ from fluidattacks_etl_utils.bug import Bug
19
+ from fluidattacks_etl_utils.decode import int_to_str
20
+ from fluidattacks_etl_utils.paginate import cursor_pagination
21
+
22
+ from fluidattacks_gitlab_sdk._decoders import assert_multiple
23
+ from fluidattacks_gitlab_sdk._http_client import (
24
+ ClientFactory,
25
+ Credentials,
26
+ HttpJsonClient,
27
+ RelativeEndpoint,
28
+ )
29
+ from fluidattacks_gitlab_sdk.ids import MrInternalId, ProjectId
30
+
31
+ from ._core import MrNote, NotesClient
32
+ from ._decode import decode_mr_notes
33
+
34
+ _NOTES_PER_PAGE = 100
35
+
36
+
37
+ def _get_notes_page(
38
+ client: HttpJsonClient,
39
+ project: ProjectId,
40
+ mr_id: MrInternalId,
41
+ page: int,
42
+ ) -> Cmd[ResultE[tuple[FrozenList[MrNote], Maybe[int]]]]:
43
+ endpoint = RelativeEndpoint.new(
44
+ "projects",
45
+ int_to_str(project.project_id.value),
46
+ "merge_requests",
47
+ int_to_str(mr_id.internal.value),
48
+ "notes",
49
+ )
50
+ params: dict[str, Primitive] = {
51
+ "order_by": "created_at",
52
+ "sort": "asc",
53
+ "page": int_to_str(page),
54
+ "per_page": _NOTES_PER_PAGE,
55
+ }
56
+ return client.get(
57
+ endpoint,
58
+ UnfoldedFactory.from_dict(params),
59
+ ).map(
60
+ lambda r: (
61
+ r.alt(
62
+ lambda e: cast_exception(
63
+ Bug.new(
64
+ "_get_notes_page",
65
+ inspect.currentframe(),
66
+ e,
67
+ (),
68
+ ),
69
+ ),
70
+ )
71
+ .bind(assert_multiple)
72
+ .bind(decode_mr_notes)
73
+ .map(
74
+ lambda notes: (
75
+ notes,
76
+ Maybe.empty() if len(notes) < _NOTES_PER_PAGE else Maybe.some(page + 1),
77
+ ),
78
+ )
79
+ ),
80
+ )
81
+
82
+
83
+ def _get_page(
84
+ client: HttpJsonClient,
85
+ project: ProjectId,
86
+ mr_id: MrInternalId,
87
+ maybe_page: Maybe[int],
88
+ ) -> Cmd[tuple[FrozenList[MrNote], Maybe[int]]]:
89
+ page = maybe_page.value_or(1)
90
+ return _get_notes_page(client, project, mr_id, page).map(
91
+ lambda r: r.alt(raise_exception).to_union(),
92
+ )
93
+
94
+
95
+ def _get_mr_notes(
96
+ client: HttpJsonClient,
97
+ project: ProjectId,
98
+ mr_id: MrInternalId,
99
+ ) -> Stream[FrozenList[MrNote]]:
100
+ return cursor_pagination(
101
+ lambda maybe_page: _get_page(client, project, mr_id, maybe_page),
102
+ )
103
+
104
+
105
+ def _from_client(client: HttpJsonClient) -> NotesClient:
106
+ return NotesClient(lambda p, i: _get_mr_notes(client, p, i))
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class MrNotesFactory:
111
+ @staticmethod
112
+ def new(creds: Credentials) -> NotesClient:
113
+ return _from_client(ClientFactory.new(creds))
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+
6
+ from fa_purity import FrozenList, Stream
7
+ from fa_purity.date_time import DatetimeUTC
8
+
9
+ from fluidattacks_gitlab_sdk.ids import MrInternalId, ProjectId
10
+ from fluidattacks_gitlab_sdk.users import UserObj
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class MrNote:
15
+ body: str
16
+ author: UserObj
17
+ created_at: DatetimeUTC
18
+ system: bool
19
+
20
+
21
+ @dataclass
22
+ class NotesClient:
23
+ get_mr_notes: Callable[[ProjectId, MrInternalId], Stream[FrozenList[MrNote]]]
@@ -0,0 +1,24 @@
1
+ from fa_purity import FrozenList, PureIterFactory, Result, ResultE, ResultTransform
2
+ from fa_purity.json import JsonObj, JsonUnfolder, Unfolder
3
+ from fluidattacks_etl_utils import smash
4
+ from fluidattacks_etl_utils.decode import DecodeUtils
5
+
6
+ from fluidattacks_gitlab_sdk.mr_notes._core import MrNote
7
+ from fluidattacks_gitlab_sdk.users.decode import decode_user_obj
8
+
9
+
10
+ def decode_mr_note(raw: JsonObj) -> ResultE[MrNote]:
11
+ return smash.smash_result_4(
12
+ JsonUnfolder.require(raw, "body", DecodeUtils.to_str),
13
+ JsonUnfolder.require(raw, "author", lambda v: Unfolder.to_json(v).bind(decode_user_obj)),
14
+ JsonUnfolder.require(raw, "created_at", DecodeUtils.to_date_time),
15
+ JsonUnfolder.require(raw, "system", DecodeUtils.to_bool),
16
+ ).map(lambda t: MrNote(*t))
17
+
18
+
19
+ def decode_mr_notes(raw: FrozenList[JsonObj]) -> ResultE[FrozenList[MrNote]]:
20
+ if not raw:
21
+ return Result.success(FrozenList[MrNote](()), Exception)
22
+ return ResultTransform.all_ok(
23
+ PureIterFactory.from_list(raw).map(decode_mr_note).to_list(),
24
+ )
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
 
5
+ from fa_purity import Maybe
6
+
5
7
  from fluidattacks_gitlab_sdk.ids import UserId
6
8
 
7
9
 
@@ -24,3 +26,4 @@ class UserObj:
24
26
  user_id: UserId
25
27
  user: User
26
28
  name: UserName
29
+ public_email: Maybe[str]
@@ -20,8 +20,19 @@ def decode_user_obj(raw: JsonObj) -> ResultE[UserObj]:
20
20
  lambda user: (
21
21
  JsonUnfolder.require(raw, "name", DecodeUtils.to_str)
22
22
  .map(UserName)
23
- .map(
24
- lambda name: UserObj(user_id, user, name),
23
+ .bind(
24
+ lambda name: JsonUnfolder.optional(
25
+ raw,
26
+ "public_email",
27
+ DecodeUtils.to_opt_str,
28
+ ).map(
29
+ lambda email: UserObj(
30
+ user_id,
31
+ user,
32
+ name,
33
+ email.bind(lambda m: m),
34
+ ),
35
+ ),
25
36
  )
26
37
  ),
27
38
  )
@@ -34,7 +34,7 @@ dev = [
34
34
 
35
35
  [tool.uv]
36
36
  exclude-newer = "7 days"
37
- exclude-newer-package = { fluidattacks-core = "2099-01-01", fluid-blends = "2099-01-01" }
37
+ exclude-newer-package = { fluidattacks-core = "2099-01-01", fluid-blends = "2099-01-01" , fluidattacks-utils-logger = "2099-01-01" }
38
38
  override-dependencies = [
39
39
  "cryptography==48.0.1"
40
40
  ]
@@ -26,7 +26,7 @@ def _module(path: str) -> FullPathModule | NoReturn:
26
26
 
27
27
  _dag: Dict[str, tuple[tuple[str, ...] | str, ...]] = {
28
28
  "fluidattacks_gitlab_sdk": (
29
- ("merge_requests", "members", "issues", "milestones", "mr_approvals", "jobs"),
29
+ ("merge_requests", "members", "issues", "milestones", "mr_approvals", "mr_notes", "jobs"),
30
30
  "users",
31
31
  "_handlers",
32
32
  ("_http_client", "_gql_client", "_decoders"),
@@ -74,6 +74,11 @@ _dag: Dict[str, tuple[tuple[str, ...] | str, ...]] = {
74
74
  "_decode",
75
75
  "core",
76
76
  ),
77
+ "fluidattacks_gitlab_sdk.mr_notes": (
78
+ "_client",
79
+ "_decode",
80
+ "_core",
81
+ ),
77
82
  "fluidattacks_gitlab_sdk.jobs": ("_client", "_decode", "core"),
78
83
  }
79
84
 
@@ -32,11 +32,17 @@ from fluidattacks_gitlab_sdk.users.core import User, UserName, UserObj
32
32
 
33
33
 
34
34
  def test_decode() -> None:
35
- author = UserObj(UserId(NaturalOperations.absolute(1)), User("ghost1"), UserName("Ghost User"))
35
+ author = UserObj(
36
+ UserId(NaturalOperations.absolute(1)),
37
+ User("ghost1"),
38
+ UserName("Ghost User"),
39
+ Maybe.some(""),
40
+ )
36
41
  assignee = UserObj(
37
42
  UserId(NaturalOperations.absolute(333)),
38
43
  User("the_user"),
39
44
  UserName("The User"),
45
+ Maybe.some(""),
40
46
  )
41
47
  closed_by = assignee
42
48
  issue_def = IssueDef("The title", "closed", IssueType.ISSUE, Maybe.some(""))
@@ -38,11 +38,13 @@ def test_decode() -> None:
38
38
  UserId(NaturalOperations.absolute(987654321)),
39
39
  User("the_author"),
40
40
  UserName("The Author"),
41
+ Maybe.empty(),
41
42
  )
42
43
  merger = UserObj(
43
44
  UserId(NaturalOperations.absolute(1234567)),
44
45
  User("the_merger"),
45
46
  UserName("The merger"),
47
+ Maybe.empty(),
46
48
  )
47
49
  project = ProjectId(NaturalOperations.absolute(123456789))
48
50
  expected = (
@@ -115,11 +117,13 @@ def test_decode_mrs_updated() -> None:
115
117
  UserId(NaturalOperations.absolute(987654321)),
116
118
  User("the_author"),
117
119
  UserName("The Author"),
120
+ Maybe.empty(),
118
121
  )
119
122
  merger = UserObj(
120
123
  UserId(NaturalOperations.absolute(1234567)),
121
124
  User("the_merger"),
122
125
  UserName("The merger"),
126
+ Maybe.empty(),
123
127
  )
124
128
  project = ProjectId(NaturalOperations.absolute(123456789))
125
129
  expected_mrs = (
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from fa_purity import FrozenList, Unsafe
3
+ from fa_purity import FrozenList, Maybe, Unsafe
4
4
  from fa_purity.json import JsonValueFactory, Unfolder
5
5
  from fluidattacks_etl_utils.natural import NaturalOperations
6
6
 
@@ -31,6 +31,7 @@ def test_decode() -> None:
31
31
  UserId(NaturalOperations.absolute(26904760)),
32
32
  User("useratfluid"),
33
33
  UserName("User Fluid"),
34
+ Maybe.some(""),
34
35
  ),
35
36
  ),
36
37
  Approver(
@@ -38,6 +39,7 @@ def test_decode() -> None:
38
39
  UserId(NaturalOperations.absolute(34547217)),
39
40
  User("project_20741933_bot_7328383692d798602683c341e690"),
40
41
  UserName("****"),
42
+ Maybe.empty(),
41
43
  ),
42
44
  ),
43
45
  )
@@ -0,0 +1,62 @@
1
+ [
2
+ {
3
+ "id": 2712345601,
4
+ "type": null,
5
+ "body": "required: [major-version] change the major version to publish the new indicators package",
6
+ "author": {
7
+ "id": 12345678,
8
+ "username": "dmurciaatfluid",
9
+ "public_email": "",
10
+ "name": "Daniel Murcia",
11
+ "state": "active",
12
+ "locked": false,
13
+ "avatar_url": "https://secure.gravatar.com/avatar/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?s=80&d=identicon",
14
+ "web_url": "https://gitlab.com/dmurciaatfluid"
15
+ },
16
+ "created_at": "2026-07-08T12:31:03.134Z",
17
+ "updated_at": "2026-07-08T12:31:03.134Z",
18
+ "system": false,
19
+ "noteable_id": 345678912,
20
+ "noteable_type": "MergeRequest"
21
+ },
22
+ {
23
+ "id": 2712345602,
24
+ "type": null,
25
+ "body": "<img src=\"https://res.cloudinary.com/fluid-attacks/image/upload/v1727795769/airs/logo/logo_icon.png\" width=\"24\" alt=\"Fluid Attacks\" /> **Fluid Attacks** completed security analysis\n\n**Nice job!** No vulnerabilities found",
26
+ "author": {
27
+ "id": 34547217,
28
+ "username": "project_20741933_bot_8385943dd3fb82db18695bfce6854920",
29
+ "public_email": null,
30
+ "name": "****",
31
+ "state": "active",
32
+ "locked": false,
33
+ "avatar_url": "https://secure.gravatar.com/avatar/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?s=80&d=identicon",
34
+ "web_url": "https://gitlab.com/project_20741933_bot_8385943dd3fb82db18695bfce6854920"
35
+ },
36
+ "created_at": "2026-07-08T12:19:27.612Z",
37
+ "updated_at": "2026-07-08T12:19:27.612Z",
38
+ "system": false,
39
+ "noteable_id": 345678912,
40
+ "noteable_type": "MergeRequest"
41
+ },
42
+ {
43
+ "id": 2712345603,
44
+ "type": "ApprovalSystemNote",
45
+ "body": "approved this merge request",
46
+ "author": {
47
+ "id": 12345678,
48
+ "username": "dmurciaatfluid",
49
+ "public_email": "",
50
+ "name": "Daniel Murcia",
51
+ "state": "active",
52
+ "locked": false,
53
+ "avatar_url": "https://secure.gravatar.com/avatar/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?s=80&d=identicon",
54
+ "web_url": "https://gitlab.com/dmurciaatfluid"
55
+ },
56
+ "created_at": "2026-07-08T13:38:56.341Z",
57
+ "updated_at": "2026-07-08T13:38:56.341Z",
58
+ "system": true,
59
+ "noteable_id": 345678912,
60
+ "noteable_type": "MergeRequest"
61
+ }
62
+ ]
@@ -0,0 +1,65 @@
1
+ from datetime import UTC, datetime
2
+ from pathlib import Path
3
+
4
+ from fa_purity import FrozenList, Maybe, Unsafe
5
+ from fa_purity.date_time import DatetimeUTC
6
+ from fa_purity.json import JsonValueFactory, Unfolder
7
+ from fluidattacks_etl_utils.natural import NaturalOperations
8
+
9
+ from fluidattacks_gitlab_sdk.ids import UserId
10
+ from fluidattacks_gitlab_sdk.mr_notes import MrNote
11
+ from fluidattacks_gitlab_sdk.mr_notes._decode import decode_mr_notes
12
+ from fluidattacks_gitlab_sdk.users.core import User, UserName, UserObj
13
+
14
+
15
+ def _assert_utc(date_time: datetime) -> DatetimeUTC:
16
+ return DatetimeUTC.assert_utc(date_time).alt(Unsafe.raise_exception).to_union()
17
+
18
+
19
+ def test_decode() -> None:
20
+ human_author = UserObj(
21
+ UserId(NaturalOperations.absolute(12345678)),
22
+ User("dmurciaatfluid"),
23
+ UserName("Daniel Murcia"),
24
+ Maybe.some(""),
25
+ )
26
+ bot_author = UserObj(
27
+ UserId(NaturalOperations.absolute(34547217)),
28
+ User("project_20741933_bot_8385943dd3fb82db18695bfce6854920"),
29
+ UserName("****"),
30
+ Maybe.empty(),
31
+ )
32
+ expected: FrozenList[MrNote] = (
33
+ MrNote(
34
+ "required: [major-version] change the major version to publish "
35
+ "the new indicators package",
36
+ human_author,
37
+ _assert_utc(datetime(2026, 7, 8, 12, 31, 3, 134000, tzinfo=UTC)),
38
+ system=False,
39
+ ),
40
+ MrNote(
41
+ '<img src="https://res.cloudinary.com/fluid-attacks/image/upload/v1727795769/airs/logo'
42
+ '/logo_icon.png" width="24" alt="Fluid Attacks" /> **Fluid Attacks** completed securit'
43
+ "y analysis\n\n**Nice job!** No vulnerabilities found",
44
+ bot_author,
45
+ _assert_utc(datetime(2026, 7, 8, 12, 19, 27, 612000, tzinfo=UTC)),
46
+ system=False,
47
+ ),
48
+ MrNote(
49
+ "approved this merge request",
50
+ human_author,
51
+ _assert_utc(datetime(2026, 7, 8, 13, 38, 56, 341000, tzinfo=UTC)),
52
+ system=True,
53
+ ),
54
+ )
55
+
56
+ raw_data_path = Path(__file__).parent / "data.json"
57
+ json_value = JsonValueFactory.load(raw_data_path.open(encoding="utf-8"))
58
+ raw_notes = (
59
+ json_value.bind(lambda v: Unfolder.to_list_of(v, Unfolder.to_json))
60
+ .alt(Unsafe.raise_exception)
61
+ .to_union()
62
+ )
63
+
64
+ decoded_notes = decode_mr_notes(raw_notes).alt(Unsafe.raise_exception).to_union()
65
+ assert decoded_notes == expected
File without changes
@@ -6,6 +6,15 @@ resolution-markers = [
6
6
  "python_full_version < '3.15'",
7
7
  ]
8
8
 
9
+ [options]
10
+ exclude-newer = "2026-06-30T22:09:12.854339Z"
11
+ exclude-newer-span = "P7D"
12
+
13
+ [options.exclude-newer-package]
14
+ fluid-blends = "2099-01-02T00:00:00Z"
15
+ fluidattacks-utils-logger = "2099-01-02T00:00:00Z"
16
+ fluidattacks-core = "2099-01-02T00:00:00Z"
17
+
9
18
  [manifest]
10
19
  overrides = [{ name = "cryptography", specifier = "==48.0.1" }]
11
20
 
@@ -345,18 +354,18 @@ wheels = [
345
354
 
346
355
  [[package]]
347
356
  name = "fluidattacks-core"
348
- version = "8.2.0"
357
+ version = "9.3.0"
349
358
  source = { registry = "https://pypi.org/simple" }
350
359
  wheels = [
351
- { url = "https://files.pythonhosted.org/packages/26/8f/23a49b485d7ab7ac5d2bb9fc69e5229213fbb1e900d863edcca9a4837b95/fluidattacks_core-8.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb8c447fc19bacd25a9c29ebaeb17b2961338d5223dda3c78dc067fbc0a1aca7", size = 7332195, upload-time = "2026-06-12T22:36:11.385Z" },
352
- { url = "https://files.pythonhosted.org/packages/cd/51/9f34ef276040327a1045e96d291b209fd8307e4da909f70145c1f38121f2/fluidattacks_core-8.2.0-cp311-cp311-manylinux_2_39_aarch64.whl", hash = "sha256:ee6d266216d787d6027570281e4ad2dd4f5a81d93da8d094f8b4f1032f28b7a0", size = 8483155, upload-time = "2026-06-12T22:36:13.974Z" },
353
- { url = "https://files.pythonhosted.org/packages/5f/bb/7f68a44281faf2672f027809e604acc350fbbafe9bb21a13ae73d629e9f8/fluidattacks_core-8.2.0-cp311-cp311-manylinux_2_39_x86_64.whl", hash = "sha256:9487ed3ba4875d99c79ff38f1d2c15c9d4982f3449da661c35da043ae66e18cc", size = 8473961, upload-time = "2026-06-12T22:36:23.289Z" },
354
- { url = "https://files.pythonhosted.org/packages/ec/7a/79891b0dd195687035d944742fdc68d13d1df5a3ecf03ef3f2040a2e6117/fluidattacks_core-8.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:729b1639c7fe18bc8131d4958e935e2b73bf9b38f8c66ae0648894ae2da26ea8", size = 7330127, upload-time = "2026-06-12T22:36:25.096Z" },
355
- { url = "https://files.pythonhosted.org/packages/9d/86/b0f665cc3761ee9acd60351cfaf78c4356430c1297d19847fd72fdd10ce0/fluidattacks_core-8.2.0-cp312-cp312-manylinux_2_39_aarch64.whl", hash = "sha256:1c15069eb91d7966001f1fc12e4569b7923364e634869375c01d7315a548e3ca", size = 8487797, upload-time = "2026-06-12T22:36:21.387Z" },
356
- { url = "https://files.pythonhosted.org/packages/74/66/359ca98a709b46f2f4686acf670c23baef6ac21ca83c535f72db21741826/fluidattacks_core-8.2.0-cp312-cp312-manylinux_2_39_x86_64.whl", hash = "sha256:9e51410b1fadf64484545b810c482d53baf4e5c39dfdb04e2762b3045da579a1", size = 8473395, upload-time = "2026-06-12T22:36:19.508Z" },
357
- { url = "https://files.pythonhosted.org/packages/b1/37/51295f9a14128a18113e24df5ee814e0a940a3627116ee033fb508f63c7a/fluidattacks_core-8.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:71493dd5eba680d1845e5e8ce2e75b80c917e81de6e2f3bba0f7c6e511c95e36", size = 7330498, upload-time = "2026-06-12T22:36:17.968Z" },
358
- { url = "https://files.pythonhosted.org/packages/49/05/f9d408a01c1378c648a6605e7d25c0808c2f8cab2e6dbb6b00c45f6b7caa/fluidattacks_core-8.2.0-cp313-cp313-manylinux_2_39_aarch64.whl", hash = "sha256:a874418ccb3eb097d85d3ac18bf644853ed65cc46c4f38ba3a3176ac89dcbd83", size = 8487257, upload-time = "2026-06-12T22:36:09.29Z" },
359
- { url = "https://files.pythonhosted.org/packages/c3/a0/a9eb9483b6f88936e160decdecfd945367418605ed7d2384d952135633fe/fluidattacks_core-8.2.0-cp313-cp313-manylinux_2_39_x86_64.whl", hash = "sha256:4d86b354ded45db31270a75de46414383f1b04d452e610cf55f798c78d836979", size = 8474079, upload-time = "2026-06-12T22:36:15.973Z" },
360
+ { url = "https://files.pythonhosted.org/packages/27/b2/d956e9939c166d4b5a8929f66cf5dc5edce877d648956ec8ad291afc42b7/fluidattacks_core-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ec5dba7ef4537da0717fe14fbfff8ff161aeb8bcacb15a624894140a1c4291e", size = 6608034, upload-time = "2026-07-07T18:58:59.653Z" },
361
+ { url = "https://files.pythonhosted.org/packages/2e/5c/aa071cff0e301f906604e6d8e691fd86b412c6a99ba484119e16886ebf73/fluidattacks_core-9.3.0-cp311-cp311-manylinux_2_39_aarch64.whl", hash = "sha256:b08a5318bf02e59b1f9842ac9414fbf21fb6397f0ddd108c182f9c2d2acf6368", size = 7598882, upload-time = "2026-07-07T18:59:06.246Z" },
362
+ { url = "https://files.pythonhosted.org/packages/3c/60/a89b39fa1ce5aeba5171bb83dbfe026a25c3a14791818374364fd550efdc/fluidattacks_core-9.3.0-cp311-cp311-manylinux_2_39_x86_64.whl", hash = "sha256:5a04c40e2e515ab96313eb98c34c83963a5084649d423aaf07adad2d701de007", size = 7631955, upload-time = "2026-07-07T18:59:04.637Z" },
363
+ { url = "https://files.pythonhosted.org/packages/90/90/99e1d19021316387edf7c536e1e8be1f89bbf48a7bcefebaea8472dfa003/fluidattacks_core-9.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4425c3ed98e0f374a5beac4a5aebbf9c308b13603305ab3bf9fa75dd8f40831c", size = 6602958, upload-time = "2026-07-07T18:59:07.945Z" },
364
+ { url = "https://files.pythonhosted.org/packages/e4/26/7d5e9f29e7d9b739776b4f76c46562bbf1bdd8024c579b9d49d7f24a2fd0/fluidattacks_core-9.3.0-cp312-cp312-manylinux_2_39_aarch64.whl", hash = "sha256:b16ead5cb8af27186ed348072ba952d15e9a0209d90c7678fe4a94d11733e25e", size = 7595799, upload-time = "2026-07-07T18:59:09.597Z" },
365
+ { url = "https://files.pythonhosted.org/packages/9b/37/cb1cd7b60fab669d3d9c6233ef2e7ef8030ef7ff8fb8c3d84de91c9588a7/fluidattacks_core-9.3.0-cp312-cp312-manylinux_2_39_x86_64.whl", hash = "sha256:10e2ede5108197daa7d8ac9e2e02e9b86eb3d470dab70db134681d6e845163f2", size = 7625910, upload-time = "2026-07-07T18:59:01.614Z" },
366
+ { url = "https://files.pythonhosted.org/packages/e2/cb/5e610ff985b10f1beeffb2d8872903adc0e92deede845af095792b31e129/fluidattacks_core-9.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e9eb00fbbe449b43414a97a6585fb7fd72b85c3fb848cdfe9996aa20ecec53dc", size = 6602582, upload-time = "2026-07-07T18:58:58.141Z" },
367
+ { url = "https://files.pythonhosted.org/packages/43/f6/2141cbce306a07bfcde9c36c70c8f38f4f8ef7a350fac9a9a48de8a8f361/fluidattacks_core-9.3.0-cp313-cp313-manylinux_2_39_aarch64.whl", hash = "sha256:018b3cf09f08b92fb6b54a74cad52f75da817ee1a38a612b5ddeb6e5da666865", size = 7595633, upload-time = "2026-07-07T18:59:03.109Z" },
368
+ { url = "https://files.pythonhosted.org/packages/c7/7c/aef426597596d5e5b5cb3e7c1ea8723b3cd4010da86e0fe5926af76cddec/fluidattacks_core-9.3.0-cp313-cp313-manylinux_2_39_x86_64.whl", hash = "sha256:5865553a7c3374690a644852d70ed597ebd77b827be84fb213f19cc79c8dca9d", size = 7626399, upload-time = "2026-07-07T18:58:56.348Z" },
360
369
  ]
361
370
 
362
371
  [[package]]
@@ -430,16 +439,16 @@ dev = [
430
439
 
431
440
  [[package]]
432
441
  name = "fluidattacks-utils-logger"
433
- version = "3.0.2"
442
+ version = "3.0.3"
434
443
  source = { registry = "https://pypi.org/simple" }
435
444
  dependencies = [
436
445
  { name = "fa-purity" },
437
446
  { name = "fluidattacks-core" },
438
447
  { name = "python-json-logger" },
439
448
  ]
440
- sdist = { url = "https://files.pythonhosted.org/packages/fd/fd/0b2f5f10d966417bfa4876067ec759902e47c5999fd0be392d56260c0f83/fluidattacks_utils_logger-3.0.2.tar.gz", hash = "sha256:ed5d4c4ec8f27e3b1db4ee4ebc9ec201737deaa538f743c1e71bbb11b807842e", size = 49647, upload-time = "2026-06-19T20:43:22.985Z" }
449
+ sdist = { url = "https://files.pythonhosted.org/packages/2b/3e/349b0eeda139fa281a92fdd6246f7b341ed10a0397b5680a525efaa9f7a5/fluidattacks_utils_logger-3.0.3.tar.gz", hash = "sha256:94753730cd3f7b1c4e763df562dc7acc512bc20f63e46b6935af35da152ddbd3", size = 49723, upload-time = "2026-07-03T23:08:11.876Z" }
441
450
  wheels = [
442
- { url = "https://files.pythonhosted.org/packages/a5/c0/d4395f4c158e8e6ce34aaaa687429f3f8f73686215543ce09ed3487d4fa1/fluidattacks_utils_logger-3.0.2-py3-none-any.whl", hash = "sha256:10908e6104ae7f6761619827258ac044a8473156d0a8390f9319f74bdf40e197", size = 3826, upload-time = "2026-06-19T20:43:22.059Z" },
451
+ { url = "https://files.pythonhosted.org/packages/2c/33/5a75a47c6e9c9d21cb080f55ba17eca8de0a0d8aabcd303d7cfd4f3bc942/fluidattacks_utils_logger-3.0.3-py3-none-any.whl", hash = "sha256:01067b226a10761904590cdbf675c8c181f32872511cd66c184a95062f6ffbdc", size = 3825, upload-time = "2026-07-03T23:08:11.078Z" },
443
452
  ]
444
453
 
445
454
  [[package]]