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.
- {meterflow-0.2.0 → meterflow-0.4.0}/PKG-INFO +20 -4
- {meterflow-0.2.0 → meterflow-0.4.0}/README.md +19 -3
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/_http.py +11 -5
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/_version.py +1 -1
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/entitlements.py +8 -2
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/subscriptions.py +34 -9
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/webhooks.py +39 -1
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/types.py +18 -1
- {meterflow-0.2.0 → meterflow-0.4.0}/LICENSE +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/pyproject.toml +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/__init__.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/_retry.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/client.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/errors.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/py.typed +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/__init__.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/credits.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/plans.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/portal.py +0 -0
- {meterflow-0.2.0 → meterflow-0.4.0}/src/meterflow/resources/usage.py +0 -0
- {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.
|
|
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.
|
|
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 —
|
|
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.
|
|
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 —
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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(
|
|
19
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|