meterbase 0.1.0__tar.gz → 0.1.2__tar.gz

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 (41) hide show
  1. {meterbase-0.1.0 → meterbase-0.1.2}/PKG-INFO +46 -20
  2. {meterbase-0.1.0 → meterbase-0.1.2}/README.md +45 -19
  3. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/resources/customer_plan.py +9 -7
  4. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/resources/customers.py +3 -1
  5. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/resources/plans.py +7 -3
  6. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/resources/customer_plan.py +9 -7
  7. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/resources/customers.py +3 -1
  8. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/resources/plans.py +7 -3
  9. meterbase-0.1.2/src/meterbase/_version.py +1 -0
  10. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/errors.py +5 -5
  11. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/types.py +21 -5
  12. meterbase-0.1.0/src/meterbase/_version.py +0 -1
  13. {meterbase-0.1.0 → meterbase-0.1.2}/.gitignore +0 -0
  14. {meterbase-0.1.0 → meterbase-0.1.2}/LICENSE +0 -0
  15. {meterbase-0.1.0 → meterbase-0.1.2}/pyproject.toml +0 -0
  16. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/__init__.py +0 -0
  17. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/__init__.py +0 -0
  18. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/_io.py +0 -0
  19. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/_transport.py +0 -0
  20. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/client.py +0 -0
  21. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/resources/__init__.py +0 -0
  22. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/resources/customer_allowances.py +0 -0
  23. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/resources/customer_periods.py +0 -0
  24. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_async/resources/meters.py +0 -0
  25. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_http.py +0 -0
  26. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_json.py +0 -0
  27. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_keys.py +0 -0
  28. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_models.py +0 -0
  29. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_not_given.py +0 -0
  30. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/__init__.py +0 -0
  31. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/_io.py +0 -0
  32. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/_transport.py +0 -0
  33. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/client.py +0 -0
  34. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/resources/__init__.py +0 -0
  35. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/resources/customer_allowances.py +0 -0
  36. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/resources/customer_periods.py +0 -0
  37. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_sync/resources/meters.py +0 -0
  38. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/_time.py +0 -0
  39. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/py.typed +0 -0
  40. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/resources.py +0 -0
  41. {meterbase-0.1.0 → meterbase-0.1.2}/src/meterbase/webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: meterbase
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python SDK for the Meterbase engine
5
5
  Project-URL: Homepage, https://meterbase.tech
6
6
  Project-URL: Documentation, https://meterbase.tech/docs
@@ -81,10 +81,11 @@ denial — that is an answer, not a failure. Absence of entitlement is not
81
81
  permission: a customer with no plan and no grant is denied.
82
82
 
83
83
  `available` is the whole capacity — the plan's unused entitlement for the
84
- current period plus every open grant — and is `None` under `no_cap`, where
85
- capacity is not a number. It is one figure and not a breakdown: `check` answers
86
- from a single cached integer, and itemising where the capacity came from would
87
- cost it that. For the per-grant detail, read a customer's allowances.
84
+ current period, with what is left of any [rollover](#plan-allowances) carry,
85
+ plus every open grant — and is `None` under `no_cap`, where capacity is not a
86
+ number. It is one figure and not a breakdown: `check` answers from a single
87
+ cached integer, and itemising where the capacity came from would cost it that.
88
+ For the per-grant detail, read a customer's allowances.
88
89
 
89
90
  A check answers **two gates**, and `allowed` needs both: the capacity has to
90
91
  cover the quantity, and every rate limit in force — _at most so much of this
@@ -392,6 +393,22 @@ is refused beside `no_cap` and on `one_time`. Like the amount, it is a term of
392
393
  the version, so turning it on or off follows `apply_mode`: at each customer's
393
394
  next cycle by default.
394
395
 
396
+ **Rollover** carries what a period leaves of `amount` into the next one, which
397
+ spends it first. Whatever that period leaves of the carry is lost, so a carry
398
+ never stacks.
399
+
400
+ ```python
401
+ meterbase.plans.allowances.set(plan.id, meter_id=meter.id, amount=1_000, rollover=True)
402
+ ```
403
+
404
+ Both periods' versions must roll over, so moving to a plan that does not ends
405
+ the carry. A `restart` passes nothing on, and usage the carry paid reaches no
406
+ threshold. Read it as `carried_in` and `carried_used` on
407
+ [the entitlement](#how-much-they-have-used). `rollover` defaults to `False`,
408
+ goes beside `allow_overage` and on an amount of `0`, and is refused beside
409
+ `no_cap` and on `one_time`. It follows `apply_mode` too, and the first carry
410
+ arrives a boundary after the version lands.
411
+
395
412
  ### A customer's plan
396
413
 
397
414
  A customer's plan is an append-only history, not a field: assigning is the only
@@ -448,10 +465,11 @@ is the wire shape underneath all four, and still takes `plan_id`.
448
465
 
449
466
  #### Two plans on different cycles
450
467
 
451
- A weekly plan and a monthly one share no boundary to wait for and no period to
452
- weight, so `change_at_next_cycle` and `change_now` + `"prorate"` both raise
453
- `CycleChangeRequiresResetError`. `restart` is the move that works, and plain
454
- `change_now` is the other way out — it keeps the period that is running.
468
+ Plans on different cycles share no period to keep, so `change_now` between
469
+ them raises `CycleChangeRequiresResetError`, with `"none"` or `"prorate"`
470
+ alike. A weekly plan and a month-based one share no boundary to wait for
471
+ either, so `change_at_next_cycle` between those two raises it too. `restart` is
472
+ the move that works: it starts a fresh period on the new cycle.
455
473
 
456
474
  ```python
457
475
  from meterbase import CycleChangeRequiresResetError
@@ -473,8 +491,9 @@ trail = meterbase.customers.plan.history(customer.id).data
473
491
  `retrieve` is the plan **in force now**, which is not always the newest
474
492
  instruction — a change dated ahead does not govern yet — and is `None` for a
475
493
  customer holding no plan. That is a default deny rather than a failure, so it
476
- is an answer here rather than a raised `NotFoundError`; a customer who does not
477
- exist at all still raises one.
494
+ is an answer here rather than a raised `NotFoundError`. An id that names no
495
+ customer reads as `None` too: the engine answers it as a customer holding no
496
+ plan.
478
497
 
479
498
  `pending` is the change waiting to land, and at most one ever is: a newer
480
499
  instruction supersedes the one before it. `history` is the whole trail,
@@ -504,12 +523,16 @@ for meter in entitlement.meters if entitlement else []:
504
523
 
505
524
  `entitlement` is the plan's side of the current period, one entry per meter the
506
525
  plan meters: the read behind a usage bar in your own app. `used` is what
507
- `amount` is measured against, `total` adds what grants paid for, and
508
- `available` is what `check` would answer. `overage_used` is how far `used` has
509
- run past `amount` — what a [soft cap](#plan-allowances) bills for, and reported
510
- under a hard cap too — and `None` where there is no amount; `allow_overage`
511
- says whether the cap in effect is soft. Meters are named by id, and like
512
- `retrieve` it is `None` for a customer holding no plan.
526
+ `amount` is measured against, `total` adds what the carry and grants paid for,
527
+ and `available` is what `check` would answer. `overage_used` is how far `used`
528
+ has run past `amount` — what a [soft cap](#plan-allowances) bills for, and
529
+ reported under a hard cap too — and `None` where there is no amount;
530
+ `allow_overage` says whether the cap in effect is soft. `rollover` says whether
531
+ it [rolls over](#plan-allowances): `carried_in` is what the last period left of
532
+ its amount, `carried_used` what this one has spent of it, and
533
+ `carried_in - carried_used` what goes at the period's end. Both are `0` where
534
+ nothing rolled over and `None` where there is no amount. Meters are named by
535
+ id, and like `retrieve` it is `None` for a customer holding no plan.
513
536
 
514
537
  Unlike `check`, it is not answered from a cache, so read it where usage is
515
538
  shown rather than on every request.
@@ -518,7 +541,9 @@ shown rather than on every request.
518
541
 
519
542
  `customers.periods.last` is the newest period that has ended, with the figures
520
543
  to bill from: per meter, the cap `check` answered at its last instant, `used`
521
- against it, and `overage_used`, which is what a soft cap bills; `total - used`
544
+ against it, and `overage_used`, which is what a soft cap bills. `carried_in`,
545
+ `carried_used` and `carried_out` are what a rolling allowance took from the
546
+ period before, spent, and passed to the next, and `total - used - carried_used`
522
547
  is what grants absorbed. They come from the counters behind `check`, which
523
548
  count each event exactly once. [Usage by day](#usage-by-day) is for where
524
549
  usage went, not for billing.
@@ -854,8 +879,9 @@ exactly that much away, that a replay takes nothing, and that a reused key
854
879
  replays whatever the body says; that a hold's commit records what the work cost
855
880
  and gives the rest back; that a rate limit comes back on every `check` and
856
881
  refuses with the allowance untouched; that a soft cap admits and holds past its
857
- amount and reports the overage; that a period a `restart` closes settles for a
858
- minute, which the run waits out, and then reads final; that plan changes
882
+ amount and reports the overage; that a rolling allowance carries nothing into a
883
+ first period or across a `restart`; that a period a `restart` closes settles
884
+ for a minute, which the run waits out, and then reads final; that plan changes
859
885
  schedule, cancel and re-anchor as documented; and — reading each model's
860
886
  annotations — that the engine's fields still match the SDK's types exactly, in
861
887
  both directions.
@@ -54,10 +54,11 @@ denial — that is an answer, not a failure. Absence of entitlement is not
54
54
  permission: a customer with no plan and no grant is denied.
55
55
 
56
56
  `available` is the whole capacity — the plan's unused entitlement for the
57
- current period plus every open grant — and is `None` under `no_cap`, where
58
- capacity is not a number. It is one figure and not a breakdown: `check` answers
59
- from a single cached integer, and itemising where the capacity came from would
60
- cost it that. For the per-grant detail, read a customer's allowances.
57
+ current period, with what is left of any [rollover](#plan-allowances) carry,
58
+ plus every open grant — and is `None` under `no_cap`, where capacity is not a
59
+ number. It is one figure and not a breakdown: `check` answers from a single
60
+ cached integer, and itemising where the capacity came from would cost it that.
61
+ For the per-grant detail, read a customer's allowances.
61
62
 
62
63
  A check answers **two gates**, and `allowed` needs both: the capacity has to
63
64
  cover the quantity, and every rate limit in force — _at most so much of this
@@ -365,6 +366,22 @@ is refused beside `no_cap` and on `one_time`. Like the amount, it is a term of
365
366
  the version, so turning it on or off follows `apply_mode`: at each customer's
366
367
  next cycle by default.
367
368
 
369
+ **Rollover** carries what a period leaves of `amount` into the next one, which
370
+ spends it first. Whatever that period leaves of the carry is lost, so a carry
371
+ never stacks.
372
+
373
+ ```python
374
+ meterbase.plans.allowances.set(plan.id, meter_id=meter.id, amount=1_000, rollover=True)
375
+ ```
376
+
377
+ Both periods' versions must roll over, so moving to a plan that does not ends
378
+ the carry. A `restart` passes nothing on, and usage the carry paid reaches no
379
+ threshold. Read it as `carried_in` and `carried_used` on
380
+ [the entitlement](#how-much-they-have-used). `rollover` defaults to `False`,
381
+ goes beside `allow_overage` and on an amount of `0`, and is refused beside
382
+ `no_cap` and on `one_time`. It follows `apply_mode` too, and the first carry
383
+ arrives a boundary after the version lands.
384
+
368
385
  ### A customer's plan
369
386
 
370
387
  A customer's plan is an append-only history, not a field: assigning is the only
@@ -421,10 +438,11 @@ is the wire shape underneath all four, and still takes `plan_id`.
421
438
 
422
439
  #### Two plans on different cycles
423
440
 
424
- A weekly plan and a monthly one share no boundary to wait for and no period to
425
- weight, so `change_at_next_cycle` and `change_now` + `"prorate"` both raise
426
- `CycleChangeRequiresResetError`. `restart` is the move that works, and plain
427
- `change_now` is the other way out — it keeps the period that is running.
441
+ Plans on different cycles share no period to keep, so `change_now` between
442
+ them raises `CycleChangeRequiresResetError`, with `"none"` or `"prorate"`
443
+ alike. A weekly plan and a month-based one share no boundary to wait for
444
+ either, so `change_at_next_cycle` between those two raises it too. `restart` is
445
+ the move that works: it starts a fresh period on the new cycle.
428
446
 
429
447
  ```python
430
448
  from meterbase import CycleChangeRequiresResetError
@@ -446,8 +464,9 @@ trail = meterbase.customers.plan.history(customer.id).data
446
464
  `retrieve` is the plan **in force now**, which is not always the newest
447
465
  instruction — a change dated ahead does not govern yet — and is `None` for a
448
466
  customer holding no plan. That is a default deny rather than a failure, so it
449
- is an answer here rather than a raised `NotFoundError`; a customer who does not
450
- exist at all still raises one.
467
+ is an answer here rather than a raised `NotFoundError`. An id that names no
468
+ customer reads as `None` too: the engine answers it as a customer holding no
469
+ plan.
451
470
 
452
471
  `pending` is the change waiting to land, and at most one ever is: a newer
453
472
  instruction supersedes the one before it. `history` is the whole trail,
@@ -477,12 +496,16 @@ for meter in entitlement.meters if entitlement else []:
477
496
 
478
497
  `entitlement` is the plan's side of the current period, one entry per meter the
479
498
  plan meters: the read behind a usage bar in your own app. `used` is what
480
- `amount` is measured against, `total` adds what grants paid for, and
481
- `available` is what `check` would answer. `overage_used` is how far `used` has
482
- run past `amount` — what a [soft cap](#plan-allowances) bills for, and reported
483
- under a hard cap too — and `None` where there is no amount; `allow_overage`
484
- says whether the cap in effect is soft. Meters are named by id, and like
485
- `retrieve` it is `None` for a customer holding no plan.
499
+ `amount` is measured against, `total` adds what the carry and grants paid for,
500
+ and `available` is what `check` would answer. `overage_used` is how far `used`
501
+ has run past `amount` — what a [soft cap](#plan-allowances) bills for, and
502
+ reported under a hard cap too — and `None` where there is no amount;
503
+ `allow_overage` says whether the cap in effect is soft. `rollover` says whether
504
+ it [rolls over](#plan-allowances): `carried_in` is what the last period left of
505
+ its amount, `carried_used` what this one has spent of it, and
506
+ `carried_in - carried_used` what goes at the period's end. Both are `0` where
507
+ nothing rolled over and `None` where there is no amount. Meters are named by
508
+ id, and like `retrieve` it is `None` for a customer holding no plan.
486
509
 
487
510
  Unlike `check`, it is not answered from a cache, so read it where usage is
488
511
  shown rather than on every request.
@@ -491,7 +514,9 @@ shown rather than on every request.
491
514
 
492
515
  `customers.periods.last` is the newest period that has ended, with the figures
493
516
  to bill from: per meter, the cap `check` answered at its last instant, `used`
494
- against it, and `overage_used`, which is what a soft cap bills; `total - used`
517
+ against it, and `overage_used`, which is what a soft cap bills. `carried_in`,
518
+ `carried_used` and `carried_out` are what a rolling allowance took from the
519
+ period before, spent, and passed to the next, and `total - used - carried_used`
495
520
  is what grants absorbed. They come from the counters behind `check`, which
496
521
  count each event exactly once. [Usage by day](#usage-by-day) is for where
497
522
  usage went, not for billing.
@@ -827,8 +852,9 @@ exactly that much away, that a replay takes nothing, and that a reused key
827
852
  replays whatever the body says; that a hold's commit records what the work cost
828
853
  and gives the rest back; that a rate limit comes back on every `check` and
829
854
  refuses with the allowance untouched; that a soft cap admits and holds past its
830
- amount and reports the overage; that a period a `restart` closes settles for a
831
- minute, which the run waits out, and then reads final; that plan changes
855
+ amount and reports the overage; that a rolling allowance carries nothing into a
856
+ first period or across a `restart`; that a period a `restart` closes settles
857
+ for a minute, which the run waits out, and then reads final; that plan changes
832
858
  schedule, cancel and re-anchor as documented; and — reading each model's
833
859
  annotations — that the engine's fields still match the SDK's types exactly, in
834
860
  both directions.
@@ -96,8 +96,9 @@ class AsyncCustomerPlan:
96
96
  plan they are paying for until it runs out, and the new one starts at
97
97
  their next boundary. Nothing is pro-rated, because nothing is split.
98
98
 
99
- Two plans on different cycles share no boundary, and this answers
100
- `CycleChangeRequiresResetError` — use `restart` for that move.
99
+ A weekly plan and a month-based one share no boundary, and between
100
+ them this raises `CycleChangeRequiresResetError`: use `restart` for
101
+ that move. Other cycles land on the coarser plan's boundary.
101
102
  """
102
103
  return await self.change(customer_id, plan=plan, effective="next_cycle")
103
104
 
@@ -117,10 +118,11 @@ class AsyncCustomerPlan:
117
118
  period. Usage already spent still counts, so a downgrade can deny
118
119
  until the period ends.
119
120
  - `prorate` — each plan's amount weighted by the fraction of the period
120
- it was held for. Refused between plans on different cycles, which
121
- share no period to weight.
121
+ it was held for.
122
122
 
123
- To have the period itself start over instead, use `restart`.
123
+ Between plans on different cycles both are refused with
124
+ `CycleChangeRequiresResetError`: there is no period to keep, so use
125
+ `restart`. To have the period itself start over, use `restart` too.
124
126
  """
125
127
  return await self.change(
126
128
  customer_id,
@@ -170,8 +172,8 @@ class AsyncCustomerPlan:
170
172
 
171
173
  `None` when the customer holds no plan. That is a default deny rather
172
174
  than a failure — they are entitled to nothing — so it is an answer here
173
- and not a raised `NotFoundError`. A customer who does not exist at all
174
- still raises one.
175
+ and not a raised `NotFoundError`. An id that names no customer answers
176
+ `None` too: the engine reads it as a customer holding no plan.
175
177
  """
176
178
  try:
177
179
  return await self._api.request(
@@ -98,7 +98,9 @@ class AsyncCustomers:
98
98
  return found.data[0] if found.data else None
99
99
 
100
100
  async def entitlement(self, id: str) -> CustomerEntitlement | None:
101
- """`None` for a customer holding no plan, as `plan.retrieve` answers."""
101
+ """`None` for a customer holding no plan, as `plan.retrieve` answers,
102
+ and for an id that names no customer, which the engine reads the same
103
+ way."""
102
104
  try:
103
105
  return await self._api.request(
104
106
  "GET",
@@ -32,6 +32,7 @@ class AsyncPlanAllowances:
32
32
  amount: int | NotGiven = NOT_GIVEN,
33
33
  no_cap: bool | NotGiven = NOT_GIVEN,
34
34
  allow_overage: bool | NotGiven = NOT_GIVEN,
35
+ rollover: bool | NotGiven = NOT_GIVEN,
35
36
  kind: PlanAllowanceKind | NotGiven = NOT_GIVEN,
36
37
  valid_for: ValidityParam | Validity | NotGiven = NOT_GIVEN,
37
38
  apply_mode: ApplyMode | NotGiven = NOT_GIVEN,
@@ -41,9 +42,11 @@ class AsyncPlanAllowances:
41
42
  cycle. Give exactly one of `amount` or `no_cap`.
42
43
 
43
44
  `allow_overage` is a soft cap: past `amount`, admit rather than refuse.
44
- Defaults to `False`; refused beside `no_cap` and on `one_time`. `kind`
45
- defaults to `recurring`. `valid_for` is `one_time` only. `apply_mode`
46
- defaults to `next_cycle`, except `one_time` which is always immediate.
45
+ `rollover` carries what a period leaves of `amount` into the next one,
46
+ which spends it first. Both default to `False` and are refused beside
47
+ `no_cap` and on `one_time`. `kind` defaults to `recurring`. `valid_for`
48
+ is `one_time` only. `apply_mode` defaults to `next_cycle`, except
49
+ `one_time` which is always immediate.
47
50
  """
48
51
  return await self._api.request(
49
52
  "POST",
@@ -54,6 +57,7 @@ class AsyncPlanAllowances:
54
57
  "amount": amount,
55
58
  "no_cap": no_cap,
56
59
  "allow_overage": allow_overage,
60
+ "rollover": rollover,
57
61
  "kind": kind,
58
62
  "valid_for": valid_for,
59
63
  "apply_mode": apply_mode,
@@ -100,8 +100,9 @@ class CustomerPlan:
100
100
  plan they are paying for until it runs out, and the new one starts at
101
101
  their next boundary. Nothing is pro-rated, because nothing is split.
102
102
 
103
- Two plans on different cycles share no boundary, and this answers
104
- `CycleChangeRequiresResetError` — use `restart` for that move.
103
+ A weekly plan and a month-based one share no boundary, and between
104
+ them this raises `CycleChangeRequiresResetError`: use `restart` for
105
+ that move. Other cycles land on the coarser plan's boundary.
105
106
  """
106
107
  return self.change(customer_id, plan=plan, effective="next_cycle")
107
108
 
@@ -121,10 +122,11 @@ class CustomerPlan:
121
122
  period. Usage already spent still counts, so a downgrade can deny
122
123
  until the period ends.
123
124
  - `prorate` — each plan's amount weighted by the fraction of the period
124
- it was held for. Refused between plans on different cycles, which
125
- share no period to weight.
125
+ it was held for.
126
126
 
127
- To have the period itself start over instead, use `restart`.
127
+ Between plans on different cycles both are refused with
128
+ `CycleChangeRequiresResetError`: there is no period to keep, so use
129
+ `restart`. To have the period itself start over, use `restart` too.
128
130
  """
129
131
  return self.change(
130
132
  customer_id,
@@ -174,8 +176,8 @@ class CustomerPlan:
174
176
 
175
177
  `None` when the customer holds no plan. That is a default deny rather
176
178
  than a failure — they are entitled to nothing — so it is an answer here
177
- and not a raised `NotFoundError`. A customer who does not exist at all
178
- still raises one.
179
+ and not a raised `NotFoundError`. An id that names no customer answers
180
+ `None` too: the engine reads it as a customer holding no plan.
179
181
  """
180
182
  try:
181
183
  return self._api.request(
@@ -100,7 +100,9 @@ class Customers:
100
100
  return found.data[0] if found.data else None
101
101
 
102
102
  def entitlement(self, id: str) -> CustomerEntitlement | None:
103
- """`None` for a customer holding no plan, as `plan.retrieve` answers."""
103
+ """`None` for a customer holding no plan, as `plan.retrieve` answers,
104
+ and for an id that names no customer, which the engine reads the same
105
+ way."""
104
106
  try:
105
107
  return self._api.request(
106
108
  "GET",
@@ -36,6 +36,7 @@ class PlanAllowances:
36
36
  amount: int | NotGiven = NOT_GIVEN,
37
37
  no_cap: bool | NotGiven = NOT_GIVEN,
38
38
  allow_overage: bool | NotGiven = NOT_GIVEN,
39
+ rollover: bool | NotGiven = NOT_GIVEN,
39
40
  kind: PlanAllowanceKind | NotGiven = NOT_GIVEN,
40
41
  valid_for: ValidityParam | Validity | NotGiven = NOT_GIVEN,
41
42
  apply_mode: ApplyMode | NotGiven = NOT_GIVEN,
@@ -45,9 +46,11 @@ class PlanAllowances:
45
46
  cycle. Give exactly one of `amount` or `no_cap`.
46
47
 
47
48
  `allow_overage` is a soft cap: past `amount`, admit rather than refuse.
48
- Defaults to `False`; refused beside `no_cap` and on `one_time`. `kind`
49
- defaults to `recurring`. `valid_for` is `one_time` only. `apply_mode`
50
- defaults to `next_cycle`, except `one_time` which is always immediate.
49
+ `rollover` carries what a period leaves of `amount` into the next one,
50
+ which spends it first. Both default to `False` and are refused beside
51
+ `no_cap` and on `one_time`. `kind` defaults to `recurring`. `valid_for`
52
+ is `one_time` only. `apply_mode` defaults to `next_cycle`, except
53
+ `one_time` which is always immediate.
51
54
  """
52
55
  return self._api.request(
53
56
  "POST",
@@ -58,6 +61,7 @@ class PlanAllowances:
58
61
  "amount": amount,
59
62
  "no_cap": no_cap,
60
63
  "allow_overage": allow_overage,
64
+ "rollover": rollover,
61
65
  "kind": kind,
62
66
  "valid_for": valid_for,
63
67
  "apply_mode": apply_mode,
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -162,11 +162,11 @@ class ReservationMismatchError(InvalidRequestError):
162
162
  class CycleChangeRequiresResetError(InvalidRequestError):
163
163
  """
164
164
  `422 cycle_change_requires_reset`: the two plans measure different cycles,
165
- so a `next_cycle` change has no shared boundary to wait for and a
166
- `prorate` no shared period to weight. The move is `customers.plan.restart`,
167
- which closes the current period and opens a fresh one on the new cycle —
168
- or `change_now` with the default `reconciliation="none"` to keep the
169
- period that is running. The engine's `message` says as much.
165
+ so the change has no period to keep. An immediate change between them is
166
+ refused unless it resets, and a `next_cycle` change is refused between a
167
+ weekly plan and a month-based one, which share no boundary to wait for.
168
+ The move is `customers.plan.restart`, which closes the current period and
169
+ opens a fresh one on the new cycle. The engine's `message` says as much.
170
170
 
171
171
  An `InvalidRequestError` still, so an existing `except` keeps working.
172
172
  """
@@ -190,9 +190,10 @@ class CheckResult(Model):
190
190
  available: int | None
191
191
  """
192
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.
193
+ floored at 0, plus what is left of a rollover carry, plus every open
194
+ grant. `None` under `no_cap`, where capacity is not a number. Under a soft
195
+ cap it is what is left of the included amount, and running out of it
196
+ refuses nothing.
196
197
 
197
198
  One figure and no breakdown: `check` answers from a single cached integer,
198
199
  and itemising where the capacity came from would cost it that. For the
@@ -353,6 +354,9 @@ class PlanAllowance(Model):
353
354
  allow_overage: bool
354
355
  """A soft cap: once `amount` is used up, `check` and `reserve` admit rather
355
356
  than refuse, and the usage past it is overage."""
357
+ rollover: bool
358
+ """What a period leaves unused of `amount` carries into the next, which
359
+ spends it first. The carry lasts that one period."""
356
360
  kind: PlanAllowanceKind
357
361
  valid_for: Validity | None
358
362
  """`one_time` only; `None` is a grant that never expires."""
@@ -415,14 +419,22 @@ class _MeterFigures(Model):
415
419
  no_cap: bool
416
420
  allow_overage: bool
417
421
  """The version in effect's; `False` while `awaiting`."""
422
+ rollover: bool
423
+ """The version in effect's; `False` while `awaiting`."""
418
424
  used: int
419
425
  """What counts toward `amount`."""
420
426
  total: int
421
- """`used`, plus what grants paid for."""
427
+ """`used`, plus what the carry and grants paid for."""
422
428
  remaining: int | None
423
429
  overage_used: int | None
424
430
  """`max(used - amount, 0)`, under a hard cap too. `None` where `remaining`
425
431
  is."""
432
+ carried_in: int | None
433
+ """What the period before left of its amount, when both versions roll
434
+ over; `0` otherwise. `None` where `remaining` is."""
435
+ carried_used: int | None
436
+ """What this period has spent of `carried_in`, which goes first and is not
437
+ in `used`. `None` where `remaining` is."""
426
438
 
427
439
 
428
440
  class MeterEntitlement(_MeterFigures):
@@ -449,7 +461,11 @@ class CustomerEntitlement(Model):
449
461
 
450
462
  class ClosedPeriodMeter(_MeterFigures):
451
463
  """The entitlement meter at the period's last instant, less what describes
452
- now. `total - used` is what grants absorbed."""
464
+ now. `total - used - carried_used` is what grants absorbed."""
465
+
466
+ carried_out: int | None
467
+ """What it passed on: `max(amount - used, 0)` under a version that rolled
468
+ over, `0` otherwise and after a reset. `None` where `remaining` is."""
453
469
 
454
470
 
455
471
  class ClosedPeriod(Model):
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
File without changes
File without changes
File without changes