11"""One-call MPP server setup wrapping the official `pympp` Python package.
22
33Wires Tempo charge, Tempo session (channel-based for variable-cost /
4- streaming), and Stripe SPT methods from symbolic rail config — replaces
5- the boilerplate of constructing each method by hand.
4+ streaming), and Stripe SPT methods from rail specs — replaces the boilerplate
5+ of constructing each method by hand.
66
77Usage::
88
9- from agentscore_commerce.payment import create_mppx_server, MppxRails, TempoChargeRail
9+ from agentscore_commerce.payment import (
10+ create_mppx_server,
11+ TempoRailSpec,
12+ StripeRailSpec,
13+ )
1014
1115 mpp = await create_mppx_server(
12- rails=MppxRails(
13- tempo=TempoChargeRail(recipient=os.environ["TEMPO_RECIPIENT"]),
14- stripe=StripeRail(
16+ secret_key=os.environ["MPP_SECRET_KEY"],
17+ rails={
18+ "tempo": TempoRailSpec(recipient=os.environ["TEMPO_RECIPIENT"]),
19+ "stripe": StripeRailSpec(
1520 profile_id=os.environ["STRIPE_PROFILE_ID"],
1621 secret_key=os.environ["STRIPE_SECRET_KEY"],
1722 ),
18- ),
19- secret_key=os.environ["MPP_SECRET_KEY"],
23+ },
2024 )
2125
26+ Keys are rail names (``"tempo"``, ``"tempo_session"``, ``"stripe"``); values are
27+ the canonical ``*RailSpec`` instances every other helper also consumes.
28+
2229`pympp` is an OPTIONAL peer dependency — install only if you accept MPP rails::
2330
2431 pip install 'pympp[server,tempo,stripe]>=0.6,<1'
2734from __future__ import annotations
2835
2936import importlib
30- from dataclasses import dataclass
3137from typing import Any
3238
39+ from agentscore_commerce .payment .rail_spec import (
40+ RecipientLike ,
41+ StripeRailSpec ,
42+ TempoRailSpec ,
43+ TempoSessionRailSpec ,
44+ resolve_recipient ,
45+ )
3346from agentscore_commerce .payment .usdc import USDC
3447
48+ MppxRailSpec = TempoRailSpec | TempoSessionRailSpec | StripeRailSpec
3549
36- @dataclass
37- class TempoChargeRail :
38- """One-shot Tempo USDC charge (intent: ``charge``)."""
39-
40- recipient : str
41- """Tempo wallet address that receives settled funds."""
42-
43- currency : str | None = None
44- """Token contract address. Defaults to USDC on Tempo (selected by ``testnet`` flag)."""
45-
46- testnet : bool = False
47- """Use Tempo testnet (Moderato) instead of mainnet."""
48-
49-
50- @dataclass
51- class TempoSessionRail :
52- """Tempo session (intent: ``session``) — pay-as-you-go channel.
53-
54- Used for repeated calls or SSE-streamed responses. Vendor brings their own
55- ``ChannelStore`` and ``escrow_contract`` address.
56- """
5750
58- recipient : str
59- escrow_contract : str
60- """On-chain escrow contract address that holds channel deposits and pays out
61- cumulative vouchers on settlement. Vendor-deployed."""
62-
63- store : Any
64- """ChannelStore implementation tracking open channels + cumulative voucher state.
65- Pass an instance of pympp's ``ChannelStore`` interface (in-memory default for
66- dev or a Postgres/Redis-backed store for production)."""
67-
68- currency : str | None = None
69- testnet : bool = False
70- chains : Any | None = None
71- """Optional supported chains; defaults to pympp defaults if omitted."""
72-
73-
74- @dataclass
75- class StripeRail :
76- """Stripe SPT (Shared Payment Token) rail config.
77-
78- See :mod:`agentscore_commerce.stripe_multichain` for the multichain
79- PaymentIntent helpers used alongside this rail.
80- """
51+ def _import_optional (module_name : str ) -> Any | None :
52+ try :
53+ return importlib .import_module (module_name )
54+ except ImportError :
55+ return None
8156
82- profile_id : str
83- secret_key : str
84- payment_method_types : list [str ] | None = None
8557
58+ async def _resolve_recipient_for_method (recipient : RecipientLike ) -> str :
59+ return await resolve_recipient (recipient )
8660
87- @dataclass
88- class MppxRails :
89- """Symbolic rail config for :func:`create_mppx_server`.
9061
91- Commerce wires the boilerplate (``tempo.charge()``, ``mpp_stripe.charge()``,
92- etc.) so vendors only declare the rails they accept.
93- """
62+ async def _tempo_method (spec : TempoRailSpec ) -> Any :
63+ tempo_module = _import_optional ("mpp.methods.tempo" )
64+ tempo_factory = getattr (tempo_module , "tempo" , None ) if tempo_module else None
65+ if not callable (tempo_factory ):
66+ msg = "pympp[tempo] not installed — run `pip install 'pympp[tempo]'` for Tempo MPP rails."
67+ raise ImportError (msg )
68+ charge_intent_cls = getattr (tempo_module , "ChargeIntent" , None ) if tempo_module else None
69+ if charge_intent_cls is None :
70+ msg = "pympp[tempo] missing ChargeIntent — upgrade pympp to 0.6+."
71+ raise ImportError (msg )
72+ default_currency = USDC .tempo .testnet .address if spec .testnet else USDC .tempo .mainnet .address
73+ chain_id = 42431 if spec .testnet else (spec .chain_id or 4217 )
74+ return tempo_factory (
75+ intents = {"charge" : charge_intent_cls ()},
76+ currency = spec .token or default_currency ,
77+ recipient = await _resolve_recipient_for_method (spec .recipient ),
78+ chain_id = chain_id ,
79+ )
9480
95- tempo : TempoChargeRail | None = None
96- tempo_session : TempoSessionRail | None = None
97- stripe : StripeRail | None = None
9881
82+ async def _stripe_method (spec : StripeRailSpec ) -> Any :
83+ from agentscore_commerce .stripe_multichain .mppx_stripe import create_mppx_stripe
9984
100- def _import_optional (module_name : str ) -> Any | None :
101- try :
102- return importlib .import_module (module_name )
103- except ImportError :
104- return None
85+ if not spec .profile_id or not spec .secret_key :
86+ msg = "StripeRailSpec for create_mppx_server requires both profile_id and secret_key."
87+ raise ValueError (msg )
88+ return await create_mppx_stripe (
89+ profile_id = spec .profile_id ,
90+ secret_key = spec .secret_key ,
91+ payment_method_types = spec .payment_method_types ,
92+ )
10593
10694
10795async def create_mppx_server (
96+ * ,
10897 secret_key : str ,
109- rails : MppxRails | None = None ,
98+ rails : dict [ str , MppxRailSpec ] | None = None ,
11099 method : Any = None ,
111100 realm : str | None = None ,
112101) -> Any :
@@ -116,71 +105,46 @@ async def create_mppx_server(
116105 ``ImportError`` with a guiding install command when ``pympp`` or a per-rail
117106 extra is missing.
118107
119- Async because Stripe SPT method construction may require an HTTP setup call
120- to the Stripe API.
108+ ``rails`` keys are rail names (``"tempo"``, ``"tempo_session"``, ``"stripe"``);
109+ values are the canonical ``*RailSpec`` instances every other helper also
110+ consumes. Tempo session is reserved for future pympp ``SessionIntent``
111+ support — passing it today raises ``ImportError``.
121112
122- Note: pympp 0.6 takes a single ``method`` per ``Mpp`` instance (the prior
123- multi-method ``Mppx`` API was removed). If multiple rails are configured on
124- ``rails``, the first non-None one wins; merchants supporting multiple
125- distinct methods (e.g. tempo charge + tempo session, or tempo + Stripe SPT)
126- construct a separate ``Mpp`` instance per method and route by the method
127- name they detect on the request. Mirrors how pympp 0.6 separates methods.
113+ pympp 0.6 takes a single ``method`` per ``Mpp`` instance. When ``rails`` is
114+ provided, the first resolvable rail in dict-insertion order wins; merchants
115+ supporting multiple distinct methods construct a separate ``Mpp`` per method
116+ and route by name at the request layer.
128117 """
129- # The pympp distribution publishes its modules under the top-level `mpp`
130- # package (the dist name is `pympp` but `import pympp` doesn't resolve —
131- # only `import mpp`).
132118 pympp = _import_optional ("mpp.server" )
133119 if pympp is None or not hasattr (pympp , "Mpp" ):
134120 msg = "pympp not installed — run `pip install 'pympp[server,tempo,stripe]>=0.6,<1'` to use create_mppx_server."
135121 raise ImportError (msg )
136122
137- rails_cfg = rails or MppxRails ()
138123 resolved_method : Any = method
124+ rails_map : dict [str , MppxRailSpec ] = rails or {}
139125
140- if resolved_method is None and rails_cfg .tempo is not None :
141- tempo_module = _import_optional ("mpp.methods.tempo" )
142- tempo_factory = getattr (tempo_module , "tempo" , None ) if tempo_module else None
143- if not callable (tempo_factory ):
144- msg = "pympp[tempo] not installed — run `pip install 'pympp[tempo]'` for Tempo MPP rails."
145- raise ImportError (msg )
146- charge_intent_cls = getattr (tempo_module , "ChargeIntent" , None ) if tempo_module else None
147- if charge_intent_cls is None :
148- msg = "pympp[tempo] missing ChargeIntent — upgrade pympp to 0.6+."
149- raise ImportError (msg )
150- t = rails_cfg .tempo
151- default_currency = USDC .tempo .testnet .address if t .testnet else USDC .tempo .mainnet .address
152- chain_id = 42431 if t .testnet else 4217
153- resolved_method = tempo_factory (
154- intents = {"charge" : charge_intent_cls ()},
155- currency = t .currency or default_currency ,
156- recipient = t .recipient ,
157- chain_id = chain_id ,
158- )
159-
160- if resolved_method is None and rails_cfg .tempo_session is not None :
161- # pympp 0.6 has not shipped a session intent factory under the same
162- # naming. Keep the surface (TempoSessionRail), but vendors must wait
163- # for pympp to expose ``SessionIntent`` before this branch resolves.
164- msg = (
165- "pympp[tempo] session support not available — pympp 0.6 has not "
166- "shipped a SessionIntent factory yet. Upgrade pympp when it does "
167- "or pass `method=` directly with a hand-built TempoMethod."
168- )
169- raise ImportError (msg )
170-
171- if resolved_method is None and rails_cfg .stripe is not None :
172- from agentscore_commerce .stripe_multichain .mppx_stripe import create_mppx_stripe
173-
174- resolved_method = await create_mppx_stripe (
175- profile_id = rails_cfg .stripe .profile_id ,
176- secret_key = rails_cfg .stripe .secret_key ,
177- payment_method_types = rails_cfg .stripe .payment_method_types ,
178- )
126+ if resolved_method is None :
127+ for name , spec in rails_map .items ():
128+ if isinstance (spec , TempoRailSpec ):
129+ resolved_method = await _tempo_method (spec )
130+ break
131+ if isinstance (spec , TempoSessionRailSpec ):
132+ msg = (
133+ "pympp[tempo] session support not available — pympp 0.6 has not "
134+ "shipped a SessionIntent factory yet. Upgrade pympp when it does "
135+ "or pass `method=` directly with a hand-built TempoMethod."
136+ )
137+ raise ImportError (msg )
138+ if isinstance (spec , StripeRailSpec ):
139+ resolved_method = await _stripe_method (spec )
140+ break
141+ msg = f"create_mppx_server: unsupported rail spec for key { name !r} : { type (spec ).__name__ } "
142+ raise TypeError (msg )
179143
180144 if resolved_method is None :
181145 msg = (
182- "create_mppx_server called with no method or rails — pass at least one of "
183- "`method=`, `rails. tempo`, `rails. tempo_session`, or `rails. stripe`."
146+ "create_mppx_server called with no method or rails — pass `method=` or a "
147+ "non-empty `rails={...}` map keyed by rail name (` tempo`, `tempo_session`, ` stripe`) ."
184148 )
185149 raise ValueError (msg )
186150
@@ -191,9 +155,6 @@ async def create_mppx_server(
191155
192156
193157__all__ = [
194- "MppxRails" ,
195- "StripeRail" ,
196- "TempoChargeRail" ,
197- "TempoSessionRail" ,
158+ "MppxRailSpec" ,
198159 "create_mppx_server" ,
199160]
0 commit comments