usdf 0.1.2__py3-none-any.whl

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.
usdf/__init__.py ADDED
@@ -0,0 +1,102 @@
1
+ """Client for the USDF metered inference gateway."""
2
+
3
+ from .client import DEFAULT_BASE_URL, SDK_VERSION, USDF, ChatCompletionStream, RunResult, X402
4
+ from .errors import USDFError
5
+ from .estimate import cost_units, estimate_prompt_tokens, max_cost_units, max_prompt_tokens, units
6
+ from .types import (
7
+ Account,
8
+ Amount,
9
+ ApiKey,
10
+ Balance,
11
+ ChatCompletion,
12
+ CreatedKey,
13
+ KeyLimits,
14
+ Pricing,
15
+ PricingModel,
16
+ Quote,
17
+ Receipt,
18
+ ReceiptLookup,
19
+ Session,
20
+ X402Receipt,
21
+ )
22
+ from .x402 import (
23
+ CHAIN_ID,
24
+ DEFAULT_MAX_AMOUNT,
25
+ GATEWAY_PAYEE,
26
+ GATEWAY_ORIGIN,
27
+ PERMIT2_ADDRESS,
28
+ PUBLIC_RPC_URL,
29
+ USDF_ADDRESS,
30
+ USDG_ADDRESS,
31
+ X402_EXACT_PERMIT2_PROXY,
32
+ PaymentLimits,
33
+ Signer,
34
+ check_accept,
35
+ choose_accept,
36
+ decode_payment_required,
37
+ decode_payment_response,
38
+ decode_uint256,
39
+ encode_payment_signature,
40
+ approve_permit2,
41
+ eth_account_signer,
42
+ permit2_allowance_call,
43
+ permit2_approval_call,
44
+ payment_limits,
45
+ read_permit2_allowance,
46
+ sign_exact_payment,
47
+ wallet_signer,
48
+ )
49
+
50
+ __all__ = [
51
+ "USDF",
52
+ "USDFError",
53
+ "X402",
54
+ "DEFAULT_BASE_URL",
55
+ "SDK_VERSION",
56
+ "RunResult",
57
+ "ChatCompletionStream",
58
+ "Account",
59
+ "Amount",
60
+ "ApiKey",
61
+ "Balance",
62
+ "ChatCompletion",
63
+ "CreatedKey",
64
+ "KeyLimits",
65
+ "Pricing",
66
+ "PricingModel",
67
+ "Quote",
68
+ "Receipt",
69
+ "ReceiptLookup",
70
+ "Session",
71
+ "X402Receipt",
72
+ "cost_units",
73
+ "estimate_prompt_tokens",
74
+ "max_cost_units",
75
+ "max_prompt_tokens",
76
+ "units",
77
+ "PERMIT2_ADDRESS",
78
+ "X402_EXACT_PERMIT2_PROXY",
79
+ "CHAIN_ID",
80
+ "USDF_ADDRESS",
81
+ "USDG_ADDRESS",
82
+ "GATEWAY_PAYEE",
83
+ "GATEWAY_ORIGIN",
84
+ "PUBLIC_RPC_URL",
85
+ "DEFAULT_MAX_AMOUNT",
86
+ "PaymentLimits",
87
+ "payment_limits",
88
+ "check_accept",
89
+ "Signer",
90
+ "choose_accept",
91
+ "decode_payment_required",
92
+ "decode_payment_response",
93
+ "decode_uint256",
94
+ "encode_payment_signature",
95
+ "eth_account_signer",
96
+ "permit2_allowance_call",
97
+ "permit2_approval_call",
98
+ "sign_exact_payment",
99
+ "wallet_signer",
100
+ "read_permit2_allowance",
101
+ "approve_permit2",
102
+ ]
usdf/_http.py ADDED
@@ -0,0 +1,91 @@
1
+ """HTTP transport: httpx when installed (and not disabled), the standard library's urllib otherwise.
2
+
3
+ Both return the same shape so the client never knows which ran: a status, lower-cased headers, and
4
+ either the whole body or an iterator of byte chunks for streams.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import urllib.error
12
+ import urllib.request
13
+ from dataclasses import dataclass
14
+ from typing import Any, Dict, Iterator, Optional
15
+
16
+ try: # optional
17
+ import httpx # type: ignore
18
+ except Exception: # pragma: no cover - depends on the environment
19
+ httpx = None
20
+
21
+
22
+ @dataclass
23
+ class Response:
24
+ status: int
25
+ headers: Dict[str, str]
26
+ body: bytes
27
+
28
+ def json(self) -> Any:
29
+ if not self.body:
30
+ return None
31
+ try:
32
+ return json.loads(self.body.decode("utf-8"))
33
+ except ValueError:
34
+ return None
35
+
36
+
37
+ @dataclass
38
+ class StreamResponse:
39
+ status: int
40
+ headers: Dict[str, str]
41
+ chunks: Iterator[bytes]
42
+ close: Any
43
+
44
+
45
+ def pick_transport(name: Optional[str]) -> str:
46
+ name = name or os.environ.get("USDF_TRANSPORT") or ("urllib" if os.environ.get("USDF_NO_HTTPX") else None)
47
+ if name == "httpx":
48
+ if httpx is None:
49
+ raise RuntimeError("httpx is not installed; pip install usdf[httpx]")
50
+ return "httpx"
51
+ if name == "urllib":
52
+ return "urllib"
53
+ return "httpx" if httpx is not None else "urllib"
54
+
55
+
56
+ class Transport:
57
+ def __init__(self, name: Optional[str] = None):
58
+ self.name = pick_transport(name)
59
+ self._client = httpx.Client(timeout=None) if self.name == "httpx" else None
60
+
61
+ def request(self, method: str, url: str, headers: Dict[str, str], body: Optional[bytes], timeout: float) -> Response:
62
+ if self._client is not None:
63
+ r = self._client.request(method, url, headers=headers, content=body, timeout=timeout)
64
+ return Response(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.content)
65
+ req = urllib.request.Request(url, data=body, method=method, headers=headers)
66
+ try:
67
+ with urllib.request.urlopen(req, timeout=timeout) as res:
68
+ return Response(res.status, {k.lower(): v for k, v in res.headers.items()}, res.read())
69
+ except urllib.error.HTTPError as e:
70
+ return Response(e.code, {k.lower(): v for k, v in e.headers.items()}, e.read())
71
+
72
+ def stream(self, method: str, url: str, headers: Dict[str, str], body: Optional[bytes], timeout: float) -> StreamResponse:
73
+ if self._client is not None:
74
+ req = self._client.build_request(method, url, headers=headers, content=body, timeout=timeout)
75
+ r = self._client.send(req, stream=True)
76
+ return StreamResponse(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.iter_bytes(), r.close)
77
+ req = urllib.request.Request(url, data=body, method=method, headers=headers)
78
+ try:
79
+ res = urllib.request.urlopen(req, timeout=timeout)
80
+ except urllib.error.HTTPError as e:
81
+ data = e.read()
82
+ return StreamResponse(e.code, {k.lower(): v for k, v in e.headers.items()}, iter([data]), lambda: None)
83
+
84
+ def chunks() -> Iterator[bytes]:
85
+ while True:
86
+ piece = res.read1(8192) if hasattr(res, "read1") else res.read(8192)
87
+ if not piece:
88
+ return
89
+ yield piece
90
+
91
+ return StreamResponse(res.status, {k.lower(): v for k, v in res.headers.items()}, chunks(), res.close)
usdf/accept.py ADDED
@@ -0,0 +1,187 @@
1
+ """Accept USDF in one integration: charge your own API's callers in USDF over x402, settled by Foundry's facilitator
2
+ and paid on to your payout address in USDF (or in USDG, 1:1, with settle_as="usdg"). Your server needs your Foundry API
3
+ key (an approved partner's, in USDF_API_KEY) and a price.
4
+
5
+ from usdf.accept import AcceptUSDF
6
+
7
+ paid = AcceptUSDF(price="0.01")
8
+ app = paid.wsgi(app) # any WSGI app (Flask's app.wsgi_app, Django's application)
9
+
10
+ Or for one request, whatever the framework:
11
+
12
+ result = paid.charge(url, headers)
13
+ if not result.paid:
14
+ return result.status, result.headers, result.body # 402 asking for payment, or a refusal
15
+ # serve it; add result.headers (PAYMENT-RESPONSE) to the answer
16
+
17
+ A request without a payment is answered 402 with the PAYMENT-REQUIRED header any x402 client reads; one with a payment
18
+ (PAYMENT-SIGNATURE, or X-PAYMENT) is settled before the app runs, and the app runs only for a payment that settled.
19
+ Standard library only."""
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import re
26
+ import urllib.error
27
+ import urllib.request
28
+ from dataclasses import dataclass, field
29
+ from typing import Any, Callable, Dict, Iterable, Mapping, Optional, Union
30
+
31
+ DEFAULT_FACILITATOR_URL = "https://api.usdf.fi"
32
+ _PRICE = re.compile(r"^\d{1,5}(\.\d{1,6})?$")
33
+
34
+
35
+ @dataclass
36
+ class Accepted:
37
+ """A settled payment: its transaction, payer and network, and what it came to for you."""
38
+
39
+ transaction: str
40
+ payer: str
41
+ network: str
42
+ amount: str
43
+ fee: str
44
+ paid_on: str
45
+ settle_as: str
46
+ settlement_id: str
47
+
48
+
49
+ @dataclass
50
+ class ChargeResult:
51
+ """paid: the payment settled (payment holds it; add headers to your answer). Otherwise answer with status,
52
+ headers and body instead of running the request."""
53
+
54
+ paid: bool
55
+ status: int = 200
56
+ headers: Dict[str, str] = field(default_factory=dict)
57
+ body: bytes = b""
58
+ payment: Optional[Accepted] = None
59
+
60
+
61
+ def _refusal(status: int, code: str, message: str) -> ChargeResult:
62
+ body = json.dumps({"error": {"message": message, "type": "payment_error", "code": code}}).encode()
63
+ return ChargeResult(paid=False, status=status, headers={"content-type": "application/json"}, body=body)
64
+
65
+
66
+ def _header(headers: Mapping[str, str], name: str) -> Optional[str]:
67
+ for k, v in headers.items():
68
+ if k.lower() == name:
69
+ return v
70
+ return None
71
+
72
+
73
+ class AcceptUSDF:
74
+ """Charges each request `price` USDF (a string like "0.01", or a function of the request's URL and headers)."""
75
+
76
+ def __init__(
77
+ self,
78
+ price: Union[str, Callable[[str, Mapping[str, str]], str]],
79
+ api_key: Optional[str] = None,
80
+ settle_as: str = "usdf",
81
+ description: str = "",
82
+ mime_type: str = "application/json",
83
+ base_url: Optional[str] = None,
84
+ timeout: float = 30.0,
85
+ opener: Optional[Callable[..., Any]] = None,
86
+ ) -> None:
87
+ if settle_as not in ("usdf", "usdg"):
88
+ raise ValueError('settle_as is "usdf" or "usdg"')
89
+ self.price = price
90
+ self.api_key = api_key
91
+ self.settle_as = settle_as
92
+ self.description = description
93
+ self.mime_type = mime_type
94
+ self.base_url = (base_url or os.environ.get("USDF_BASE_URL") or DEFAULT_FACILITATOR_URL).rstrip("/")
95
+ self.timeout = timeout
96
+ self._open = opener or urllib.request.urlopen
97
+
98
+ def charge(self, url: str, headers: Mapping[str, str]) -> ChargeResult:
99
+ """One request checked: settled, or the answer to give instead."""
100
+ key = self.api_key or os.environ.get("USDF_API_KEY")
101
+ if not key:
102
+ return _refusal(503, "not_configured", "this API takes USDF, but its key is not set")
103
+ price = self.price(url, headers) if callable(self.price) else self.price
104
+ if not isinstance(price, str) or not _PRICE.match(price.strip()):
105
+ return _refusal(500, "bad_price", "this API's price is not an amount of USDF")
106
+ payment = _header(headers, "payment-signature") or _header(headers, "x-payment")
107
+ body: Dict[str, Any] = {
108
+ "amount": price.strip(),
109
+ "settle_as": self.settle_as,
110
+ "resource": {"url": url, "description": self.description, "mime_type": self.mime_type},
111
+ }
112
+ if payment:
113
+ body["payment"] = payment
114
+ req = urllib.request.Request(
115
+ self.base_url + "/v1/facilitator/charge",
116
+ data=json.dumps(body).encode(),
117
+ method="POST",
118
+ headers={"authorization": "Bearer " + key, "content-type": "application/json"},
119
+ )
120
+ try:
121
+ with self._open(req, timeout=self.timeout) as r:
122
+ status, raw, got = r.status, r.read(), {k.lower(): v for k, v in r.headers.items()}
123
+ except urllib.error.HTTPError as e:
124
+ status, raw, got = e.code, e.read(), {k.lower(): v for k, v in (e.headers or {}).items()}
125
+ except (urllib.error.URLError, OSError, ValueError):
126
+ return _refusal(503, "facilitator_unreachable", "the payment could not be checked just now; nothing was charged, try again")
127
+ if status == 402:
128
+ out = {"content-type": "application/json"}
129
+ for h in ("payment-required", "www-authenticate"):
130
+ if got.get(h):
131
+ out[h] = got[h]
132
+ return ChargeResult(paid=False, status=402, headers=out, body=raw)
133
+ if status != 200:
134
+ code = "payment_unavailable"
135
+ try:
136
+ code = str(json.loads(raw.decode() or "{}").get("error", {}).get("code") or code)
137
+ except (ValueError, AttributeError):
138
+ pass
139
+ return _refusal(429 if status == 429 else 503, code, "the payment could not be taken just now; nothing was charged, try again")
140
+ try:
141
+ v = json.loads(raw.decode())
142
+ except ValueError:
143
+ return _refusal(503, "payment_unavailable", "the payment could not be taken just now; try again")
144
+ if v.get("paid") is not True or not isinstance(v.get("transaction"), str):
145
+ return _refusal(503, "payment_unavailable", "the payment could not be taken just now; try again")
146
+ response = v.get("payment_response") or got.get("payment-response")
147
+ s = v.get("settlement") or {}
148
+
149
+ def usdf(x: Any) -> str:
150
+ return str((x or {}).get("usdf", ""))
151
+
152
+ return ChargeResult(
153
+ paid=True,
154
+ headers={"payment-response": response, "x-payment-response": response} if response else {},
155
+ payment=Accepted(
156
+ transaction=v["transaction"],
157
+ payer=str(v.get("payer", "")),
158
+ network=str(v.get("network", "")),
159
+ amount=usdf(s.get("amount")),
160
+ fee=usdf(s.get("fee")),
161
+ paid_on=usdf(s.get("paid_on")),
162
+ settle_as="usdg" if s.get("settle_as") == "usdg" else "usdf",
163
+ settlement_id=str(s.get("id", "")),
164
+ ),
165
+ )
166
+
167
+ def wsgi(self, app: Callable[..., Iterable[bytes]]) -> Callable[..., Iterable[bytes]]:
168
+ """A WSGI app that charges every request before `app` runs it (the payment in environ["usdf.payment"])."""
169
+
170
+ def wrapped(environ: Dict[str, Any], start_response: Callable[..., Any]) -> Iterable[bytes]:
171
+ headers = {k[5:].replace("_", "-").lower(): v for k, v in environ.items() if k.startswith("HTTP_")}
172
+ scheme = environ.get("HTTP_X_FORWARDED_PROTO", environ.get("wsgi.url_scheme", "https")).split(",")[0].strip()
173
+ host = environ.get("HTTP_X_FORWARDED_HOST") or environ.get("HTTP_HOST") or environ.get("SERVER_NAME", "localhost")
174
+ path = environ.get("PATH_INFO", "/") + (("?" + environ["QUERY_STRING"]) if environ.get("QUERY_STRING") else "")
175
+ result = self.charge(f"{'http' if scheme == 'http' else 'https'}://{host}{path}", headers)
176
+ if not result.paid:
177
+ reason = {402: "Payment Required", 429: "Too Many Requests", 500: "Internal Server Error"}.get(result.status, "Service Unavailable")
178
+ start_response(f"{result.status} {reason}", list(result.headers.items()))
179
+ return [result.body]
180
+ environ["usdf.payment"] = result.payment
181
+
182
+ def start(status: str, response_headers: list, exc_info: Any = None) -> Any:
183
+ return start_response(status, list(response_headers) + list(result.headers.items()), exc_info) if exc_info else start_response(status, list(response_headers) + list(result.headers.items()))
184
+
185
+ return app(environ, start)
186
+
187
+ return wrapped