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
9
10
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
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
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
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
142
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
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
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
229
230
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
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)