Skip to content

Add a Python (Flask) example - #6

Merged
evsinev merged 4 commits into
mainfrom
feat/python-example
Sep 11, 2026
Merged

Add a Python (Flask) example#6
evsinev merged 4 commits into
mainfrom
feat/python-example

Conversation

@evsinev

@evsinev evsinev commented Sep 11, 2026

Copy link
Copy Markdown

A fifth example: the same payment, on Flask. python-flask-js/ implements exactly the seven
routes of the server contract, with the same status codes, the same OAuth base string and the
same sha1 callback checksum as the other four.

The model here is nodejs-express-js, not go-js — it is the other example built on a
framework with a router and a static mount, so every handler is a transcription of one of its.

Two runtime dependencies, both unavoidable: Flask, which is the subject, and
cryptography, because Python has no RSA in the standard library. The gateway calls go through
urllib.request and the tests through unittest, so nothing is installed for either.

settings.py   environment variables, validation and the key
oauth.py      OAuth 1.0a RSA-SHA256 signing
control.py    the 3DS callback checksum
paynet.py     the three gateway calls
app.py        one blueprint under BASE_PATH, and the generated config.js

Port 3004, prefix /hosted-fields-examples-python. python app.py for development, gunicorn
under systemd behind nginx in deploy/.

What the fifth language cost

  • scripts/sync-shared.sh: one line;
  • e2e-tests/src/apps.ts: one registry entry. No spec changes;
  • nothing in go-js/, nodejs-express-js/, php-js/ or nextjs/.

One small harness improvement came with it: playwright.config.ts had
app.name === 'nextjs' ? 300_000 : 60_000 for the readiness timeout, which a third slow-starting
app would have turned into a chain of name checks. It is now a startTimeout field on
AppUnderTest, so each entry states its own.

Four places Python pushes back, and what was done about them

  • urlopen raises on every 4xx, and a gateway rejection is a 4xx carrying the JSON body
    with error-message. _post reads error.read() and error.code off the HTTPError and
    decodes them like any other reply; letting it propagate would turn every decline into a 502.
    Checked end to end: the sale-error scenario answers 200 with the gateway's message.
  • A global errorhandler(Exception) would swallow Werkzeug's NotFound and answer 502 to
    every 404, so the two handlers that call the gateway catch for themselves, as Go and PHP do.
  • quote's default is safe='/', which would leave the slashes in a URL-as-parameter
    unescaped and break the signature. The encoder is quote(value, safe=''), and there is a test
    vector for exactly that.
  • json.dumps has to be told separators=(',', ':') and ensure_ascii=False to emit what
    JSON.stringify does, so config.js is the same script the other examples hand the page.

Two things recorded rather than hidden: the bare prefix redirects with a 308, from Flask's own
strict_slashes (Express writes 302, Go and PHP 307 — all for the same relative-asset reason);
and ProxyFix(x_for=1) counts one trusted proxy from the right, so unlike the other examples
a caller cannot prepend an address of their own through a single nginx.

Checks

  • python -m compileall and python -m unittest discover -s tests -t . — 13 tests, on the same
    OAuth base-string and callback-checksum vectors the Go, Node and PHP suites pin. Added as a CI
    job; no credentials needed, since the tests stub what settings.py validates at import.
  • cd e2e-tests && npm test50 passed, ten per app. The emulator verifies the OAuth
    signature against the generated public key and rejects a Sale whose amount is not the server's,
    so the cryptography signing and the server-owned amount are checked on the wire. The
    virtualenv is built in e2e-tests/.tmp/, next to the Go binary, so the suite still leaves every
    app directory as it found it.
  • ./scripts/sync-shared.sh && git diff --exit-code clean; the four existing apps still build
    and test untouched.
  • By hand against the emulator: config.js carries a fresh ticket and no-store, the CSP names
    the SDK origin, a forged control gives 403 on both /result and /result/callback, and
    {prefix}/app.py, {prefix}/.env, {prefix}/settings.py and {prefix}/views/checkout.html
    all answer 404.

No key, no MERCHANT_CONTROL and no real ENDPOINT_ID anywhere: .env.example and the deploy
template carry placeholders only.

@evsinev
evsinev merged commit d7c4e61 into main Sep 11, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant