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,22 @@
1
+ import logging
2
+
3
+ from fa_purity import (
4
+ Unsafe,
5
+ )
6
+
7
+ from fluidattacks_gitlab_sdk._http_client import Credentials
8
+
9
+ from ._handlers import NotFound
10
+ from ._logger import (
11
+ set_logger,
12
+ )
13
+
14
+ __version__ = "1.0.0"
15
+
16
+ Unsafe.compute(set_logger(__name__, __version__))
17
+ LOG = logging.getLogger(__name__)
18
+
19
+ __all__ = [
20
+ "Credentials",
21
+ "NotFound",
22
+ ]
@@ -0,0 +1,170 @@
1
+ from collections.abc import Callable
2
+ from datetime import date
3
+ from typing import TypeVar
4
+
5
+ from fa_purity import (
6
+ Bool,
7
+ Coproduct,
8
+ CoproductFactory,
9
+ FrozenList,
10
+ Maybe,
11
+ NewFrozenList,
12
+ Result,
13
+ ResultE,
14
+ ResultSmash,
15
+ ResultTransform,
16
+ )
17
+ from fa_purity.json import JsonObj, JsonUnfolder
18
+ from fluidattacks_etl_utils import smash
19
+ from fluidattacks_etl_utils.decode import DecodeUtils
20
+ from fluidattacks_etl_utils.natural import Natural
21
+
22
+ from fluidattacks_gitlab_sdk._handlers import handle_value_error
23
+ from fluidattacks_gitlab_sdk.ids import (
24
+ EpicFullId,
25
+ EpicFullInternalId,
26
+ EpicGlobalId,
27
+ EpicInternalId,
28
+ GroupId,
29
+ IssueFullId,
30
+ IssueFullInternalId,
31
+ IssueGlobalId,
32
+ IssueInternalId,
33
+ MilestoneFullId,
34
+ MilestoneFullInternalId,
35
+ MilestoneGlobalId,
36
+ MilestoneInternalId,
37
+ MrFullId,
38
+ MrFullInternalId,
39
+ MrGlobalId,
40
+ MrInternalId,
41
+ ProjectId,
42
+ )
43
+
44
+ _T = TypeVar("_T")
45
+
46
+
47
+ def decode_maybe_single(items: NewFrozenList[_T]) -> Maybe[_T]:
48
+ return Bool.from_primitive(len(items.items) <= 1).map(
49
+ lambda _: ResultTransform.get_index(items, 0).map(Maybe.some).value_or(Maybe.empty()),
50
+ lambda _: Maybe.empty(),
51
+ )
52
+
53
+
54
+ def _decode_internal_id(
55
+ raw: JsonObj,
56
+ iid_transform: Callable[[Natural], _T],
57
+ ) -> ResultE[tuple[ProjectId, _T]]:
58
+ _project = (
59
+ JsonUnfolder.require(raw, "project_id", DecodeUtils.to_int)
60
+ .bind(Natural.from_int)
61
+ .map(ProjectId)
62
+ )
63
+ return smash.smash_result_2(
64
+ _project,
65
+ JsonUnfolder.require(raw, "iid", DecodeUtils.to_int)
66
+ .bind(Natural.from_int)
67
+ .map(iid_transform),
68
+ )
69
+
70
+
71
+ def _decode_id(
72
+ raw: JsonObj,
73
+ ) -> ResultE[Natural]:
74
+ return JsonUnfolder.require(raw, "id", DecodeUtils.to_int).bind(Natural.from_int)
75
+
76
+
77
+ def decode_milestone_internal_id(raw: JsonObj) -> ResultE[MilestoneFullInternalId]:
78
+ _decode_project = (
79
+ JsonUnfolder.require(raw, "project_id", DecodeUtils.to_int)
80
+ .bind(Natural.from_int)
81
+ .map(ProjectId)
82
+ )
83
+ _decode_group = (
84
+ JsonUnfolder.require(raw, "group_id", DecodeUtils.to_int)
85
+ .bind(Natural.from_int)
86
+ .map(GroupId)
87
+ )
88
+ _decode_iid = (
89
+ JsonUnfolder.require(raw, "iid", DecodeUtils.to_int)
90
+ .bind(Natural.from_int)
91
+ .map(MilestoneInternalId)
92
+ )
93
+ _union: CoproductFactory[ProjectId, GroupId] = CoproductFactory()
94
+ _project_or_group = _decode_project.map(_union.inl).lash(
95
+ lambda _: _decode_group.map(_union.inr),
96
+ )
97
+ return ResultSmash.smash_result_2(
98
+ _project_or_group,
99
+ _decode_iid,
100
+ ).map(lambda t: MilestoneFullInternalId(*t))
101
+
102
+
103
+ def decode_milestone_full_id(raw: JsonObj) -> ResultE[MilestoneFullId]:
104
+ return smash.smash_result_2(
105
+ _decode_id(raw).map(MilestoneGlobalId),
106
+ decode_milestone_internal_id(raw),
107
+ ).map(lambda t: MilestoneFullId(*t))
108
+
109
+
110
+ def decode_epic_internal_id(raw: JsonObj) -> ResultE[EpicFullInternalId]:
111
+ _decode_group = (
112
+ JsonUnfolder.require(raw, "group_id", DecodeUtils.to_int)
113
+ .bind(Natural.from_int)
114
+ .map(GroupId)
115
+ )
116
+ _decode_iid = (
117
+ JsonUnfolder.require(raw, "iid", DecodeUtils.to_int)
118
+ .bind(Natural.from_int)
119
+ .map(EpicInternalId)
120
+ )
121
+ return ResultSmash.smash_result_2(
122
+ _decode_group,
123
+ _decode_iid,
124
+ ).map(lambda t: EpicFullInternalId(*t))
125
+
126
+
127
+ def decode_epic_full_id(raw: JsonObj) -> ResultE[EpicFullId]:
128
+ return smash.smash_result_2(
129
+ _decode_id(raw).map(EpicGlobalId),
130
+ decode_epic_internal_id(raw),
131
+ ).map(lambda t: EpicFullId(*t))
132
+
133
+
134
+ def decode_mr_internal_id(raw: JsonObj) -> ResultE[MrFullInternalId]:
135
+ return _decode_internal_id(raw, MrInternalId).map(lambda t: MrFullInternalId(*t))
136
+
137
+
138
+ def decode_mr_full_id(raw: JsonObj) -> ResultE[MrFullId]:
139
+ return smash.smash_result_2(_decode_id(raw).map(MrGlobalId), decode_mr_internal_id(raw)).map(
140
+ lambda t: MrFullId(*t),
141
+ )
142
+
143
+
144
+ def decode_issue_internal_id(raw: JsonObj) -> ResultE[IssueFullInternalId]:
145
+ return _decode_internal_id(raw, IssueInternalId).map(lambda t: IssueFullInternalId(*t))
146
+
147
+
148
+ def decode_issue_full_id(raw: JsonObj) -> ResultE[IssueFullId]:
149
+ return smash.smash_result_2(
150
+ _decode_id(raw).map(IssueGlobalId),
151
+ decode_issue_internal_id(raw),
152
+ ).map(lambda t: IssueFullId(*t))
153
+
154
+
155
+ def decode_date(raw: str) -> ResultE[date]:
156
+ return handle_value_error(lambda: date.fromisoformat(raw))
157
+
158
+
159
+ def assert_single(item: Coproduct[JsonObj, FrozenList[JsonObj]]) -> ResultE[JsonObj]:
160
+ return item.map(
161
+ Result.success,
162
+ lambda _: Result.failure(ValueError("Expected a json not a list")),
163
+ )
164
+
165
+
166
+ def assert_multiple(item: Coproduct[JsonObj, FrozenList[JsonObj]]) -> ResultE[FrozenList[JsonObj]]:
167
+ return item.map(
168
+ lambda _: Result.failure(ValueError("Expected a json list not a single json")),
169
+ Result.success,
170
+ )
@@ -0,0 +1,8 @@
1
+ from ._client import GraphQlGitlabClient
2
+ from ._error import ApiError, ApiResult
3
+
4
+ __all__ = [
5
+ "ApiError",
6
+ "ApiResult",
7
+ "GraphQlGitlabClient",
8
+ ]
@@ -0,0 +1,101 @@
1
+ from __future__ import (
2
+ annotations,
3
+ )
4
+
5
+ import inspect
6
+ from dataclasses import (
7
+ dataclass,
8
+ )
9
+
10
+ from fa_purity import (
11
+ Cmd,
12
+ FrozenDict,
13
+ Result,
14
+ ResultE,
15
+ )
16
+ from fa_purity.json import (
17
+ JsonObj,
18
+ JsonValueFactory,
19
+ Unfolder,
20
+ )
21
+ from fluidattacks_etl_utils.bug import (
22
+ Bug,
23
+ )
24
+ from fluidattacks_etl_utils.retry import (
25
+ cmd_if_fail,
26
+ retry_cmd,
27
+ sleep_cmd,
28
+ )
29
+ from fluidattacks_etl_utils.typing import (
30
+ Dict,
31
+ TypeVar,
32
+ )
33
+ from gql import (
34
+ Client,
35
+ gql,
36
+ )
37
+ from gql.transport.requests import (
38
+ RequestsHTTPTransport,
39
+ )
40
+
41
+ from . import _handlers
42
+ from ._error import (
43
+ ApiError,
44
+ )
45
+
46
+ API_ENDPOINT = "https://gitlab.com/api/graphql"
47
+ _T = TypeVar("_T")
48
+
49
+
50
+ def error_handler(cmd: Cmd[_T]) -> Cmd[ResultE[_T]]:
51
+ return _handlers.too_many_requests_handler(
52
+ _handlers.server_error_handler(_handlers.connection_error_handler(cmd)),
53
+ ).map(lambda a: a.bind(lambda b: b.bind(lambda c: c)))
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class _GraphQlAsmClient:
58
+ client: Client
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class GraphQlGitlabClient:
63
+ _inner: _GraphQlAsmClient
64
+
65
+ @staticmethod
66
+ def new(token: str) -> Cmd[GraphQlGitlabClient]:
67
+ def _new() -> GraphQlGitlabClient:
68
+ headers: Dict[str, str] = {"Authorization": f"Bearer {token}"}
69
+ transport = RequestsHTTPTransport(API_ENDPOINT, headers)
70
+ client = Client(transport=transport, fetch_schema_from_transport=False)
71
+ return GraphQlGitlabClient(_GraphQlAsmClient(client))
72
+
73
+ return Cmd.wrap_impure(_new)
74
+
75
+ def _get(self, query: str, values: FrozenDict[str, str]) -> Cmd[JsonObj]:
76
+ def _action() -> JsonObj:
77
+ return Bug.assume_success(
78
+ "gql_decode_get_response",
79
+ inspect.currentframe(),
80
+ (query, str(values)),
81
+ JsonValueFactory.from_any(
82
+ self._inner.client.execute(gql(query), dict(values)), # type: ignore[misc]
83
+ ).bind(Unfolder.to_json),
84
+ )
85
+
86
+ return Cmd.wrap_impure(_action)
87
+
88
+ def get(self, query: str, values: FrozenDict[str, str]) -> Cmd[Result[JsonObj, ApiError]]:
89
+ result = retry_cmd(
90
+ error_handler(self._get(query, values)),
91
+ lambda i, r: cmd_if_fail(r, sleep_cmd(i**2)),
92
+ 10,
93
+ ).map(
94
+ lambda r: Bug.assume_success(
95
+ "gql_get_response",
96
+ inspect.currentframe(),
97
+ (query, str(values)),
98
+ r,
99
+ ),
100
+ )
101
+ return _handlers.api_error_handler(result)
@@ -0,0 +1,24 @@
1
+ from dataclasses import (
2
+ dataclass,
3
+ )
4
+
5
+ from fa_purity import (
6
+ FrozenList,
7
+ Result,
8
+ )
9
+ from fa_purity.json import (
10
+ JsonValue,
11
+ )
12
+ from fluidattacks_etl_utils.typing import (
13
+ TypeVar,
14
+ )
15
+
16
+ _T = TypeVar("_T")
17
+
18
+
19
+ @dataclass
20
+ class ApiError(Exception):
21
+ errors: FrozenList[JsonValue]
22
+
23
+
24
+ ApiResult = Result[_T, ApiError] # type: ignore[misc]
@@ -0,0 +1,87 @@
1
+ import inspect
2
+
3
+ from fa_purity import (
4
+ Cmd,
5
+ CmdUnwrapper,
6
+ Result,
7
+ ResultE,
8
+ ResultFactory,
9
+ )
10
+ from fa_purity.json import (
11
+ JsonValueFactory,
12
+ Unfolder,
13
+ )
14
+ from fluidattacks_etl_utils.bug import (
15
+ Bug,
16
+ )
17
+ from fluidattacks_etl_utils.typing import (
18
+ Callable,
19
+ TypeVar,
20
+ )
21
+ from gql.transport.exceptions import (
22
+ TransportQueryError,
23
+ TransportServerError,
24
+ )
25
+ from requests.exceptions import (
26
+ ConnectionError as RequestsConnectionError,
27
+ )
28
+
29
+ from ._error import (
30
+ ApiError,
31
+ )
32
+
33
+ _T = TypeVar("_T")
34
+
35
+
36
+ def http_status_handler(is_handled: Callable[[int], bool], cmd: Cmd[_T]) -> Cmd[ResultE[_T]]:
37
+ factory: ResultFactory[_T, Exception] = ResultFactory()
38
+
39
+ def _action(unwrapper: CmdUnwrapper) -> ResultE[_T]:
40
+ try:
41
+ return factory.success(unwrapper.act(cmd))
42
+ except TransportServerError as err:
43
+ if err.code is not None and is_handled(err.code):
44
+ return factory.failure(err)
45
+ raise
46
+
47
+ return Cmd.new_cmd(_action)
48
+
49
+
50
+ def api_error_handler(cmd: Cmd[_T]) -> Cmd[Result[_T, ApiError]]:
51
+ factory: ResultFactory[_T, ApiError] = ResultFactory()
52
+
53
+ def _action(unwrapper: CmdUnwrapper) -> Result[_T, ApiError]:
54
+ try:
55
+ return factory.success(unwrapper.act(cmd))
56
+ except TransportQueryError as err: # type: ignore[misc]
57
+ _errors = JsonValueFactory.from_any(err.errors).bind( # type: ignore[misc]
58
+ lambda x: Unfolder.to_list(x),
59
+ )
60
+ errors = Bug.assume_success(
61
+ "decode_errors",
62
+ inspect.currentframe(),
63
+ (str(_errors),),
64
+ _errors,
65
+ )
66
+ return factory.failure(ApiError(errors))
67
+
68
+ return Cmd.new_cmd(_action)
69
+
70
+
71
+ def too_many_requests_handler(cmd: Cmd[_T]) -> Cmd[ResultE[_T]]:
72
+ too_many_requests_num = 429
73
+ return http_status_handler(lambda c: c == too_many_requests_num, cmd)
74
+
75
+
76
+ def server_error_handler(cmd: Cmd[_T]) -> Cmd[ResultE[_T]]:
77
+ return http_status_handler(lambda c: c in range(500, 600), cmd)
78
+
79
+
80
+ def connection_error_handler(cmd: Cmd[_T]) -> Cmd[ResultE[_T]]:
81
+ def _action(unwrapper: CmdUnwrapper) -> ResultE[_T]:
82
+ try:
83
+ return Result.success(unwrapper.act(cmd))
84
+ except RequestsConnectionError as err:
85
+ return Result.failure(Exception(err))
86
+
87
+ return Cmd.new_cmd(_action)
@@ -0,0 +1,40 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import TypeVar
4
+
5
+ from fa_purity import Coproduct, Result, ResultE, cast_exception
6
+
7
+ from fluidattacks_gitlab_sdk._http_client import HTTPError, UnhandledErrors
8
+
9
+ _T = TypeVar("_T")
10
+
11
+
12
+ @dataclass
13
+ class NotFound(Exception):
14
+ parent: Exception | None
15
+
16
+
17
+ def handle_value_error(value: Callable[[], _T]) -> ResultE[_T]:
18
+ try:
19
+ return Result.success(value())
20
+ except ValueError as error:
21
+ return Result.failure(error)
22
+
23
+
24
+ def _handle_not_found(error: HTTPError) -> Coproduct[NotFound, Exception]:
25
+ not_found_code = 404
26
+ err_code: int = error.raw.response.status_code # type: ignore[misc]
27
+
28
+ if err_code == not_found_code:
29
+ return Coproduct.inl(NotFound(error.raw))
30
+ return Coproduct.inr(error.raw)
31
+
32
+
33
+ def handle_not_found(error: UnhandledErrors) -> Coproduct[NotFound, Exception]:
34
+ return error.error.map(
35
+ lambda e: Coproduct.inr(cast_exception(e.raw)),
36
+ lambda c: c.map(
37
+ _handle_not_found,
38
+ lambda e: Coproduct.inr(cast_exception(e.raw)),
39
+ ),
40
+ )
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fluidattacks_gitlab_sdk._http_client._client_1 import Client1
4
+ from fluidattacks_gitlab_sdk._http_client._core import (
5
+ Credentials,
6
+ HTTPError,
7
+ HttpJsonClient,
8
+ Page,
9
+ RelativeEndpoint,
10
+ UnhandledErrors,
11
+ )
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ClientFactory:
16
+ @staticmethod
17
+ def new(creds: Credentials) -> HttpJsonClient:
18
+ return Client1.new(creds).client
19
+
20
+
21
+ __all__ = [
22
+ "Credentials",
23
+ "HTTPError",
24
+ "HttpJsonClient",
25
+ "Page",
26
+ "RelativeEndpoint",
27
+ "UnhandledErrors",
28
+ ]
@@ -0,0 +1,206 @@
1
+ from __future__ import (
2
+ annotations,
3
+ )
4
+
5
+ import logging
6
+ from dataclasses import (
7
+ dataclass,
8
+ )
9
+ from typing import (
10
+ TypeVar,
11
+ )
12
+
13
+ from fa_purity import (
14
+ Cmd,
15
+ Coproduct,
16
+ FrozenDict,
17
+ FrozenList,
18
+ Result,
19
+ UnitType,
20
+ unit,
21
+ )
22
+ from fa_purity.json import (
23
+ JsonObj,
24
+ JsonPrimitiveFactory,
25
+ JsonUnfolder,
26
+ JsonValue,
27
+ )
28
+ from fluidattacks_etl_utils.smash import bind_chain
29
+ from pure_requests import (
30
+ response,
31
+ )
32
+ from pure_requests import (
33
+ retry as _retry,
34
+ )
35
+ from pure_requests.basic import (
36
+ Data,
37
+ Endpoint,
38
+ HttpClientFactory,
39
+ Params,
40
+ )
41
+ from pure_requests.retry import (
42
+ HandledError,
43
+ MaxRetriesReached,
44
+ )
45
+
46
+ from ._core import (
47
+ Credentials,
48
+ HandledErrors,
49
+ HTTPError,
50
+ HttpJsonClient,
51
+ JSONDecodeError,
52
+ RelativeEndpoint,
53
+ RequestException,
54
+ UnhandledErrors,
55
+ )
56
+
57
+ LOG = logging.getLogger(__name__)
58
+
59
+
60
+ _S = TypeVar("_S")
61
+ _F = TypeVar("_F")
62
+
63
+
64
+ def _retry_cmd(retry: int, item: Result[_S, _F]) -> Cmd[Result[_S, _F]]:
65
+ log = Cmd.wrap_impure(lambda: LOG.info("retry #%2s waiting...", retry))
66
+ return _retry.cmd_if_fail(item, log + _retry.sleep_cmd(retry**2))
67
+
68
+
69
+ def _http_error_handler(
70
+ error: HTTPError,
71
+ ) -> HandledError[HandledErrors, UnhandledErrors]:
72
+ err_code: int = error.raw.response.status_code # type: ignore[misc]
73
+ handled = (
74
+ 409,
75
+ 429,
76
+ )
77
+ if err_code in range(500, 600) or err_code in handled:
78
+ return HandledError.handled(HandledErrors(Coproduct.inl(error)))
79
+ return HandledError.unhandled(UnhandledErrors(Coproduct.inr(Coproduct.inl(error))))
80
+
81
+
82
+ def _handled_request_exception(
83
+ error: RequestException,
84
+ ) -> HandledError[HandledErrors, UnhandledErrors]:
85
+ return (
86
+ error.to_chunk_error()
87
+ .map(lambda e: HandledError.handled(HandledErrors(Coproduct.inr(Coproduct.inl(e)))))
88
+ .lash(
89
+ lambda _: error.to_connection_error().map(
90
+ lambda e: HandledError.handled(HandledErrors(Coproduct.inr(Coproduct.inr(e)))),
91
+ ),
92
+ )
93
+ .value_or(HandledError.unhandled(UnhandledErrors(Coproduct.inr(Coproduct.inr(error)))))
94
+ )
95
+
96
+
97
+ def _handled_errors(
98
+ error: Coproduct[JSONDecodeError, Coproduct[HTTPError, RequestException]],
99
+ ) -> HandledError[HandledErrors, UnhandledErrors]:
100
+ """Classify errors."""
101
+ return error.map(
102
+ lambda _: HandledError.unhandled(UnhandledErrors(error)),
103
+ lambda c: c.map(
104
+ _http_error_handler,
105
+ _handled_request_exception,
106
+ ),
107
+ )
108
+
109
+
110
+ def _adjust_unhandled(
111
+ error: UnhandledErrors | MaxRetriesReached,
112
+ ) -> Coproduct[UnhandledErrors, MaxRetriesReached]:
113
+ return Coproduct.inr(error) if isinstance(error, MaxRetriesReached) else Coproduct.inl(error)
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class Client1:
118
+ _creds: Credentials
119
+ _max_retries: int
120
+
121
+ def _full_endpoint(self, endpoint: RelativeEndpoint) -> Endpoint:
122
+ return Endpoint("/".join(("https://gitlab.com/api/v4", *endpoint.paths)))
123
+
124
+ @staticmethod
125
+ def new(creds: Credentials) -> Client1:
126
+ return Client1(
127
+ creds,
128
+ 150,
129
+ )
130
+
131
+ @property
132
+ def _headers(self) -> JsonObj:
133
+ return FrozenDict(
134
+ {
135
+ "Private-Token": JsonValue.from_primitive(
136
+ JsonPrimitiveFactory.from_raw(self._creds.api_key),
137
+ ),
138
+ },
139
+ )
140
+
141
+ def get(
142
+ self,
143
+ endpoint: RelativeEndpoint,
144
+ params: JsonObj,
145
+ ) -> Cmd[
146
+ Result[
147
+ Coproduct[JsonObj, FrozenList[JsonObj]],
148
+ Coproduct[UnhandledErrors, MaxRetriesReached],
149
+ ]
150
+ ]:
151
+ _full = self._full_endpoint(endpoint)
152
+ log = Cmd.wrap_impure(
153
+ lambda: LOG.debug(
154
+ "[API] get: %s\nparams = %s",
155
+ _full,
156
+ JsonUnfolder.dumps(params),
157
+ ),
158
+ )
159
+ client = HttpClientFactory.new_client(None, self._headers, False)
160
+ handled = log + client.get(_full, Params(params)).map(
161
+ lambda r: r.alt(RequestException),
162
+ ).map(
163
+ lambda r: bind_chain(r, lambda i: response.handle_status(i).alt(HTTPError)),
164
+ ).map(
165
+ lambda r: bind_chain(r, lambda i: response.json_decode(i).alt(JSONDecodeError)).alt(
166
+ _handled_errors,
167
+ ),
168
+ )
169
+ return _retry.retry_cmd(
170
+ handled,
171
+ _retry_cmd,
172
+ self._max_retries,
173
+ ).map(lambda r: r.alt(_adjust_unhandled))
174
+
175
+ def post(
176
+ self,
177
+ endpoint: RelativeEndpoint,
178
+ ) -> Cmd[Result[UnitType, Coproduct[UnhandledErrors, MaxRetriesReached]]]:
179
+ _full = self._full_endpoint(endpoint)
180
+ log = Cmd.wrap_impure(lambda: LOG.info("API call (post): %s", _full))
181
+ client = HttpClientFactory.new_client(None, self._headers, False)
182
+ handled = log + client.post(
183
+ self._full_endpoint(endpoint),
184
+ Params(FrozenDict({})),
185
+ Data(FrozenDict({})),
186
+ ).map(lambda r: r.alt(RequestException)).map(
187
+ lambda r: bind_chain(r, lambda i: response.handle_status(i).alt(HTTPError)).alt(
188
+ lambda e: _handled_errors(Coproduct.inr(e)),
189
+ ),
190
+ )
191
+ return _retry.retry_cmd(
192
+ handled,
193
+ _retry_cmd,
194
+ self._max_retries,
195
+ ).map(
196
+ lambda r: r.map(
197
+ lambda _: unit,
198
+ ).alt(_adjust_unhandled),
199
+ )
200
+
201
+ @property
202
+ def client(self) -> HttpJsonClient:
203
+ return HttpJsonClient(
204
+ self.get,
205
+ self.post,
206
+ )