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.
@@ -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,8 @@
1
+ pyproject.toml
2
+ metr/__init__.py
3
+ metr/middleware.py
4
+ metr_sdk.egg-info/PKG-INFO
5
+ metr_sdk.egg-info/SOURCES.txt
6
+ metr_sdk.egg-info/dependency_links.txt
7
+ metr_sdk.egg-info/requires.txt
8
+ metr_sdk.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ httpx>=0.25.0
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ pytest-asyncio>=0.21
6
+ fastapi>=0.104.0
7
+ uvicorn>=0.24.0
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+