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.
Files changed (39) hide show
  1. meterbase/__init__.py +59 -0
  2. meterbase/_async/__init__.py +0 -0
  3. meterbase/_async/_io.py +22 -0
  4. meterbase/_async/_transport.py +181 -0
  5. meterbase/_async/client.py +427 -0
  6. meterbase/_async/resources/__init__.py +16 -0
  7. meterbase/_async/resources/customer_allowances.py +89 -0
  8. meterbase/_async/resources/customer_periods.py +41 -0
  9. meterbase/_async/resources/customer_plan.py +228 -0
  10. meterbase/_async/resources/customers.py +176 -0
  11. meterbase/_async/resources/meters.py +102 -0
  12. meterbase/_async/resources/plans.py +140 -0
  13. meterbase/_http.py +205 -0
  14. meterbase/_json.py +18 -0
  15. meterbase/_keys.py +39 -0
  16. meterbase/_models.py +177 -0
  17. meterbase/_not_given.py +32 -0
  18. meterbase/_sync/__init__.py +4 -0
  19. meterbase/_sync/_io.py +21 -0
  20. meterbase/_sync/_transport.py +185 -0
  21. meterbase/_sync/client.py +431 -0
  22. meterbase/_sync/resources/__init__.py +20 -0
  23. meterbase/_sync/resources/customer_allowances.py +93 -0
  24. meterbase/_sync/resources/customer_periods.py +45 -0
  25. meterbase/_sync/resources/customer_plan.py +230 -0
  26. meterbase/_sync/resources/customers.py +176 -0
  27. meterbase/_sync/resources/meters.py +106 -0
  28. meterbase/_sync/resources/plans.py +144 -0
  29. meterbase/_time.py +49 -0
  30. meterbase/_version.py +1 -0
  31. meterbase/errors.py +309 -0
  32. meterbase/py.typed +0 -0
  33. meterbase/resources.py +41 -0
  34. meterbase/types.py +600 -0
  35. meterbase/webhooks.py +154 -0
  36. meterbase-0.1.0.dist-info/METADATA +867 -0
  37. meterbase-0.1.0.dist-info/RECORD +39 -0
  38. meterbase-0.1.0.dist-info/WHEEL +4 -0
  39. 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