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.
@@ -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
+ *~
@@ -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