alloc-context 0.1.0__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.
- alloc_context-0.1.0.dist-info/METADATA +154 -0
- alloc_context-0.1.0.dist-info/RECORD +85 -0
- alloc_context-0.1.0.dist-info/WHEEL +5 -0
- alloc_context-0.1.0.dist-info/entry_points.txt +4 -0
- alloc_context-0.1.0.dist-info/licenses/LICENSE +21 -0
- alloc_context-0.1.0.dist-info/top_level.txt +1 -0
- alloccontext/__init__.py +3 -0
- alloccontext/__main__.py +149 -0
- alloccontext/config.py +415 -0
- alloccontext/horizon.py +30 -0
- alloccontext/ingest/__init__.py +1 -0
- alloccontext/ingest/cf_benchmarks.py +38 -0
- alloccontext/ingest/cf_history.py +65 -0
- alloccontext/ingest/coinbase_client.py +234 -0
- alloccontext/ingest/coinbase_portfolio.py +53 -0
- alloccontext/ingest/coingecko.py +148 -0
- alloccontext/ingest/coinmarketcap.py +135 -0
- alloccontext/ingest/env_keys.py +12 -0
- alloccontext/ingest/etf_flows.py +282 -0
- alloccontext/ingest/exchange/__init__.py +4 -0
- alloccontext/ingest/exchange/coinbase_adapter.py +64 -0
- alloccontext/ingest/exchange/kraken_adapter.py +66 -0
- alloccontext/ingest/exchange/live.py +95 -0
- alloccontext/ingest/exchange/portfolio.py +8 -0
- alloccontext/ingest/exchange/registry.py +27 -0
- alloccontext/ingest/exchange/types.py +5 -0
- alloccontext/ingest/exchange_http.py +28 -0
- alloccontext/ingest/fear_greed.py +89 -0
- alloccontext/ingest/fred.py +138 -0
- alloccontext/ingest/http_errors.py +29 -0
- alloccontext/ingest/kalshi.py +84 -0
- alloccontext/ingest/kalshi_api.py +199 -0
- alloccontext/ingest/kalshi_client.py +95 -0
- alloccontext/ingest/kalshi_files.py +44 -0
- alloccontext/ingest/kalshi_state.py +67 -0
- alloccontext/ingest/kraken_client.py +177 -0
- alloccontext/ingest/kraken_portfolio.py +161 -0
- alloccontext/ingest/macro_calendar.py +310 -0
- alloccontext/ingest/macro_normalize.py +98 -0
- alloccontext/ingest/market_snapshots.py +113 -0
- alloccontext/ingest/outcome.py +110 -0
- alloccontext/ingest/parse_helpers.py +23 -0
- alloccontext/ingest/runner.py +148 -0
- alloccontext/mcp/__init__.py +1 -0
- alloccontext/mcp/assets.py +153 -0
- alloccontext/mcp/bazaar.py +630 -0
- alloccontext/mcp/contracts.py +286 -0
- alloccontext/mcp/handlers.py +487 -0
- alloccontext/mcp/http.py +250 -0
- alloccontext/mcp/payment_middleware.py +211 -0
- alloccontext/mcp/server.py +319 -0
- alloccontext/mcp/staleness.py +30 -0
- alloccontext/mcp/validation.py +56 -0
- alloccontext/mcp/x402_bazaar_dynamic.py +104 -0
- alloccontext/mcp/x402_config.py +131 -0
- alloccontext/mcp/x402_pricing.py +55 -0
- alloccontext/mcp/x402_stables.py +179 -0
- alloccontext/rollup/__init__.py +1 -0
- alloccontext/rollup/band.py +50 -0
- alloccontext/rollup/breadth.py +45 -0
- alloccontext/rollup/cf_math.py +103 -0
- alloccontext/rollup/cluster.py +149 -0
- alloccontext/rollup/cluster_config.py +86 -0
- alloccontext/rollup/comparison.py +67 -0
- alloccontext/rollup/context.py +118 -0
- alloccontext/rollup/delta.py +109 -0
- alloccontext/rollup/etf.py +113 -0
- alloccontext/rollup/fear_greed.py +61 -0
- alloccontext/rollup/macro.py +185 -0
- alloccontext/rollup/portfolio.py +137 -0
- alloccontext/rollup/rebalance.py +125 -0
- alloccontext/rollup/regime.py +188 -0
- alloccontext/rollup/sentiment.py +118 -0
- alloccontext/rollup/snapshots.py +64 -0
- alloccontext/rollup/tape.py +176 -0
- alloccontext/status_report.py +321 -0
- alloccontext/store/__init__.py +0 -0
- alloccontext/store/db.py +216 -0
- alloccontext/store/jsonutil.py +10 -0
- alloccontext/store/meta.py +20 -0
- alloccontext/store/retention.py +63 -0
- alloccontext/store/status.py +89 -0
- alloccontext/timeutil.py +11 -0
- alloccontext/x402_production_check.py +193 -0
- alloccontext/x402_smoke_redact.py +41 -0
alloccontext/mcp/http.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from starlette.applications import Starlette
|
|
9
|
+
from starlette.middleware import Middleware
|
|
10
|
+
from starlette.responses import JSONResponse, PlainTextResponse
|
|
11
|
+
from starlette.routing import Mount, Route
|
|
12
|
+
|
|
13
|
+
from alloccontext.mcp.bazaar import (
|
|
14
|
+
build_llms_txt,
|
|
15
|
+
build_well_known_x402,
|
|
16
|
+
resolve_public_base_url,
|
|
17
|
+
)
|
|
18
|
+
from alloccontext.mcp.server import create_server
|
|
19
|
+
from alloccontext.mcp.x402_config import (
|
|
20
|
+
CDP_FACILITATOR_URL,
|
|
21
|
+
X402Settings,
|
|
22
|
+
build_x402_resource_server,
|
|
23
|
+
build_x402_routes,
|
|
24
|
+
load_x402_settings,
|
|
25
|
+
)
|
|
26
|
+
from alloccontext.mcp.x402_stables import effective_accepted_stable_symbols
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _health_verbose_enabled() -> bool:
|
|
30
|
+
return os.environ.get("ALLOC_CONTEXT_HEALTH_VERBOSE", "").lower() in (
|
|
31
|
+
"1",
|
|
32
|
+
"true",
|
|
33
|
+
"yes",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _discovery_link_headers() -> dict[str, str]:
|
|
38
|
+
if resolve_public_base_url():
|
|
39
|
+
return {"Link": '</llms.txt>; rel="describedby"'}
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _make_health_handler(config_path: str | None) -> Any:
|
|
44
|
+
def _health(_: Any) -> JSONResponse:
|
|
45
|
+
payload: dict[str, Any] = {"ok": True, "service": "alloc-context-mcp"}
|
|
46
|
+
verbose = _health_verbose_enabled()
|
|
47
|
+
try:
|
|
48
|
+
from alloccontext.config import load_config
|
|
49
|
+
from alloccontext.store.db import connect
|
|
50
|
+
from alloccontext.status_report import mcp_health_ingest_summary
|
|
51
|
+
|
|
52
|
+
config = load_config(config_path)
|
|
53
|
+
conn = connect(config.paths.db)
|
|
54
|
+
try:
|
|
55
|
+
summary = mcp_health_ingest_summary(config, conn)
|
|
56
|
+
payload["ingest_ok"] = summary["ingest_ok"]
|
|
57
|
+
optional_failures = summary.get("optional_feed_failures") or []
|
|
58
|
+
if optional_failures:
|
|
59
|
+
payload["optional_feed_failures"] = optional_failures
|
|
60
|
+
if verbose:
|
|
61
|
+
payload["source_health"] = summary.get("source_health")
|
|
62
|
+
required_failures = summary.get("required_failures") or []
|
|
63
|
+
if required_failures:
|
|
64
|
+
payload["required_failures"] = required_failures
|
|
65
|
+
finally:
|
|
66
|
+
conn.close()
|
|
67
|
+
except Exception:
|
|
68
|
+
payload["status_detail"] = "database_unavailable"
|
|
69
|
+
payload["ok"] = False
|
|
70
|
+
payload["ingest_ok"] = False
|
|
71
|
+
return JSONResponse(payload, headers=_discovery_link_headers())
|
|
72
|
+
|
|
73
|
+
return _health
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _health(_: Any) -> JSONResponse:
|
|
77
|
+
"""Default handler for tests; production apps use _make_health_handler."""
|
|
78
|
+
return _make_health_handler(None)(_)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _llms_txt(settings: X402Settings) -> PlainTextResponse:
|
|
82
|
+
public_base = resolve_public_base_url()
|
|
83
|
+
if not public_base:
|
|
84
|
+
return PlainTextResponse(
|
|
85
|
+
"Set X402_PUBLIC_URL for discovery metadata.\n",
|
|
86
|
+
status_code=404,
|
|
87
|
+
)
|
|
88
|
+
stables = effective_accepted_stable_symbols(settings.accepted_stables)
|
|
89
|
+
body = build_llms_txt(
|
|
90
|
+
public_url=public_base,
|
|
91
|
+
mcp_path=settings.mcp_path,
|
|
92
|
+
accepted_stables=stables,
|
|
93
|
+
)
|
|
94
|
+
return PlainTextResponse(body, media_type="text/plain; charset=utf-8")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _well_known_x402(settings: X402Settings) -> JSONResponse:
|
|
98
|
+
public_base = resolve_public_base_url()
|
|
99
|
+
if not public_base or not settings.pay_to:
|
|
100
|
+
return JSONResponse({"error": "discovery metadata unavailable"}, status_code=404)
|
|
101
|
+
stables = effective_accepted_stable_symbols(settings.accepted_stables)
|
|
102
|
+
payload = build_well_known_x402(
|
|
103
|
+
public_url=public_base,
|
|
104
|
+
mcp_path=settings.mcp_path,
|
|
105
|
+
pay_to=settings.pay_to,
|
|
106
|
+
price_light=settings.mcp_price,
|
|
107
|
+
price_heavy=settings.mcp_price_heavy,
|
|
108
|
+
network=settings.network,
|
|
109
|
+
accepted_stables=stables,
|
|
110
|
+
)
|
|
111
|
+
return JSONResponse(payload)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_loopback_host(host: str) -> bool:
|
|
115
|
+
normalized = host.strip().lower()
|
|
116
|
+
return normalized in {"127.0.0.1", "localhost", "::1"}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_http_app(
|
|
120
|
+
*,
|
|
121
|
+
config_path: str | None = None,
|
|
122
|
+
host: str = "127.0.0.1",
|
|
123
|
+
port: int = 8000,
|
|
124
|
+
stateless_http: bool = True,
|
|
125
|
+
x402: bool = False,
|
|
126
|
+
) -> Starlette:
|
|
127
|
+
if not _is_loopback_host(host) and not x402:
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
"HTTP MCP on a non-loopback host requires x402 payment protection"
|
|
130
|
+
)
|
|
131
|
+
mcp = create_server(
|
|
132
|
+
config_path=config_path,
|
|
133
|
+
host=host,
|
|
134
|
+
port=port,
|
|
135
|
+
stateless_http=stateless_http,
|
|
136
|
+
)
|
|
137
|
+
inner = mcp.streamable_http_app()
|
|
138
|
+
settings = load_x402_settings(require_payment=x402)
|
|
139
|
+
|
|
140
|
+
@contextlib.asynccontextmanager
|
|
141
|
+
async def mcp_lifespan(_app: Starlette) -> AsyncIterator[None]:
|
|
142
|
+
async with mcp.session_manager.run():
|
|
143
|
+
yield
|
|
144
|
+
|
|
145
|
+
discovery_routes = [
|
|
146
|
+
Route("/health", _make_health_handler(config_path)),
|
|
147
|
+
Route("/llms.txt", lambda req: _llms_txt(settings)),
|
|
148
|
+
Route("/.well-known/x402.json", lambda req: _well_known_x402(settings)),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
if not settings.enabled:
|
|
152
|
+
return Starlette(
|
|
153
|
+
routes=[
|
|
154
|
+
*discovery_routes,
|
|
155
|
+
Mount("/", app=inner),
|
|
156
|
+
],
|
|
157
|
+
lifespan=mcp_lifespan,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
from alloccontext.mcp.payment_middleware import AllocContextPaymentMiddlewareASGI
|
|
161
|
+
|
|
162
|
+
resource_server = build_x402_resource_server(settings)
|
|
163
|
+
routes = build_x402_routes(settings)
|
|
164
|
+
return Starlette(
|
|
165
|
+
middleware=[
|
|
166
|
+
Middleware(
|
|
167
|
+
AllocContextPaymentMiddlewareASGI,
|
|
168
|
+
routes=routes,
|
|
169
|
+
server=resource_server,
|
|
170
|
+
),
|
|
171
|
+
],
|
|
172
|
+
routes=[
|
|
173
|
+
*discovery_routes,
|
|
174
|
+
Mount("/", app=inner),
|
|
175
|
+
],
|
|
176
|
+
lifespan=mcp_lifespan,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def run_http(
|
|
181
|
+
*,
|
|
182
|
+
config_path: str | None = None,
|
|
183
|
+
host: str = "127.0.0.1",
|
|
184
|
+
port: int = 8000,
|
|
185
|
+
x402: bool = False,
|
|
186
|
+
) -> None:
|
|
187
|
+
import uvicorn
|
|
188
|
+
|
|
189
|
+
app = build_http_app(
|
|
190
|
+
config_path=config_path,
|
|
191
|
+
host=host,
|
|
192
|
+
port=port,
|
|
193
|
+
x402=x402,
|
|
194
|
+
)
|
|
195
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def run_http_async(
|
|
199
|
+
*,
|
|
200
|
+
config_path: str | None = None,
|
|
201
|
+
host: str = "127.0.0.1",
|
|
202
|
+
port: int = 8000,
|
|
203
|
+
x402: bool = False,
|
|
204
|
+
) -> None:
|
|
205
|
+
import uvicorn
|
|
206
|
+
|
|
207
|
+
app = build_http_app(
|
|
208
|
+
config_path=config_path,
|
|
209
|
+
host=host,
|
|
210
|
+
port=port,
|
|
211
|
+
x402=x402,
|
|
212
|
+
)
|
|
213
|
+
config = uvicorn.Config(app, host=host, port=port, log_level="info")
|
|
214
|
+
server = uvicorn.Server(config)
|
|
215
|
+
await server.serve()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _parse_mcp_port(raw: str) -> int:
|
|
219
|
+
try:
|
|
220
|
+
port = int(raw)
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
raise SystemExit(
|
|
223
|
+
f"ALLOC_CONTEXT_MCP_PORT must be an integer, got {raw!r}"
|
|
224
|
+
) from exc
|
|
225
|
+
if not 1 <= port <= 65535:
|
|
226
|
+
raise SystemExit(f"ALLOC_CONTEXT_MCP_PORT out of range: {port}")
|
|
227
|
+
return port
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main() -> None:
|
|
231
|
+
import logging
|
|
232
|
+
|
|
233
|
+
logger = logging.getLogger(__name__)
|
|
234
|
+
host = os.environ.get("ALLOC_CONTEXT_MCP_HOST", "127.0.0.1")
|
|
235
|
+
port = _parse_mcp_port(os.environ.get("ALLOC_CONTEXT_MCP_PORT", "8000"))
|
|
236
|
+
x402 = os.environ.get("X402_ENABLED", "").lower() in ("1", "true", "yes")
|
|
237
|
+
payment_env = (
|
|
238
|
+
os.environ.get("X402_FACILITATOR_URL", "").startswith(CDP_FACILITATOR_URL)
|
|
239
|
+
and os.environ.get("X402_PAY_TO", "").strip()
|
|
240
|
+
)
|
|
241
|
+
if payment_env and not x402:
|
|
242
|
+
logger.warning(
|
|
243
|
+
"X402 payment env vars are set but X402 is not enabled; "
|
|
244
|
+
"set X402_ENABLED=1 or pass --x402 to require payment"
|
|
245
|
+
)
|
|
246
|
+
run_http(host=host, port=port, x402=x402)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
main()
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""AllocContext payment middleware with per-tool Bazaar discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
10
|
+
from starlette.requests import Request
|
|
11
|
+
from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
12
|
+
from starlette.types import ASGIApp
|
|
13
|
+
|
|
14
|
+
from alloccontext.mcp.x402_bazaar_dynamic import (
|
|
15
|
+
AllocContextHTTPResourceServer,
|
|
16
|
+
patch_resource_info_for_bazaar,
|
|
17
|
+
)
|
|
18
|
+
from x402.http.constants import SETTLEMENT_OVERRIDES_HEADER
|
|
19
|
+
from x402.http.facilitator_client_base import FacilitatorResponseError
|
|
20
|
+
from x402.http.middleware.fastapi import (
|
|
21
|
+
FastAPIAdapter,
|
|
22
|
+
_check_if_bazaar_needed,
|
|
23
|
+
_facilitator_error_response,
|
|
24
|
+
_register_bazaar_extension,
|
|
25
|
+
)
|
|
26
|
+
from x402.http.types import HTTPRequestContext, HTTPTransportContext, PaywallConfig, RoutesConfig
|
|
27
|
+
from x402.schemas.hooks import VerifiedPaymentCancelOptions
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from x402.http.x402_http_server import PaywallProvider
|
|
31
|
+
from x402.server import x402ResourceServer
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def alloc_payment_middleware(
|
|
35
|
+
routes: RoutesConfig,
|
|
36
|
+
server: x402ResourceServer,
|
|
37
|
+
paywall_config: PaywallConfig | None = None,
|
|
38
|
+
paywall_provider: PaywallProvider | None = None,
|
|
39
|
+
sync_facilitator_on_start: bool = True,
|
|
40
|
+
) -> Callable[[Request, Callable[[Request], Awaitable[Response]]], Awaitable[Response]]:
|
|
41
|
+
"""Like x402 fastapi payment_middleware but with per-tool Bazaar metadata."""
|
|
42
|
+
patch_resource_info_for_bazaar()
|
|
43
|
+
if _check_if_bazaar_needed(routes):
|
|
44
|
+
_register_bazaar_extension(server)
|
|
45
|
+
|
|
46
|
+
http_server = AllocContextHTTPResourceServer(server, routes)
|
|
47
|
+
if paywall_provider:
|
|
48
|
+
http_server.register_paywall_provider(paywall_provider)
|
|
49
|
+
|
|
50
|
+
init_done = False
|
|
51
|
+
init_lock = asyncio.Lock()
|
|
52
|
+
|
|
53
|
+
async def middleware(
|
|
54
|
+
request: Request,
|
|
55
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
56
|
+
) -> Response:
|
|
57
|
+
nonlocal init_done
|
|
58
|
+
|
|
59
|
+
adapter = FastAPIAdapter(request)
|
|
60
|
+
context = HTTPRequestContext(
|
|
61
|
+
adapter=adapter,
|
|
62
|
+
path=request.url.path,
|
|
63
|
+
method=request.method,
|
|
64
|
+
payment_header=(
|
|
65
|
+
adapter.get_header("payment-signature") or adapter.get_header("x-payment")
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if not http_server.requires_payment(context):
|
|
70
|
+
return await call_next(request)
|
|
71
|
+
|
|
72
|
+
if sync_facilitator_on_start and not init_done:
|
|
73
|
+
async with init_lock:
|
|
74
|
+
if not init_done:
|
|
75
|
+
try:
|
|
76
|
+
http_server.initialize()
|
|
77
|
+
except FacilitatorResponseError as error:
|
|
78
|
+
return _facilitator_error_response(error)
|
|
79
|
+
init_done = True
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = await http_server.process_http_request(context, paywall_config)
|
|
83
|
+
except FacilitatorResponseError as error:
|
|
84
|
+
return _facilitator_error_response(error)
|
|
85
|
+
|
|
86
|
+
if result.type == "no-payment-required":
|
|
87
|
+
return await call_next(request)
|
|
88
|
+
|
|
89
|
+
if result.type == "payment-error":
|
|
90
|
+
response = result.response
|
|
91
|
+
if response is None:
|
|
92
|
+
return JSONResponse(content={"error": "Payment required"}, status_code=402)
|
|
93
|
+
if response.is_html:
|
|
94
|
+
return HTMLResponse(
|
|
95
|
+
content=response.body,
|
|
96
|
+
status_code=response.status,
|
|
97
|
+
headers=response.headers,
|
|
98
|
+
)
|
|
99
|
+
return JSONResponse(
|
|
100
|
+
content=response.body or {},
|
|
101
|
+
status_code=response.status,
|
|
102
|
+
headers=response.headers,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if result.type == "payment-verified":
|
|
106
|
+
request.state.payment_payload = result.payment_payload
|
|
107
|
+
request.state.payment_requirements = result.payment_requirements
|
|
108
|
+
dispatcher = result.cancellation_dispatcher
|
|
109
|
+
transport_context = HTTPTransportContext(request=context)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
response = await call_next(request)
|
|
113
|
+
except Exception as error:
|
|
114
|
+
if dispatcher is not None:
|
|
115
|
+
await dispatcher.cancel(
|
|
116
|
+
VerifiedPaymentCancelOptions(reason="handler_threw", error=error)
|
|
117
|
+
)
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
if response.status_code >= 400:
|
|
121
|
+
if dispatcher is not None:
|
|
122
|
+
await dispatcher.cancel(
|
|
123
|
+
VerifiedPaymentCancelOptions(
|
|
124
|
+
reason="handler_failed",
|
|
125
|
+
response_status=response.status_code,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return response
|
|
129
|
+
|
|
130
|
+
body = b""
|
|
131
|
+
async for chunk in response.body_iterator:
|
|
132
|
+
body += chunk
|
|
133
|
+
|
|
134
|
+
overrides = http_server._extract_settlement_overrides(dict(response.headers))
|
|
135
|
+
if overrides is not None:
|
|
136
|
+
for key in list(response.headers.keys()):
|
|
137
|
+
if key.lower() == SETTLEMENT_OVERRIDES_HEADER.lower():
|
|
138
|
+
del response.headers[key]
|
|
139
|
+
|
|
140
|
+
transport_context.response_headers = dict(response.headers)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
settle_result = await http_server.process_settlement(
|
|
144
|
+
result.payment_payload,
|
|
145
|
+
result.payment_requirements,
|
|
146
|
+
context=context,
|
|
147
|
+
settlement_overrides=overrides,
|
|
148
|
+
declared_extensions=result.declared_extensions,
|
|
149
|
+
transport_context=transport_context,
|
|
150
|
+
)
|
|
151
|
+
except FacilitatorResponseError as error:
|
|
152
|
+
return _facilitator_error_response(error)
|
|
153
|
+
except Exception:
|
|
154
|
+
return JSONResponse(content={}, status_code=402)
|
|
155
|
+
|
|
156
|
+
if not settle_result.success:
|
|
157
|
+
resp = settle_result.response
|
|
158
|
+
if resp is None:
|
|
159
|
+
return JSONResponse(content={}, status_code=402)
|
|
160
|
+
if resp.is_html:
|
|
161
|
+
return Response(
|
|
162
|
+
content=resp.body,
|
|
163
|
+
status_code=resp.status,
|
|
164
|
+
headers=resp.headers,
|
|
165
|
+
media_type="text/html",
|
|
166
|
+
)
|
|
167
|
+
return JSONResponse(
|
|
168
|
+
content=resp.body or {},
|
|
169
|
+
status_code=resp.status,
|
|
170
|
+
headers=resp.headers,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
headers = dict(response.headers)
|
|
174
|
+
headers.update(settle_result.headers)
|
|
175
|
+
return Response(
|
|
176
|
+
content=body,
|
|
177
|
+
status_code=response.status_code,
|
|
178
|
+
headers=headers,
|
|
179
|
+
media_type=response.media_type,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return await call_next(request)
|
|
183
|
+
|
|
184
|
+
return middleware
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class AllocContextPaymentMiddlewareASGI(BaseHTTPMiddleware):
|
|
188
|
+
"""Starlette middleware with AllocContext Bazaar discovery behavior."""
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
app: ASGIApp,
|
|
193
|
+
routes: RoutesConfig,
|
|
194
|
+
server: x402ResourceServer,
|
|
195
|
+
paywall_config: PaywallConfig | None = None,
|
|
196
|
+
paywall_provider: PaywallProvider | None = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
super().__init__(app)
|
|
199
|
+
self._middleware = alloc_payment_middleware(
|
|
200
|
+
routes,
|
|
201
|
+
server,
|
|
202
|
+
paywall_config,
|
|
203
|
+
paywall_provider,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
async def dispatch(
|
|
207
|
+
self,
|
|
208
|
+
request: Request,
|
|
209
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
210
|
+
) -> Response:
|
|
211
|
+
return await self._middleware(request, call_next)
|