meterbase 0.3.0__tar.gz → 0.4.0__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.3.0 → meterbase-0.4.0}/PKG-INFO +29 -3
  2. {meterbase-0.3.0 → meterbase-0.4.0}/README.md +28 -2
  3. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/resources/customers.py +68 -10
  4. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_http.py +15 -0
  5. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_models.py +12 -0
  6. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/resources/customers.py +68 -10
  7. meterbase-0.4.0/src/meterbase/_version.py +1 -0
  8. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/types.py +2 -1
  9. meterbase-0.3.0/src/meterbase/_version.py +0 -1
  10. {meterbase-0.3.0 → meterbase-0.4.0}/.gitignore +0 -0
  11. {meterbase-0.3.0 → meterbase-0.4.0}/LICENSE +0 -0
  12. {meterbase-0.3.0 → meterbase-0.4.0}/pyproject.toml +0 -0
  13. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/__init__.py +0 -0
  14. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/__init__.py +0 -0
  15. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/_io.py +0 -0
  16. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/_transport.py +0 -0
  17. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/client.py +0 -0
  18. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/resources/__init__.py +0 -0
  19. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/resources/customer_allowances.py +0 -0
  20. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/resources/customer_periods.py +0 -0
  21. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/resources/customer_plan.py +0 -0
  22. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/resources/meters.py +0 -0
  23. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_async/resources/plans.py +0 -0
  24. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_json.py +0 -0
  25. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_keys.py +0 -0
  26. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_not_given.py +0 -0
  27. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/__init__.py +0 -0
  28. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/_io.py +0 -0
  29. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/_transport.py +0 -0
  30. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/client.py +0 -0
  31. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/resources/__init__.py +0 -0
  32. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/resources/customer_allowances.py +0 -0
  33. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/resources/customer_periods.py +0 -0
  34. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/resources/customer_plan.py +0 -0
  35. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/resources/meters.py +0 -0
  36. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_sync/resources/plans.py +0 -0
  37. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/_time.py +0 -0
  38. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/errors.py +0 -0
  39. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/py.typed +0 -0
  40. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/resources.py +0 -0
  41. {meterbase-0.3.0 → meterbase-0.4.0}/src/meterbase/webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: meterbase
3
- Version: 0.3.0
3
+ Version: 0.4.0
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
@@ -278,7 +278,7 @@ id and gives them a first plan, and running it again changes nothing:
278
278
  ensured = meterbase.customers.ensure(
279
279
  external_id="acct_1",
280
280
  name="Acme", # applied only when it creates the customer
281
- plan="free", # a plan key
281
+ plan="free", # a plan's key or id
282
282
  )
283
283
  ensured.created, ensured.plan_result, ensured.customer.plan
284
284
  ```
@@ -303,7 +303,6 @@ customer = meterbase.customers.create(
303
303
  )
304
304
 
305
305
  found = meterbase.customers.retrieve_by_external_id("acct_1") # or None
306
- everyone = meterbase.customers.list(include_deleted=True).data
307
306
 
308
307
  meterbase.customers.update(customer.id, name="Acme Inc")
309
308
  meterbase.customers.delete(customer.id) # soft delete, idempotent
@@ -328,6 +327,27 @@ embed is which plan, and `customers.plan.retrieve` is where instructions are
328
327
  identified. `None` means they hold no plan, which is a default deny — a
329
328
  customer with no plan and no grant is denied.
330
329
 
330
+ **Lists come a page at a time.** `customers.list` answers one page, newest
331
+ first: `limit` customers, 100 unless you set it and at most 1,000. `list_all`
332
+ follows the pages for you, fetching each one as the loop reaches it:
333
+
334
+ ```python
335
+ page = meterbase.customers.list(plan="pro", limit=1000)
336
+ page.data # the newest 1,000 customers on pro
337
+ page.has_more # True when another page follows the last of them
338
+
339
+ for customer in meterbase.customers.list_all(has_plan=False):
340
+ ... # every customer holding no plan
341
+ ```
342
+
343
+ On the async client it is `async for customer in meterbase.customers.list_all()`.
344
+ `plan`, a key or an id, and `has_plan` filter on the plan in force now; a
345
+ pending change counts once it lands. `include_deleted=True` adds deleted
346
+ customers. To page by hand, pass the last `id` as `starting_after` while
347
+ `has_more` is `True`. A walk reads no customer twice, and one created while it
348
+ runs is not in it. Both raise before sending when `plan` or `starting_after` is
349
+ `None`, which would otherwise widen the list.
350
+
331
351
  `customers.list` stays lean and does not embed, along with `create`, `update`
332
352
  and `delete`; the same read would run per row for an answer most callers of a
333
353
  list do not want.
@@ -994,6 +1014,12 @@ Pre-1.0, the minor version is where breaking changes land — deliberately, so a
994
1014
  change that will not run against your code cannot arrive as a patch. The SDK
995
1015
  follows the engine's API, as the TypeScript SDK (`mbase-sdk`) does.
996
1016
 
1017
+ **0.4.0** pages the customer list, and adds `customers.list_all` and the
1018
+ `limit`, `starting_after`, `plan` and `has_plan` parameters. One change can break
1019
+ code: `customers.list()` answers one page of 100, newest first, where it used to
1020
+ answer every customer. Iterate `list_all` to read them all. The engine pages the
1021
+ same way, so 0.3.0 and earlier now read only the newest 100.
1022
+
997
1023
  **0.3.0** adds an `idempotency_key` on grants and plan changes, which also makes
998
1024
  them safe to retry, `replayed` on their answers, and `IdempotencyKeyReusedError`.
999
1025
  Nothing in it breaks code written for 0.2.0.
@@ -251,7 +251,7 @@ id and gives them a first plan, and running it again changes nothing:
251
251
  ensured = meterbase.customers.ensure(
252
252
  external_id="acct_1",
253
253
  name="Acme", # applied only when it creates the customer
254
- plan="free", # a plan key
254
+ plan="free", # a plan's key or id
255
255
  )
256
256
  ensured.created, ensured.plan_result, ensured.customer.plan
257
257
  ```
@@ -276,7 +276,6 @@ customer = meterbase.customers.create(
276
276
  )
277
277
 
278
278
  found = meterbase.customers.retrieve_by_external_id("acct_1") # or None
279
- everyone = meterbase.customers.list(include_deleted=True).data
280
279
 
281
280
  meterbase.customers.update(customer.id, name="Acme Inc")
282
281
  meterbase.customers.delete(customer.id) # soft delete, idempotent
@@ -301,6 +300,27 @@ embed is which plan, and `customers.plan.retrieve` is where instructions are
301
300
  identified. `None` means they hold no plan, which is a default deny — a
302
301
  customer with no plan and no grant is denied.
303
302
 
303
+ **Lists come a page at a time.** `customers.list` answers one page, newest
304
+ first: `limit` customers, 100 unless you set it and at most 1,000. `list_all`
305
+ follows the pages for you, fetching each one as the loop reaches it:
306
+
307
+ ```python
308
+ page = meterbase.customers.list(plan="pro", limit=1000)
309
+ page.data # the newest 1,000 customers on pro
310
+ page.has_more # True when another page follows the last of them
311
+
312
+ for customer in meterbase.customers.list_all(has_plan=False):
313
+ ... # every customer holding no plan
314
+ ```
315
+
316
+ On the async client it is `async for customer in meterbase.customers.list_all()`.
317
+ `plan`, a key or an id, and `has_plan` filter on the plan in force now; a
318
+ pending change counts once it lands. `include_deleted=True` adds deleted
319
+ customers. To page by hand, pass the last `id` as `starting_after` while
320
+ `has_more` is `True`. A walk reads no customer twice, and one created while it
321
+ runs is not in it. Both raise before sending when `plan` or `starting_after` is
322
+ `None`, which would otherwise widen the list.
323
+
304
324
  `customers.list` stays lean and does not embed, along with `create`, `update`
305
325
  and `delete`; the same read would run per row for an answer most callers of a
306
326
  list do not want.
@@ -967,6 +987,12 @@ Pre-1.0, the minor version is where breaking changes land — deliberately, so a
967
987
  change that will not run against your code cannot arrive as a patch. The SDK
968
988
  follows the engine's API, as the TypeScript SDK (`mbase-sdk`) does.
969
989
 
990
+ **0.4.0** pages the customer list, and adds `customers.list_all` and the
991
+ `limit`, `starting_after`, `plan` and `has_plan` parameters. One change can break
992
+ code: `customers.list()` answers one page of 100, newest first, where it used to
993
+ answer every customer. Iterate `list_all` to read them all. The engine pages the
994
+ same way, so 0.3.0 and earlier now read only the newest 100.
995
+
970
996
  **0.3.0** adds an `idempotency_key` on grants and plan changes, which also makes
971
997
  them safe to retry, `replayed` on their answers, and `IdempotencyKeyReusedError`.
972
998
  Nothing in it breaks code written for 0.2.0.
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Mapping, Sequence
3
+ from collections.abc import AsyncIterator, Mapping, Sequence
4
4
  from typing import Any, overload
5
5
 
6
- from ..._http import require_external_id, segment
7
- from ..._models import ListResult, list_of
6
+ from ..._http import require_external_id, require_list_filters, segment
7
+ from ..._models import Page, list_of, page_of
8
8
  from ..._not_given import NOT_GIVEN, NotGiven
9
9
  from ...errors import NotFoundError
10
10
  from ...types import (
@@ -78,7 +78,7 @@ class AsyncCustomers:
78
78
  happened.
79
79
 
80
80
  `name` and `metadata` apply only to a customer this creates. `plan` is
81
- a plan key; omit it to leave the plan alone. `cycle_anchor`, RFC 3339,
81
+ a plan's key or id; omit it to leave the plan alone. `cycle_anchor`, RFC 3339,
82
82
  only with `plan` and never in the future, is used only when this call
83
83
  assigns the plan. `mint_one_time=False` writes the plan's one-time
84
84
  grants already spent, for an imported customer whose remaining bonus
@@ -105,17 +105,75 @@ class AsyncCustomers:
105
105
  )
106
106
 
107
107
  async def list(
108
- self, *, include_deleted: bool | NotGiven = NOT_GIVEN
109
- ) -> ListResult[Customer]:
110
- """Lean: a listing does not embed the plan. Read one customer for
111
- that."""
108
+ self,
109
+ *,
110
+ limit: int | NotGiven = NOT_GIVEN,
111
+ starting_after: str | NotGiven = NOT_GIVEN,
112
+ plan: str | NotGiven = NOT_GIVEN,
113
+ has_plan: bool | NotGiven = NOT_GIVEN,
114
+ include_deleted: bool | NotGiven = NOT_GIVEN,
115
+ ) -> Page[Customer]:
116
+ """
117
+ One page, newest first: `limit` customers, 100 unless set and at most
118
+ 1,000. Pass the last `id` as `starting_after` for the next page, while
119
+ `has_more` is `True`; `list_all` does that for you.
120
+
121
+ `plan`, a key or an id, and `has_plan` filter on the plan in force now;
122
+ a pending change counts once it lands. Lean: a page does not embed the
123
+ plan. Read one customer for that.
124
+
125
+ Raises `MeterbaseError` before sending when `plan` or `starting_after`
126
+ is `None`.
127
+ """
128
+ require_list_filters("list", plan, starting_after)
129
+
112
130
  return await self._api.request(
113
131
  "GET",
114
132
  "/v1/customers",
115
- list_of(Customer),
116
- query={"include_deleted": include_deleted},
133
+ page_of(Customer),
134
+ query={
135
+ "limit": limit,
136
+ "starting_after": starting_after,
137
+ "plan": plan,
138
+ "has_plan": has_plan,
139
+ "include_deleted": include_deleted,
140
+ },
117
141
  )
118
142
 
143
+ async def list_all(
144
+ self,
145
+ *,
146
+ limit: int | NotGiven = NOT_GIVEN,
147
+ starting_after: str | NotGiven = NOT_GIVEN,
148
+ plan: str | NotGiven = NOT_GIVEN,
149
+ has_plan: bool | NotGiven = NOT_GIVEN,
150
+ include_deleted: bool | NotGiven = NOT_GIVEN,
151
+ ) -> AsyncIterator[Customer]:
152
+ """
153
+ Every customer, newest first, from `starting_after` on when it is
154
+ given, one page of `limit` at a time as the loop asks for more:
155
+ `async for customer in client.customers.list_all(): ...`.
156
+
157
+ A customer created during the walk is not in it, and none is read
158
+ twice.
159
+ """
160
+ require_list_filters("list_all", plan, starting_after)
161
+
162
+ while True:
163
+ page = await self.list(
164
+ limit=limit,
165
+ starting_after=starting_after,
166
+ plan=plan,
167
+ has_plan=has_plan,
168
+ include_deleted=include_deleted,
169
+ )
170
+ for customer in page.data:
171
+ starting_after = customer.id
172
+ yield customer
173
+
174
+ if not page.has_more or not page.data:
175
+ return
176
+
119
177
  async def retrieve(self, id: str) -> CustomerWithPlan:
120
178
  """
121
179
  One customer, with the plan they hold. `plan` is the one in force now
@@ -86,6 +86,21 @@ def require_external_id(method: str, external_id: object) -> None:
86
86
  )
87
87
 
88
88
 
89
+ def require_list_filters(method: str, plan: object, starting_after: object) -> None:
90
+ """
91
+ Dropped from the query, a `None` would widen the list: every customer
92
+ rather than one plan's, or the first page again rather than the next.
93
+ """
94
+ for name, value, instead in (
95
+ ("plan", plan, "omit it to list every customer"),
96
+ ("starting_after", starting_after, "omit it to start at the newest"),
97
+ ):
98
+ if value is None:
99
+ raise MeterbaseError(
100
+ f"customers.{method} was given {name}=None; {instead}."
101
+ )
102
+
103
+
89
104
  def require_key_filter(key: object) -> None:
90
105
  """
91
106
  Dropped from the query, a `None` key would list every grant where a
@@ -111,11 +111,23 @@ class ListResult(Model, Generic[_T]):
111
111
  ]
112
112
 
113
113
 
114
+ class Page(ListResult[_T]):
115
+ """One page of a paged list. `has_more` is `True` when another page follows
116
+ the last item in `data`."""
117
+
118
+ has_more: bool
119
+
120
+
114
121
  def list_of(item: type[_M]) -> Callable[[Mapping[str, Any]], ListResult[_M]]:
115
122
  """Reads a listing whose rows are `item`."""
116
123
  return lambda raw: ListResult(raw, item)
117
124
 
118
125
 
126
+ def page_of(item: type[_M]) -> Callable[[Mapping[str, Any]], Page[_M]]:
127
+ """Reads a page whose rows are `item`."""
128
+ return lambda raw: Page(raw, item)
129
+
130
+
119
131
  def construct(cast_to: Callable[[dict[str, Any]], _T], data: Any) -> _T:
120
132
  """Reads a response body as the object a call answers with."""
121
133
  if not is_object(data):
@@ -4,11 +4,11 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from collections.abc import Mapping, Sequence
7
+ from collections.abc import Iterator, Mapping, Sequence
8
8
  from typing import Any, overload
9
9
 
10
- from ..._http import require_external_id, segment
11
- from ..._models import ListResult, list_of
10
+ from ..._http import require_external_id, require_list_filters, segment
11
+ from ..._models import Page, list_of, page_of
12
12
  from ..._not_given import NOT_GIVEN, NotGiven
13
13
  from ...errors import NotFoundError
14
14
  from ...types import (
@@ -82,7 +82,7 @@ class Customers:
82
82
  happened.
83
83
 
84
84
  `name` and `metadata` apply only to a customer this creates. `plan` is
85
- a plan key; omit it to leave the plan alone. `cycle_anchor`, RFC 3339,
85
+ a plan's key or id; omit it to leave the plan alone. `cycle_anchor`, RFC 3339,
86
86
  only with `plan` and never in the future, is used only when this call
87
87
  assigns the plan. `mint_one_time=False` writes the plan's one-time
88
88
  grants already spent, for an imported customer whose remaining bonus
@@ -109,17 +109,75 @@ class Customers:
109
109
  )
110
110
 
111
111
  def list(
112
- self, *, include_deleted: bool | NotGiven = NOT_GIVEN
113
- ) -> ListResult[Customer]:
114
- """Lean: a listing does not embed the plan. Read one customer for
115
- that."""
112
+ self,
113
+ *,
114
+ limit: int | NotGiven = NOT_GIVEN,
115
+ starting_after: str | NotGiven = NOT_GIVEN,
116
+ plan: str | NotGiven = NOT_GIVEN,
117
+ has_plan: bool | NotGiven = NOT_GIVEN,
118
+ include_deleted: bool | NotGiven = NOT_GIVEN,
119
+ ) -> Page[Customer]:
120
+ """
121
+ One page, newest first: `limit` customers, 100 unless set and at most
122
+ 1,000. Pass the last `id` as `starting_after` for the next page, while
123
+ `has_more` is `True`; `list_all` does that for you.
124
+
125
+ `plan`, a key or an id, and `has_plan` filter on the plan in force now;
126
+ a pending change counts once it lands. Lean: a page does not embed the
127
+ plan. Read one customer for that.
128
+
129
+ Raises `MeterbaseError` before sending when `plan` or `starting_after`
130
+ is `None`.
131
+ """
132
+ require_list_filters("list", plan, starting_after)
133
+
116
134
  return self._api.request(
117
135
  "GET",
118
136
  "/v1/customers",
119
- list_of(Customer),
120
- query={"include_deleted": include_deleted},
137
+ page_of(Customer),
138
+ query={
139
+ "limit": limit,
140
+ "starting_after": starting_after,
141
+ "plan": plan,
142
+ "has_plan": has_plan,
143
+ "include_deleted": include_deleted,
144
+ },
121
145
  )
122
146
 
147
+ def list_all(
148
+ self,
149
+ *,
150
+ limit: int | NotGiven = NOT_GIVEN,
151
+ starting_after: str | NotGiven = NOT_GIVEN,
152
+ plan: str | NotGiven = NOT_GIVEN,
153
+ has_plan: bool | NotGiven = NOT_GIVEN,
154
+ include_deleted: bool | NotGiven = NOT_GIVEN,
155
+ ) -> Iterator[Customer]:
156
+ """
157
+ Every customer, newest first, from `starting_after` on when it is
158
+ given, one page of `limit` at a time as the loop asks for more:
159
+ `for customer in client.customers.list_all(): ...`.
160
+
161
+ A customer created during the walk is not in it, and none is read
162
+ twice.
163
+ """
164
+ require_list_filters("list_all", plan, starting_after)
165
+
166
+ while True:
167
+ page = self.list(
168
+ limit=limit,
169
+ starting_after=starting_after,
170
+ plan=plan,
171
+ has_plan=has_plan,
172
+ include_deleted=include_deleted,
173
+ )
174
+ for customer in page.data:
175
+ starting_after = customer.id
176
+ yield customer
177
+
178
+ if not page.has_more or not page.data:
179
+ return
180
+
123
181
  def retrieve(self, id: str) -> CustomerWithPlan:
124
182
  """
125
183
  One customer, with the plan they hold. `plan` is the one in force now
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  from typing import Any, Literal, TypedDict
14
14
 
15
- from ._models import ListResult, Model
15
+ from ._models import ListResult, Model, Page
16
16
 
17
17
  __all__ = [
18
18
  "Allowance",
@@ -45,6 +45,7 @@ __all__ = [
45
45
  "MeterUsage",
46
46
  "MeterUsageHistory",
47
47
  "Model",
48
+ "Page",
48
49
  "Period",
49
50
  "Plan",
50
51
  "PlanAllowance",
@@ -1 +0,0 @@
1
- __version__ = "0.3.0"
File without changes
File without changes
File without changes