metr-sdk 0.1.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.
- metr_sdk-0.1.0/PKG-INFO +14 -0
- metr_sdk-0.1.0/metr/__init__.py +16 -0
- metr_sdk-0.1.0/metr/middleware.py +149 -0
- metr_sdk-0.1.0/metr_sdk.egg-info/PKG-INFO +14 -0
- metr_sdk-0.1.0/metr_sdk.egg-info/SOURCES.txt +8 -0
- metr_sdk-0.1.0/metr_sdk.egg-info/dependency_links.txt +1 -0
- metr_sdk-0.1.0/metr_sdk.egg-info/requires.txt +7 -0
- metr_sdk-0.1.0/metr_sdk.egg-info/top_level.txt +1 -0
- metr_sdk-0.1.0/pyproject.toml +23 -0
- metr_sdk-0.1.0/setup.cfg +4 -0
metr_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metr-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One line of code to monetize any FastAPI endpoint
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: metr,api,monetization,metering,billing,stripe,fastapi
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.25.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
13
|
+
Requires-Dist: fastapi>=0.104.0; extra == "dev"
|
|
14
|
+
Requires-Dist: uvicorn>=0.24.0; extra == "dev"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
metr - One line of code to monetize any FastAPI endpoint.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from metr import require_payment
|
|
6
|
+
|
|
7
|
+
@app.post("/api/summarize")
|
|
8
|
+
@require_payment(price=0.001)
|
|
9
|
+
async def summarize():
|
|
10
|
+
return {"summary": "Your text summary..."}
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .middleware import require_payment, MetrConfig, MetrPaymentInfo
|
|
14
|
+
|
|
15
|
+
__all__ = ["require_payment", "MetrConfig", "MetrPaymentInfo"]
|
|
16
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""metr FastAPI middleware - one decorator to monetize any endpoint."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from fastapi import HTTPException, Request
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_GATEWAY_URL = "https://api.metr.dev"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MetrConfig:
|
|
18
|
+
"""Configuration for metr middleware."""
|
|
19
|
+
price: float
|
|
20
|
+
unit_type: str = "request"
|
|
21
|
+
api_key: Optional[str] = None
|
|
22
|
+
endpoint_id: Optional[str] = None
|
|
23
|
+
gateway_url: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MetrPaymentInfo:
|
|
28
|
+
"""Payment info attached to verified requests."""
|
|
29
|
+
session_token: str
|
|
30
|
+
buyer_id: str
|
|
31
|
+
balance_usd: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def require_payment(
|
|
35
|
+
price: float,
|
|
36
|
+
unit_type: str = "request",
|
|
37
|
+
api_key: Optional[str] = None,
|
|
38
|
+
endpoint_id: Optional[str] = None,
|
|
39
|
+
gateway_url: Optional[str] = None,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Decorator that requires payment for a FastAPI endpoint.
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
@app.post("/api/summarize")
|
|
46
|
+
@require_payment(price=0.001)
|
|
47
|
+
async def summarize(request: Request):
|
|
48
|
+
payment = request.state.metr # MetrPaymentInfo
|
|
49
|
+
return {"summary": "..."}
|
|
50
|
+
"""
|
|
51
|
+
_api_key = api_key or os.environ.get("METR_API_KEY")
|
|
52
|
+
_endpoint_id = endpoint_id or os.environ.get("METR_ENDPOINT_ID")
|
|
53
|
+
_gateway_url = gateway_url or os.environ.get("METR_GATEWAY_URL", DEFAULT_GATEWAY_URL)
|
|
54
|
+
|
|
55
|
+
if not _api_key:
|
|
56
|
+
raise ValueError("metr: API key required. Set api_key or METR_API_KEY env var.")
|
|
57
|
+
|
|
58
|
+
def decorator(func):
|
|
59
|
+
@wraps(func)
|
|
60
|
+
async def wrapper(*args, **kwargs):
|
|
61
|
+
# Find the Request object in args/kwargs
|
|
62
|
+
request = kwargs.get("request")
|
|
63
|
+
if request is None:
|
|
64
|
+
for arg in args:
|
|
65
|
+
if isinstance(arg, Request):
|
|
66
|
+
request = arg
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
if request is None:
|
|
70
|
+
raise HTTPException(status_code=500, detail="metr: Request object not found")
|
|
71
|
+
|
|
72
|
+
# Extract session token
|
|
73
|
+
auth_header = request.headers.get("authorization", "")
|
|
74
|
+
session_token = None
|
|
75
|
+
if auth_header.startswith("Bearer metr_sess_"):
|
|
76
|
+
session_token = auth_header[7:] # Strip "Bearer "
|
|
77
|
+
|
|
78
|
+
if not session_token:
|
|
79
|
+
# No session — create checkout
|
|
80
|
+
async with httpx.AsyncClient() as client:
|
|
81
|
+
checkout_res = await client.post(
|
|
82
|
+
f"{_gateway_url}/api/v1/checkout/create",
|
|
83
|
+
headers={"X-Metr-Api-Key": _api_key},
|
|
84
|
+
json={
|
|
85
|
+
"endpoint_id": _endpoint_id,
|
|
86
|
+
"amount_usd": price,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
checkout = checkout_res.json()
|
|
90
|
+
|
|
91
|
+
return JSONResponse(
|
|
92
|
+
status_code=402,
|
|
93
|
+
content={
|
|
94
|
+
"error": "payment_required",
|
|
95
|
+
"message": "This API requires payment. Complete checkout to get access.",
|
|
96
|
+
"checkout_url": checkout.get("checkout_url"),
|
|
97
|
+
"price": price,
|
|
98
|
+
"unit_type": unit_type,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Verify session
|
|
103
|
+
async with httpx.AsyncClient() as client:
|
|
104
|
+
verify_res = await client.post(
|
|
105
|
+
f"{_gateway_url}/api/v1/verify",
|
|
106
|
+
headers={"X-Metr-Api-Key": _api_key},
|
|
107
|
+
json={"session_token": session_token},
|
|
108
|
+
)
|
|
109
|
+
session = verify_res.json()
|
|
110
|
+
|
|
111
|
+
if not session.get("valid"):
|
|
112
|
+
return JSONResponse(
|
|
113
|
+
status_code=402,
|
|
114
|
+
content={
|
|
115
|
+
"error": "session_expired",
|
|
116
|
+
"message": "Your billing session has expired.",
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Attach payment info to request
|
|
121
|
+
request.state.metr = MetrPaymentInfo(
|
|
122
|
+
session_token=session_token,
|
|
123
|
+
buyer_id=session.get("buyer_id", ""),
|
|
124
|
+
balance_usd=session.get("remaining_balance_usd", 0),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Execute handler
|
|
128
|
+
result = await func(*args, **kwargs)
|
|
129
|
+
|
|
130
|
+
# Record usage (fire and forget)
|
|
131
|
+
try:
|
|
132
|
+
async with httpx.AsyncClient() as client:
|
|
133
|
+
await client.post(
|
|
134
|
+
f"{_gateway_url}/api/v1/meter",
|
|
135
|
+
headers={"X-Metr-Api-Key": _api_key},
|
|
136
|
+
json={
|
|
137
|
+
"endpoint_id": _endpoint_id,
|
|
138
|
+
"session_token": session_token,
|
|
139
|
+
"units": 1,
|
|
140
|
+
"unit_type": unit_type,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass # Don't fail the request if metering fails
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
return wrapper
|
|
149
|
+
return decorator
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metr-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One line of code to monetize any FastAPI endpoint
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: metr,api,monetization,metering,billing,stripe,fastapi
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.25.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
13
|
+
Requires-Dist: fastapi>=0.104.0; extra == "dev"
|
|
14
|
+
Requires-Dist: uvicorn>=0.24.0; extra == "dev"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
metr
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "metr-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "One line of code to monetize any FastAPI endpoint"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["metr", "api", "monetization", "metering", "billing", "stripe", "fastapi"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.25.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=7.0",
|
|
20
|
+
"pytest-asyncio>=0.21",
|
|
21
|
+
"fastapi>=0.104.0",
|
|
22
|
+
"uvicorn>=0.24.0",
|
|
23
|
+
]
|
metr_sdk-0.1.0/setup.cfg
ADDED