fluidattacks_gitlab_sdk 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. fluidattacks_gitlab_sdk/__init__.py +22 -0
  2. fluidattacks_gitlab_sdk/_decoders.py +170 -0
  3. fluidattacks_gitlab_sdk/_gql_client/__init__.py +8 -0
  4. fluidattacks_gitlab_sdk/_gql_client/_client.py +101 -0
  5. fluidattacks_gitlab_sdk/_gql_client/_error.py +24 -0
  6. fluidattacks_gitlab_sdk/_gql_client/_handlers.py +87 -0
  7. fluidattacks_gitlab_sdk/_handlers.py +40 -0
  8. fluidattacks_gitlab_sdk/_http_client/__init__.py +28 -0
  9. fluidattacks_gitlab_sdk/_http_client/_client_1.py +206 -0
  10. fluidattacks_gitlab_sdk/_http_client/_core.py +152 -0
  11. fluidattacks_gitlab_sdk/_logger.py +36 -0
  12. fluidattacks_gitlab_sdk/ids.py +135 -0
  13. fluidattacks_gitlab_sdk/issues/__init__.py +7 -0
  14. fluidattacks_gitlab_sdk/issues/_client/__init__.py +28 -0
  15. fluidattacks_gitlab_sdk/issues/_client/_decode.py +165 -0
  16. fluidattacks_gitlab_sdk/issues/_client/_get_issue.py +79 -0
  17. fluidattacks_gitlab_sdk/issues/_client/_most_recent.py +119 -0
  18. fluidattacks_gitlab_sdk/issues/_client/_updated_by.py +157 -0
  19. fluidattacks_gitlab_sdk/issues/core.py +99 -0
  20. fluidattacks_gitlab_sdk/members/__init__.py +25 -0
  21. fluidattacks_gitlab_sdk/members/_client.py +49 -0
  22. fluidattacks_gitlab_sdk/members/_decode.py +40 -0
  23. fluidattacks_gitlab_sdk/members/core.py +23 -0
  24. fluidattacks_gitlab_sdk/merge_requests/__init__.py +9 -0
  25. fluidattacks_gitlab_sdk/merge_requests/_client.py +265 -0
  26. fluidattacks_gitlab_sdk/merge_requests/_decode.py +234 -0
  27. fluidattacks_gitlab_sdk/merge_requests/core.py +137 -0
  28. fluidattacks_gitlab_sdk/milestones/__init__.py +4 -0
  29. fluidattacks_gitlab_sdk/milestones/_client.py +147 -0
  30. fluidattacks_gitlab_sdk/milestones/_decode.py +78 -0
  31. fluidattacks_gitlab_sdk/milestones/core.py +40 -0
  32. fluidattacks_gitlab_sdk/mr_approvals/__init__.py +4 -0
  33. fluidattacks_gitlab_sdk/mr_approvals/_client.py +69 -0
  34. fluidattacks_gitlab_sdk/mr_approvals/_decode.py +30 -0
  35. fluidattacks_gitlab_sdk/mr_approvals/core.py +25 -0
  36. fluidattacks_gitlab_sdk/py.typed +0 -0
  37. fluidattacks_gitlab_sdk/users/__init__.py +5 -0
  38. fluidattacks_gitlab_sdk/users/core.py +26 -0
  39. fluidattacks_gitlab_sdk/users/decode.py +25 -0
  40. fluidattacks_gitlab_sdk-1.0.0.dist-info/METADATA +13 -0
  41. fluidattacks_gitlab_sdk-1.0.0.dist-info/RECORD +42 -0
  42. fluidattacks_gitlab_sdk-1.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,119 @@
1
+ import inspect
2
+ import logging
3
+
4
+ from fa_purity import (
5
+ Cmd,
6
+ Maybe,
7
+ NewFrozenList,
8
+ Result,
9
+ ResultE,
10
+ cast_exception,
11
+ )
12
+ from fa_purity.json import JsonObj, JsonUnfolder, Primitive, UnfoldedFactory
13
+ from fluidattacks_etl_utils.bug import Bug
14
+ from fluidattacks_etl_utils.decode import int_to_str
15
+
16
+ from fluidattacks_gitlab_sdk._decoders import (
17
+ assert_multiple,
18
+ decode_issue_internal_id,
19
+ decode_maybe_single,
20
+ )
21
+ from fluidattacks_gitlab_sdk._gql_client import GraphQlGitlabClient
22
+ from fluidattacks_gitlab_sdk._http_client import HttpJsonClient, RelativeEndpoint
23
+ from fluidattacks_gitlab_sdk.ids import IssueFullId, ProjectPath
24
+ from fluidattacks_gitlab_sdk.issues._client._updated_by import get_updated_by
25
+ from fluidattacks_gitlab_sdk.issues.core import Issue, ProjectIdObj
26
+ from fluidattacks_gitlab_sdk.users.core import UserObj
27
+
28
+ from ._decode import decode_issue_and_id
29
+
30
+ LOG = logging.getLogger(__name__)
31
+
32
+
33
+ def _most_recent_issue(
34
+ gql_client: GraphQlGitlabClient,
35
+ raw: JsonObj,
36
+ project: ProjectPath,
37
+ ) -> Cmd[ResultE[tuple[IssueFullId, Issue]]]:
38
+ """
39
+ Completes the raw issue obj by getting the updated by field.
40
+
41
+ This function should be called always after getting the raw issue.
42
+ It can fail when:
43
+ - decode fails
44
+ - raw issue id not exist and/or is fake
45
+ """
46
+ _updated_by: Cmd[ResultE[Maybe[UserObj]]] = (
47
+ decode_issue_internal_id(raw)
48
+ .to_coproduct()
49
+ .map(
50
+ lambda i: get_updated_by(gql_client, project, i.internal_id).map(
51
+ lambda r: r.alt(
52
+ lambda c: c.map(
53
+ lambda e: Bug.new(
54
+ "most_recent_issue: race condition",
55
+ inspect.currentframe(),
56
+ e,
57
+ (str(project), JsonUnfolder.dumps(raw)),
58
+ ), # this should be not possible, the issue was retrieved before
59
+ lambda e: e,
60
+ ),
61
+ ),
62
+ ),
63
+ lambda e: Cmd.wrap_value(Result.failure(e)),
64
+ )
65
+ )
66
+ return _updated_by.map(
67
+ lambda r: r.bind(
68
+ lambda u: decode_issue_and_id(u, raw),
69
+ ),
70
+ )
71
+
72
+
73
+ def most_recent_issue(
74
+ client: HttpJsonClient,
75
+ gql_client: GraphQlGitlabClient,
76
+ project: ProjectIdObj,
77
+ ) -> Cmd[ResultE[Maybe[tuple[IssueFullId, Issue]]]]:
78
+ endpoint = RelativeEndpoint.new(
79
+ "projects",
80
+ int_to_str(project.project_id.project_id.value),
81
+ "issues",
82
+ )
83
+ params: dict[str, Primitive] = {
84
+ "order_by": "created_at",
85
+ "sort": "desc",
86
+ "per_page": 1,
87
+ }
88
+ empty: Maybe[tuple[IssueFullId, Issue]] = Maybe.empty()
89
+ msg: Cmd[None] = Cmd.wrap_impure(lambda: LOG.info("[API] most_recent_issue(%s)", project))
90
+ get_maybe_single = client.get(
91
+ endpoint,
92
+ UnfoldedFactory.from_dict(params),
93
+ ).map(
94
+ lambda r: r.alt(
95
+ lambda e: cast_exception(
96
+ Bug.new(
97
+ "most_recent_issue",
98
+ inspect.currentframe(),
99
+ e,
100
+ (),
101
+ ),
102
+ ),
103
+ )
104
+ .bind(assert_multiple)
105
+ .map(NewFrozenList)
106
+ .map(decode_maybe_single),
107
+ )
108
+ get_issue: Cmd[ResultE[Maybe[tuple[IssueFullId, Issue]]]] = get_maybe_single.bind(
109
+ lambda r: r.to_coproduct().map(
110
+ lambda m: m.to_coproduct().map(
111
+ lambda raw: _most_recent_issue(gql_client, raw, project.project_path).map(
112
+ lambda r: r.map(Maybe.some),
113
+ ),
114
+ lambda _: Cmd.wrap_value(Result.success(empty)),
115
+ ),
116
+ lambda e: Cmd.wrap_value(Result.failure(e)),
117
+ ),
118
+ )
119
+ return msg + get_issue
@@ -0,0 +1,157 @@
1
+ import inspect
2
+ import logging
3
+ from collections.abc import Callable
4
+ from typing import TypeVar
5
+
6
+ from fa_purity import (
7
+ Cmd,
8
+ Coproduct,
9
+ FrozenList,
10
+ FrozenTools,
11
+ Maybe,
12
+ Result,
13
+ ResultE,
14
+ )
15
+ from fa_purity.json import JsonObj, JsonUnfolder, JsonValue, Unfolder
16
+ from fluidattacks_etl_utils.bug import Bug
17
+ from fluidattacks_etl_utils.decode import DecodeUtils, int_to_str, str_to_int
18
+ from fluidattacks_etl_utils.natural import Natural
19
+ from fluidattacks_etl_utils.smash import right_map
20
+
21
+ from fluidattacks_gitlab_sdk._gql_client import GraphQlGitlabClient
22
+ from fluidattacks_gitlab_sdk._handlers import NotFound
23
+ from fluidattacks_gitlab_sdk.ids import IssueInternalId, ProjectPath, UserId
24
+ from fluidattacks_gitlab_sdk.users.core import User, UserName, UserObj
25
+
26
+ LOG = logging.getLogger(__name__)
27
+ _T = TypeVar("_T")
28
+
29
+
30
+ def _nested_key(raw: JsonObj, first_key: str, next_keys: FrozenList[str]) -> ResultE[JsonValue]:
31
+ if next_keys:
32
+ return JsonUnfolder.require(
33
+ raw,
34
+ first_key,
35
+ lambda v: Unfolder.to_json(v).bind(
36
+ lambda v: _nested_key(v, next_keys[0], next_keys[1:]),
37
+ ),
38
+ )
39
+ return JsonUnfolder.require(raw, first_key, lambda v: Result.success(v))
40
+
41
+
42
+ def _decode_gid(raw: str) -> ResultE[UserId]:
43
+ return str_to_int(raw.removeprefix("gid://gitlab/User/")).bind(Natural.from_int).map(UserId)
44
+
45
+
46
+ def decode_user_obj(raw: JsonObj) -> ResultE[UserObj]:
47
+ return (
48
+ JsonUnfolder.require(raw, "id", DecodeUtils.to_str)
49
+ .bind(_decode_gid)
50
+ .bind(
51
+ lambda user_id: JsonUnfolder.require(raw, "username", DecodeUtils.to_str)
52
+ .map(User)
53
+ .bind(
54
+ lambda user: JsonUnfolder.require(raw, "name", DecodeUtils.to_str)
55
+ .map(UserName)
56
+ .map(
57
+ lambda name: UserObj(user_id, user, name),
58
+ ),
59
+ ),
60
+ )
61
+ )
62
+
63
+
64
+ def decode_updated_by(raw: JsonObj) -> ResultE[Maybe[UserObj]]:
65
+ return JsonUnfolder.require(
66
+ raw,
67
+ "updatedBy",
68
+ lambda v: DecodeUtils.to_maybe(v, lambda v: Unfolder.to_json(v).bind(decode_user_obj)),
69
+ )
70
+
71
+
72
+ def _set_not_found_if_empty(
73
+ value: JsonValue,
74
+ transform: Callable[[JsonValue], ResultE[_T]],
75
+ ) -> Result[_T, Coproduct[NotFound, Exception]]:
76
+ return (
77
+ DecodeUtils.to_maybe(value, transform)
78
+ .alt(Coproduct[NotFound, Exception].inr)
79
+ .bind(
80
+ lambda m: m.to_coproduct().map(
81
+ lambda m: Result.success(m),
82
+ lambda _: Result.failure(Coproduct.inl(NotFound(ValueError("Issue not found")))),
83
+ ),
84
+ )
85
+ )
86
+
87
+
88
+ def _decode(raw: JsonObj) -> Result[Maybe[UserObj], Coproduct[NotFound, Exception]]:
89
+ return (
90
+ _nested_key(raw, "project", ("issue",))
91
+ .alt(Coproduct[NotFound, Exception].inr)
92
+ .bind(
93
+ lambda v: _set_not_found_if_empty(
94
+ v,
95
+ lambda v: Unfolder.to_json(v).bind(decode_updated_by),
96
+ ),
97
+ )
98
+ .alt(
99
+ lambda c: c.map(
100
+ Coproduct.inl,
101
+ lambda e: Coproduct.inr(
102
+ Bug.new(
103
+ "decode_updated_by",
104
+ inspect.currentframe(),
105
+ e,
106
+ (JsonUnfolder.dumps(raw),),
107
+ ),
108
+ ),
109
+ ),
110
+ )
111
+ )
112
+
113
+
114
+ def get_updated_by(
115
+ client: GraphQlGitlabClient,
116
+ project: ProjectPath,
117
+ issue_id: IssueInternalId,
118
+ ) -> Cmd[Result[Maybe[UserObj], Coproduct[NotFound, Exception]]]:
119
+ """
120
+ Get the updatedBy field of an issue.
121
+
122
+ To avoid failure ensure:
123
+ - The issue exist
124
+ - The credentials are correct
125
+ """
126
+ query = """
127
+ query getIssueUpdatedBy($project: ID!, $issue_iid: String!){
128
+ project(fullPath: $project) {
129
+ issue(iid: $issue_iid) {
130
+ updatedBy {
131
+ id
132
+ name
133
+ username
134
+ }
135
+ }
136
+ }
137
+ }
138
+ """
139
+ values: dict[str, str] = {
140
+ "project": project.path,
141
+ "issue_iid": int_to_str(issue_id.internal_id.value),
142
+ }
143
+ return client.get(query, FrozenTools.freeze(values)).map(
144
+ lambda r: r.alt(Coproduct[NotFound, Exception].inr)
145
+ .bind(_decode)
146
+ .alt(
147
+ lambda c: right_map(
148
+ c,
149
+ lambda e: Bug.new(
150
+ "get_updated_by",
151
+ inspect.currentframe(),
152
+ e,
153
+ (str(project), str(issue_id)),
154
+ ),
155
+ ),
156
+ ),
157
+ )
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from datetime import date
6
+ from enum import (
7
+ Enum,
8
+ )
9
+
10
+ from fa_purity import Cmd, Coproduct, FrozenList, Maybe, Result, ResultE
11
+ from fa_purity.date_time import DatetimeUTC
12
+
13
+ from fluidattacks_gitlab_sdk._handlers import NotFound
14
+ from fluidattacks_gitlab_sdk.ids import (
15
+ EpicFullId,
16
+ IssueFullId,
17
+ IssueInternalId,
18
+ MilestoneFullId,
19
+ ProjectId,
20
+ ProjectPath,
21
+ )
22
+ from fluidattacks_gitlab_sdk.users.core import UserObj
23
+
24
+
25
+ class IssueType(Enum):
26
+ ISSUE = "issue"
27
+ INCIDENT = "incident"
28
+ TASK = "task"
29
+ TEST_CASE = "test_case"
30
+ REQUIREMENT = "requirement"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class IssueCounts:
35
+ up_votes: int
36
+ down_votes: int
37
+ merge_requests_count: int
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class IssueDates:
42
+ created_at: DatetimeUTC
43
+ updated_at: Maybe[DatetimeUTC]
44
+ closed_at: Maybe[DatetimeUTC]
45
+ due_date: Maybe[date]
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class IssueOtherProperties:
50
+ confidential: bool
51
+ discussion_locked: Maybe[bool]
52
+ labels: FrozenList[str]
53
+ health_status: Maybe[str]
54
+ weight: Maybe[int]
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class IssueReferences:
59
+ author: UserObj
60
+ milestone: Maybe[MilestoneFullId]
61
+ epic: Maybe[EpicFullId]
62
+ closed_by: Maybe[UserObj]
63
+ assignees: FrozenList[UserObj]
64
+ updated_by: Maybe[UserObj]
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class IssueDef:
69
+ title: str
70
+ state: str
71
+ issue_type: IssueType
72
+ description: Maybe[str]
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class Issue:
77
+ definition: IssueDef
78
+ references: IssueReferences
79
+ properties: IssueOtherProperties
80
+ dates: IssueDates
81
+ stats: IssueCounts
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ProjectIdObj:
86
+ project_id: ProjectId
87
+ project_path: ProjectPath
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class IssueClient:
92
+ get_issue: Callable[
93
+ [ProjectIdObj, IssueInternalId],
94
+ Cmd[Result[tuple[IssueFullId, Issue], Coproduct[NotFound, Exception]]],
95
+ ]
96
+ most_recent_issue: Callable[
97
+ [ProjectIdObj],
98
+ Cmd[ResultE[Maybe[tuple[IssueFullId, Issue]]]],
99
+ ]
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fluidattacks_gitlab_sdk._http_client import ClientFactory, Credentials, HttpJsonClient
4
+ from fluidattacks_gitlab_sdk.members.core import Member, MemberClient
5
+
6
+ from . import _client
7
+
8
+
9
+ def _from_client(client: HttpJsonClient) -> MemberClient:
10
+ return MemberClient(
11
+ lambda p: _client.get_members(client, p),
12
+ )
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class MembersClientFactory:
17
+ @staticmethod
18
+ def new(creds: Credentials) -> MemberClient:
19
+ return _from_client(ClientFactory.new(creds))
20
+
21
+
22
+ __all__ = [
23
+ "Member",
24
+ "MemberClient",
25
+ ]
@@ -0,0 +1,49 @@
1
+ import inspect
2
+
3
+ from fa_purity import (
4
+ Cmd,
5
+ FrozenList,
6
+ ResultE,
7
+ cast_exception,
8
+ )
9
+ from fa_purity.json import Primitive, UnfoldedFactory
10
+ from fluidattacks_etl_utils.bug import Bug
11
+ from fluidattacks_etl_utils.decode import int_to_str
12
+
13
+ from fluidattacks_gitlab_sdk._decoders import assert_multiple
14
+ from fluidattacks_gitlab_sdk._http_client import HttpJsonClient, RelativeEndpoint
15
+ from fluidattacks_gitlab_sdk.ids import MemberId, ProjectId
16
+
17
+ from ._decode import decode_members
18
+ from .core import Member
19
+
20
+
21
+ def get_members(
22
+ client: HttpJsonClient,
23
+ project: ProjectId,
24
+ ) -> Cmd[ResultE[FrozenList[tuple[MemberId, Member]]]]:
25
+ endpoint = RelativeEndpoint.new(
26
+ "projects",
27
+ int_to_str(project.project_id.value),
28
+ "members",
29
+ )
30
+
31
+ params: dict[str, Primitive] = {
32
+ "page": 1,
33
+ "per_page": 100,
34
+ }
35
+
36
+ return client.get(
37
+ endpoint,
38
+ UnfoldedFactory.from_dict(params),
39
+ ).map(
40
+ lambda result: (
41
+ result.alt(
42
+ lambda e: cast_exception(
43
+ Bug.new("_get_members", inspect.currentframe(), e, ()),
44
+ ),
45
+ )
46
+ .bind(assert_multiple)
47
+ .bind(lambda members: decode_members(members, project))
48
+ ),
49
+ )
@@ -0,0 +1,40 @@
1
+ import logging
2
+
3
+ from fa_purity import FrozenList, PureIterFactory, Result, ResultE, ResultTransform
4
+ from fa_purity.json import JsonObj, JsonUnfolder
5
+ from fluidattacks_etl_utils import smash
6
+ from fluidattacks_etl_utils.decode import DecodeUtils
7
+ from fluidattacks_etl_utils.natural import Natural
8
+
9
+ from fluidattacks_gitlab_sdk.ids import MemberId, ProjectId
10
+ from fluidattacks_gitlab_sdk.members.core import Member
11
+
12
+ LOG = logging.getLogger(__name__)
13
+
14
+
15
+ def decode_member(raw: JsonObj, id_project: ProjectId) -> ResultE[Member]:
16
+ return smash.smash_result_2(
17
+ JsonUnfolder.require(raw, "username", DecodeUtils.to_str),
18
+ JsonUnfolder.require(raw, "name", DecodeUtils.to_str),
19
+ ).map(lambda m: Member(m[0], m[1], id_project))
20
+
21
+
22
+ def decode_member_and_id(raw: JsonObj, id_project: ProjectId) -> ResultE[tuple[MemberId, Member]]:
23
+ return smash.smash_result_2(
24
+ JsonUnfolder.require(raw, "id", DecodeUtils.to_int).bind(Natural.from_int).map(MemberId),
25
+ decode_member(raw, id_project),
26
+ )
27
+
28
+
29
+ def decode_members(
30
+ members: FrozenList[JsonObj],
31
+ id_project: ProjectId,
32
+ ) -> ResultE[FrozenList[tuple[MemberId, Member]]]:
33
+ if not members:
34
+ return Result.failure(ValueError("Expected a list of members"))
35
+
36
+ return ResultTransform.all_ok(
37
+ PureIterFactory.from_list(members)
38
+ .map(lambda c: decode_member_and_id(c, id_project))
39
+ .to_list(),
40
+ )
@@ -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 Cmd, FrozenList, ResultE
7
+
8
+ from fluidattacks_gitlab_sdk.ids import MemberId, ProjectId
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Member:
13
+ member_user_name: str
14
+ member_name: str
15
+ id_project: ProjectId
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class MemberClient:
20
+ get_members: Callable[
21
+ [ProjectId],
22
+ Cmd[ResultE[FrozenList[tuple[MemberId, Member]]]],
23
+ ]
@@ -0,0 +1,9 @@
1
+ """Main documentation at https://docs.gitlab.com/ee/api/merge_requests.html ."""
2
+
3
+ from ._client import MrClientFactory
4
+ from .core import MrsClient
5
+
6
+ __all__ = [
7
+ "MrClientFactory",
8
+ "MrsClient",
9
+ ]