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.
- fluidattacks_gitlab_sdk/__init__.py +22 -0
- fluidattacks_gitlab_sdk/_decoders.py +170 -0
- fluidattacks_gitlab_sdk/_gql_client/__init__.py +8 -0
- fluidattacks_gitlab_sdk/_gql_client/_client.py +101 -0
- fluidattacks_gitlab_sdk/_gql_client/_error.py +24 -0
- fluidattacks_gitlab_sdk/_gql_client/_handlers.py +87 -0
- fluidattacks_gitlab_sdk/_handlers.py +40 -0
- fluidattacks_gitlab_sdk/_http_client/__init__.py +28 -0
- fluidattacks_gitlab_sdk/_http_client/_client_1.py +206 -0
- fluidattacks_gitlab_sdk/_http_client/_core.py +152 -0
- fluidattacks_gitlab_sdk/_logger.py +36 -0
- fluidattacks_gitlab_sdk/ids.py +135 -0
- fluidattacks_gitlab_sdk/issues/__init__.py +7 -0
- fluidattacks_gitlab_sdk/issues/_client/__init__.py +28 -0
- fluidattacks_gitlab_sdk/issues/_client/_decode.py +165 -0
- fluidattacks_gitlab_sdk/issues/_client/_get_issue.py +79 -0
- fluidattacks_gitlab_sdk/issues/_client/_most_recent.py +119 -0
- fluidattacks_gitlab_sdk/issues/_client/_updated_by.py +157 -0
- fluidattacks_gitlab_sdk/issues/core.py +99 -0
- fluidattacks_gitlab_sdk/members/__init__.py +25 -0
- fluidattacks_gitlab_sdk/members/_client.py +49 -0
- fluidattacks_gitlab_sdk/members/_decode.py +40 -0
- fluidattacks_gitlab_sdk/members/core.py +23 -0
- fluidattacks_gitlab_sdk/merge_requests/__init__.py +9 -0
- fluidattacks_gitlab_sdk/merge_requests/_client.py +265 -0
- fluidattacks_gitlab_sdk/merge_requests/_decode.py +234 -0
- fluidattacks_gitlab_sdk/merge_requests/core.py +137 -0
- fluidattacks_gitlab_sdk/milestones/__init__.py +4 -0
- fluidattacks_gitlab_sdk/milestones/_client.py +147 -0
- fluidattacks_gitlab_sdk/milestones/_decode.py +78 -0
- fluidattacks_gitlab_sdk/milestones/core.py +40 -0
- fluidattacks_gitlab_sdk/mr_approvals/__init__.py +4 -0
- fluidattacks_gitlab_sdk/mr_approvals/_client.py +69 -0
- fluidattacks_gitlab_sdk/mr_approvals/_decode.py +30 -0
- fluidattacks_gitlab_sdk/mr_approvals/core.py +25 -0
- fluidattacks_gitlab_sdk/py.typed +0 -0
- fluidattacks_gitlab_sdk/users/__init__.py +5 -0
- fluidattacks_gitlab_sdk/users/core.py +26 -0
- fluidattacks_gitlab_sdk/users/decode.py +25 -0
- fluidattacks_gitlab_sdk-1.0.0.dist-info/METADATA +13 -0
- fluidattacks_gitlab_sdk-1.0.0.dist-info/RECORD +42 -0
- fluidattacks_gitlab_sdk-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import (
|
|
2
|
+
annotations,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import (
|
|
8
|
+
dataclass,
|
|
9
|
+
field,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from fa_purity import (
|
|
13
|
+
Cmd,
|
|
14
|
+
Coproduct,
|
|
15
|
+
FrozenList,
|
|
16
|
+
Result,
|
|
17
|
+
ResultE,
|
|
18
|
+
UnitType,
|
|
19
|
+
)
|
|
20
|
+
from fa_purity.json import (
|
|
21
|
+
JsonObj,
|
|
22
|
+
)
|
|
23
|
+
from pure_requests.retry import (
|
|
24
|
+
MaxRetriesReached,
|
|
25
|
+
)
|
|
26
|
+
from requests.exceptions import (
|
|
27
|
+
ChunkedEncodingError as RawChunkedEncodingError,
|
|
28
|
+
)
|
|
29
|
+
from requests.exceptions import (
|
|
30
|
+
ConnectionError as RawConnectionError,
|
|
31
|
+
)
|
|
32
|
+
from requests.exceptions import (
|
|
33
|
+
HTTPError as RawHTTPError,
|
|
34
|
+
)
|
|
35
|
+
from requests.exceptions import (
|
|
36
|
+
JSONDecodeError as RawJSONDecodeError,
|
|
37
|
+
)
|
|
38
|
+
from requests.exceptions import (
|
|
39
|
+
RequestException as RawRequestException,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
LOG = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Credentials:
|
|
47
|
+
api_key: str
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return "Credentials([masked])"
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return "Credentials([masked])"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class _Private:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class HTTPError:
|
|
63
|
+
raw: RawHTTPError
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class JSONDecodeError:
|
|
68
|
+
raw: RawJSONDecodeError
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class ChunkedEncodingError:
|
|
73
|
+
_private: _Private = field(repr=False, hash=False, compare=False)
|
|
74
|
+
raw: RawChunkedEncodingError
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class RequestsConnectionError:
|
|
79
|
+
_private: _Private = field(repr=False, hash=False, compare=False)
|
|
80
|
+
raw: RawConnectionError
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class RequestException:
|
|
85
|
+
raw: RawRequestException
|
|
86
|
+
|
|
87
|
+
def to_chunk_error(self) -> ResultE[ChunkedEncodingError]:
|
|
88
|
+
if isinstance(self.raw, RawChunkedEncodingError):
|
|
89
|
+
return Result.success(ChunkedEncodingError(_Private(), self.raw))
|
|
90
|
+
return Result.failure(ValueError("Not a ChunkedEncodingError"))
|
|
91
|
+
|
|
92
|
+
def to_connection_error(self) -> ResultE[RequestsConnectionError]:
|
|
93
|
+
if isinstance(self.raw, RawConnectionError):
|
|
94
|
+
return Result.success(RequestsConnectionError(_Private(), self.raw))
|
|
95
|
+
return Result.failure(ValueError("Not a RequestsConnectionError"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class UnhandledErrors:
|
|
100
|
+
error: Coproduct[JSONDecodeError, Coproduct[HTTPError, RequestException]]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class HandledErrors:
|
|
105
|
+
error: Coproduct[
|
|
106
|
+
HTTPError,
|
|
107
|
+
Coproduct[ChunkedEncodingError, RequestsConnectionError],
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class RelativeEndpoint:
|
|
113
|
+
paths: FrozenList[str]
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def new(*args: str) -> RelativeEndpoint:
|
|
117
|
+
return RelativeEndpoint(tuple(args))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class HttpJsonClient:
|
|
122
|
+
get: Callable[
|
|
123
|
+
[RelativeEndpoint, JsonObj],
|
|
124
|
+
Cmd[
|
|
125
|
+
Result[
|
|
126
|
+
Coproduct[JsonObj, FrozenList[JsonObj]],
|
|
127
|
+
Coproduct[UnhandledErrors, MaxRetriesReached],
|
|
128
|
+
]
|
|
129
|
+
],
|
|
130
|
+
]
|
|
131
|
+
post: Callable[
|
|
132
|
+
[RelativeEndpoint],
|
|
133
|
+
Cmd[Result[UnitType, Coproduct[UnhandledErrors, MaxRetriesReached]]],
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class Page:
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class _Private:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
private: Page._Private = field(repr=False, hash=False, compare=False)
|
|
144
|
+
page_num: int
|
|
145
|
+
per_page: int
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def new_page(page_num: int, per_page: int) -> ResultE[Page]:
|
|
149
|
+
if page_num > 0 and per_page in range(1, 101):
|
|
150
|
+
pag = Page(Page._Private(), page_num, per_page)
|
|
151
|
+
return Result.success(pag)
|
|
152
|
+
return Result.failure(ValueError("Invalid page"))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from fa_purity import (
|
|
2
|
+
Cmd,
|
|
3
|
+
)
|
|
4
|
+
from fluidattacks_utils_logger import (
|
|
5
|
+
set_main_log,
|
|
6
|
+
)
|
|
7
|
+
from fluidattacks_utils_logger.env import (
|
|
8
|
+
current_app_env,
|
|
9
|
+
notifier_key,
|
|
10
|
+
observes_debug,
|
|
11
|
+
)
|
|
12
|
+
from fluidattacks_utils_logger.handlers import (
|
|
13
|
+
LoggingConf,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_logger(root_name: str, version: str) -> Cmd[None]:
|
|
18
|
+
n_key = notifier_key()
|
|
19
|
+
app_env = current_app_env()
|
|
20
|
+
debug = observes_debug()
|
|
21
|
+
conf = n_key.bind(
|
|
22
|
+
lambda key: app_env.map(
|
|
23
|
+
lambda env: LoggingConf(
|
|
24
|
+
"sdk",
|
|
25
|
+
version,
|
|
26
|
+
"./observes/sdk/fluidattacks_gitlab_sdk",
|
|
27
|
+
False,
|
|
28
|
+
key,
|
|
29
|
+
env,
|
|
30
|
+
"observes",
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
return debug.bind(
|
|
35
|
+
lambda d: conf.bind(lambda c: set_main_log(root_name, c, d, False)),
|
|
36
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from fa_purity import Coproduct
|
|
6
|
+
from fluidattacks_etl_utils.natural import Natural
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ProjectId:
|
|
11
|
+
"""Represents a global project id."""
|
|
12
|
+
|
|
13
|
+
project_id: Natural
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ProjectPath:
|
|
18
|
+
"""Represents a project path."""
|
|
19
|
+
|
|
20
|
+
path: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class GroupId:
|
|
25
|
+
"""Represents a global group id."""
|
|
26
|
+
|
|
27
|
+
group_id: Natural
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class UserId:
|
|
32
|
+
"""Represents an user id."""
|
|
33
|
+
|
|
34
|
+
user_id: Natural
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class MilestoneGlobalId:
|
|
39
|
+
"""Represents an milestone global id."""
|
|
40
|
+
|
|
41
|
+
global_id: Natural
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class MilestoneInternalId:
|
|
46
|
+
"""Represents an milestone internal id."""
|
|
47
|
+
|
|
48
|
+
internal: Natural
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class MilestoneFullInternalId:
|
|
53
|
+
parent: Coproduct[ProjectId, GroupId]
|
|
54
|
+
internal: MilestoneInternalId
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class MilestoneFullId:
|
|
59
|
+
global_id: MilestoneGlobalId
|
|
60
|
+
internal_id: MilestoneFullInternalId
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class MrGlobalId:
|
|
65
|
+
"""Represents an MR global id."""
|
|
66
|
+
|
|
67
|
+
global_id: Natural
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class MrInternalId:
|
|
72
|
+
"""Represents an MR internal id."""
|
|
73
|
+
|
|
74
|
+
internal: Natural
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class MrFullInternalId:
|
|
79
|
+
project: ProjectId
|
|
80
|
+
internal: MrInternalId
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class MrFullId:
|
|
85
|
+
global_id: MrGlobalId
|
|
86
|
+
internal_id: MrFullInternalId
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class MemberId:
|
|
91
|
+
member_id: Natural
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class EpicGlobalId:
|
|
96
|
+
global_id: Natural
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class EpicInternalId:
|
|
101
|
+
internal_id: Natural
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class EpicFullInternalId:
|
|
106
|
+
group: GroupId
|
|
107
|
+
internal_id: EpicInternalId
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class EpicFullId:
|
|
112
|
+
global_id: EpicGlobalId
|
|
113
|
+
internal_id: EpicFullInternalId
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class IssueGlobalId:
|
|
118
|
+
global_id: Natural
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class IssueInternalId:
|
|
123
|
+
internal_id: Natural
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class IssueFullInternalId:
|
|
128
|
+
project: ProjectId
|
|
129
|
+
internal_id: IssueInternalId
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class IssueFullId:
|
|
134
|
+
global_id: IssueGlobalId
|
|
135
|
+
internal_id: IssueFullInternalId
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import (
|
|
2
|
+
annotations,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from fa_purity import Cmd
|
|
8
|
+
|
|
9
|
+
from fluidattacks_gitlab_sdk._gql_client import GraphQlGitlabClient
|
|
10
|
+
from fluidattacks_gitlab_sdk._http_client import ClientFactory, Credentials, HttpJsonClient
|
|
11
|
+
from fluidattacks_gitlab_sdk.issues.core import IssueClient
|
|
12
|
+
|
|
13
|
+
from ._get_issue import get_issue
|
|
14
|
+
from ._most_recent import most_recent_issue
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _from_client(client: HttpJsonClient, new_gql_client: Cmd[GraphQlGitlabClient]) -> IssueClient:
|
|
18
|
+
return IssueClient(
|
|
19
|
+
lambda p, i: new_gql_client.bind(lambda c: get_issue(client, c, p, i)),
|
|
20
|
+
lambda p: new_gql_client.bind(lambda c: most_recent_issue(client, c, p)),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class IssueClientFactory:
|
|
26
|
+
@staticmethod
|
|
27
|
+
def new(creds: Credentials) -> IssueClient:
|
|
28
|
+
return _from_client(ClientFactory.new(creds), GraphQlGitlabClient.new(creds.api_key))
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from fa_purity import Maybe, ResultE
|
|
4
|
+
from fa_purity.json import JsonObj, JsonPrimitiveUnfolder, JsonUnfolder, JsonValue, Unfolder
|
|
5
|
+
from fluidattacks_etl_utils import smash
|
|
6
|
+
from fluidattacks_etl_utils.bug import Bug
|
|
7
|
+
from fluidattacks_etl_utils.decode import DecodeUtils
|
|
8
|
+
|
|
9
|
+
from fluidattacks_gitlab_sdk import _handlers
|
|
10
|
+
from fluidattacks_gitlab_sdk._decoders import (
|
|
11
|
+
decode_date,
|
|
12
|
+
decode_epic_full_id,
|
|
13
|
+
decode_issue_full_id,
|
|
14
|
+
decode_milestone_full_id,
|
|
15
|
+
)
|
|
16
|
+
from fluidattacks_gitlab_sdk.ids import (
|
|
17
|
+
IssueFullId,
|
|
18
|
+
)
|
|
19
|
+
from fluidattacks_gitlab_sdk.issues.core import (
|
|
20
|
+
Issue,
|
|
21
|
+
IssueCounts,
|
|
22
|
+
IssueDates,
|
|
23
|
+
IssueDef,
|
|
24
|
+
IssueOtherProperties,
|
|
25
|
+
IssueReferences,
|
|
26
|
+
IssueType,
|
|
27
|
+
)
|
|
28
|
+
from fluidattacks_gitlab_sdk.users.core import UserObj
|
|
29
|
+
from fluidattacks_gitlab_sdk.users.decode import decode_user_obj
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def decode_type(raw: JsonValue) -> ResultE[IssueType]:
|
|
33
|
+
return Unfolder.to_primitive(raw).bind(
|
|
34
|
+
lambda p: JsonPrimitiveUnfolder.to_str(p).bind(
|
|
35
|
+
lambda r: _handlers.handle_value_error(lambda: IssueType(r)),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _decode_stats(raw: JsonObj) -> ResultE[IssueCounts]:
|
|
41
|
+
return smash.smash_result_3(
|
|
42
|
+
JsonUnfolder.require(raw, "upvotes", DecodeUtils.to_int),
|
|
43
|
+
JsonUnfolder.require(raw, "downvotes", DecodeUtils.to_int),
|
|
44
|
+
JsonUnfolder.require(raw, "merge_requests_count", DecodeUtils.to_int),
|
|
45
|
+
).map(lambda t: IssueCounts(*t))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _decode_properties(raw: JsonObj) -> ResultE[IssueOtherProperties]:
|
|
49
|
+
return smash.smash_result_5(
|
|
50
|
+
JsonUnfolder.require(raw, "confidential", DecodeUtils.to_bool),
|
|
51
|
+
JsonUnfolder.require(
|
|
52
|
+
raw,
|
|
53
|
+
"discussion_locked",
|
|
54
|
+
lambda v: DecodeUtils.to_maybe(v, DecodeUtils.to_bool),
|
|
55
|
+
),
|
|
56
|
+
JsonUnfolder.require(raw, "labels", lambda v: Unfolder.to_list_of(v, DecodeUtils.to_str)),
|
|
57
|
+
JsonUnfolder.optional(
|
|
58
|
+
raw,
|
|
59
|
+
"health_status",
|
|
60
|
+
lambda v: DecodeUtils.to_maybe(v, DecodeUtils.to_str),
|
|
61
|
+
).map(lambda m: m.bind(lambda x: x)),
|
|
62
|
+
JsonUnfolder.optional(
|
|
63
|
+
raw,
|
|
64
|
+
"weight",
|
|
65
|
+
lambda v: DecodeUtils.to_maybe(v, DecodeUtils.to_int),
|
|
66
|
+
).map(lambda m: m.bind(lambda x: x)),
|
|
67
|
+
).map(lambda t: IssueOtherProperties(*t))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _decode_refs(updated_by: Maybe[UserObj], raw: JsonObj) -> ResultE[IssueReferences]:
|
|
71
|
+
return smash.smash_result_5(
|
|
72
|
+
JsonUnfolder.require(raw, "author", lambda v: Unfolder.to_json(v).bind(decode_user_obj)),
|
|
73
|
+
JsonUnfolder.require(
|
|
74
|
+
raw,
|
|
75
|
+
"milestone",
|
|
76
|
+
lambda v: DecodeUtils.to_maybe(
|
|
77
|
+
v,
|
|
78
|
+
lambda v: Unfolder.to_json(v).bind(decode_milestone_full_id),
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
JsonUnfolder.optional(
|
|
82
|
+
raw,
|
|
83
|
+
"epic",
|
|
84
|
+
lambda v: DecodeUtils.to_maybe(
|
|
85
|
+
v,
|
|
86
|
+
lambda v: Unfolder.to_json(v).bind(decode_epic_full_id),
|
|
87
|
+
),
|
|
88
|
+
).map(lambda m: m.bind(lambda x: x)),
|
|
89
|
+
JsonUnfolder.require(
|
|
90
|
+
raw,
|
|
91
|
+
"closed_by",
|
|
92
|
+
lambda v: DecodeUtils.to_maybe(v, lambda v: Unfolder.to_json(v).bind(decode_user_obj)),
|
|
93
|
+
),
|
|
94
|
+
JsonUnfolder.require(
|
|
95
|
+
raw,
|
|
96
|
+
"assignees",
|
|
97
|
+
lambda v: Unfolder.to_list_of(v, lambda v: Unfolder.to_json(v).bind(decode_user_obj)),
|
|
98
|
+
),
|
|
99
|
+
).map(lambda t: IssueReferences(*t, updated_by))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _decode_def(raw: JsonObj) -> ResultE[IssueDef]:
|
|
103
|
+
return smash.smash_result_4(
|
|
104
|
+
JsonUnfolder.require(raw, "title", DecodeUtils.to_str),
|
|
105
|
+
JsonUnfolder.require(raw, "state", DecodeUtils.to_str),
|
|
106
|
+
JsonUnfolder.require(raw, "issue_type", decode_type),
|
|
107
|
+
JsonUnfolder.require(
|
|
108
|
+
raw,
|
|
109
|
+
"description",
|
|
110
|
+
lambda v: DecodeUtils.to_maybe(v, DecodeUtils.to_str),
|
|
111
|
+
),
|
|
112
|
+
).map(lambda t: IssueDef(*t))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _decode_dates(raw: JsonObj) -> ResultE[IssueDates]:
|
|
116
|
+
return smash.smash_result_4(
|
|
117
|
+
JsonUnfolder.require(raw, "created_at", DecodeUtils.to_date_time),
|
|
118
|
+
JsonUnfolder.require(
|
|
119
|
+
raw,
|
|
120
|
+
"updated_at",
|
|
121
|
+
lambda v: DecodeUtils.to_maybe(v, DecodeUtils.to_date_time),
|
|
122
|
+
),
|
|
123
|
+
JsonUnfolder.require(
|
|
124
|
+
raw,
|
|
125
|
+
"closed_at",
|
|
126
|
+
lambda v: DecodeUtils.to_maybe(v, DecodeUtils.to_date_time),
|
|
127
|
+
),
|
|
128
|
+
JsonUnfolder.optional(
|
|
129
|
+
raw,
|
|
130
|
+
"due_date",
|
|
131
|
+
lambda v: DecodeUtils.to_maybe(v, lambda i: DecodeUtils.to_str(i).bind(decode_date)),
|
|
132
|
+
).map(lambda m: m.bind(lambda x: x)),
|
|
133
|
+
).map(lambda t: IssueDates(*t))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def decode_issue(updated_by: Maybe[UserObj], raw: JsonObj) -> ResultE[Issue]:
|
|
137
|
+
return (
|
|
138
|
+
smash.smash_result_5(
|
|
139
|
+
_decode_def(raw),
|
|
140
|
+
_decode_refs(updated_by, raw),
|
|
141
|
+
_decode_properties(raw),
|
|
142
|
+
_decode_dates(raw),
|
|
143
|
+
_decode_stats(raw),
|
|
144
|
+
)
|
|
145
|
+
.map(lambda t: Issue(*t))
|
|
146
|
+
.alt(
|
|
147
|
+
lambda e: Bug.new(
|
|
148
|
+
"decode_issue",
|
|
149
|
+
inspect.currentframe(),
|
|
150
|
+
e,
|
|
151
|
+
(JsonUnfolder.dumps(raw),),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def decode_issue_and_id(
|
|
158
|
+
updated_by: Maybe[UserObj],
|
|
159
|
+
raw: JsonObj,
|
|
160
|
+
) -> ResultE[tuple[IssueFullId, Issue]]:
|
|
161
|
+
_id = decode_issue_full_id(raw)
|
|
162
|
+
return smash.smash_result_2(
|
|
163
|
+
_id,
|
|
164
|
+
decode_issue(updated_by, raw),
|
|
165
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from fa_purity import (
|
|
5
|
+
Cmd,
|
|
6
|
+
CmdTransform,
|
|
7
|
+
Coproduct,
|
|
8
|
+
FrozenDict,
|
|
9
|
+
Maybe,
|
|
10
|
+
Result,
|
|
11
|
+
cast_exception,
|
|
12
|
+
)
|
|
13
|
+
from fluidattacks_etl_utils.bug import Bug
|
|
14
|
+
from fluidattacks_etl_utils.decode import int_to_str
|
|
15
|
+
from fluidattacks_etl_utils.smash import merge_coproduct, right_map
|
|
16
|
+
|
|
17
|
+
from fluidattacks_gitlab_sdk._decoders import assert_single
|
|
18
|
+
from fluidattacks_gitlab_sdk._gql_client import GraphQlGitlabClient
|
|
19
|
+
from fluidattacks_gitlab_sdk._handlers import NotFound, handle_not_found
|
|
20
|
+
from fluidattacks_gitlab_sdk._http_client import HttpJsonClient, RelativeEndpoint
|
|
21
|
+
from fluidattacks_gitlab_sdk.ids import IssueFullId, IssueInternalId, ProjectId
|
|
22
|
+
from fluidattacks_gitlab_sdk.issues._client._updated_by import get_updated_by
|
|
23
|
+
from fluidattacks_gitlab_sdk.issues.core import Issue, ProjectIdObj
|
|
24
|
+
from fluidattacks_gitlab_sdk.users.core import UserObj
|
|
25
|
+
|
|
26
|
+
from ._decode import decode_issue_and_id
|
|
27
|
+
|
|
28
|
+
LOG = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_issue(
|
|
32
|
+
client: HttpJsonClient,
|
|
33
|
+
project: ProjectId,
|
|
34
|
+
updated_by: Maybe[UserObj],
|
|
35
|
+
issue_id: IssueInternalId,
|
|
36
|
+
) -> Cmd[Result[tuple[IssueFullId, Issue], Coproduct[NotFound, Exception]]]:
|
|
37
|
+
endpoint = RelativeEndpoint.new(
|
|
38
|
+
"projects",
|
|
39
|
+
int_to_str(project.project_id.value),
|
|
40
|
+
"issues",
|
|
41
|
+
int_to_str(issue_id.internal_id.value),
|
|
42
|
+
)
|
|
43
|
+
msg = Cmd.wrap_impure(lambda: LOG.info("[API] get_issue(%s, %s)", project, issue_id))
|
|
44
|
+
return msg + client.get(
|
|
45
|
+
endpoint,
|
|
46
|
+
FrozenDict({}),
|
|
47
|
+
).map(
|
|
48
|
+
lambda r: r.alt(
|
|
49
|
+
lambda e: e.map(handle_not_found, lambda e: Coproduct.inr(cast_exception(e))),
|
|
50
|
+
)
|
|
51
|
+
.bind(lambda i: assert_single(i).alt(Coproduct.inr))
|
|
52
|
+
.bind(lambda i: decode_issue_and_id(updated_by, i).alt(Coproduct.inr))
|
|
53
|
+
.alt(
|
|
54
|
+
lambda c: right_map(
|
|
55
|
+
c,
|
|
56
|
+
lambda e: cast_exception(
|
|
57
|
+
Bug.new(
|
|
58
|
+
"_get_issue",
|
|
59
|
+
inspect.currentframe(),
|
|
60
|
+
e,
|
|
61
|
+
(),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_issue(
|
|
70
|
+
client: HttpJsonClient,
|
|
71
|
+
gql_client: GraphQlGitlabClient,
|
|
72
|
+
project: ProjectIdObj,
|
|
73
|
+
issue_id: IssueInternalId,
|
|
74
|
+
) -> Cmd[Result[tuple[IssueFullId, Issue], Coproduct[NotFound, Exception]]]:
|
|
75
|
+
updated_by = get_updated_by(gql_client, project.project_path, issue_id)
|
|
76
|
+
return CmdTransform.chain_cmd_result(
|
|
77
|
+
updated_by,
|
|
78
|
+
lambda u: _get_issue(client, project.project_id, u, issue_id),
|
|
79
|
+
).map(lambda r: r.alt(merge_coproduct))
|