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
@@ -0,0 +1,93 @@
1
+ # Generated by scripts/unasync.py from
2
+ # src/meterbase/_async/resources/customer_allowances.py.
3
+ # Do not edit: change the async source, then run `make unasync`.
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Mapping
8
+ from typing import Any
9
+
10
+ from ..._http import segment
11
+ from ..._models import ListResult, list_of
12
+ from ..._not_given import NOT_GIVEN, NotGiven
13
+ from ...types import Allowance, AllowanceSource
14
+ from .._transport import Requester
15
+
16
+
17
+ class CustomerAllowances:
18
+ """
19
+ Capacity handed to one customer on top of their plan. A grant with
20
+ `source="plan"` is the engine's own, minted when a plan is assigned, and
21
+ cannot be created here.
22
+ """
23
+
24
+ def __init__(self, api: Requester) -> None:
25
+ self._api = api
26
+
27
+ def grant(
28
+ self,
29
+ customer_id: str,
30
+ *,
31
+ meter_id: str,
32
+ source: AllowanceSource,
33
+ amount: int | NotGiven = NOT_GIVEN,
34
+ no_cap: bool | NotGiven = NOT_GIVEN,
35
+ metadata: Mapping[str, Any] | NotGiven = NOT_GIVEN,
36
+ effective_at: str | NotGiven = NOT_GIVEN,
37
+ expires_at: str | NotGiven = NOT_GIVEN,
38
+ ) -> Allowance:
39
+ """
40
+ Give exactly one of `amount` or `no_cap`. Unlike a plan allowance, a
41
+ grant of 0 is refused: a grant that gives nothing is a no-op, not a
42
+ state, so `amount` is at least 1.
43
+
44
+ `source` is required: the engine will not guess why capacity was handed
45
+ out. `effective_at` defaults to now; dated ahead, the grant is inert
46
+ until then.
47
+ """
48
+ return self._api.request(
49
+ "POST",
50
+ f"/v1/customers/{segment(customer_id)}/allowances",
51
+ Allowance,
52
+ body={
53
+ "meter_id": meter_id,
54
+ "amount": amount,
55
+ "no_cap": no_cap,
56
+ "source": source,
57
+ "metadata": metadata,
58
+ "effective_at": effective_at,
59
+ "expires_at": expires_at,
60
+ },
61
+ )
62
+
63
+ def list(
64
+ self,
65
+ customer_id: str,
66
+ *,
67
+ meter_id: str | NotGiven = NOT_GIVEN,
68
+ include_closed: bool | NotGiven = NOT_GIVEN,
69
+ ) -> ListResult[Allowance]:
70
+ """Open grants only, unless `include_closed`: revoked, expired and
71
+ exhausted grants as well."""
72
+ return self._api.request(
73
+ "GET",
74
+ f"/v1/customers/{segment(customer_id)}/allowances",
75
+ list_of(Allowance),
76
+ query={"meter_id": meter_id, "include_closed": include_closed},
77
+ )
78
+
79
+ def retrieve(self, customer_id: str, allowance_id: str) -> Allowance:
80
+ return self._api.request(
81
+ "GET",
82
+ f"/v1/customers/{segment(customer_id)}/allowances/{segment(allowance_id)}",
83
+ Allowance,
84
+ )
85
+
86
+ def revoke(self, customer_id: str, allowance_id: str) -> Allowance:
87
+ """Withdraws what is left without erasing what was consumed.
88
+ Idempotent."""
89
+ return self._api.request(
90
+ "DELETE",
91
+ f"/v1/customers/{segment(customer_id)}/allowances/{segment(allowance_id)}",
92
+ Allowance,
93
+ )
@@ -0,0 +1,45 @@
1
+ # Generated by scripts/unasync.py from
2
+ # src/meterbase/_async/resources/customer_periods.py.
3
+ # Do not edit: change the async source, then run `make unasync`.
4
+
5
+ from __future__ import annotations
6
+
7
+ from ..._http import segment
8
+ from ..._not_given import NOT_GIVEN, NotGiven
9
+ from ...errors import NotFoundError
10
+ from ...types import ClosedPeriod
11
+ from .._transport import Requester
12
+
13
+
14
+ class CustomerPeriods:
15
+ """A customer's ended periods, exact from the engine's counters: the
16
+ figures to bill from."""
17
+
18
+ def __init__(self, api: Requester) -> None:
19
+ self._api = api
20
+
21
+ def last(
22
+ self, id: str, *, before: str | NotGiven = NOT_GIVEN
23
+ ) -> ClosedPeriod | None:
24
+ """
25
+ The newest period that has ended, or that ended at or before `before`;
26
+ `None` when there is none. Walk back by passing each period's `from_`
27
+ as `before`, exactly as it came. Raises `PeriodSettlingError` for a
28
+ minute after a period ends.
29
+
30
+ `before` is an RFC 3339 instant; omitted, now. It is a string rather
31
+ than a `datetime` so that a period's `from_` goes back verbatim: the
32
+ engine's carry microseconds, and a `before` short of one names the
33
+ period before it.
34
+ """
35
+ try:
36
+ return self._api.request(
37
+ "GET",
38
+ f"/v1/customers/{segment(id)}/periods/last",
39
+ ClosedPeriod,
40
+ query={"before": before},
41
+ )
42
+ except NotFoundError as error:
43
+ if error.code in ("no_plan_assigned", "no_previous_period"):
44
+ return None
45
+ raise
@@ -0,0 +1,230 @@
1
+ # Generated by scripts/unasync.py from
2
+ # src/meterbase/_async/resources/customer_plan.py.
3
+ # Do not edit: change the async source, then run `make unasync`.
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import datetime, timezone
8
+
9
+ from ..._http import segment
10
+ from ..._models import ListResult, list_of
11
+ from ..._not_given import NOT_GIVEN, NotGiven
12
+ from ..._time import parse_instant
13
+ from ...errors import NotFoundError
14
+ from ...types import ApplyMode, Assignment, Reconciliation
15
+ from .._io import both
16
+ from .._transport import Requester
17
+
18
+
19
+ class CustomerPlan:
20
+ """
21
+ Which plan a customer holds. The history is append-only: there is no patch
22
+ and no delete, and moving a customer to a different plan is the same call
23
+ as their first.
24
+
25
+ `assign` is that call, mirroring the wire. The rest name the four moves a
26
+ caller actually makes — later, now, now-and-start-over, and never mind — so
27
+ that picking one does not mean knowing what `effective` and
28
+ `reconciliation` do to a half-used period. They name the plan `plan`
29
+ rather than `plan_id`: they sit a layer above the wire, where `assign`
30
+ still mirrors it field for field.
31
+ """
32
+
33
+ def __init__(self, api: Requester) -> None:
34
+ self._api = api
35
+
36
+ def assign(
37
+ self,
38
+ customer_id: str,
39
+ *,
40
+ plan_id: str,
41
+ effective: ApplyMode | NotGiven = NOT_GIVEN,
42
+ reconciliation: Reconciliation | NotGiven = NOT_GIVEN,
43
+ cycle_anchor: str | NotGiven = NOT_GIVEN,
44
+ ) -> Assignment:
45
+ """
46
+ Put a customer on a plan. This is the wire call, and the only one that
47
+ takes `cycle_anchor` — a first assignment's one chance to put their
48
+ periods on a date they already have.
49
+
50
+ Returns the instruction as recorded, which is not always as asked: a
51
+ first plan is recorded `immediate` whatever `effective` said.
52
+
53
+ `effective` defaults to `next_cycle` on a change, and is forced to
54
+ `immediate` on a customer's first plan: there is no period to wait
55
+ out. `reconciliation` is `immediate` only; the engine refuses it beside
56
+ `next_cycle`. `cycle_anchor` defaults to the instant the plan lands,
57
+ and is refused beside `reconciliation="reset"`, which anchors at the
58
+ change instant itself.
59
+ """
60
+ return self._api.request(
61
+ "POST",
62
+ f"/v1/customers/{segment(customer_id)}/plan",
63
+ Assignment,
64
+ body={
65
+ "plan_id": plan_id,
66
+ "effective": effective,
67
+ "reconciliation": reconciliation,
68
+ "cycle_anchor": cycle_anchor,
69
+ },
70
+ )
71
+
72
+ def change(
73
+ self,
74
+ customer_id: str,
75
+ *,
76
+ plan: str,
77
+ effective: ApplyMode | NotGiven = NOT_GIVEN,
78
+ reconciliation: Reconciliation | NotGiven = NOT_GIVEN,
79
+ ) -> Assignment:
80
+ """
81
+ Move a customer to a different plan, named by its Meterbase id, with
82
+ both knobs in the open. `effective` defaults to `next_cycle`;
83
+ `reconciliation` applies only to an `immediate` change and defaults to
84
+ `none`.
85
+
86
+ Reach for `change_at_next_cycle`, `change_now` or `restart` when one of
87
+ them says what you mean — this is the form to fall back to when the two
88
+ are chosen at runtime.
89
+ """
90
+ return self.assign(
91
+ customer_id,
92
+ plan_id=plan,
93
+ effective=effective,
94
+ reconciliation=reconciliation,
95
+ )
96
+
97
+ def change_at_next_cycle(self, customer_id: str, *, plan: str) -> Assignment:
98
+ """
99
+ Schedule the move for the end of the period they are in: they keep the
100
+ plan they are paying for until it runs out, and the new one starts at
101
+ their next boundary. Nothing is pro-rated, because nothing is split.
102
+
103
+ Two plans on different cycles share no boundary, and this answers
104
+ `CycleChangeRequiresResetError` — use `restart` for that move.
105
+ """
106
+ return self.change(customer_id, plan=plan, effective="next_cycle")
107
+
108
+ def change_now(
109
+ self,
110
+ customer_id: str,
111
+ *,
112
+ plan: str,
113
+ reconciliation: Reconciliation | NotGiven = NOT_GIVEN,
114
+ ) -> Assignment:
115
+ """
116
+ Move them now, inside the period already running. Their renewal day
117
+ does not move; what changes is this period's cap, and `reconciliation`
118
+ says how:
119
+
120
+ - `none` (the default) — the new plan's amount governs the whole
121
+ period. Usage already spent still counts, so a downgrade can deny
122
+ until the period ends.
123
+ - `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.
126
+
127
+ To have the period itself start over instead, use `restart`.
128
+ """
129
+ return self.change(
130
+ customer_id,
131
+ plan=plan,
132
+ effective="immediate",
133
+ reconciliation=reconciliation,
134
+ )
135
+
136
+ def restart(self, customer_id: str, *, plan: str) -> Assignment:
137
+ """
138
+ Move them now and start a fresh period today: the period they were in
139
+ closes where the change lands, keeping the usage it had, and the new
140
+ plan's full amount opens immediately.
141
+
142
+ This moves the customer's anchor, so their renewal day becomes today.
143
+ It is the move for a customer starting over — a new contract, a
144
+ re-signup — and the only one that can take a customer between plans
145
+ whose cycles differ.
146
+ """
147
+ return self.change(
148
+ customer_id,
149
+ plan=plan,
150
+ effective="immediate",
151
+ reconciliation="reset",
152
+ )
153
+
154
+ def cancel_scheduled_change(self, customer_id: str) -> Assignment | None:
155
+ """
156
+ Call off a change that has not landed yet, leaving the customer on the
157
+ plan they hold. Naming the plan already held is what supersedes a
158
+ pending instruction, so this reads the plan in force and names it back.
159
+
160
+ Returns the assignment still in force, or `None` for a customer holding
161
+ no plan — who can have nothing pending, since a first assignment
162
+ always lands at once.
163
+ """
164
+ current = self.retrieve(customer_id)
165
+ if current is None:
166
+ return None
167
+
168
+ return self.assign(customer_id, plan_id=current.plan_id)
169
+
170
+ def retrieve(self, customer_id: str) -> Assignment | None:
171
+ """
172
+ The plan in force now, which is not always the newest instruction: a
173
+ change dated ahead does not govern yet.
174
+
175
+ `None` when the customer holds no plan. That is a default deny rather
176
+ 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
+ """
180
+ try:
181
+ return self._api.request(
182
+ "GET", f"/v1/customers/{segment(customer_id)}/plan", Assignment
183
+ )
184
+ except NotFoundError as error:
185
+ if error.code == "no_plan_assigned":
186
+ return None
187
+ raise
188
+
189
+ def pending(self, customer_id: str) -> Assignment | None:
190
+ """
191
+ The change waiting to land, or `None` when none is. At most one is ever
192
+ live: a newer instruction supersedes the one before it.
193
+
194
+ Read against the assignment in force rather than against the local
195
+ clock, so a change landing seconds from now is not reported as already
196
+ governing.
197
+ """
198
+ current, history = both(self.retrieve(customer_id), self.history(customer_id))
199
+
200
+ if current is None:
201
+ in_force = datetime.min.replace(tzinfo=timezone.utc)
202
+ else:
203
+ parsed = parse_instant(current.effective_at)
204
+ if parsed is None:
205
+ return None
206
+ in_force = parsed
207
+
208
+ return next(
209
+ (
210
+ row
211
+ for row in history.data
212
+ if row.superseded_at is None
213
+ and _lands_after(row.effective_at, in_force)
214
+ ),
215
+ None,
216
+ )
217
+
218
+ def history(self, customer_id: str) -> ListResult[Assignment]:
219
+ """Every instruction, superseded ones included: it is the whole
220
+ trail."""
221
+ return self._api.request(
222
+ "GET",
223
+ f"/v1/customers/{segment(customer_id)}/plan/history",
224
+ list_of(Assignment),
225
+ )
226
+
227
+
228
+ def _lands_after(effective_at: str, in_force: datetime) -> bool:
229
+ at = parse_instant(effective_at)
230
+ return at is not None and at > in_force
@@ -0,0 +1,176 @@
1
+ # Generated by scripts/unasync.py from
2
+ # src/meterbase/_async/resources/customers.py.
3
+ # Do not edit: change the async source, then run `make unasync`.
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Mapping, Sequence
8
+ from typing import Any, overload
9
+
10
+ from ..._http import segment
11
+ from ..._models import ListResult, list_of
12
+ from ..._not_given import NOT_GIVEN, NotGiven
13
+ from ...errors import NotFoundError
14
+ from ...types import (
15
+ Customer,
16
+ CustomerEntitlement,
17
+ CustomerUsage,
18
+ CustomerWithPlan,
19
+ UsagePeriod,
20
+ )
21
+ from .._transport import Requester
22
+ from .customer_allowances import CustomerAllowances
23
+ from .customer_periods import CustomerPeriods
24
+ from .customer_plan import CustomerPlan
25
+
26
+
27
+ class Customers:
28
+ plan: CustomerPlan
29
+ """The plan they hold, and the trail of instructions that got them
30
+ there."""
31
+ allowances: CustomerAllowances
32
+ """Grants, which sit on top of whatever the plan gives."""
33
+ periods: CustomerPeriods
34
+ """Periods that have ended, with the figures to bill them from."""
35
+
36
+ def __init__(self, api: Requester) -> None:
37
+ self._api = api
38
+ self.plan = CustomerPlan(api)
39
+ self.allowances = CustomerAllowances(api)
40
+ self.periods = CustomerPeriods(api)
41
+
42
+ def create(
43
+ self,
44
+ *,
45
+ external_id: str,
46
+ name: str | NotGiven = NOT_GIVEN,
47
+ metadata: Mapping[str, Any] | NotGiven = NOT_GIVEN,
48
+ thresholds: Sequence[int] | NotGiven = NOT_GIVEN,
49
+ ) -> Customer:
50
+ """
51
+ `thresholds` is whole percents, 1–1000, at most 32 of them; a mark over
52
+ 100 is an overage alert. Omitted, the customer inherits.
53
+ """
54
+ return self._api.request(
55
+ "POST",
56
+ "/v1/customers",
57
+ Customer,
58
+ body={
59
+ "external_id": external_id,
60
+ "name": name,
61
+ "metadata": metadata,
62
+ "thresholds": thresholds,
63
+ },
64
+ )
65
+
66
+ def list(
67
+ self, *, include_deleted: bool | NotGiven = NOT_GIVEN
68
+ ) -> ListResult[Customer]:
69
+ """Lean: a listing does not embed the plan. Read one customer for
70
+ that."""
71
+ return self._api.request(
72
+ "GET",
73
+ "/v1/customers",
74
+ list_of(Customer),
75
+ query={"include_deleted": include_deleted},
76
+ )
77
+
78
+ def retrieve(self, id: str) -> CustomerWithPlan:
79
+ """
80
+ One customer, with the plan they hold. `plan` is the one in force now
81
+ and `pending_plan` the change waiting to land, each `None` when there
82
+ is none — so knowing what a customer is on costs no second request.
83
+ """
84
+ return self._api.request(
85
+ "GET", f"/v1/customers/{segment(id)}", CustomerWithPlan
86
+ )
87
+
88
+ def retrieve_by_external_id(self, external_id: str) -> CustomerWithPlan | None:
89
+ """
90
+ Resolves the tenant's own identifier, or `None`, embedding the plan the
91
+ way `retrieve` does. The engine answers this as a filtered list, so a
92
+ miss is an empty collection rather than a 404.
93
+ """
94
+ found = self._api.request(
95
+ "GET",
96
+ "/v1/customers",
97
+ list_of(CustomerWithPlan),
98
+ query={"external_id": external_id},
99
+ )
100
+ return found.data[0] if found.data else None
101
+
102
+ def entitlement(self, id: str) -> CustomerEntitlement | None:
103
+ """`None` for a customer holding no plan, as `plan.retrieve` answers."""
104
+ try:
105
+ return self._api.request(
106
+ "GET",
107
+ f"/v1/customers/{segment(id)}/entitlement",
108
+ CustomerEntitlement,
109
+ )
110
+ except NotFoundError as error:
111
+ if error.code == "no_plan_assigned":
112
+ return None
113
+ raise
114
+
115
+ @overload
116
+ def usage(self, id: str, *, days: int) -> CustomerUsage: ...
117
+
118
+ @overload
119
+ def usage(self, id: str, *, period: UsagePeriod) -> CustomerUsage | None: ...
120
+
121
+ def usage(
122
+ self,
123
+ id: str,
124
+ *,
125
+ days: int | NotGiven = NOT_GIVEN,
126
+ period: UsagePeriod | NotGiven = NOT_GIVEN,
127
+ ) -> CustomerUsage | None:
128
+ """
129
+ Usage per UTC day: `days` is 1–90 of them ending today, or `period` is
130
+ `"current"` or `"last"`. A period the customer never had, holding no
131
+ plan now or none before this period, is `None`.
132
+ """
133
+ try:
134
+ return self._api.request(
135
+ "GET",
136
+ f"/v1/customers/{segment(id)}/usage",
137
+ CustomerUsage,
138
+ query={"days": days, "period": period},
139
+ )
140
+ except NotFoundError as error:
141
+ if error.code in ("no_plan_assigned", "no_previous_period"):
142
+ return None
143
+ raise
144
+
145
+ def update(
146
+ self,
147
+ id: str,
148
+ *,
149
+ external_id: str | NotGiven = NOT_GIVEN,
150
+ name: str | NotGiven = NOT_GIVEN,
151
+ metadata: Mapping[str, Any] | NotGiven = NOT_GIVEN,
152
+ thresholds: Sequence[int] | NotGiven | None = NOT_GIVEN,
153
+ ) -> Customer:
154
+ """
155
+ An omitted field is left alone. `external_id` cannot be cleared, and
156
+ `metadata` is replaced wholesale rather than merged.
157
+
158
+ `thresholds` reads `None` as a value, not as silence: omit it to keep
159
+ the stored list, pass `None` to go back to inheriting the meter's or
160
+ the workspace's, pass `[]` to turn alerts off for this customer alone.
161
+ """
162
+ return self._api.request(
163
+ "PATCH",
164
+ f"/v1/customers/{segment(id)}",
165
+ Customer,
166
+ body={
167
+ "external_id": external_id,
168
+ "name": name,
169
+ "metadata": metadata,
170
+ "thresholds": thresholds,
171
+ },
172
+ )
173
+
174
+ def delete(self, id: str) -> Customer:
175
+ """Soft delete: usage and assignments keep referencing the customer."""
176
+ return self._api.request("DELETE", f"/v1/customers/{segment(id)}", Customer)
@@ -0,0 +1,106 @@
1
+ # Generated by scripts/unasync.py from
2
+ # src/meterbase/_async/resources/meters.py.
3
+ # Do not edit: change the async source, then run `make unasync`.
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Sequence
8
+
9
+ from ..._http import segment
10
+ from ..._models import ListResult, list_of
11
+ from ..._not_given import NOT_GIVEN, NotGiven
12
+ from ...types import Meter, MeterUsageHistory
13
+ from .._transport import Requester
14
+
15
+
16
+ class Meters:
17
+ def __init__(self, api: Requester) -> None:
18
+ self._api = api
19
+
20
+ def create(
21
+ self,
22
+ *,
23
+ key: str,
24
+ name: str,
25
+ description: str | NotGiven = NOT_GIVEN,
26
+ unit: str | NotGiven = NOT_GIVEN,
27
+ value: str | NotGiven = NOT_GIVEN,
28
+ thresholds: Sequence[int] | NotGiven = NOT_GIVEN,
29
+ ) -> Meter:
30
+ """
31
+ `thresholds` is whole percents, 1–1000, at most 32 of them; a mark over
32
+ 100 is an overage alert. Omitted, the meter inherits the workspace's.
33
+ """
34
+ return self._api.request(
35
+ "POST",
36
+ "/v1/meters",
37
+ Meter,
38
+ body={
39
+ "key": key,
40
+ "name": name,
41
+ "description": description,
42
+ "unit": unit,
43
+ "value": value,
44
+ "thresholds": thresholds,
45
+ },
46
+ )
47
+
48
+ def list(
49
+ self, *, include_archived: bool | NotGiven = NOT_GIVEN
50
+ ) -> ListResult[Meter]:
51
+ return self._api.request(
52
+ "GET",
53
+ "/v1/meters",
54
+ list_of(Meter),
55
+ query={"include_archived": include_archived},
56
+ )
57
+
58
+ def retrieve(self, id: str) -> Meter:
59
+ return self._api.request("GET", f"/v1/meters/{segment(id)}", Meter)
60
+
61
+ def update(
62
+ self,
63
+ id: str,
64
+ *,
65
+ key: str | NotGiven = NOT_GIVEN,
66
+ name: str | NotGiven = NOT_GIVEN,
67
+ description: str | NotGiven = NOT_GIVEN,
68
+ unit: str | NotGiven = NOT_GIVEN,
69
+ value: str | NotGiven = NOT_GIVEN,
70
+ thresholds: Sequence[int] | NotGiven | None = NOT_GIVEN,
71
+ ) -> Meter:
72
+ """
73
+ An omitted field is left alone; `""` clears an optional one.
74
+
75
+ `thresholds` is the exception on both counts: `None` is a value there,
76
+ not silence. Omit it to keep the stored list, pass `None` to inherit
77
+ the workspace's again, pass `[]` to turn alerts off for this meter.
78
+ """
79
+ return self._api.request(
80
+ "PATCH",
81
+ f"/v1/meters/{segment(id)}",
82
+ Meter,
83
+ body={
84
+ "key": key,
85
+ "name": name,
86
+ "description": description,
87
+ "unit": unit,
88
+ "value": value,
89
+ "thresholds": thresholds,
90
+ },
91
+ )
92
+
93
+ def usage(self, id: str, *, days: int) -> MeterUsageHistory:
94
+ """Every customer's usage of the meter per UTC day: `days` is 1–90 of
95
+ them, ending today."""
96
+ return self._api.request(
97
+ "GET",
98
+ f"/v1/meters/{segment(id)}/usage",
99
+ MeterUsageHistory,
100
+ query={"days": days},
101
+ )
102
+
103
+ def archive(self, id: str) -> Meter:
104
+ """Soft delete: plans and usage keep referencing the meter.
105
+ Idempotent."""
106
+ return self._api.request("DELETE", f"/v1/meters/{segment(id)}", Meter)