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.
@@ -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__ = "1.0.0"
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 Maybe, ResultE, Unsafe
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, "accountId", DecodeUtils.to_str)
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(get_sub_json(raw, "zohoCRMContact"), "id", DecodeUtils.to_str)
188
+ JsonUnfolder.require(raw, "Department", DecodeUtils.to_str)
124
189
  .bind(lambda v: Natural.from_int(int(v)))
125
- .map(lambda obj: CrmId(obj))
190
+ .map(DeparmentId)
126
191
  )
127
192
 
128
193
 
129
- def decode_deparment_id(raw: JsonObj) -> ResultE[DeparmentId]:
194
+ def decode_department_id_team(raw: JsonObj) -> ResultE[DeparmentId]:
130
195
  return (
131
- JsonUnfolder.optional(raw, "departmentId", DecodeUtils.to_opt_str)
132
- .map(
133
- lambda v: v.bind(lambda x: x).map(
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 decode_product_id(raw: JsonObj) -> ResultE[ProductId]:
142
- return (
143
- JsonUnfolder.optional(raw, "productId", DecodeUtils.to_opt_str)
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, "teamId", DecodeUtils.to_str).bind(
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, "id", DecodeUtils.to_str).bind(
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(get_sub_json(raw, "contact"), "id", DecodeUtils.to_str).bind(
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 decode_account_id_ticket(raw: JsonObj) -> ResultE[AccountId]:
172
- account_obj = get_sub_json(raw, "contact")
173
- return JsonUnfolder.require(
174
- get_sub_json(account_obj, "account"),
175
- "id",
176
- DecodeUtils.to_str,
177
- ).bind(lambda v: Natural.from_int(int(v)).map(lambda obj: AccountId(obj)))
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)