view.bend

cswap-bend-cli@0.1.0.0 · 359 lines · 68 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/pollrow.bend as Pollrow
3import cswap-bend-cli@0.1.0.0/usage/norm.bend as UNorm
4import cswap-bend-cli@0.1.0.0/json.bend as Json
5import cswap-bend-cli@0.1.0.0/clock.bend as Clock
6import cswap-bend-cli@0.1.0.0/poll.bend as Poll
7
8# The --json projections of a usage reading (json_output.py, pace.py): the
9# camelCase windows with their reset strings recomputed at print time and the
10# weekly pace fields, and the account rows list and status print.
11
12def week() -> U32:
13  604800
14
15def day() -> U32:
16  86400
17
18def jv(+k: String, +j: Json.Json) -> Json.Json:
19  Maybe.default(&2, Json.Json, Json.get(k, j), Json.JNull{})
20
21def has(+k: String, +j: Json.Json) -> Bool:
22  Maybe.is_some(&2, Json.Json, Json.get(k, j))
23
24def put(k: String, v: Json.Json, j: Json.Json) -> Json.Json:
25  Json.set(k, v, j)
26
27def put_if(on: Bool, k: String, v: Json.Json, j: Json.Json) -> Json.Json:
28  match on:
29    case True{}:
30      Json.set(k, v, j)
31    case False{}:
32      j
33
34def put_m(k: String, m: Maybe<&2, Json.Json>, j: Json.Json) -> Json.Json:
35  match m:
36    case Some{v}:
37      Json.set(k, v, j)
38    case None{}:
39      j
40
41def jstr(s: String) -> Json.Json:
42  Json.JStr{s}
43
44def jbool(b: Bool) -> Json.Json:
45  Json.JBool{b}
46
47type Cell is Data:
48  Cell{countdown: String, clock: String}
49
50# The reset string pair stored with the window when its stamp cannot be read.
51def cell.of_clock(m: Maybe<&2, String>, +cd: String) -> Maybe<&2, Cell>:
52  match m:
53    case Some{c}:
54      Some{Cell{cd, c}}
55    case None{}:
56      None{}
57
58def cell.stored(+w: Json.Json) -> Maybe<&2, Cell>:
59  cell.of_clock(Json.as_string(jv("clock", w)), Maybe.default(&2, String, Json.as_string(jv("countdown", w)), "?"))
60
61def cell.at(t: Maybe<&2, U32>, +w: Json.Json, +now: U32, +off: U32) -> Maybe<&2, Cell>:
62  match t:
63    case Some{+u}:
64      Some{Cell{UNorm.countdown(UNorm.remaining(u, now)), UNorm.clock.text(u, now, off)}}
65    case None{}:
66      cell.stored(w)
67
68def cell.reset(m: Maybe<&2, String>, +w: Json.Json, +now: U32, +off: U32) -> Maybe<&2, Cell>:
69  match m:
70    case Some{s}:
71      cell.at(Clock.parse_iso8601(s), w, now, off)
72    case None{}:
73      cell.stored(w)
74
75# oauth.fresh_reset_strings: countdown and clock recomputed from resets_at.
76def fresh(+w: Json.Json, +now: U32, +off: U32) -> Maybe<&2, Cell>:
77  cell.reset(UNorm.resets_of(w), w, now, off)
78
79def with_cell(j: Json.Json, m: Maybe<&2, Cell>) -> Json.Json:
80  match m:
81    case Some{Cell{cd, ck}}:
82      Json.set("clock", jstr(ck), Json.set("countdown", jstr(cd), j))
83    case None{}:
84      j
85
86# expected: tenths of the week elapsed; eta: when usage would reach 100%.
87type Pace is Data:
88  Pace{expected: U32, ahead: Bool, eta: Maybe<&2, U32>, last: Maybe<&2, Bool>}
89
90def elapsed.fin(+rem: U32) -> U32:
91  Clock.sel(U32.is_eq(rem, 0), 0, (604800 - rem : U32))
92
93def elapsed_of(+reset: U32, +fetched: U32) -> U32:
94  elapsed.fin((((reset % 604800) + 604800 - (fetched % 604800)) % 604800 : U32))
95
96def eta.pick(none: Bool, +p: U32, +elapsed: U32, +fetched: U32) -> Maybe<&2, U32>:
97  match none:
98    case True{}:
99      None{}
100    case False{}:
101      Some{Clock.sel(U32.is_ge(p, 1000), fetched, (fetched + (1000 - Poll.min_u(p, 1000)) * elapsed / p : U32))}
102
103def eta.of(+p: U32, +elapsed: U32, +fetched: U32) -> Maybe<&2, U32>:
104  eta.pick(U32.is_eq(elapsed, 0) || U32.is_eq(p, 0), p, elapsed, fetched)
105
106def last.el(none: Bool, +elapsed: U32, +p: U32) -> Maybe<&2, Bool>:
107  match none:
108    case True{}:
109      None{}
110    case False{}:
111      Some{U32.is_le((p * 604800 : U32), (1000 * elapsed : U32))}
112
113def last.pick(zero: Bool, +p: U32, +elapsed: U32) -> Maybe<&2, Bool>:
114  match zero:
115    case True{}:
116      Some{True{}}
117    case False{}:
118      last.el(U32.is_eq(elapsed, 0), elapsed, p)
119
120def last.of(+p: U32, +elapsed: U32) -> Maybe<&2, Bool>:
121  last.pick(U32.is_eq(p, 0), p, elapsed)
122
123def pace.pick(early: Bool, +p: U32, +elapsed: U32, +fetched: U32) -> Maybe<&2, Pace>:
124  match early:
125    case True{}:
126      None{}
127    case False{}:
128      Some{Pace{Poll.min_u(1000, ((elapsed * 1000 * 2 + 604800) / 1209600 : U32)),
129        U32.is_ge((p * 604800 : U32), ((elapsed * 1000 + 150 * 604800) : U32)), eta.of(p, elapsed, fetched), last.of(p, elapsed)}}
130
131def pace.mk(+p: U32, +elapsed: U32, +fetched: U32) -> Maybe<&2, Pace>:
132  pace.pick(U32.is_lt(elapsed, 86400), p, elapsed, fetched)
133
134def pace.reset(p: Maybe<&2, U32>, r: Maybe<&2, U32>, +fetched: U32) -> Maybe<&2, Pace>:
135  match p r:
136    case Some{x} Some{t}:
137      pace.mk(x, elapsed_of(t, fetched), fetched)
138    case _ _:
139      None{}
140
141# compute_pace for one weekly window; None when it cannot be told or is
142# inside the first day after a reset.
143def pace_of(+w: Json.Json, fetched: Maybe<&2, U32>) -> Maybe<&2, Pace>:
144  match fetched:
145    case None{}:
146      None{}
147    case Some{f}:
148      pace.reset(Poll.tenths.json(jv("pct", w)), Poll.win.reset(w), f)
149
150def tenths_text(+n: U32) -> String:
151  U32.show((n / 10 : U32)) ++ "." ++ U32.show((n % 10 : U32))
152
153def eta.json(t: U32) -> Json.Json:
154  jstr(Clock.iso8601_utc(t))
155
156def last.json(m: Maybe<&2, Bool>) -> Maybe<&2, Json.Json>:
157  match m:
158    case Some{b}:
159      Some{jbool(b)}
160    case None{}:
161      None{}
162
163def eta.m(m: Maybe<&2, U32>) -> Maybe<&2, Json.Json>:
164  match m:
165    case Some{t}:
166      Some{eta.json(t)}
167    case None{}:
168      None{}
169
170def with_pace(j: Json.Json, m: Maybe<&2, Pace>) -> Json.Json:
171  match m:
172    case None{}:
173      j
174    case Some{Pace{e, a, eta, last}}:
175      put_m("willLastToReset", last.json(last),
176        put_m("projectedExhaustionAt", eta.m(eta),
177          put("aheadOfPace", jbool(a), put("expectedPct", Json.JNum{tenths_text(e)}, j))))
178
179def reset_set(m: Maybe<&2, Json.Json>, j: Json.Json) -> Json.Json:
180  match m:
181    case Some{v}:
182      Json.set("resetsAt", v, j)
183    case None{}:
184      j
185
186def reset_field(+w: Json.Json, j: Json.Json) -> Json.Json:
187  reset_set(Json.get("resets_at", w), j)
188
189# A 5h window: pct, resetsAt and the fresh countdown and clock.
190def window_json(+w: Json.Json, +now: U32, +off: U32) -> Json.Json:
191  with_cell(reset_field(w, Json.of_pairs([("pct", jv("pct", w))])), fresh(w, now, off))
192
193def weekly_json(+w: Json.Json, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Json.Json:
194  with_pace(window_json(w, now, off), pace_of(w, fetched))
195
196def scoped_json(+w: Json.Json, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Json.Json:
197  Json.set("name", jv("name", w), weekly_json(w, fetched, now, off))
198
199def scoped_list(ls: List<&2, Json.Json>, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> List<&2, Json.Json>:
200  match ls:
201    case Nil{}:
202      []
203    case Con{h, t}:
204      scoped_json(h, fetched, now, off) <> scoped_list(t, fetched, now, off)
205
206def spend_json(+s: Json.Json, +now: U32, +off: U32) -> Json.Json:
207  with_cell(reset_field(s, Json.of_pairs([("used", jv("used", s)), ("limit", jv("limit", s)), ("pct", jv("pct", s)), ("currency", jv("currency", s))])), fresh(s, now, off))
208
209def five_json(+usage: Json.Json, +now: U32, +off: U32) -> Maybe<&2, Json.Json>:
210  Maybe.bind(&2, Json.Json, Json.Json, Json.get("five_hour", usage), w => Some{window_json(w, now, off)})
211
212def seven_json(+usage: Json.Json, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Maybe<&2, Json.Json>:
213  Maybe.bind(&2, Json.Json, Json.Json, Json.get("seven_day", usage), w => Some{weekly_json(w, fetched, now, off)})
214
215def spend_m(+usage: Json.Json, +now: U32, +off: U32) -> Maybe<&2, Json.Json>:
216  Maybe.bind(&2, Json.Json, Json.Json, Json.get("spend", usage), w => Some{spend_json(w, now, off)})
217
218def scoped_m(+usage: Json.Json, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Maybe<&2, Json.Json>:
219  Maybe.bind(&2, Json.Json, Json.Json, Json.get("scoped", usage), l => Some{Json.arr(scoped_list(Json.to_list(l), fetched, now, off))})
220
221# usage_to_json: sevenDay and scoped carry pace fields when `fetched` is known.
222def usage_to_json(+usage: Json.Json, +fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Json.Json:
223  put_m("scoped", scoped_m(usage, fetched, now, off),
224    put_m("spend", spend_m(usage, now, off),
225      put_m("sevenDay", seven_json(usage, fetched, now, off),
226        put_m("fiveHour", five_json(usage, now, off), Json.of_pairs([])))))
227
228# One managed account as list and status see it. `sentinel` is the usageStatus
229# an overlay forces (api_key, no_credentials, token_expired, relogin_required,
230# ...), already derived by the caller.
231type Acct is Data:
232  Acct{num: U32, email: String, org_name: String, org_uuid: String, active: Bool, alias: String, disabled: Bool,
233       login_expires: Maybe<&2, String>, sentinel: Maybe<&2, String>, row: Pollrow.Row}
234
235def is_obj(j: Json.Json) -> Bool:
236  match j:
237    case Json.JObj{f}:
238      True{}
239    case _:
240      False{}
241
242def age_text(+n: U32) -> String:
243  U32.show(n) ++ ".0"
244
245def ts(+t: U32) -> String:
246  Clock.iso8601_utc(t)
247
248def freshness(fetched: Maybe<&2, U32>, +now: U32, j: Json.Json) -> Json.Json:
249  match fetched:
250    case None{}:
251      j
252    case Some{+f}:
253      Json.set("usageAgeSeconds", Json.JNum{age_text(Poll.sub0(now, f))}, Json.set("usageFetchedAt", jstr(ts(f)), j))
254
255def lg.fields(ok: Bool, +lg: Json.Json, +f: U32, +now: U32, +off: U32, j: Json.Json) -> Json.Json:
256  match ok:
257    case False{}:
258      j
259    case True{}:
260      Json.set("lastGoodAgeSeconds", Json.JNum{age_text(Poll.sub0(now, f))},
261        Json.set("lastGoodFetchedAt", jstr(ts(f)),
262          Json.set("lastGoodUsage", usage_to_json(lg, Some{f}, now, off), j)))
263
264def last_good_fields(+lg: Json.Json, fetched: Maybe<&2, U32>, +now: U32, +off: U32, j: Json.Json) -> Json.Json:
265  match fetched:
266    case None{}:
267      j
268    case Some{+f}:
269      lg.fields(is_obj(lg), lg, f, now, off, j)
270
271def retry_at(live: Bool, b: Maybe<&2, U32>, j: Json.Json) -> Json.Json:
272  match live b:
273    case True{} Some{t}:
274      Json.set("usageRetryAt", jstr(ts(t)), j)
275    case _ _:
276      j
277
278def failure_fields(unavailable: Bool, last_error: Maybe<&2, String>, +backoff: Maybe<&2, U32>, +now: U32, j: Json.Json) -> Json.Json:
279  match unavailable last_error:
280    case True{} Some{e}:
281      retry_at(Pollrow.before.m(backoff, now), backoff, Json.set("usageError", jstr(e), j))
282    case _ _:
283      j
284
285def status_of(sentinel: Maybe<&2, String>, +usage: Json.Json) -> String:
286  match sentinel:
287    case Some{s}:
288      s
289    case None{}:
290      Clock.sel.s(is_obj(usage), "ok", "unavailable")
291
292def base_row(+a: Acct, status: String, usage: Json.Json) -> Json.Json:
293  match a:
294    case Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}:
295      Json.of_pairs([("number", Json.JNum{U32.show(num)}), ("email", jstr(email)), ("organizationName", jstr(org_name)),
296        ("organizationUuid", jstr(org_uuid)), ("isOrganization", jbool(Bool.not(String.is_empty(org_uuid)))), ("active", jbool(active)),
297        ("usageStatus", jstr(status)), ("usage", usage)])
298
299def extras(+a: Acct, j: Json.Json) -> Json.Json:
300  match a:
301    case Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}:
302      put_m("loginExpiresAt", Maybe.bind(&2, String, Json.Json, login, s => Some{jstr(s)}),
303        put_if(disabled, "disabled", jbool(True{}), put_if(Bool.not(String.is_empty(alias)), "alias", jstr(alias), j)))
304
305def tail.pick(has_usage: Bool, +fetched: Maybe<&2, U32>, +lg: Json.Json, le: Maybe<&2, String>, bu: Maybe<&2, U32>, unavailable: Bool, +now: U32, +off: U32, j: Json.Json) -> Json.Json:
306  match has_usage:
307    case True{}:
308      freshness(fetched, now, j)
309    case False{}:
310      failure_fields(unavailable, le, bu, now, last_good_fields(lg, fetched, now, off, j))
311
312def tail_fields(row: Pollrow.Row, status: String, usage: Json.Json, +now: U32, +off: U32, j: Json.Json) -> Json.Json:
313  match row:
314    case Pollrow.Row{fetched, lg, la, fl, le, bu, np, iv, l429, st, sfp, cu, hu}:
315      tail.pick(is_obj(usage), fetched, lg, le, bu, String.eq(status, "unavailable"), now, off, j)
316
317def row_json(+a: Acct, +usage: Json.Json, +status: String, +now: U32, +off: U32) -> Json.Json:
318  match a:
319    case Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}:
320      tail_fields(row, status, usage, now, off, extras(a, base_row(a, status, usage)))
321
322def project.pick(ok: Bool, +d: Json.Json, fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Json.Json:
323  match ok:
324    case True{}:
325      usage_to_json(d, fetched, now, off)
326    case False{}:
327      Json.JNull{}
328
329def project.of(+d: Json.Json, fetched: Maybe<&2, U32>, +now: U32, +off: U32) -> Json.Json:
330  project.pick(is_obj(d), d, fetched, now, off)
331
332def project(+row: Pollrow.Row, models: List<&2, String>, +now: U32, +off: U32) -> Json.Json:
333  match row:
334    case Pollrow.Row{fetched, lg, la, fl, le, bu, np, iv, l429, st, sfp, cu, hu}:
335      project.of(Pollrow.decision_usage(row, models, now), fetched, now, off)
336
337def usage_for(sentinel: Maybe<&2, String>, row: Pollrow.Row, models: List<&2, String>, +now: U32, +off: U32) -> Json.Json:
338  match sentinel:
339    case Some{s}:
340      Json.JNull{}
341    case None{}:
342      project(row, models, now, off)
343
344def usage_part(+a: Acct, models: List<&2, String>, +now: U32, +off: U32) -> Json.Json:
345  match a:
346    case Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}:
347      usage_for(sentinel, row, models, now, off)
348
349def sentinel_of(+a: Acct) -> Maybe<&2, String>:
350  match a:
351    case Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}:
352      sentinel
353
354def account_row2(+a: Acct, +usage: Json.Json, +now: U32, +off: U32) -> Json.Json:
355  row_json(a, usage, status_of(sentinel_of(a), usage), now, off)
356
357# The `list --json` row of one account (account_row).
358def account_row(+a: Acct, models: List<&2, String>, +now: U32, +off: U32) -> Json.Json:
359  account_row2(a, usage_part(a, models, now, off), now, off)