drip-sdk 1.0.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.
- drip_sdk-1.0.0/.gitignore +73 -0
- drip_sdk-1.0.0/PKG-INFO +538 -0
- drip_sdk-1.0.0/README.md +495 -0
- drip_sdk-1.0.0/pyproject.toml +135 -0
- drip_sdk-1.0.0/src/drip/__init__.py +226 -0
- drip_sdk-1.0.0/src/drip/client.py +1538 -0
- drip_sdk-1.0.0/src/drip/errors.py +220 -0
- drip_sdk-1.0.0/src/drip/integrations/__init__.py +20 -0
- drip_sdk-1.0.0/src/drip/integrations/langchain.py +1437 -0
- drip_sdk-1.0.0/src/drip/middleware/__init__.py +58 -0
- drip_sdk-1.0.0/src/drip/middleware/core.py +754 -0
- drip_sdk-1.0.0/src/drip/middleware/fastapi.py +415 -0
- drip_sdk-1.0.0/src/drip/middleware/flask.py +381 -0
- drip_sdk-1.0.0/src/drip/middleware/types.py +174 -0
- drip_sdk-1.0.0/src/drip/models.py +698 -0
- drip_sdk-1.0.0/src/drip/stream.py +324 -0
- drip_sdk-1.0.0/src/drip/utils.py +245 -0
- drip_sdk-1.0.0/tests/__init__.py +1 -0
- drip_sdk-1.0.0/tests/test_client.py +577 -0
- drip_sdk-1.0.0/tests/test_langchain.py +924 -0
- drip_sdk-1.0.0/tests/test_middleware.py +250 -0
- drip_sdk-1.0.0/tests/test_stream.py +245 -0
- drip_sdk-1.0.0/tests/test_utils.py +190 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Environments
|
|
54
|
+
.env
|
|
55
|
+
.venv
|
|
56
|
+
env/
|
|
57
|
+
venv/
|
|
58
|
+
ENV/
|
|
59
|
+
|
|
60
|
+
# mypy
|
|
61
|
+
.mypy_cache/
|
|
62
|
+
.dmypy.json
|
|
63
|
+
dmypy.json
|
|
64
|
+
|
|
65
|
+
# ruff
|
|
66
|
+
.ruff_cache/
|
|
67
|
+
|
|
68
|
+
# IDE
|
|
69
|
+
.idea/
|
|
70
|
+
.vscode/
|
|
71
|
+
*.swp
|
|
72
|
+
*.swo
|
|
73
|
+
*~
|
drip_sdk-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drip-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Drip - usage-based billing infrastructure with on-chain settlement
|
|
5
|
+
Project-URL: Homepage, https://drip.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.drip.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/drip/sdk-python
|
|
8
|
+
Author-email: Drip <sdk@drip.dev>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: api,billing,blockchain,drip,metered-billing,payments,sdk,usage-based
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.25.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'all'
|
|
25
|
+
Requires-Dist: flask>=2.0.0; extra == 'all'
|
|
26
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'all'
|
|
27
|
+
Requires-Dist: starlette>=0.27.0; extra == 'all'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: respx>=0.20.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
35
|
+
Provides-Extra: fastapi
|
|
36
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
|
|
37
|
+
Requires-Dist: starlette>=0.27.0; extra == 'fastapi'
|
|
38
|
+
Provides-Extra: flask
|
|
39
|
+
Requires-Dist: flask>=2.0.0; extra == 'flask'
|
|
40
|
+
Provides-Extra: langchain
|
|
41
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# Drip SDK for Python
|
|
45
|
+
|
|
46
|
+
Official Python SDK for **Drip** - usage-based billing infrastructure with on-chain settlement.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install drip-sdk
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With framework support:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# FastAPI support
|
|
58
|
+
pip install drip-sdk[fastapi]
|
|
59
|
+
|
|
60
|
+
# Flask support
|
|
61
|
+
pip install drip-sdk[flask]
|
|
62
|
+
|
|
63
|
+
# All frameworks
|
|
64
|
+
pip install drip-sdk[all]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from drip import Drip
|
|
71
|
+
|
|
72
|
+
# Initialize the client
|
|
73
|
+
client = Drip(api_key="drip_sk_...")
|
|
74
|
+
|
|
75
|
+
# Create a customer
|
|
76
|
+
customer = client.create_customer(
|
|
77
|
+
onchain_address="0x1234567890abcdef...",
|
|
78
|
+
external_customer_id="user_123"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Create a charge
|
|
82
|
+
result = client.charge(
|
|
83
|
+
customer_id=customer.id,
|
|
84
|
+
meter="api_calls",
|
|
85
|
+
quantity=1
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
print(f"Charged: {result.charge.amount_usdc} USDC")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Async Support
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from drip import AsyncDrip
|
|
95
|
+
|
|
96
|
+
async with AsyncDrip(api_key="drip_sk_...") as client:
|
|
97
|
+
customer = await client.create_customer(
|
|
98
|
+
onchain_address="0x1234..."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
result = await client.charge(
|
|
102
|
+
customer_id=customer.id,
|
|
103
|
+
meter="api_calls",
|
|
104
|
+
quantity=1
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## FastAPI Integration
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from fastapi import FastAPI, Request, Depends
|
|
112
|
+
from drip.middleware.fastapi import DripMiddleware, get_drip_context, DripContext
|
|
113
|
+
|
|
114
|
+
app = FastAPI()
|
|
115
|
+
|
|
116
|
+
# Apply middleware to all routes
|
|
117
|
+
app.add_middleware(
|
|
118
|
+
DripMiddleware,
|
|
119
|
+
meter="api_calls",
|
|
120
|
+
quantity=1,
|
|
121
|
+
exclude_paths=["/health", "/docs"]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@app.post("/api/generate")
|
|
125
|
+
async def generate(request: Request):
|
|
126
|
+
drip = get_drip_context(request)
|
|
127
|
+
print(f"Charged customer {drip.customer_id}: {drip.charge.charge.amount_usdc} USDC")
|
|
128
|
+
return {"success": True}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Per-Route Configuration
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from drip.middleware.fastapi import with_drip
|
|
135
|
+
|
|
136
|
+
@app.post("/api/expensive")
|
|
137
|
+
@with_drip(meter="tokens", quantity=lambda req: calculate_tokens(req))
|
|
138
|
+
async def expensive_operation(request: Request):
|
|
139
|
+
drip = get_drip_context(request)
|
|
140
|
+
return {"charged": drip.charge.charge.amount_usdc}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Dependency Injection
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from drip.middleware.fastapi import create_drip_dependency
|
|
147
|
+
|
|
148
|
+
charge_tokens = create_drip_dependency(
|
|
149
|
+
meter="tokens",
|
|
150
|
+
quantity=100,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
@app.post("/api/generate")
|
|
154
|
+
async def generate(drip: DripContext = Depends(charge_tokens)):
|
|
155
|
+
return {"charged": drip.charge.charge.amount_usdc}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Flask Integration
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from flask import Flask
|
|
162
|
+
from drip.middleware.flask import drip_middleware, get_drip_context
|
|
163
|
+
|
|
164
|
+
app = Flask(__name__)
|
|
165
|
+
|
|
166
|
+
@app.route("/api/generate", methods=["POST"])
|
|
167
|
+
@drip_middleware(meter="api_calls", quantity=1)
|
|
168
|
+
def generate():
|
|
169
|
+
drip = get_drip_context()
|
|
170
|
+
print(f"Charged: {drip.charge.charge.amount_usdc}")
|
|
171
|
+
return {"success": True}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Application-Wide Middleware
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from drip.middleware.flask import DripFlaskMiddleware
|
|
178
|
+
|
|
179
|
+
app = Flask(__name__)
|
|
180
|
+
drip = DripFlaskMiddleware(
|
|
181
|
+
app,
|
|
182
|
+
meter="api_calls",
|
|
183
|
+
quantity=1,
|
|
184
|
+
exclude_paths=["/health"]
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Customer Resolution
|
|
189
|
+
|
|
190
|
+
The middleware needs to identify which customer to charge. Configure this with `customer_resolver`:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# From X-Drip-Customer-Id header (default)
|
|
194
|
+
DripMiddleware(app, meter="api_calls", quantity=1, customer_resolver="header")
|
|
195
|
+
|
|
196
|
+
# From query parameter (?customer_id=xxx)
|
|
197
|
+
DripMiddleware(app, meter="api_calls", quantity=1, customer_resolver="query")
|
|
198
|
+
|
|
199
|
+
# Custom resolver
|
|
200
|
+
def get_customer_from_jwt(request):
|
|
201
|
+
token = request.headers.get("Authorization")
|
|
202
|
+
return decode_jwt(token)["customer_id"]
|
|
203
|
+
|
|
204
|
+
DripMiddleware(app, meter="api_calls", quantity=1, customer_resolver=get_customer_from_jwt)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## API Reference
|
|
208
|
+
|
|
209
|
+
### Customer Management
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# Create customer
|
|
213
|
+
customer = client.create_customer(
|
|
214
|
+
onchain_address="0x...",
|
|
215
|
+
external_customer_id="user_123",
|
|
216
|
+
metadata={"plan": "pro"}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Get customer
|
|
220
|
+
customer = client.get_customer("cus_123")
|
|
221
|
+
|
|
222
|
+
# List customers
|
|
223
|
+
result = client.list_customers(status=CustomerStatus.ACTIVE, limit=50)
|
|
224
|
+
|
|
225
|
+
# Get balance
|
|
226
|
+
balance = client.get_balance("cus_123")
|
|
227
|
+
print(f"Balance: {balance.balance_usdc} USDC")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Charging
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# Create a charge
|
|
234
|
+
result = client.charge(
|
|
235
|
+
customer_id="cus_123",
|
|
236
|
+
meter="api_calls",
|
|
237
|
+
quantity=1,
|
|
238
|
+
idempotency_key="unique_key_123",
|
|
239
|
+
metadata={"request_id": "req_456"}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Check if it was a replay (idempotent)
|
|
243
|
+
if result.is_replay:
|
|
244
|
+
print("Charge was already processed")
|
|
245
|
+
|
|
246
|
+
# Get charge details
|
|
247
|
+
charge = client.get_charge("chg_123")
|
|
248
|
+
|
|
249
|
+
# List charges
|
|
250
|
+
charges = client.list_charges(customer_id="cus_123", limit=100)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Streaming Meter
|
|
254
|
+
|
|
255
|
+
For LLM token streaming and other high-frequency metering scenarios:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from drip import Drip
|
|
259
|
+
|
|
260
|
+
client = Drip(api_key="drip_sk_...")
|
|
261
|
+
|
|
262
|
+
# Create a stream meter for a customer
|
|
263
|
+
meter = client.create_stream_meter(
|
|
264
|
+
customer_id="cust_abc123",
|
|
265
|
+
meter="tokens",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Stream from your LLM provider
|
|
269
|
+
for chunk in openai_stream:
|
|
270
|
+
tokens = chunk.usage.completion_tokens if chunk.usage else 1
|
|
271
|
+
meter.add_sync(tokens) # Accumulates locally, no API call
|
|
272
|
+
yield chunk
|
|
273
|
+
|
|
274
|
+
# Single charge at end of stream
|
|
275
|
+
result = meter.flush()
|
|
276
|
+
print(f"Charged {result.charge.amount_usdc} USDC for {result.quantity} tokens")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### Async Streaming
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
async with AsyncDrip(api_key="drip_sk_...") as client:
|
|
283
|
+
meter = client.create_stream_meter(
|
|
284
|
+
customer_id="cust_abc123",
|
|
285
|
+
meter="tokens",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async for chunk in openai_stream:
|
|
289
|
+
await meter.add(chunk.tokens) # May auto-flush if threshold set
|
|
290
|
+
|
|
291
|
+
result = await meter.flush_async()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Stream Meter Options
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
meter = client.create_stream_meter(
|
|
298
|
+
customer_id="cust_abc123",
|
|
299
|
+
meter="tokens",
|
|
300
|
+
|
|
301
|
+
# Optional: Custom idempotency key
|
|
302
|
+
idempotency_key="stream_req_123",
|
|
303
|
+
|
|
304
|
+
# Optional: Metadata attached to the charge
|
|
305
|
+
metadata={"model": "gpt-4", "endpoint": "/v1/chat"},
|
|
306
|
+
|
|
307
|
+
# Optional: Auto-flush when threshold reached
|
|
308
|
+
flush_threshold=10000, # Flush every 10k tokens
|
|
309
|
+
|
|
310
|
+
# Optional: Callback on each add
|
|
311
|
+
on_add=lambda qty, total: print(f"Added {qty}, total: {total}"),
|
|
312
|
+
)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### Handling Partial Failures
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
meter = client.create_stream_meter(
|
|
319
|
+
customer_id="cust_abc123",
|
|
320
|
+
meter="tokens",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
for chunk in stream:
|
|
325
|
+
meter.add_sync(chunk.tokens)
|
|
326
|
+
yield chunk
|
|
327
|
+
meter.flush()
|
|
328
|
+
except Exception as error:
|
|
329
|
+
# Charge for tokens delivered before failure
|
|
330
|
+
if meter.total > 0:
|
|
331
|
+
meter.flush()
|
|
332
|
+
raise
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Checkout (Fiat On-Ramp)
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
# Create checkout session
|
|
339
|
+
checkout = client.checkout(
|
|
340
|
+
amount=5000, # $50.00 in cents
|
|
341
|
+
return_url="https://yourapp.com/success",
|
|
342
|
+
customer_id="cus_123"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Redirect user to checkout.url
|
|
346
|
+
print(f"Checkout URL: {checkout.url}")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Webhooks
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
# Create webhook
|
|
353
|
+
webhook = client.create_webhook(
|
|
354
|
+
url="https://yourapp.com/webhooks/drip",
|
|
355
|
+
events=["charge.succeeded", "customer.balance.low"],
|
|
356
|
+
description="Production webhook"
|
|
357
|
+
)
|
|
358
|
+
# IMPORTANT: Store webhook.secret securely!
|
|
359
|
+
|
|
360
|
+
# Verify webhook signature
|
|
361
|
+
from drip import verify_webhook_signature
|
|
362
|
+
|
|
363
|
+
is_valid = verify_webhook_signature(
|
|
364
|
+
payload=request.body,
|
|
365
|
+
signature=request.headers["X-Drip-Signature"],
|
|
366
|
+
secret=webhook_secret
|
|
367
|
+
)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Agent Run Tracking
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
# Create a workflow
|
|
374
|
+
workflow = client.create_workflow(
|
|
375
|
+
name="Text Generation",
|
|
376
|
+
slug="text-generation",
|
|
377
|
+
product_surface="AGENT"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Start a run
|
|
381
|
+
run = client.start_run(
|
|
382
|
+
customer_id="cus_123",
|
|
383
|
+
workflow_id=workflow.id,
|
|
384
|
+
correlation_id="trace_456"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Emit events
|
|
388
|
+
event = client.emit_event(
|
|
389
|
+
run_id=run.id,
|
|
390
|
+
event_type="tokens.generated",
|
|
391
|
+
quantity=1500,
|
|
392
|
+
units="tokens",
|
|
393
|
+
cost_units=0.015
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# End the run
|
|
397
|
+
result = client.end_run(run.id, status="COMPLETED")
|
|
398
|
+
|
|
399
|
+
# Get timeline
|
|
400
|
+
timeline = client.get_run_timeline(run.id)
|
|
401
|
+
print(f"Total cost: {timeline.totals.total_cost_units}")
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Simplified Record Run API
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
# Record a complete run in one call
|
|
408
|
+
result = client.record_run(
|
|
409
|
+
customer_id="cus_123",
|
|
410
|
+
workflow="text-generation", # Creates workflow if doesn't exist
|
|
411
|
+
events=[
|
|
412
|
+
{"eventType": "prompt.received", "quantity": 100, "units": "tokens"},
|
|
413
|
+
{"eventType": "completion.generated", "quantity": 500, "units": "tokens"},
|
|
414
|
+
{"eventType": "tool.called", "description": "web_search"},
|
|
415
|
+
],
|
|
416
|
+
status="COMPLETED"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
print(f"Run ID: {result.run.id}")
|
|
420
|
+
print(f"Total cost: {result.total_cost_units}")
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### LangChain Integration
|
|
424
|
+
|
|
425
|
+
Auto-track token usage and tool calls with LangChain:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
from drip.integrations.langchain import DripCallbackHandler
|
|
429
|
+
from langchain_openai import ChatOpenAI
|
|
430
|
+
|
|
431
|
+
handler = DripCallbackHandler(
|
|
432
|
+
api_key="drip_sk_...",
|
|
433
|
+
customer_id="cust_123",
|
|
434
|
+
workflow="chatbot",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
llm = ChatOpenAI(model="gpt-4", callbacks=[handler])
|
|
438
|
+
response = llm.invoke("Hello!") # Automatically metered and billed
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Built-in pricing for popular models (GPT-4, GPT-3.5, Claude 3, etc.) with automatic cost calculation.
|
|
442
|
+
|
|
443
|
+
## Idempotency
|
|
444
|
+
|
|
445
|
+
Prevent duplicate charges with idempotency keys:
|
|
446
|
+
|
|
447
|
+
```python
|
|
448
|
+
# Generate deterministic key
|
|
449
|
+
key = Drip.generate_idempotency_key(
|
|
450
|
+
customer_id="cus_123",
|
|
451
|
+
step_name="process_document",
|
|
452
|
+
run_id="run_456",
|
|
453
|
+
sequence=1
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Use in charge
|
|
457
|
+
result = client.charge(
|
|
458
|
+
customer_id="cus_123",
|
|
459
|
+
meter="api_calls",
|
|
460
|
+
quantity=1,
|
|
461
|
+
idempotency_key=key
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Safe to retry - won't double charge
|
|
465
|
+
if result.is_replay:
|
|
466
|
+
print("Already processed")
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Error Handling
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
from drip import (
|
|
473
|
+
DripError,
|
|
474
|
+
DripAPIError,
|
|
475
|
+
DripAuthenticationError,
|
|
476
|
+
DripPaymentRequiredError,
|
|
477
|
+
DripRateLimitError,
|
|
478
|
+
DripNetworkError,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
result = client.charge(customer_id="cus_123", meter="api_calls", quantity=1)
|
|
483
|
+
except DripAuthenticationError:
|
|
484
|
+
print("Invalid API key")
|
|
485
|
+
except DripPaymentRequiredError as e:
|
|
486
|
+
print(f"Insufficient balance: {e.payment_request}")
|
|
487
|
+
except DripRateLimitError as e:
|
|
488
|
+
print(f"Rate limited, retry after {e.retry_after} seconds")
|
|
489
|
+
except DripNetworkError:
|
|
490
|
+
print("Network error, please retry")
|
|
491
|
+
except DripAPIError as e:
|
|
492
|
+
print(f"API error {e.status_code}: {e.message}")
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Environment Variables
|
|
496
|
+
|
|
497
|
+
| Variable | Description |
|
|
498
|
+
|----------|-------------|
|
|
499
|
+
| `DRIP_API_KEY` | Your Drip API key |
|
|
500
|
+
| `DRIP_API_URL` | Custom API base URL (default: https://api.drip.dev/v1) |
|
|
501
|
+
| `DRIP_ENV` | Environment (development/production) for `skip_in_development` |
|
|
502
|
+
|
|
503
|
+
## Development Mode
|
|
504
|
+
|
|
505
|
+
Skip charging in development:
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
DripMiddleware(
|
|
509
|
+
app,
|
|
510
|
+
meter="api_calls",
|
|
511
|
+
quantity=1,
|
|
512
|
+
skip_in_development=True # Skips charging when DRIP_ENV=development
|
|
513
|
+
)
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Requirements
|
|
517
|
+
|
|
518
|
+
- Python 3.10+
|
|
519
|
+
- httpx
|
|
520
|
+
- pydantic
|
|
521
|
+
|
|
522
|
+
## Feature Summary
|
|
523
|
+
|
|
524
|
+
| Feature | Status | Description |
|
|
525
|
+
|---------|--------|-------------|
|
|
526
|
+
| FastAPI Middleware | ✅ | `DripMiddleware` |
|
|
527
|
+
| Flask Decorator | ✅ | `@drip_middleware` |
|
|
528
|
+
| Async Support | ✅ | `AsyncDrip` client |
|
|
529
|
+
| Streaming Meter | ✅ | For LLM token streams |
|
|
530
|
+
| LangChain Integration | ✅ | `DripCallbackHandler` |
|
|
531
|
+
| x402 Payment Flow | ✅ | Automatic 402 handling |
|
|
532
|
+
| Webhook Verification | ✅ | HMAC-SHA256 |
|
|
533
|
+
| Idempotency | ✅ | Built-in or custom keys |
|
|
534
|
+
| Type Safety | ✅ | Full Pydantic models |
|
|
535
|
+
|
|
536
|
+
## License
|
|
537
|
+
|
|
538
|
+
MIT
|