meterflow 0.2.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 (21) hide show
  1. {meterflow-0.2.0 → meterflow-0.4.0}/PKG-INFO +20 -4
  2. {meterflow-0.2.0 → meterflow-0.4.0}/README.md +19 -3
  3. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/_http.py +11 -5
  4. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/_version.py +1 -1
  5. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/entitlements.py +8 -2
  6. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/subscriptions.py +34 -9
  7. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/webhooks.py +39 -1
  8. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/types.py +18 -1
  9. {meterflow-0.2.0 → meterflow-0.4.0}/LICENSE +0 -0
  10. {meterflow-0.2.0 → meterflow-0.4.0}/pyproject.toml +0 -0
  11. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/__init__.py +0 -0
  12. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/_retry.py +0 -0
  13. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/client.py +0 -0
  14. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/errors.py +0 -0
  15. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/py.typed +0 -0
  16. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/__init__.py +0 -0
  17. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/credits.py +0 -0
  18. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/plans.py +0 -0
  19. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/portal.py +0 -0
  20. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/usage.py +0 -0
  21. {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: meterflow
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Official Python SDK for MeterFlow — usage-based billing, credit management, and metering.
5
5
  Project-URL: Homepage, https://meter-flow.com
6
6
  Project-URL: Documentation, https://meter-flow.com/guide
@@ -59,6 +59,10 @@ Official Python SDK for [MeterFlow](https://meter-flow.com) — usage-based bill
59
59
 
60
60
  Track what your customers use, enforce credit balances, gate features, and manage subscriptions with a few lines of code. Fully typed, sync **and** async, one dependency (`httpx`).
61
61
 
62
+ - **Flat $29, never a percentage of your revenue.** MeterFlow's [plans](https://meter-flow.com/pricing) follow how many usage events you send, not what your customers pay you.
63
+ - **Works next to any payment provider.** MeterFlow never moves money: your provider charges your customers, and this SDK records their usage, keeps their credit balances and asks whether they may go on. Paddle, Polar, Lemon Squeezy, Chargebee and Razorpay connect from the dashboard; anything else calls `credits.grant` when a payment lands.
64
+ - **Test and live by key prefix.** An `mf_test_` key reads and writes test data, an `mf_live_` key live data — the same code, two worlds that never mix.
65
+
62
66
  ---
63
67
 
64
68
  ## Contents
@@ -234,8 +238,11 @@ bal = client.credits.balance("customer_123")
234
238
  bal["balance"] # "3545.000000" ← decimal string, see note below
235
239
  bal["total_granted"] # "5250.000000"
236
240
  bal["total_consumed"] # "1705.000000"
241
+ bal["usage_draws_credits"] # True | False | None
237
242
  ```
238
243
 
244
+ `usage_draws_credits` says whether the customer's plan charges usage to this balance: `True` when a metered limit carries a rate, `False` when the plan never does (the balance then moves only through your own `credits.deduct`), `None` without an active or trialing subscription.
245
+
239
246
  **Transactions** — the history, newest first, **a page at a time**:
240
247
 
241
248
  ```python
@@ -371,6 +378,7 @@ print(sub["status"]) # "trialing" if the plan has trial days, else "active"
371
378
  page = client.subscriptions.list() # whole project (in your key's environment)
372
379
  theirs = client.subscriptions.list(customer_id="customer_123") # one customer
373
380
  more = client.subscriptions.list(limit=200, cursor=page["next_cursor"])
381
+ behind = client.subscriptions.list(q="acme", status=["past_due", "paused"]) # ids containing "acme", in either status
374
382
 
375
383
  for subscription in page["data"]:
376
384
  print(subscription["customer_external_id"], subscription["status"])
@@ -421,13 +429,17 @@ receipts[0]["event_type"] # "usage.recorded"
421
429
  receipts[0]["status"] # "pending" | "retrying" | "success" | "failed"
422
430
  receipts[0]["response_code"] # what your server answered — 200, 500, or None if it never answered
423
431
  receipts[0]["attempts"] # how many times we have tried
424
- receipts[0]["next_retry_at"] # when we will try again, or None
425
432
  receipts[0]["payload"] # exactly what we sent
433
+
434
+ page = client.webhooks.delivery_page(endpoint_id, limit=200) # the same rows, a page at a time
435
+ older = client.webhooks.delivery_page(endpoint_id, cursor=page["next_cursor"]) # None on the last page
426
436
  ```
427
437
 
438
+ A delivery is tried up to five times: at once, then after 30 s, 5 min, 30 min and 2 h, each later try scheduled through the queue, where it survives a worker restart. `attempts` counts the tries; the receipt shows `pending` before the first try and `retrying` between tries.
439
+
428
440
  Typical use: answering "did that event reach my endpoint?" from your own tooling instead of opening the dashboard — and reading `response_body` when the answer is no.
429
441
 
430
- Two things to know. The list is capped at the **most recent 100** attempts per endpoint; it is a debugging surface, not an export. And it follows your key: a `mf_test_*` key reads test receipts, a `mf_live_*` key live ones, while the endpoint list itself is shared by both worlds, because an endpoint is configuration and receives events from each.
442
+ Two things to know. `deliveries()` returns the **most recent 100** attempts per endpoint; `delivery_page()` reads further back, one page at a time. And both follow your key: a `mf_test_*` key reads test receipts, a `mf_live_*` key live ones, while the endpoint list itself is shared by both worlds, because an endpoint is configuration and receives events from each.
431
443
 
432
444
  > This is not the same as [verifying webhooks](#verifying-webhooks) below. That proves a request your server just received really came from us; this reads what we sent.
433
445
 
@@ -462,6 +474,8 @@ result["entitlements"]
462
474
  # {"feature": "images.generated", "feature_type": "metered", "used": "120", "included": 500, "remaining": "380", ...}]
463
475
  ```
464
476
 
477
+ Each metered row answers for the **next** unit, exactly as `check()` would, so its `allowed` and `reason` match the gate. Reading the list fires no webhook; only `check()` announces `balance.exhausted`.
478
+
465
479
  Prefer to be told rather than to ask? Subscribe a webhook to `limit.reached` (see below).
466
480
 
467
481
  ### The balance portal — a page you don't have to build
@@ -502,6 +516,8 @@ client.credits.deduct(
502
516
 
503
517
  Every write method accepts the keyword: `credits.grant/deduct`, `usage.record/record_batch`, `subscriptions.create/update`. Use a key that identifies the *business operation* (order ID, job ID) — not a random value per attempt, which would defeat the purpose.
504
518
 
519
+ MeterFlow remembers a key for 24 hours; after that the same key starts a new operation, so a retry must come within the day (a late subscription retry meets the one-per-customer rule instead of replaying).
520
+
505
521
  **A key is bound to the body you first used it with.** MeterFlow stores a fingerprint of the request alongside the key, so a retry only replays when it is genuinely the same request. Reuse the key with a *different* body and the call is refused with `ConflictError` (409) instead of quietly handing back the original record:
506
522
 
507
523
  ```python
@@ -604,7 +620,7 @@ Every delivery is a flat JSON object (`Content-Type: application/json`) with two
604
620
  | `limit.reached` | a customer first reaches a **hard** or **soft** limit's included units this billing period (once per meter per period; metered limits report through `credit.deducted` instead) | `customer_id`, `subscription_id`, `feature` (the meter's `event_name`), `meter_id`, `limit_type`, `included`, `used`, `period_start`, `period_end` |
605
621
  | `balance.low` | a spend takes a customer's balance **down through** this endpoint's `low_balance_threshold` — a crossing, so it fires once and not again while they stay below. Raised by both doors that spend: `credits.deduct` and a metered usage charge. **Both crossing events follow from the endpoint's `low_balance_threshold`: set one and the API adds them, clear it and the API removes them — so a line is the only thing to configure.** | `customer_id`, `balance`, `threshold`, `suggested_topup` (the subscription's `topup_amount`, `null` when unset), `transaction_id` |
606
622
  | `balance.exhausted` | a balance reaches zero, **or** a request is refused for want of credits (a deduction returning 402, or `entitlements.check` answering `insufficient_credits`). Unlike `balance.low` this repeats: every refused request is its own blocked request | `customer_id`, `balance`, `reason` (`balance_zero` / `insufficient_credits`), `requested_amount` and `feature` where they apply, `transaction_id` on the zero case |
607
- | `subscription.renewed` | a billing period opens — either a paid period rolling forward or a **trial converting** to its first paid period, told apart by `reason` (the renewer emits no `subscription.updated` for either) | `customer_id`, `subscription_id`, `plan_id`, `status`, `reason` (`period_renewed` / `trial_converted`), `current_period_start`, `current_period_end`, `credits_granted` |
623
+ | `subscription.renewed` | a billing period opens — a paid period rolling forward, a **trial converting** to its first paid period, or a canceled subscription **reactivated** after its period lapsed, told apart by `reason` (the renewer emits no `subscription.updated` for the first two) | `customer_id`, `subscription_id`, `plan_id`, `status`, `reason` (`period_renewed` / `trial_converted` / `period_reactivated`), `current_period_start`, `current_period_end`, `credits_granted` |
608
624
  | `subscription.canceled` | a subscription's status becomes canceled — fired **alongside** `subscription.updated`, once, on the call that actually cancels | `customer_id`, `subscription_id`, `plan_id`, `canceled_at` |
609
625
  | `balance.topped_up` | a grant takes a customer's balance **up through** this endpoint's `low_balance_threshold` — the upward mirror of `balance.low`, on the same per-endpoint line, so the endpoint told "low" is the one told "recovered". Subscribed together with `balance.low` by setting the threshold, never on its own | `customer_id`, `balance`, `threshold`, `amount`, `transaction_id` |
610
626
  | `cap.reached` | a customer's credit spending reaches their subscription's `spend_cap` for the period — once per subscription per period, whether they landed on the cap or were refused by it. A refusal that still leaves headroom is not a reach and does not fire | `customer_id`, `subscription_id`, `cap`, `spent`, `period_start`, `period_end` |
@@ -7,6 +7,10 @@ Official Python SDK for [MeterFlow](https://meter-flow.com) — usage-based bill
7
7
 
8
8
  Track what your customers use, enforce credit balances, gate features, and manage subscriptions with a few lines of code. Fully typed, sync **and** async, one dependency (`httpx`).
9
9
 
10
+ - **Flat $29, never a percentage of your revenue.** MeterFlow's [plans](https://meter-flow.com/pricing) follow how many usage events you send, not what your customers pay you.
11
+ - **Works next to any payment provider.** MeterFlow never moves money: your provider charges your customers, and this SDK records their usage, keeps their credit balances and asks whether they may go on. Paddle, Polar, Lemon Squeezy, Chargebee and Razorpay connect from the dashboard; anything else calls `credits.grant` when a payment lands.
12
+ - **Test and live by key prefix.** An `mf_test_` key reads and writes test data, an `mf_live_` key live data — the same code, two worlds that never mix.
13
+
10
14
  ---
11
15
 
12
16
  ## Contents
@@ -182,8 +186,11 @@ bal = client.credits.balance("customer_123")
182
186
  bal["balance"] # "3545.000000" ← decimal string, see note below
183
187
  bal["total_granted"] # "5250.000000"
184
188
  bal["total_consumed"] # "1705.000000"
189
+ bal["usage_draws_credits"] # True | False | None
185
190
  ```
186
191
 
192
+ `usage_draws_credits` says whether the customer's plan charges usage to this balance: `True` when a metered limit carries a rate, `False` when the plan never does (the balance then moves only through your own `credits.deduct`), `None` without an active or trialing subscription.
193
+
187
194
  **Transactions** — the history, newest first, **a page at a time**:
188
195
 
189
196
  ```python
@@ -319,6 +326,7 @@ print(sub["status"]) # "trialing" if the plan has trial days, else "active"
319
326
  page = client.subscriptions.list() # whole project (in your key's environment)
320
327
  theirs = client.subscriptions.list(customer_id="customer_123") # one customer
321
328
  more = client.subscriptions.list(limit=200, cursor=page["next_cursor"])
329
+ behind = client.subscriptions.list(q="acme", status=["past_due", "paused"]) # ids containing "acme", in either status
322
330
 
323
331
  for subscription in page["data"]:
324
332
  print(subscription["customer_external_id"], subscription["status"])
@@ -369,13 +377,17 @@ receipts[0]["event_type"] # "usage.recorded"
369
377
  receipts[0]["status"] # "pending" | "retrying" | "success" | "failed"
370
378
  receipts[0]["response_code"] # what your server answered — 200, 500, or None if it never answered
371
379
  receipts[0]["attempts"] # how many times we have tried
372
- receipts[0]["next_retry_at"] # when we will try again, or None
373
380
  receipts[0]["payload"] # exactly what we sent
381
+
382
+ page = client.webhooks.delivery_page(endpoint_id, limit=200) # the same rows, a page at a time
383
+ older = client.webhooks.delivery_page(endpoint_id, cursor=page["next_cursor"]) # None on the last page
374
384
  ```
375
385
 
386
+ A delivery is tried up to five times: at once, then after 30 s, 5 min, 30 min and 2 h, each later try scheduled through the queue, where it survives a worker restart. `attempts` counts the tries; the receipt shows `pending` before the first try and `retrying` between tries.
387
+
376
388
  Typical use: answering "did that event reach my endpoint?" from your own tooling instead of opening the dashboard — and reading `response_body` when the answer is no.
377
389
 
378
- Two things to know. The list is capped at the **most recent 100** attempts per endpoint; it is a debugging surface, not an export. And it follows your key: a `mf_test_*` key reads test receipts, a `mf_live_*` key live ones, while the endpoint list itself is shared by both worlds, because an endpoint is configuration and receives events from each.
390
+ Two things to know. `deliveries()` returns the **most recent 100** attempts per endpoint; `delivery_page()` reads further back, one page at a time. And both follow your key: a `mf_test_*` key reads test receipts, a `mf_live_*` key live ones, while the endpoint list itself is shared by both worlds, because an endpoint is configuration and receives events from each.
379
391
 
380
392
  > This is not the same as [verifying webhooks](#verifying-webhooks) below. That proves a request your server just received really came from us; this reads what we sent.
381
393
 
@@ -410,6 +422,8 @@ result["entitlements"]
410
422
  # {"feature": "images.generated", "feature_type": "metered", "used": "120", "included": 500, "remaining": "380", ...}]
411
423
  ```
412
424
 
425
+ Each metered row answers for the **next** unit, exactly as `check()` would, so its `allowed` and `reason` match the gate. Reading the list fires no webhook; only `check()` announces `balance.exhausted`.
426
+
413
427
  Prefer to be told rather than to ask? Subscribe a webhook to `limit.reached` (see below).
414
428
 
415
429
  ### The balance portal — a page you don't have to build
@@ -450,6 +464,8 @@ client.credits.deduct(
450
464
 
451
465
  Every write method accepts the keyword: `credits.grant/deduct`, `usage.record/record_batch`, `subscriptions.create/update`. Use a key that identifies the *business operation* (order ID, job ID) — not a random value per attempt, which would defeat the purpose.
452
466
 
467
+ MeterFlow remembers a key for 24 hours; after that the same key starts a new operation, so a retry must come within the day (a late subscription retry meets the one-per-customer rule instead of replaying).
468
+
453
469
  **A key is bound to the body you first used it with.** MeterFlow stores a fingerprint of the request alongside the key, so a retry only replays when it is genuinely the same request. Reuse the key with a *different* body and the call is refused with `ConflictError` (409) instead of quietly handing back the original record:
454
470
 
455
471
  ```python
@@ -552,7 +568,7 @@ Every delivery is a flat JSON object (`Content-Type: application/json`) with two
552
568
  | `limit.reached` | a customer first reaches a **hard** or **soft** limit's included units this billing period (once per meter per period; metered limits report through `credit.deducted` instead) | `customer_id`, `subscription_id`, `feature` (the meter's `event_name`), `meter_id`, `limit_type`, `included`, `used`, `period_start`, `period_end` |
553
569
  | `balance.low` | a spend takes a customer's balance **down through** this endpoint's `low_balance_threshold` — a crossing, so it fires once and not again while they stay below. Raised by both doors that spend: `credits.deduct` and a metered usage charge. **Both crossing events follow from the endpoint's `low_balance_threshold`: set one and the API adds them, clear it and the API removes them — so a line is the only thing to configure.** | `customer_id`, `balance`, `threshold`, `suggested_topup` (the subscription's `topup_amount`, `null` when unset), `transaction_id` |
554
570
  | `balance.exhausted` | a balance reaches zero, **or** a request is refused for want of credits (a deduction returning 402, or `entitlements.check` answering `insufficient_credits`). Unlike `balance.low` this repeats: every refused request is its own blocked request | `customer_id`, `balance`, `reason` (`balance_zero` / `insufficient_credits`), `requested_amount` and `feature` where they apply, `transaction_id` on the zero case |
555
- | `subscription.renewed` | a billing period opens — either a paid period rolling forward or a **trial converting** to its first paid period, told apart by `reason` (the renewer emits no `subscription.updated` for either) | `customer_id`, `subscription_id`, `plan_id`, `status`, `reason` (`period_renewed` / `trial_converted`), `current_period_start`, `current_period_end`, `credits_granted` |
571
+ | `subscription.renewed` | a billing period opens — a paid period rolling forward, a **trial converting** to its first paid period, or a canceled subscription **reactivated** after its period lapsed, told apart by `reason` (the renewer emits no `subscription.updated` for the first two) | `customer_id`, `subscription_id`, `plan_id`, `status`, `reason` (`period_renewed` / `trial_converted` / `period_reactivated`), `current_period_start`, `current_period_end`, `credits_granted` |
556
572
  | `subscription.canceled` | a subscription's status becomes canceled — fired **alongside** `subscription.updated`, once, on the call that actually cancels | `customer_id`, `subscription_id`, `plan_id`, `canceled_at` |
557
573
  | `balance.topped_up` | a grant takes a customer's balance **up through** this endpoint's `low_balance_threshold` — the upward mirror of `balance.low`, on the same per-endpoint line, so the endpoint told "low" is the one told "recovered". Subscribed together with `balance.low` by setting the threshold, never on its own | `customer_id`, `balance`, `threshold`, `amount`, `transaction_id` |
558
574
  | `cap.reached` | a customer's credit spending reaches their subscription's `spend_cap` for the period — once per subscription per period, whether they landed on the cap or were refused by it. A refusal that still leaves headroom is not a reach and does not fire | `customer_id`, `subscription_id`, `cap`, `spent`, `period_start`, `period_end` |
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import asyncio
10
10
  import json
11
11
  import time
12
+ from collections.abc import Sequence
12
13
  from dataclasses import dataclass, field
13
14
  from typing import Any
14
15
  from urllib.parse import quote
@@ -31,7 +32,7 @@ from .errors import (
31
32
 
32
33
  USER_AGENT = f"meterflow-python/{__version__}"
33
34
 
34
- QueryValue = str | int | float | bool | None
35
+ QueryValue = str | int | float | bool | Sequence[str] | None
35
36
 
36
37
  # Indirections so tests can make the retry loop instantaneous without touching the clock.
37
38
  _sleep = time.sleep
@@ -67,13 +68,18 @@ def build_headers(api_key: str, spec: RequestSpec) -> dict[str, str]:
67
68
  return headers
68
69
 
69
70
 
70
- def build_params(query: dict[str, QueryValue]) -> dict[str, str]:
71
- """Unset (``None``) filters are simply not sent; booleans go as ``true``/``false`` like the Node SDK."""
72
- params: dict[str, str] = {}
71
+ def build_params(query: dict[str, QueryValue]) -> dict[str, str | list[str]]:
72
+ """Unset (``None``) filters are simply not sent; booleans go as ``true``/``false`` like the Node SDK; a list repeats its key."""
73
+ params: dict[str, str | list[str]] = {}
73
74
  for key, value in query.items():
74
75
  if value is None:
75
76
  continue
76
- params[key] = ("true" if value else "false") if isinstance(value, bool) else str(value)
77
+ if isinstance(value, bool):
78
+ params[key] = "true" if value else "false"
79
+ elif isinstance(value, str | int | float):
80
+ params[key] = str(value)
81
+ else:
82
+ params[key] = [str(item) for item in value]
77
83
  return params
78
84
 
79
85
 
@@ -1,3 +1,3 @@
1
1
  # The one place the version lives. pyproject.toml reads it (hatch dynamic version) and the
2
2
  # User-Agent header reports it, so a release bump edits this file and nothing else.
3
- __version__ = "0.2.0"
3
+ __version__ = "0.4.0"
@@ -35,7 +35,10 @@ class EntitlementsResource:
35
35
  self._client = client
36
36
 
37
37
  def get(self, customer_id: str) -> EntitlementsResponse:
38
- """Every feature on the customer's plan, with this period's usage. Empty ``entitlements`` without a subscription."""
38
+ """Every feature on the customer's plan, with this period's usage; a metered row answers for the next unit, as ``check()`` would.
39
+
40
+ Empty ``entitlements`` without a subscription.
41
+ """
39
42
  result: EntitlementsResponse = self._client.request(get_spec(customer_id))
40
43
  return result
41
44
 
@@ -50,7 +53,10 @@ class AsyncEntitlementsResource:
50
53
  self._client = client
51
54
 
52
55
  async def get(self, customer_id: str) -> EntitlementsResponse:
53
- """Every feature on the customer's plan, with this period's usage. Empty ``entitlements`` without a subscription."""
56
+ """Every feature on the customer's plan, with this period's usage; a metered row answers for the next unit, as ``check()`` would.
57
+
58
+ Empty ``entitlements`` without a subscription.
59
+ """
54
60
  result: EntitlementsResponse = await self._client.request(get_spec(customer_id))
55
61
  return result
56
62
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Sequence
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from .._http import RequestSpec, encode_path_segment
8
- from ..types import SubscriptionCreateRequest, SubscriptionPage, SubscriptionResponse, SubscriptionUpdateRequest
9
+ from ..types import SubscriptionCreateRequest, SubscriptionPage, SubscriptionResponse, SubscriptionStatus, SubscriptionUpdateRequest
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from ..client import AsyncMeterFlow, MeterFlow
@@ -15,8 +16,12 @@ def create_spec(body: SubscriptionCreateRequest, idempotency_key: str | None) ->
15
16
  return RequestSpec("POST", "subscriptions", body=body, idempotency_key=idempotency_key)
16
17
 
17
18
 
18
- def list_spec(customer_id: str | None, limit: int | None, cursor: str | None) -> RequestSpec:
19
- return RequestSpec("GET", "subscriptions", query={"customer_id": customer_id, "limit": limit, "cursor": cursor})
19
+ def list_spec(
20
+ customer_id: str | None, q: str | None, status: Sequence[SubscriptionStatus] | None, limit: int | None, cursor: str | None
21
+ ) -> RequestSpec:
22
+ return RequestSpec(
23
+ "GET", "subscriptions", query={"customer_id": customer_id, "q": q, "status": status, "limit": limit, "cursor": cursor}
24
+ )
20
25
 
21
26
 
22
27
  def get_spec(subscription_id: str) -> RequestSpec:
@@ -40,14 +45,24 @@ class SubscriptionsResource:
40
45
  result: SubscriptionResponse = self._client.request(create_spec(body, idempotency_key))
41
46
  return result
42
47
 
43
- def list(self, *, customer_id: str | None = None, limit: int | None = None, cursor: str | None = None) -> SubscriptionPage:
48
+ def list(
49
+ self,
50
+ *,
51
+ customer_id: str | None = None,
52
+ q: str | None = None,
53
+ status: Sequence[SubscriptionStatus] | None = None,
54
+ limit: int | None = None,
55
+ cursor: str | None = None,
56
+ ) -> SubscriptionPage:
44
57
  """One page of the project's subscriptions in this key's environment, newest first.
45
58
 
46
59
  ``data`` holds the rows and ``next_cursor`` the mark for the page after this one (``None``
47
60
  on the last). Pass it back as ``cursor`` unchanged — it is opaque, and nothing but this API
48
- reads it. ``customer_id`` narrows the list to one customer.
61
+ reads it — with the same filters, which apply before the page is cut. ``customer_id``
62
+ narrows the list to one customer, matched exactly; ``q`` to customers whose id contains it,
63
+ ignoring case (``%`` and ``_`` are literal); ``status`` to subscriptions in one of those statuses.
49
64
  """
50
- result: SubscriptionPage = self._client.request(list_spec(customer_id, limit, cursor))
65
+ result: SubscriptionPage = self._client.request(list_spec(customer_id, q, status, limit, cursor))
51
66
  return result
52
67
 
53
68
  def get(self, subscription_id: str) -> SubscriptionResponse:
@@ -73,14 +88,24 @@ class AsyncSubscriptionsResource:
73
88
  result: SubscriptionResponse = await self._client.request(create_spec(body, idempotency_key))
74
89
  return result
75
90
 
76
- async def list(self, *, customer_id: str | None = None, limit: int | None = None, cursor: str | None = None) -> SubscriptionPage:
91
+ async def list(
92
+ self,
93
+ *,
94
+ customer_id: str | None = None,
95
+ q: str | None = None,
96
+ status: Sequence[SubscriptionStatus] | None = None,
97
+ limit: int | None = None,
98
+ cursor: str | None = None,
99
+ ) -> SubscriptionPage:
77
100
  """One page of the project's subscriptions in this key's environment, newest first.
78
101
 
79
102
  ``data`` holds the rows and ``next_cursor`` the mark for the page after this one (``None``
80
103
  on the last). Pass it back as ``cursor`` unchanged — it is opaque, and nothing but this API
81
- reads it. ``customer_id`` narrows the list to one customer.
104
+ reads it — with the same filters, which apply before the page is cut. ``customer_id``
105
+ narrows the list to one customer, matched exactly; ``q`` to customers whose id contains it,
106
+ ignoring case (``%`` and ``_`` are literal); ``status`` to subscriptions in one of those statuses.
82
107
  """
83
- result: SubscriptionPage = await self._client.request(list_spec(customer_id, limit, cursor))
108
+ result: SubscriptionPage = await self._client.request(list_spec(customer_id, q, status, limit, cursor))
84
109
  return result
85
110
 
86
111
  async def get(self, subscription_id: str) -> SubscriptionResponse:
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
  from typing import TYPE_CHECKING
14
14
 
15
15
  from .._http import RequestSpec, encode_path_segment
16
- from ..types import WebhookDeliveryResponse, WebhookResponse
16
+ from ..types import WebhookDeliveryPage, WebhookDeliveryResponse, WebhookResponse
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from ..client import AsyncMeterFlow, MeterFlow
@@ -35,6 +35,16 @@ def deliveries_spec(webhook_id: str) -> RequestSpec:
35
35
  return RequestSpec("GET", f"webhooks/{encode_path_segment(webhook_id)}/deliveries")
36
36
 
37
37
 
38
+ def delivery_page_spec(webhook_id: str, limit: int | None, cursor: str | None) -> RequestSpec:
39
+ """The request for one page of an endpoint's delivery attempts.
40
+
41
+ webhook_id: the endpoint whose receipts are read.
42
+ limit: rows per page, 1–200; None for the API's default.
43
+ cursor: ``next_cursor`` of the page before; None for the first page.
44
+ """
45
+ return RequestSpec("GET", f"webhooks/{encode_path_segment(webhook_id)}/deliveries/page", query={"limit": limit, "cursor": cursor})
46
+
47
+
38
48
  class WebhooksResource:
39
49
  def __init__(self, client: MeterFlow) -> None:
40
50
  self._client = client
@@ -54,6 +64,20 @@ class WebhooksResource:
54
64
  result: DeliveryList = self._client.request(deliveries_spec(webhook_id))
55
65
  return result
56
66
 
67
+ def delivery_page(self, webhook_id: str, *, limit: int | None = None, cursor: str | None = None) -> WebhookDeliveryPage:
68
+ """One page of an endpoint's delivery attempts, newest first, with no cap on how far back it reads.
69
+
70
+ The same rows as ``deliveries()``, in the environment of the key. ``data`` holds the rows and
71
+ ``next_cursor`` the mark for the page after this one (``None`` on the last); pass it back as
72
+ ``cursor`` unchanged.
73
+
74
+ webhook_id: the endpoint whose receipts are read.
75
+ limit: rows per page, 1–200; None for the API's default (50).
76
+ cursor: ``next_cursor`` of the page before; None for the first page.
77
+ """
78
+ result: WebhookDeliveryPage = self._client.request(delivery_page_spec(webhook_id, limit, cursor))
79
+ return result
80
+
57
81
 
58
82
  class AsyncWebhooksResource:
59
83
  def __init__(self, client: AsyncMeterFlow) -> None:
@@ -73,3 +97,17 @@ class AsyncWebhooksResource:
73
97
  """
74
98
  result: DeliveryList = await self._client.request(deliveries_spec(webhook_id))
75
99
  return result
100
+
101
+ async def delivery_page(self, webhook_id: str, *, limit: int | None = None, cursor: str | None = None) -> WebhookDeliveryPage:
102
+ """One page of an endpoint's delivery attempts, newest first, with no cap on how far back it reads.
103
+
104
+ The same rows as ``deliveries()``, in the environment of the key. ``data`` holds the rows and
105
+ ``next_cursor`` the mark for the page after this one (``None`` on the last); pass it back as
106
+ ``cursor`` unchanged.
107
+
108
+ webhook_id: the endpoint whose receipts are read.
109
+ limit: rows per page, 1–200; None for the API's default (50).
110
+ cursor: ``next_cursor`` of the page before; None for the first page.
111
+ """
112
+ result: WebhookDeliveryPage = await self._client.request(delivery_page_spec(webhook_id, limit, cursor))
113
+ return result
@@ -31,6 +31,7 @@ class CreditBalanceResponse(TypedDict):
31
31
  version: int
32
32
  created_at: str
33
33
  updated_at: str
34
+ usage_draws_credits: bool | None
34
35
 
35
36
 
36
37
  class _CreditDeductRequestRequired(TypedDict):
@@ -104,6 +105,7 @@ class EntitlementsResponse(TypedDict):
104
105
  environment: str
105
106
  subscription_id: str | None
106
107
  plan_id: str | None
108
+ plan_name: str | None
107
109
  period_end: str | None
108
110
  entitlements: list[EntitlementResponse]
109
111
 
@@ -118,6 +120,8 @@ class PlanMeterLimitResponse(TypedDict):
118
120
 
119
121
 
120
122
  class PlanResponse(TypedDict):
123
+ created_by: str | None
124
+ created_by_email: str | None
121
125
  id: str
122
126
  project_id: str
123
127
  name: str
@@ -260,6 +264,7 @@ class _UsageSummaryItemRequired(TypedDict):
260
264
  meter_name: str
261
265
  event_name: str
262
266
  aggregation_type: str
267
+ aggregation_field: str | None
263
268
  value: str
264
269
  event_count: int
265
270
 
@@ -277,6 +282,16 @@ class UsageSummaryResponse(TypedDict):
277
282
  meters: list[UsageSummaryItem]
278
283
 
279
284
 
285
+ class _WebhookDeliveryPageRequired(TypedDict):
286
+ data: list[WebhookDeliveryResponse]
287
+
288
+
289
+ class WebhookDeliveryPage(_WebhookDeliveryPageRequired, total=False):
290
+ """One page of an endpoint's delivery attempts, newest first; pass `next_cursor` back as `cursor` for the next."""
291
+
292
+ next_cursor: str | None
293
+
294
+
280
295
  class WebhookDeliveryResponse(TypedDict):
281
296
  id: str
282
297
  webhook_id: str
@@ -286,7 +301,6 @@ class WebhookDeliveryResponse(TypedDict):
286
301
  response_code: int | None
287
302
  response_body: str | None
288
303
  attempts: int
289
- next_retry_at: str | None
290
304
  created_at: str
291
305
  completed_at: str | None
292
306
 
@@ -294,6 +308,8 @@ class WebhookDeliveryResponse(TypedDict):
294
308
  class WebhookResponse(TypedDict):
295
309
  """Returned on all reads — secret is never included."""
296
310
 
311
+ created_by: str | None
312
+ created_by_email: str | None
297
313
  id: str
298
314
  project_id: str
299
315
  url: str
@@ -331,6 +347,7 @@ __all__ = [
331
347
  "UsageEventResponse",
332
348
  "UsageSummaryItem",
333
349
  "UsageSummaryResponse",
350
+ "WebhookDeliveryPage",
334
351
  "WebhookDeliveryResponse",
335
352
  "WebhookResponse",
336
353
  ]
File without changes
File without changes