meterbase 0.1.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.
- meterbase/__init__.py +59 -0
- meterbase/_async/__init__.py +0 -0
- meterbase/_async/_io.py +22 -0
- meterbase/_async/_transport.py +181 -0
- meterbase/_async/client.py +427 -0
- meterbase/_async/resources/__init__.py +16 -0
- meterbase/_async/resources/customer_allowances.py +89 -0
- meterbase/_async/resources/customer_periods.py +41 -0
- meterbase/_async/resources/customer_plan.py +228 -0
- meterbase/_async/resources/customers.py +176 -0
- meterbase/_async/resources/meters.py +102 -0
- meterbase/_async/resources/plans.py +140 -0
- meterbase/_http.py +205 -0
- meterbase/_json.py +18 -0
- meterbase/_keys.py +39 -0
- meterbase/_models.py +177 -0
- meterbase/_not_given.py +32 -0
- meterbase/_sync/__init__.py +4 -0
- meterbase/_sync/_io.py +21 -0
- meterbase/_sync/_transport.py +185 -0
- meterbase/_sync/client.py +431 -0
- meterbase/_sync/resources/__init__.py +20 -0
- meterbase/_sync/resources/customer_allowances.py +93 -0
- meterbase/_sync/resources/customer_periods.py +45 -0
- meterbase/_sync/resources/customer_plan.py +230 -0
- meterbase/_sync/resources/customers.py +176 -0
- meterbase/_sync/resources/meters.py +106 -0
- meterbase/_sync/resources/plans.py +144 -0
- meterbase/_time.py +49 -0
- meterbase/_version.py +1 -0
- meterbase/errors.py +309 -0
- meterbase/py.typed +0 -0
- meterbase/resources.py +41 -0
- meterbase/types.py +600 -0
- meterbase/webhooks.py +154 -0
- meterbase-0.1.0.dist-info/METADATA +867 -0
- meterbase-0.1.0.dist-info/RECORD +39 -0
- meterbase-0.1.0.dist-info/WHEEL +4 -0
- meterbase-0.1.0.dist-info/licenses/LICENSE +21 -0
meterbase/types.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wire types, shaped exactly as the engine sends them: checkable against the API
|
|
3
|
+
reference line by line, with no translation layer to drift.
|
|
4
|
+
|
|
5
|
+
Each response is a `Model`, which is the engine's JSON as it arrived with a
|
|
6
|
+
typed attribute for each field. Times are RFC 3339 strings, as sent: the
|
|
7
|
+
engine's carry microseconds, which a round trip through anything else can
|
|
8
|
+
drop.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Literal, TypedDict
|
|
14
|
+
|
|
15
|
+
from ._models import ListResult, Model
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Allowance",
|
|
19
|
+
"AllowanceSource",
|
|
20
|
+
"ApplyMode",
|
|
21
|
+
"Assignment",
|
|
22
|
+
"CheckRateLimit",
|
|
23
|
+
"CheckReason",
|
|
24
|
+
"CheckResult",
|
|
25
|
+
"ClosedPeriod",
|
|
26
|
+
"ClosedPeriodMeter",
|
|
27
|
+
"Customer",
|
|
28
|
+
"CustomerEntitlement",
|
|
29
|
+
"CustomerUsage",
|
|
30
|
+
"CustomerWithPlan",
|
|
31
|
+
"DayUsage",
|
|
32
|
+
"EmbeddedPlan",
|
|
33
|
+
"EntitlementReachedEvent",
|
|
34
|
+
"EntitlementState",
|
|
35
|
+
"IncomingVersion",
|
|
36
|
+
"ListResult",
|
|
37
|
+
"Meter",
|
|
38
|
+
"MeterEntitlement",
|
|
39
|
+
"MeterUsage",
|
|
40
|
+
"MeterUsageHistory",
|
|
41
|
+
"Model",
|
|
42
|
+
"Period",
|
|
43
|
+
"Plan",
|
|
44
|
+
"PlanAllowance",
|
|
45
|
+
"PlanAllowanceKind",
|
|
46
|
+
"PlanCycle",
|
|
47
|
+
"Reconciliation",
|
|
48
|
+
"RecordedReconciliation",
|
|
49
|
+
"ReleaseResult",
|
|
50
|
+
"ReserveResponse",
|
|
51
|
+
"ThresholdCrossedEvent",
|
|
52
|
+
"ThresholdCrossing",
|
|
53
|
+
"ThresholdPlan",
|
|
54
|
+
"TrackResult",
|
|
55
|
+
"UsageEvent",
|
|
56
|
+
"UsagePeriod",
|
|
57
|
+
"Validity",
|
|
58
|
+
"ValidityParam",
|
|
59
|
+
"WebhookEvent",
|
|
60
|
+
"WhoAmI",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Meters ---------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Meter(Model):
|
|
68
|
+
id: str
|
|
69
|
+
workspace_id: str
|
|
70
|
+
key: str
|
|
71
|
+
name: str
|
|
72
|
+
description: str
|
|
73
|
+
unit: str
|
|
74
|
+
value: str
|
|
75
|
+
thresholds: list[int] | None
|
|
76
|
+
"""Usage-alert percents for this meter. `None` inherits the workspace's
|
|
77
|
+
defaults; `[]` turns alerts off for the meter. A customer's own list beats
|
|
78
|
+
this one."""
|
|
79
|
+
created_at: str
|
|
80
|
+
updated_at: str
|
|
81
|
+
archived_at: str | None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Customers ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Customer(Model):
|
|
88
|
+
id: str
|
|
89
|
+
workspace_id: str
|
|
90
|
+
external_id: str
|
|
91
|
+
name: str
|
|
92
|
+
metadata: dict[str, Any]
|
|
93
|
+
thresholds: list[int] | None
|
|
94
|
+
"""Usage-alert percents for this customer. `None` inherits — the meter's
|
|
95
|
+
list, then the workspace's. `[]` turns alerts off for this customer. The
|
|
96
|
+
most specific of the three, and it always wins."""
|
|
97
|
+
created_at: str
|
|
98
|
+
updated_at: str
|
|
99
|
+
deleted_at: str | None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class EmbeddedPlan(Model):
|
|
103
|
+
"""
|
|
104
|
+
A plan as a customer read embeds it: the plan itself, flattened onto the
|
|
105
|
+
assignment that named it.
|
|
106
|
+
|
|
107
|
+
`id` is the **plan's**. The assignment's own id is on
|
|
108
|
+
`customers.plan.retrieve`, which is where instructions are identified;
|
|
109
|
+
what a reader wants from an embed is which plan. `cycle` is here because
|
|
110
|
+
`anchor_at` fixes a period only alongside its stride.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
id: str
|
|
114
|
+
key: str
|
|
115
|
+
name: str
|
|
116
|
+
cycle: PlanCycle
|
|
117
|
+
effective_at: str
|
|
118
|
+
"""When this instruction takes effect — ahead of now on `pending_plan`."""
|
|
119
|
+
anchor_at: str
|
|
120
|
+
"""What the customer's periods are measured from."""
|
|
121
|
+
effective: ApplyMode
|
|
122
|
+
reconciliation: RecordedReconciliation
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class CustomerWithPlan(Customer):
|
|
126
|
+
"""
|
|
127
|
+
A single-customer read. The plan travels with the customer because a
|
|
128
|
+
caller holding one almost always goes on to ask what it is entitled to.
|
|
129
|
+
|
|
130
|
+
`plan` is the one in force now and `pending_plan` the change waiting to
|
|
131
|
+
land, each `None` when there is none — holding no plan is a default deny,
|
|
132
|
+
not a failure. `customers.list`, `create`, `update` and `delete` answer a
|
|
133
|
+
plain `Customer`: a listing does not embed, where the same read would run
|
|
134
|
+
per row for an answer most callers of a list do not want.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
plan: EmbeddedPlan | None
|
|
138
|
+
pending_plan: EmbeddedPlan | None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Check ----------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
CheckReason = Literal["entitlement", "rate_limit"] | str
|
|
144
|
+
"""
|
|
145
|
+
Which gate turned the request away. `entitlement` is named ahead of
|
|
146
|
+
`rate_limit` when both refuse: running out of plan is the one the customer can
|
|
147
|
+
act on, and being told about the window instead would send them to support
|
|
148
|
+
over the wrong thing.
|
|
149
|
+
|
|
150
|
+
Open on purpose. The engine's contract says this list will grow, and a closed
|
|
151
|
+
one would turn every code the engine adds into a release the caller has to
|
|
152
|
+
wait on the SDK for. Branch on the codes you know; anything else is still a
|
|
153
|
+
refusal, and still means do not do the work.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class CheckRateLimit(Model):
|
|
158
|
+
"""
|
|
159
|
+
One rate-limit window in force for this customer and meter.
|
|
160
|
+
|
|
161
|
+
Windows are fixed and clock-aligned — a cell starts at a multiple of the
|
|
162
|
+
window counted from the epoch — which is what lets `used` be a literal
|
|
163
|
+
count of what the current cell holds and `resets_at` a clock time. A
|
|
164
|
+
sliding window would owe the reader a footnote for both.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
window_seconds: int
|
|
168
|
+
"""A whole number of seconds from `60` to `2592000`: a minute to 30
|
|
169
|
+
days."""
|
|
170
|
+
max: int
|
|
171
|
+
used: int
|
|
172
|
+
remaining: int
|
|
173
|
+
"""`max - used`, floored at 0."""
|
|
174
|
+
exceeded: bool
|
|
175
|
+
"""`used + quantity > max`: this request does not fit in this window."""
|
|
176
|
+
resets_at: str
|
|
177
|
+
"""End of the current cell, when `used` goes back to 0. Waiting for it is
|
|
178
|
+
the fix for a window that refused — unless the quantity is larger than
|
|
179
|
+
`max`, which never fits however long the caller waits."""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class CheckResult(Model):
|
|
183
|
+
allowed: bool
|
|
184
|
+
"""Both gates: the capacity covers the quantity **and** every rate-limit
|
|
185
|
+
window admits it. Under `no_cap` or a soft cap the capacity always does."""
|
|
186
|
+
no_cap: bool
|
|
187
|
+
"""Unlimited for this meter. `available` is `None`. Rate limits still
|
|
188
|
+
apply — a customer who is never refused on capacity is the one a runaway
|
|
189
|
+
loop costs most."""
|
|
190
|
+
available: int | None
|
|
191
|
+
"""
|
|
192
|
+
The whole capacity: the plan's unused entitlement for the current period,
|
|
193
|
+
floored at 0, plus every open grant. `None` under `no_cap`, where capacity
|
|
194
|
+
is not a number. Under a soft cap it is what is left of the included
|
|
195
|
+
amount, and running out of it refuses nothing.
|
|
196
|
+
|
|
197
|
+
One figure and no breakdown: `check` answers from a single cached integer,
|
|
198
|
+
and itemising where the capacity came from would cost it that. For the
|
|
199
|
+
per-grant detail, read `customers.allowances.list`.
|
|
200
|
+
"""
|
|
201
|
+
allow_overage: bool
|
|
202
|
+
"""A soft cap: past `available`, only a rate limit refuses. `False`
|
|
203
|
+
everywhere else, `no_cap` included."""
|
|
204
|
+
reason: CheckReason | None = None
|
|
205
|
+
"""Which gate said no. Absent when allowed — `None` here — so its presence
|
|
206
|
+
is the denial."""
|
|
207
|
+
rate_limits: list[CheckRateLimit]
|
|
208
|
+
"""
|
|
209
|
+
Every window in force, refused or not, which is why the engine reports
|
|
210
|
+
them on every call: a caller that only heard about a limit when it was
|
|
211
|
+
refused could not slow down before it, only after.
|
|
212
|
+
|
|
213
|
+
A limit is protective, not commercial, so it sits beside `available` and
|
|
214
|
+
is never folded into it — a customer well inside their plan can still be
|
|
215
|
+
limited, and the two are fixed by different people for different reasons.
|
|
216
|
+
`[]` when no rule applies, which is the ordinary case.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Usage ----------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class UsageEvent(Model):
|
|
224
|
+
"""
|
|
225
|
+
A receipt. The same shape whether the event is new or a replay — no
|
|
226
|
+
capacity figures, no nullable fields, nothing to branch on.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
id: str
|
|
230
|
+
"""Meterbase's id for the event. A retry of the same key returns this same
|
|
231
|
+
id."""
|
|
232
|
+
customer_id: str
|
|
233
|
+
meter_id: str
|
|
234
|
+
quantity: int
|
|
235
|
+
occurred_at: str
|
|
236
|
+
"""When Meterbase stored it, which chooses the period the event counts
|
|
237
|
+
against."""
|
|
238
|
+
recorded_at: str
|
|
239
|
+
"""The same instant as `occurred_at`. Both are on the wire so a reader of
|
|
240
|
+
either keeps working."""
|
|
241
|
+
idempotency_key: str
|
|
242
|
+
replayed: bool
|
|
243
|
+
"""`True` when this key had already been recorded: nothing was written, and
|
|
244
|
+
this is the original event."""
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TrackResult(Model):
|
|
248
|
+
"""Enveloped, like the list reads, so the receipt can grow a sibling field
|
|
249
|
+
without breaking callers."""
|
|
250
|
+
|
|
251
|
+
event: UsageEvent
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ReserveResponse(CheckResult):
|
|
255
|
+
"""
|
|
256
|
+
What the engine answers a reserve with: everything `check` answers, and
|
|
257
|
+
the hold. `reserve` hands it back as a `Hold`, which adds the two calls
|
|
258
|
+
that close it.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
reservation_id: str | None = None
|
|
262
|
+
"""The hold's id, and the event's once a track commits it. `None` on a
|
|
263
|
+
refusal, which holds nothing."""
|
|
264
|
+
quantity: int | None = None
|
|
265
|
+
"""What the hold set aside."""
|
|
266
|
+
expires_at: str | None = None
|
|
267
|
+
"""When the hold comes back on its own. `None` on a refusal."""
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ReleaseResult(Model):
|
|
271
|
+
"""The hold that was given back, and what it had set aside."""
|
|
272
|
+
|
|
273
|
+
reservation_id: str
|
|
274
|
+
quantity: int
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# Plans ----------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
PlanCycle = Literal["daily", "weekly", "monthly", "quarterly", "half_yearly", "yearly"]
|
|
280
|
+
"""
|
|
281
|
+
A plan's billing period. Every recurring allowance it holds is measured
|
|
282
|
+
against it, and it is fixed when the plan is created. `one_time` is not among
|
|
283
|
+
them: that is a kind of allowance, not a period.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class Plan(Model):
|
|
288
|
+
id: str
|
|
289
|
+
workspace_id: str
|
|
290
|
+
key: str
|
|
291
|
+
name: str
|
|
292
|
+
description: str
|
|
293
|
+
cycle: PlanCycle
|
|
294
|
+
created_at: str
|
|
295
|
+
updated_at: str
|
|
296
|
+
archived_at: str | None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# Plan allowances ------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
PlanAllowanceKind = Literal["recurring", "one_time"]
|
|
302
|
+
"""Which of a `(plan, meter)` pair's two lineages a version belongs to. Each
|
|
303
|
+
numbers itself separately."""
|
|
304
|
+
|
|
305
|
+
ApplyMode = Literal["immediate", "next_cycle"]
|
|
306
|
+
|
|
307
|
+
Reconciliation = Literal["none", "prorate", "reset"]
|
|
308
|
+
"""
|
|
309
|
+
What an `immediate` plan change does with the period it lands in: `none` lets
|
|
310
|
+
the new plan's amount govern the whole period, `prorate` weights the two plans
|
|
311
|
+
by the fraction each was held for, and `reset` closes the period at the change
|
|
312
|
+
and opens a fresh one — moving the customer's anchor, and so their renewal
|
|
313
|
+
day.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
RecordedReconciliation = Literal["none", "prorate", "reset", "rebalance"]
|
|
317
|
+
"""
|
|
318
|
+
Reconciliation as a stored row may read it. The plan history is append-only,
|
|
319
|
+
so a mode the API has since stopped accepting — `rebalance` — survives in rows
|
|
320
|
+
written while it did. Responses widen to this; requests do not.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class Validity(Model):
|
|
325
|
+
"""
|
|
326
|
+
How long a one-time grant lives once minted. Months are kept apart from
|
|
327
|
+
days because they are not the same length: three months from Jan 31 dies
|
|
328
|
+
Apr 30, ninety days dies May 1.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
months: int
|
|
332
|
+
days: int
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class ValidityParam(TypedDict):
|
|
336
|
+
"""`Validity`, as a parameter: `valid_for={"months": 3, "days": 0}`."""
|
|
337
|
+
|
|
338
|
+
months: int
|
|
339
|
+
days: int
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class PlanAllowance(Model):
|
|
343
|
+
id: str
|
|
344
|
+
workspace_id: str
|
|
345
|
+
plan_id: str
|
|
346
|
+
meter_id: str
|
|
347
|
+
version: int
|
|
348
|
+
"""1, 2, 3… per lineage. Assigned by the engine, never by the caller."""
|
|
349
|
+
amount: int | None
|
|
350
|
+
"""Units per period. `0` is legal and grants nothing. `None` under
|
|
351
|
+
`no_cap`."""
|
|
352
|
+
no_cap: bool
|
|
353
|
+
allow_overage: bool
|
|
354
|
+
"""A soft cap: once `amount` is used up, `check` and `reserve` admit rather
|
|
355
|
+
than refuse, and the usage past it is overage."""
|
|
356
|
+
kind: PlanAllowanceKind
|
|
357
|
+
valid_for: Validity | None
|
|
358
|
+
"""`one_time` only; `None` is a grant that never expires."""
|
|
359
|
+
effective_from: str
|
|
360
|
+
apply_mode: ApplyMode
|
|
361
|
+
created_at: str
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# Plan assignments -----------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class Assignment(Model):
|
|
368
|
+
id: str
|
|
369
|
+
workspace_id: str
|
|
370
|
+
customer_id: str
|
|
371
|
+
plan_id: str
|
|
372
|
+
effective_at: str
|
|
373
|
+
"""Ahead of now for a change that has not landed yet."""
|
|
374
|
+
effective: ApplyMode
|
|
375
|
+
"""As recorded, which is not always as asked: a first plan starts at
|
|
376
|
+
once."""
|
|
377
|
+
reconciliation: RecordedReconciliation
|
|
378
|
+
"""Widened for history: a row written under an older engine may name a
|
|
379
|
+
mode the API no longer takes."""
|
|
380
|
+
anchor_at: str
|
|
381
|
+
"""What periods are measured from. A plan change inherits it, so a
|
|
382
|
+
customer's renewal day never moves under them —
|
|
383
|
+
`reconciliation="reset"` is the one thing that moves it."""
|
|
384
|
+
superseded_at: str | None
|
|
385
|
+
"""Set when a newer instruction replaced this one before it took
|
|
386
|
+
effect."""
|
|
387
|
+
created_at: str
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# Entitlement ----------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
EntitlementState = Literal["granted", "awaiting"]
|
|
393
|
+
"""`awaiting`: the plan meters this, but its only versions land later."""
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class Period(Model):
|
|
397
|
+
"""`[start, end)`."""
|
|
398
|
+
|
|
399
|
+
start: str
|
|
400
|
+
end: str
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class IncomingVersion(Model):
|
|
404
|
+
"""The version that takes over this meter, and when."""
|
|
405
|
+
|
|
406
|
+
version: PlanAllowance
|
|
407
|
+
lands_at: str
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class _MeterFigures(Model):
|
|
411
|
+
meter_id: str
|
|
412
|
+
state: EntitlementState
|
|
413
|
+
version: PlanAllowance | None
|
|
414
|
+
amount: int | None
|
|
415
|
+
no_cap: bool
|
|
416
|
+
allow_overage: bool
|
|
417
|
+
"""The version in effect's; `False` while `awaiting`."""
|
|
418
|
+
used: int
|
|
419
|
+
"""What counts toward `amount`."""
|
|
420
|
+
total: int
|
|
421
|
+
"""`used`, plus what grants paid for."""
|
|
422
|
+
remaining: int | None
|
|
423
|
+
overage_used: int | None
|
|
424
|
+
"""`max(used - amount, 0)`, under a hard cap too. `None` where `remaining`
|
|
425
|
+
is."""
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class MeterEntitlement(_MeterFigures):
|
|
429
|
+
"""One meter the plan meters, in the current period."""
|
|
430
|
+
|
|
431
|
+
reserved: int
|
|
432
|
+
available: int | None
|
|
433
|
+
incoming: IncomingVersion | None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class CustomerEntitlement(Model):
|
|
437
|
+
"""The current period only, and only the meters the plan meters."""
|
|
438
|
+
|
|
439
|
+
customer_id: str
|
|
440
|
+
plan_id: str
|
|
441
|
+
cycle: PlanCycle
|
|
442
|
+
since: str
|
|
443
|
+
period: Period
|
|
444
|
+
meters: list[MeterEntitlement]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# Closed periods -------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class ClosedPeriodMeter(_MeterFigures):
|
|
451
|
+
"""The entitlement meter at the period's last instant, less what describes
|
|
452
|
+
now. `total - used` is what grants absorbed."""
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class ClosedPeriod(Model):
|
|
456
|
+
"""`[from, to)`, final: nothing written after it restates it."""
|
|
457
|
+
|
|
458
|
+
customer_id: str
|
|
459
|
+
plan_id: str
|
|
460
|
+
"""The plan in force at the period's last instant."""
|
|
461
|
+
cycle: PlanCycle
|
|
462
|
+
since: str
|
|
463
|
+
"""When that plan took effect, as on the entitlement read."""
|
|
464
|
+
from_: str
|
|
465
|
+
"""`from` on the wire. Microseconds included: pass it back as `before`
|
|
466
|
+
exactly as it came."""
|
|
467
|
+
to: str
|
|
468
|
+
"""Its boundary, or the instant a reset closed it early."""
|
|
469
|
+
meters: list[ClosedPeriodMeter]
|
|
470
|
+
"""One per meter that plan meters."""
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# Usage by day ---------------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
UsagePeriod = Literal["current", "last"]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class DayUsage(Model):
|
|
479
|
+
date: str
|
|
480
|
+
"""A UTC day, `YYYY-MM-DD`."""
|
|
481
|
+
quantity: int
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class MeterUsage(Model):
|
|
485
|
+
"""Every day of the range that has begun, zeros included."""
|
|
486
|
+
|
|
487
|
+
meter_id: str
|
|
488
|
+
total: int
|
|
489
|
+
days: list[DayUsage]
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class CustomerUsage(Model):
|
|
493
|
+
"""`[from, to)`. Only the meters with usage in the range."""
|
|
494
|
+
|
|
495
|
+
customer_id: str
|
|
496
|
+
from_: str
|
|
497
|
+
"""`from` on the wire."""
|
|
498
|
+
to: str
|
|
499
|
+
meters: list[MeterUsage]
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class MeterUsageHistory(MeterUsage):
|
|
503
|
+
"""Every customer's usage of one meter. `[from, to)`."""
|
|
504
|
+
|
|
505
|
+
from_: str
|
|
506
|
+
"""`from` on the wire."""
|
|
507
|
+
to: str
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# Customer allowances --------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
AllowanceSource = Literal["purchased", "bonus", "manual"]
|
|
513
|
+
"""`plan` is not accepted on a grant: the engine mints those itself when a
|
|
514
|
+
plan is assigned."""
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class Allowance(Model):
|
|
518
|
+
id: str
|
|
519
|
+
workspace_id: str
|
|
520
|
+
customer_id: str
|
|
521
|
+
meter_id: str
|
|
522
|
+
amount: int | None
|
|
523
|
+
no_cap: bool
|
|
524
|
+
consumed: int
|
|
525
|
+
remaining: int | None
|
|
526
|
+
"""`None` under `no_cap`."""
|
|
527
|
+
source: AllowanceSource | Literal["plan"]
|
|
528
|
+
metadata: dict[str, Any]
|
|
529
|
+
effective_at: str
|
|
530
|
+
expires_at: str | None
|
|
531
|
+
revoked_at: str | None
|
|
532
|
+
exhausted_at: str | None
|
|
533
|
+
created_at: str
|
|
534
|
+
plan_allowance_version_id: str | None
|
|
535
|
+
"""`None` unless `source` is `plan`. With `assignment_id` and
|
|
536
|
+
`cancelled_at`, it answers where the grant came from and why it went
|
|
537
|
+
away."""
|
|
538
|
+
assignment_id: str | None
|
|
539
|
+
cancelled_at: str | None
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class WhoAmI(Model):
|
|
543
|
+
workspace: str
|
|
544
|
+
caller: str
|
|
545
|
+
kind: Literal["api_key", "service"]
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# Webhooks -------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class ThresholdPlan(Model):
|
|
552
|
+
id: str
|
|
553
|
+
key: str
|
|
554
|
+
name: str
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class ThresholdCrossing(Model):
|
|
558
|
+
"""What a threshold crossing reports, as `threshold.crossed` and
|
|
559
|
+
`entitlement.reached` send it."""
|
|
560
|
+
|
|
561
|
+
customer_id: str
|
|
562
|
+
"""The tenant's own customer id."""
|
|
563
|
+
meter_id: str
|
|
564
|
+
"""The meter's key."""
|
|
565
|
+
plan: ThresholdPlan | None
|
|
566
|
+
"""The plan in force when it crossed; `None` if they held none."""
|
|
567
|
+
threshold: int
|
|
568
|
+
"""A whole percent, 1–1000; past 100 is an overage alert."""
|
|
569
|
+
used: int
|
|
570
|
+
"""The plan's figure at the crossing; it does not include grants."""
|
|
571
|
+
entitlement: int
|
|
572
|
+
"""The plan's figure at the crossing; it does not include grants."""
|
|
573
|
+
period: Period
|
|
574
|
+
usage_event_id: str | None
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class WebhookEvent(Model):
|
|
578
|
+
"""
|
|
579
|
+
A delivery. New types can be added; ignore any you do not handle.
|
|
580
|
+
`verify_webhook` answers the subclass for each type this SDK knows, so
|
|
581
|
+
`isinstance` tells them apart, and this class itself for any other.
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
id: str
|
|
585
|
+
"""The same on every attempt and every endpoint: dedupe on it."""
|
|
586
|
+
type: str
|
|
587
|
+
timestamp: str
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class ThresholdCrossedEvent(WebhookEvent):
|
|
591
|
+
"""`threshold.crossed`: sent for every mark a customer crosses."""
|
|
592
|
+
|
|
593
|
+
data: ThresholdCrossing
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class EntitlementReachedEvent(WebhookEvent):
|
|
597
|
+
"""`entitlement.reached`: sent for the 100% crossing too, under its own
|
|
598
|
+
id."""
|
|
599
|
+
|
|
600
|
+
data: ThresholdCrossing
|