Verifiers¶
once.verifiers has ready-made verify callbacks:
from once.verifiers import http_verifier, stripe_params, stripe_verifier
Nothing there imports a third party package until it is used.
Stripe¶
stripe_params tags the create params with the once key in metadata and sends the key as Stripe's Idempotency-Key. stripe_verifier finds the object by that metadata later:
import stripe
from once.verifiers import stripe_params, stripe_verifier
client = stripe.StripeClient("sk_...")
@once.tool(name="charge_card", verify=stripe_verifier("payment_intent", client=client))
def charge_card(amount: int, customer: str, idempotency_key: str | None = None) -> dict:
params = {"amount": amount, "currency": "usd", "customer": customer, "confirm": True}
return client.v1.payment_intents.create(**stripe_params(idempotency_key, params)).to_dict()
stripe_params¶
stripe_params(key, params, *, metadata_key="once_key")
Returns {"params": ..., "options": {"idempotency_key": key}}, the keyword arguments a StripeClient create call takes. Existing metadata is kept and metadata[metadata_key] is set to the key.
stripe_verifier¶
stripe_verifier(
kind,
*,
client=None,
api_key=None,
metadata_key="once_key",
list_params=None,
scan=100,
result=None,
)
kindis"payment_intent","charge"or"refund".- Charges and PaymentIntents are looked up with the search API first. Search can lag new objects by a minute or so, so if it finds nothing, the newest
scanobjects from the list API are checked too. Refunds have no search, so only the list is used. - If search isn't available to the account (Stripe doesn't offer it in some regions, or a restricted key can't use it), the list is used instead and search is skipped from then on. With
scan=0that error is raised. list_paramsmaps the tool arguments to list filters, likelambda args: {"customer": args["customer"]}, to narrow the scan.- It returns the object as a plain dict, or
result(obj)if you passresult=, so match it to what the tool returns. If nothing matches it returnsNOT_HAPPENED. Other Stripe errors propagate, which leaves the record unknown. -
The status of the object it finds decides the outcome:
kind returned (happened) NOT_HAPPENEDchargesucceededfailedpayment_intentsucceeded,requires_capturecanceledrefundsucceeded,pendingfailed,canceledAny other status, such as
processing,requires_actionorrequires_payment_method, raisesStripeInconclusive, so the record stays unknown until it settles and you retry orresetit. - It only returnsNOT_HAPPENEDafter reading the list to the end. If more thanscanobjects match the filters and the one it wants isn't among the newestscan, it raisesStripeInconclusiveand the record stays unknown. Narrowlist_paramsor raisescan. Withscan=0a search miss alone returnsNOT_HAPPENED, so only use it withstripe_params. -clientis astripe.StripeClient, or anything shaped like one. Leave it out to build aStripeClienton first use fromapi_key=orstripe.api_key; that needspip install 'agent-once[stripe]'.
Verify the PaymentIntent, not the charge. Charges made by a PaymentIntent don't copy its metadata, so a "charge" lookup only finds charges you tagged yourself.
Use both helpers together. If a lookup misses, the call runs again with the same Idempotency-Key, and Stripe replays the first response instead of charging twice. Stripe keeps idempotency keys for at least 24 hours.
examples/stripe_example.py runs the whole flow offline against a fake client.
HTTP¶
http_verifier asks your own API whether the effect happened:
from once.verifiers import http_verifier
verify = http_verifier(
"https://api.example.com/orders/{order_id}/emails/{key}",
headers={"Authorization": f"Bearer {token}"},
)
http_verifier(
url,
*,
method="GET",
headers=None,
happened=range(200, 300),
not_happened=(404,),
timeout=10.0,
result=None,
)
urlis a template.{key}is the once key and any other{name}is the tool argument of that name, each percent-encoded. Strings go in as they are and other values as JSON (null,true,3). A tool argument namedkeyraisesValueErrorwhen the template has{key}; rename the argument.- A status in
happenedreturns the body: parsed JSON, the text if it is not JSON, or None if it is empty. - A status in
not_happenedreturnsNOT_HAPPENED, so the call runs again. - Any other status, or a network error, raises, which leaves the record unknown.
resultmaps the body to what the tool returns. It may also returnNOT_HAPPENED, for APIs that answer 200 with something like{"found": false}.- A status in both
happenedandnot_happenedraisesValueError.
Writing your own¶
A verifier is any verify(key, args) callable. Return the result if the effect happened, NOT_HAPPENED if it did not, and raise if you can't tell. For async tools it may be an async function.