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,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,7 @@
1
+ from ._client import IssueClientFactory
2
+ from .core import IssueClient
3
+
4
+ __all__ = [
5
+ "IssueClient",
6
+ "IssueClientFactory",
7
+ ]
@@ -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))