fluidattacks_zoho_sdk 1.0.0__py3-none-any.whl → 2.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_zoho_sdk/__init__.py +3 -5
- fluidattacks_zoho_sdk/_decoders.py +115 -31
- fluidattacks_zoho_sdk/_http_client/__init__.py +17 -0
- fluidattacks_zoho_sdk/_http_client/_client.py +348 -0
- fluidattacks_zoho_sdk/_http_client/_core.py +151 -0
- fluidattacks_zoho_sdk/_http_client/paginate.py +35 -0
- fluidattacks_zoho_sdk/bulk_export/__init__.py +29 -0
- fluidattacks_zoho_sdk/bulk_export/_client.py +195 -0
- fluidattacks_zoho_sdk/bulk_export/_decode.py +41 -0
- fluidattacks_zoho_sdk/bulk_export/core.py +97 -0
- fluidattacks_zoho_sdk/bulk_export/utils.py +72 -0
- fluidattacks_zoho_sdk/ids.py +8 -4
- fluidattacks_zoho_sdk/zoho_desk/__init__.py +51 -0
- fluidattacks_zoho_sdk/zoho_desk/_client.py +88 -0
- fluidattacks_zoho_sdk/zoho_desk/_decode.py +201 -86
- fluidattacks_zoho_sdk/zoho_desk/core.py +41 -24
- {fluidattacks_zoho_sdk-1.0.0.dist-info → fluidattacks_zoho_sdk-2.0.0.dist-info}/METADATA +2 -2
- fluidattacks_zoho_sdk-2.0.0.dist-info/RECORD +26 -0
- fluidattacks_zoho_sdk-1.0.0.dist-info/RECORD +0 -16
- {fluidattacks_zoho_sdk-1.0.0.dist-info → fluidattacks_zoho_sdk-2.0.0.dist-info}/WHEEL +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
from fluidattacks_zoho_sdk._http_client import TokenManager
|
|
1
2
|
from fluidattacks_zoho_sdk.auth import AuthApiFactory, Credentials
|
|
2
3
|
|
|
3
|
-
__version__ = "
|
|
4
|
+
__version__ = "2.0.0"
|
|
4
5
|
|
|
5
|
-
__all__ = [
|
|
6
|
-
"AuthApiFactory",
|
|
7
|
-
"Credentials",
|
|
8
|
-
]
|
|
6
|
+
__all__ = ["AuthApiFactory", "Credentials", "TokenManager"]
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from datetime import UTC, datetime
|
|
1
3
|
from typing import (
|
|
2
4
|
IO,
|
|
5
|
+
TypeVar,
|
|
3
6
|
)
|
|
4
7
|
|
|
5
|
-
from fa_purity import
|
|
8
|
+
from fa_purity import (
|
|
9
|
+
Coproduct,
|
|
10
|
+
FrozenList,
|
|
11
|
+
Maybe,
|
|
12
|
+
PureIterFactory,
|
|
13
|
+
Result,
|
|
14
|
+
ResultE,
|
|
15
|
+
ResultTransform,
|
|
16
|
+
Unsafe,
|
|
17
|
+
)
|
|
6
18
|
from fa_purity.date_time import DatetimeUTC
|
|
7
19
|
from fa_purity.json import (
|
|
8
20
|
JsonObj,
|
|
9
21
|
JsonPrimitiveUnfolder,
|
|
10
22
|
JsonUnfolder,
|
|
23
|
+
JsonValueFactory,
|
|
11
24
|
UnfoldedFactory,
|
|
12
25
|
Unfolder,
|
|
13
26
|
)
|
|
@@ -27,6 +40,21 @@ from fluidattacks_zoho_sdk.ids import (
|
|
|
27
40
|
TicketId,
|
|
28
41
|
UserId,
|
|
29
42
|
)
|
|
43
|
+
from fluidattacks_zoho_sdk.zoho_desk.core import OptionalId
|
|
44
|
+
|
|
45
|
+
_T = TypeVar("_T")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def decode_list_objs(
|
|
49
|
+
items: FrozenList[JsonObj],
|
|
50
|
+
transaform: Callable[[JsonObj], ResultE[_T]],
|
|
51
|
+
) -> ResultE[FrozenList[_T]]:
|
|
52
|
+
if not items:
|
|
53
|
+
return Result.failure(ValueError("Expected a list"))
|
|
54
|
+
|
|
55
|
+
return ResultTransform.all_ok(
|
|
56
|
+
PureIterFactory.from_list(items).map(lambda v: transaform(v)).to_list(),
|
|
57
|
+
)
|
|
30
58
|
|
|
31
59
|
|
|
32
60
|
def _decode_zoho_creds(raw: JsonObj) -> ResultE[Credentials]:
|
|
@@ -72,6 +100,31 @@ def decode_optional_date(raw: JsonObj, key: str) -> ResultE[Maybe[DatetimeUTC]]:
|
|
|
72
100
|
)
|
|
73
101
|
|
|
74
102
|
|
|
103
|
+
def decode_datetime_assume_utc(s: str) -> ResultE[Maybe[DatetimeUTC]]:
|
|
104
|
+
try:
|
|
105
|
+
if not s or not s.strip():
|
|
106
|
+
return Result.success(Maybe.empty())
|
|
107
|
+
|
|
108
|
+
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=UTC)
|
|
109
|
+
return DecodeUtils.to_opt_date_time(JsonValueFactory.from_unfolded(str(dt)))
|
|
110
|
+
|
|
111
|
+
except ValueError as e:
|
|
112
|
+
return Result.failure(e)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def flatten_decoder_date(
|
|
116
|
+
y: Result[Maybe[ResultE[Maybe[DatetimeUTC]]], Exception],
|
|
117
|
+
) -> ResultE[Maybe[DatetimeUTC]]:
|
|
118
|
+
return y.bind(lambda maybe_inner: maybe_inner.value_or(Result.success(Maybe.empty())))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def decode_opt_date_to_utc(raw: JsonObj, key: str) -> ResultE[Maybe[DatetimeUTC]]:
|
|
122
|
+
date = JsonUnfolder.optional(raw, key, DecodeUtils.to_opt_str).map(
|
|
123
|
+
lambda maybe_str: maybe_str.bind(lambda j: j).map(lambda v: decode_datetime_assume_utc(v)),
|
|
124
|
+
)
|
|
125
|
+
return flatten_decoder_date(date)
|
|
126
|
+
|
|
127
|
+
|
|
75
128
|
def decode_require_date(raw: JsonObj, key: str) -> ResultE[DatetimeUTC]:
|
|
76
129
|
return JsonUnfolder.require(raw, key, DecodeUtils.to_date_time)
|
|
77
130
|
|
|
@@ -110,68 +163,99 @@ def decode_profile_id(raw: JsonObj) -> ResultE[ProfileId]:
|
|
|
110
163
|
)
|
|
111
164
|
|
|
112
165
|
|
|
166
|
+
def decode_account_id_bulk(raw: JsonObj) -> ResultE[AccountId]:
|
|
167
|
+
return JsonUnfolder.require(raw, "Account ID", DecodeUtils.to_str).bind(
|
|
168
|
+
lambda v: Natural.from_int(int(v)).map(AccountId),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
113
172
|
def decode_account_id(raw: JsonObj) -> ResultE[AccountId]:
|
|
114
173
|
return (
|
|
115
|
-
JsonUnfolder.require(raw, "
|
|
174
|
+
JsonUnfolder.require(raw, "Account ID", DecodeUtils.to_str)
|
|
116
175
|
.bind(lambda v: Natural.from_int(int(v)))
|
|
117
176
|
.map(AccountId)
|
|
118
177
|
)
|
|
119
178
|
|
|
120
179
|
|
|
121
180
|
def decode_crm_id(raw: JsonObj) -> ResultE[CrmId]:
|
|
181
|
+
return JsonUnfolder.require(raw, "CRM ID", DecodeUtils.to_str).bind(
|
|
182
|
+
lambda v: Natural.from_int(int(v)).map(CrmId),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def decode_department_id(raw: JsonObj) -> ResultE[DeparmentId]:
|
|
122
187
|
return (
|
|
123
|
-
JsonUnfolder.require(
|
|
188
|
+
JsonUnfolder.require(raw, "Department", DecodeUtils.to_str)
|
|
124
189
|
.bind(lambda v: Natural.from_int(int(v)))
|
|
125
|
-
.map(
|
|
190
|
+
.map(DeparmentId)
|
|
126
191
|
)
|
|
127
192
|
|
|
128
193
|
|
|
129
|
-
def
|
|
194
|
+
def decode_department_id_team(raw: JsonObj) -> ResultE[DeparmentId]:
|
|
130
195
|
return (
|
|
131
|
-
JsonUnfolder.
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
lambda j: Natural.from_int(int(j)).alt(Unsafe.raise_exception).to_union(),
|
|
135
|
-
),
|
|
136
|
-
)
|
|
137
|
-
.map(lambda obj: DeparmentId(obj))
|
|
196
|
+
JsonUnfolder.require(raw, "departmentId", DecodeUtils.to_str)
|
|
197
|
+
.bind(lambda v: Natural.from_int(int(v)))
|
|
198
|
+
.map(DeparmentId)
|
|
138
199
|
)
|
|
139
200
|
|
|
140
201
|
|
|
141
|
-
def
|
|
142
|
-
return (
|
|
143
|
-
|
|
144
|
-
.map(
|
|
145
|
-
lambda v: v.bind(lambda x: x).map(
|
|
146
|
-
lambda j: Natural.from_int(int(j)).alt(Unsafe.raise_exception).to_union(),
|
|
147
|
-
),
|
|
148
|
-
)
|
|
149
|
-
.map(lambda obj: ProductId(obj))
|
|
202
|
+
def decode_require_product_id(raw: JsonObj) -> ResultE[ProductId]:
|
|
203
|
+
return JsonUnfolder.require(raw, "Product ID", DecodeUtils.to_str).bind(
|
|
204
|
+
lambda v: Natural.from_int(int(v)).map(lambda obj: ProductId(obj)),
|
|
150
205
|
)
|
|
151
206
|
|
|
152
207
|
|
|
153
208
|
def decode_team_id(raw: JsonObj) -> ResultE[TeamId]:
|
|
154
|
-
return JsonUnfolder.require(raw, "
|
|
209
|
+
return JsonUnfolder.require(raw, "Team Id", DecodeUtils.to_str).bind(
|
|
155
210
|
lambda v: Natural.from_int(int(v)).map(lambda obj: TeamId(obj)),
|
|
156
211
|
)
|
|
157
212
|
|
|
158
213
|
|
|
159
214
|
def decode_ticket_id(raw: JsonObj) -> ResultE[TicketId]:
|
|
160
|
-
return JsonUnfolder.require(raw, "
|
|
215
|
+
return JsonUnfolder.require(raw, "ID", DecodeUtils.to_str).bind(
|
|
161
216
|
lambda v: Natural.from_int(int(v)).map(lambda obj: TicketId(obj)),
|
|
162
217
|
)
|
|
163
218
|
|
|
164
219
|
|
|
165
220
|
def decode_contact_id(raw: JsonObj) -> ResultE[ContactId]:
|
|
166
|
-
return JsonUnfolder.require(
|
|
221
|
+
return JsonUnfolder.require(raw, "Contact ID", DecodeUtils.to_str).bind(
|
|
222
|
+
lambda v: Natural.from_int(int(v)).map(lambda obj: ContactId(obj)),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def decode_contact_id_bulk(raw: JsonObj) -> ResultE[ContactId]:
|
|
227
|
+
return JsonUnfolder.require(raw, "ID", DecodeUtils.to_str).bind(
|
|
167
228
|
lambda v: Natural.from_int(int(v)).map(lambda obj: ContactId(obj)),
|
|
168
229
|
)
|
|
169
230
|
|
|
170
231
|
|
|
171
|
-
def
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
def decode_id_team(raw: JsonObj) -> ResultE[TeamId]:
|
|
233
|
+
return JsonUnfolder.require(raw, "id", DecodeUtils.to_str).bind(
|
|
234
|
+
lambda v: Natural.from_int(int(v)).map(lambda obj: TeamId(obj)),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def decode_user_id_bulk(raw: JsonObj) -> ResultE[UserId]:
|
|
239
|
+
return JsonUnfolder.require(raw, "ID", DecodeUtils.to_str).bind(
|
|
240
|
+
lambda v: Natural.from_int(int(v)).map(UserId),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def decode_optional_id(raw: JsonObj, key: str) -> ResultE[Maybe[OptionalId]]:
|
|
245
|
+
return JsonUnfolder.optional(raw, key, DecodeUtils.to_opt_str).map(
|
|
246
|
+
lambda v: v.bind(lambda x: x).map(lambda j: OptionalId(j)),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def assert_single(item: Coproduct[JsonObj, FrozenList[JsonObj]]) -> ResultE[JsonObj]:
|
|
251
|
+
return item.map(
|
|
252
|
+
Result.success,
|
|
253
|
+
lambda _: Result.failure(ValueError("Expected a json not a list")),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def assert_multiple(item: Coproduct[JsonObj, FrozenList[JsonObj]]) -> ResultE[FrozenList[JsonObj]]:
|
|
258
|
+
return item.map(
|
|
259
|
+
lambda _: Result.failure(ValueError("Expected a json list not a single json")),
|
|
260
|
+
Result.success,
|
|
261
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from fluidattacks_zoho_sdk.auth import Credentials
|
|
4
|
+
from fluidattacks_zoho_sdk.ids import OrgId
|
|
5
|
+
|
|
6
|
+
from ._client import Client
|
|
7
|
+
from ._core import HttpJsonClient, RelativeEndpoint, TokenManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ClientFactory:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def new(creds: Credentials, org_id: OrgId, token_manager: TokenManager) -> HttpJsonClient:
|
|
14
|
+
return Client.new(creds, org_id, token_manager).client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["Client", "HttpJsonClient", "RelativeEndpoint", "TokenManager"]
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
from __future__ import (
|
|
2
|
+
annotations,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import (
|
|
8
|
+
dataclass,
|
|
9
|
+
)
|
|
10
|
+
from typing import (
|
|
11
|
+
TypeVar,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from fa_purity import Cmd, Coproduct, FrozenDict, FrozenList, Maybe, Result, UnitType
|
|
15
|
+
from fa_purity.json import JsonObj, JsonPrimitiveFactory, JsonUnfolder, JsonValue
|
|
16
|
+
from fluidattacks_etl_utils.decode import int_to_str
|
|
17
|
+
from fluidattacks_etl_utils.smash import bind_chain
|
|
18
|
+
from pure_requests import (
|
|
19
|
+
response,
|
|
20
|
+
)
|
|
21
|
+
from pure_requests import (
|
|
22
|
+
retry as _retry,
|
|
23
|
+
)
|
|
24
|
+
from pure_requests.basic import Data, Endpoint, HttpClient, HttpClientFactory, Params
|
|
25
|
+
from pure_requests.retry import (
|
|
26
|
+
HandledError,
|
|
27
|
+
MaxRetriesReached,
|
|
28
|
+
)
|
|
29
|
+
from requests import Response
|
|
30
|
+
|
|
31
|
+
from fluidattacks_zoho_sdk.auth import AuthApiFactory, Credentials, Token
|
|
32
|
+
from fluidattacks_zoho_sdk.ids import OrgId
|
|
33
|
+
|
|
34
|
+
from ._core import (
|
|
35
|
+
HandledErrors,
|
|
36
|
+
HTTPError,
|
|
37
|
+
HttpJsonClient,
|
|
38
|
+
JSONDecodeError,
|
|
39
|
+
RelativeEndpoint,
|
|
40
|
+
RequestException,
|
|
41
|
+
TokenManager,
|
|
42
|
+
UnhandledErrors,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
LOG = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
_S = TypeVar("_S")
|
|
48
|
+
_F = TypeVar("_F")
|
|
49
|
+
HTTP_UNAUTHORIZED: int = 401
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _refresh_access_token(creds: Credentials) -> Cmd[Token]:
|
|
53
|
+
return AuthApiFactory.auth_api(creds).new_access_token
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_404(error: Maybe[HTTPError]) -> tuple[bool, int]:
|
|
57
|
+
status: int = error.map(
|
|
58
|
+
lambda v: v.raw.response.status_code, # type: ignore[misc]
|
|
59
|
+
).value_or(0)
|
|
60
|
+
status_is_404: bool = (
|
|
61
|
+
error.map(lambda e: e.raw.response.status_code == HTTP_UNAUTHORIZED).value_or(False) # type: ignore[misc]
|
|
62
|
+
)
|
|
63
|
+
return (status_is_404, status)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _extract_http_error(error: HandledError[HandledErrors, UnhandledErrors]) -> Maybe[HTTPError]:
|
|
67
|
+
return error.value.map(
|
|
68
|
+
lambda handled: handled.error.map(
|
|
69
|
+
lambda http_err: Maybe.some(http_err),
|
|
70
|
+
lambda _: Maybe.empty(),
|
|
71
|
+
),
|
|
72
|
+
lambda _: Maybe.empty(),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _retry_cmd(
|
|
77
|
+
retry: int,
|
|
78
|
+
item: Result[_S, _F],
|
|
79
|
+
client_zoho: Client,
|
|
80
|
+
make_request: Callable[[HttpClient], Cmd[Result[_S, _F]]],
|
|
81
|
+
) -> Cmd[Result[_S, _F]]:
|
|
82
|
+
def handle_401(error: _F) -> Cmd[Result[_S, _F]]:
|
|
83
|
+
is_401 = _is_404(_extract_http_error(error)) # type: ignore[arg-type]
|
|
84
|
+
if is_401[0]:
|
|
85
|
+
return Cmd.wrap_impure(lambda: LOG.info("Refreshing token...")) + _refresh_access_token(
|
|
86
|
+
client_zoho.creds,
|
|
87
|
+
).bind(
|
|
88
|
+
lambda new_token: client_zoho.update_token(new_token),
|
|
89
|
+
).bind(
|
|
90
|
+
lambda _: client_zoho.build_headers()
|
|
91
|
+
.map(
|
|
92
|
+
lambda headers: HttpClientFactory.new_client(
|
|
93
|
+
None,
|
|
94
|
+
headers,
|
|
95
|
+
False,
|
|
96
|
+
), # HttpClient
|
|
97
|
+
)
|
|
98
|
+
.bind(make_request),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
log = Cmd.wrap_impure(
|
|
102
|
+
lambda: LOG.info(
|
|
103
|
+
"retry #%2s waiting... ",
|
|
104
|
+
retry,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
return _retry.cmd_if_fail(item, log + _retry.sleep_cmd(retry**2))
|
|
108
|
+
|
|
109
|
+
return item.map(lambda _: Cmd.wrap_value(item)).alt(handle_401).to_union()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _http_error_handler(
|
|
113
|
+
error: HTTPError,
|
|
114
|
+
) -> HandledError[HandledErrors, UnhandledErrors]:
|
|
115
|
+
err_code: int = error.raw.response.status_code # type: ignore[misc]
|
|
116
|
+
handled = (
|
|
117
|
+
401,
|
|
118
|
+
429,
|
|
119
|
+
)
|
|
120
|
+
if err_code in range(500, 600) or err_code in handled:
|
|
121
|
+
return HandledError.handled(HandledErrors(Coproduct.inl(error)))
|
|
122
|
+
return HandledError.unhandled(UnhandledErrors(Coproduct.inr(Coproduct.inl(error))))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _handled_request_exception(
|
|
126
|
+
error: RequestException,
|
|
127
|
+
) -> HandledError[HandledErrors, UnhandledErrors]:
|
|
128
|
+
return (
|
|
129
|
+
error.to_chunk_error()
|
|
130
|
+
.map(lambda e: HandledError.handled(HandledErrors(Coproduct.inr(Coproduct.inl(e)))))
|
|
131
|
+
.lash(
|
|
132
|
+
lambda _: error.to_connection_error().map(
|
|
133
|
+
lambda e: HandledError.handled(HandledErrors(Coproduct.inr(Coproduct.inr(e)))),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
.value_or(HandledError.unhandled(UnhandledErrors(Coproduct.inr(Coproduct.inr(error)))))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _handled_errors(
|
|
141
|
+
error: Coproduct[JSONDecodeError, Coproduct[HTTPError, RequestException]],
|
|
142
|
+
) -> HandledError[HandledErrors, UnhandledErrors]:
|
|
143
|
+
"""Classify errors."""
|
|
144
|
+
return error.map(
|
|
145
|
+
lambda _: HandledError.unhandled(UnhandledErrors(error)),
|
|
146
|
+
lambda c: c.map(
|
|
147
|
+
_http_error_handler,
|
|
148
|
+
_handled_request_exception,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _handled_errors_response(
|
|
154
|
+
error: Coproduct[HTTPError, RequestException],
|
|
155
|
+
) -> HandledError[HandledErrors, UnhandledErrors]:
|
|
156
|
+
return error.map(
|
|
157
|
+
_http_error_handler,
|
|
158
|
+
_handled_request_exception,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _adjust_unhandled(
|
|
163
|
+
error: UnhandledErrors | MaxRetriesReached,
|
|
164
|
+
) -> Coproduct[UnhandledErrors, MaxRetriesReached]:
|
|
165
|
+
return Coproduct.inr(error) if isinstance(error, MaxRetriesReached) else Coproduct.inl(error)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class Client:
|
|
170
|
+
_creds: Credentials
|
|
171
|
+
_max_retries: int
|
|
172
|
+
_org_id: OrgId
|
|
173
|
+
_token: TokenManager
|
|
174
|
+
|
|
175
|
+
def _full_endpoint(self, endpoint: RelativeEndpoint) -> Endpoint:
|
|
176
|
+
return Endpoint("/".join(("https://desk.zoho.com/api/v1", *endpoint.paths)))
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def new(creds: Credentials, org_id: OrgId, token: TokenManager) -> Client:
|
|
180
|
+
return Client(creds, 3, org_id, token)
|
|
181
|
+
|
|
182
|
+
def _headers(self, access_token: Token) -> JsonObj:
|
|
183
|
+
return FrozenDict(
|
|
184
|
+
{
|
|
185
|
+
"orgId": JsonValue.from_primitive(
|
|
186
|
+
JsonPrimitiveFactory.from_raw(int_to_str(self._org_id.org_id.value)),
|
|
187
|
+
),
|
|
188
|
+
"Authorization": JsonValue.from_primitive(
|
|
189
|
+
JsonPrimitiveFactory.from_raw(f"Zoho-oauthtoken {access_token.raw_token}"),
|
|
190
|
+
),
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def get(
|
|
195
|
+
self,
|
|
196
|
+
endpoint: RelativeEndpoint,
|
|
197
|
+
params: JsonObj,
|
|
198
|
+
) -> Cmd[
|
|
199
|
+
Result[
|
|
200
|
+
Coproduct[JsonObj, FrozenList[JsonObj]],
|
|
201
|
+
Coproduct[UnhandledErrors, MaxRetriesReached],
|
|
202
|
+
]
|
|
203
|
+
]:
|
|
204
|
+
_full = self._full_endpoint(endpoint)
|
|
205
|
+
log = Cmd.wrap_impure(
|
|
206
|
+
lambda: LOG.info("[API] get: %s\nparams = %s", _full, JsonUnfolder.dumps(params)),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
token_manager_cmd = self._token.get
|
|
210
|
+
client_cmd = token_manager_cmd.map(
|
|
211
|
+
lambda token: HttpClientFactory.new_client(None, self._headers(token), False),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def make_request(
|
|
215
|
+
new_client: HttpClient,
|
|
216
|
+
) -> Cmd[
|
|
217
|
+
Result[
|
|
218
|
+
Coproduct[JsonObj, FrozenList[JsonObj]],
|
|
219
|
+
HandledError[HandledErrors, UnhandledErrors],
|
|
220
|
+
]
|
|
221
|
+
]:
|
|
222
|
+
return (
|
|
223
|
+
new_client.get(_full, Params(params))
|
|
224
|
+
.map(lambda r: r.alt(RequestException))
|
|
225
|
+
.map(lambda r: bind_chain(r, lambda i: response.handle_status(i).alt(HTTPError)))
|
|
226
|
+
.map(
|
|
227
|
+
lambda r: bind_chain(
|
|
228
|
+
r,
|
|
229
|
+
lambda i: response.json_decode(i).alt(JSONDecodeError),
|
|
230
|
+
).alt(_handled_errors),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
handled = log + client_cmd.bind(make_request)
|
|
235
|
+
|
|
236
|
+
return _retry.retry_cmd(
|
|
237
|
+
handled,
|
|
238
|
+
lambda retry, item: _retry_cmd(retry, item, self, make_request),
|
|
239
|
+
self._max_retries,
|
|
240
|
+
).map(lambda r: r.alt(_adjust_unhandled))
|
|
241
|
+
|
|
242
|
+
def post(
|
|
243
|
+
self,
|
|
244
|
+
endpoint: RelativeEndpoint,
|
|
245
|
+
params: JsonObj,
|
|
246
|
+
) -> Cmd[
|
|
247
|
+
Result[
|
|
248
|
+
Coproduct[JsonObj, FrozenList[JsonObj]],
|
|
249
|
+
Coproduct[UnhandledErrors, MaxRetriesReached],
|
|
250
|
+
]
|
|
251
|
+
]:
|
|
252
|
+
_full = self._full_endpoint(endpoint)
|
|
253
|
+
log = Cmd.wrap_impure(lambda: LOG.info("[API] call (post): %s", _full))
|
|
254
|
+
|
|
255
|
+
client_cmd = self._token.get.map(
|
|
256
|
+
lambda t: HttpClientFactory.new_client(
|
|
257
|
+
None,
|
|
258
|
+
self._headers(t),
|
|
259
|
+
False,
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def make_request(
|
|
264
|
+
new_client: HttpClient,
|
|
265
|
+
) -> Cmd[
|
|
266
|
+
Result[
|
|
267
|
+
Coproduct[JsonObj, FrozenList[JsonObj]],
|
|
268
|
+
HandledError[HandledErrors, UnhandledErrors],
|
|
269
|
+
]
|
|
270
|
+
]:
|
|
271
|
+
return (
|
|
272
|
+
new_client.post(
|
|
273
|
+
_full,
|
|
274
|
+
Params(FrozenDict({})),
|
|
275
|
+
Data(params),
|
|
276
|
+
)
|
|
277
|
+
.map(lambda r: r.alt(RequestException))
|
|
278
|
+
.map(lambda r: bind_chain(r, lambda i: response.handle_status(i).alt(HTTPError)))
|
|
279
|
+
.map(
|
|
280
|
+
lambda r: bind_chain(
|
|
281
|
+
r,
|
|
282
|
+
lambda i: response.json_decode(i).alt(JSONDecodeError),
|
|
283
|
+
).alt(_handled_errors),
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
handled = log + client_cmd.bind(make_request)
|
|
288
|
+
|
|
289
|
+
return _retry.retry_cmd(
|
|
290
|
+
handled,
|
|
291
|
+
lambda retry, item: _retry_cmd(retry, item, self, make_request),
|
|
292
|
+
self._max_retries,
|
|
293
|
+
).map(lambda r: r.alt(_adjust_unhandled))
|
|
294
|
+
|
|
295
|
+
def get_response(
|
|
296
|
+
self,
|
|
297
|
+
endpoint: RelativeEndpoint,
|
|
298
|
+
params: JsonObj,
|
|
299
|
+
) -> Cmd[Result[Response, Coproduct[UnhandledErrors, MaxRetriesReached]]]:
|
|
300
|
+
_full = self._full_endpoint(endpoint)
|
|
301
|
+
log = Cmd.wrap_impure(
|
|
302
|
+
lambda: LOG.info("[API] get: %s\nparams = %s", _full, JsonUnfolder.dumps(params)),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
client_cmd = self._token.get.map(
|
|
306
|
+
lambda t: HttpClientFactory.new_client(
|
|
307
|
+
None,
|
|
308
|
+
self._headers(t),
|
|
309
|
+
False,
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def make_request(
|
|
314
|
+
new_client: HttpClient,
|
|
315
|
+
) -> Cmd[
|
|
316
|
+
Result[
|
|
317
|
+
Response,
|
|
318
|
+
HandledError[HandledErrors, UnhandledErrors],
|
|
319
|
+
]
|
|
320
|
+
]:
|
|
321
|
+
return (
|
|
322
|
+
new_client.get(_full, Params(params))
|
|
323
|
+
.map(lambda r: r.alt(RequestException))
|
|
324
|
+
.map(lambda r: bind_chain(r, lambda i: response.handle_status(i).alt(HTTPError)))
|
|
325
|
+
.map(lambda r: r.alt(_handled_errors_response))
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
handled = log + client_cmd.bind(make_request)
|
|
329
|
+
|
|
330
|
+
return _retry.retry_cmd(
|
|
331
|
+
handled,
|
|
332
|
+
lambda retry, item: _retry_cmd(retry, item, self, make_request),
|
|
333
|
+
self._max_retries,
|
|
334
|
+
).map(lambda r: r.alt(_adjust_unhandled))
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def client(self) -> HttpJsonClient:
|
|
338
|
+
return HttpJsonClient(self.get, self.post, self.get_response)
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def creds(self) -> Credentials:
|
|
342
|
+
return self._creds
|
|
343
|
+
|
|
344
|
+
def build_headers(self) -> Cmd[JsonObj]:
|
|
345
|
+
return self._token.get.map(lambda t: self._headers(t))
|
|
346
|
+
|
|
347
|
+
def update_token(self, new_token: Token) -> Cmd[UnitType]:
|
|
348
|
+
return self._token.update(new_token)
|