tuppence 0.1.0__tar.gz

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.
Files changed (35) hide show
  1. tuppence-0.1.0/.gitignore +58 -0
  2. tuppence-0.1.0/PKG-INFO +168 -0
  3. tuppence-0.1.0/README.md +145 -0
  4. tuppence-0.1.0/pyproject.toml +53 -0
  5. tuppence-0.1.0/scripts/generate.py +142 -0
  6. tuppence-0.1.0/src/tuppence/__init__.py +111 -0
  7. tuppence-0.1.0/src/tuppence/_async_resources.py +3614 -0
  8. tuppence-0.1.0/src/tuppence/_client.py +361 -0
  9. tuppence-0.1.0/src/tuppence/_errors.py +85 -0
  10. tuppence-0.1.0/src/tuppence/_http.py +283 -0
  11. tuppence-0.1.0/src/tuppence/_model.py +25 -0
  12. tuppence-0.1.0/src/tuppence/_sync_resources.py +3616 -0
  13. tuppence-0.1.0/src/tuppence/_webhooks.py +108 -0
  14. tuppence-0.1.0/src/tuppence/http402/__init__.py +334 -0
  15. tuppence-0.1.0/src/tuppence/http402/_verify.py +159 -0
  16. tuppence-0.1.0/src/tuppence/models.py +17133 -0
  17. tuppence-0.1.0/src/tuppence/py.typed +0 -0
  18. tuppence-0.1.0/tests/conformance_runner.py +210 -0
  19. tuppence-0.1.0/tests/conftest.py +68 -0
  20. tuppence-0.1.0/tests/test_card_details.py +133 -0
  21. tuppence-0.1.0/tests/test_card_disputes.py +334 -0
  22. tuppence-0.1.0/tests/test_client.py +429 -0
  23. tuppence-0.1.0/tests/test_conftest.py +26 -0
  24. tuppence-0.1.0/tests/test_connect.py +464 -0
  25. tuppence-0.1.0/tests/test_direct_debit.py +209 -0
  26. tuppence-0.1.0/tests/test_e2e.py +245 -0
  27. tuppence-0.1.0/tests/test_http402.py +321 -0
  28. tuppence-0.1.0/tests/test_http402_e2e.py +144 -0
  29. tuppence-0.1.0/tests/test_issuing_topup_links.py +81 -0
  30. tuppence-0.1.0/tests/test_issuing_topups.py +391 -0
  31. tuppence-0.1.0/tests/test_payouts.py +202 -0
  32. tuppence-0.1.0/tests/test_store.py +468 -0
  33. tuppence-0.1.0/tests/test_store_events.py +51 -0
  34. tuppence-0.1.0/tests/test_ticketing.py +142 -0
  35. tuppence-0.1.0/tests/test_ticketing_events.py +52 -0
@@ -0,0 +1,58 @@
1
+ node_modules/
2
+ dist/
3
+ # The booking origin's quality gates build here (apps/web/playwright.book.config.ts).
4
+ dist-book/
5
+ dist-store/
6
+ # …and the browser POS's acceptance test here (apps/web/playwright.pos.config.ts).
7
+ dist-pos/
8
+ # …and M32's quality gate for it here (apps/web/playwright.pos-gate.config.ts).
9
+ dist-pos-gate/
10
+ coverage/
11
+ *.tsbuildinfo
12
+ .DS_Store
13
+
14
+ # env and secrets — never commit
15
+ .env
16
+ .env.*
17
+ !.env.example
18
+ infra/gcp/*.env
19
+ !infra/gcp/*.env.example
20
+ *-key.json
21
+ *.pem
22
+
23
+ # local tooling
24
+ .firebase/
25
+ .vite/
26
+ .worktrees/
27
+ worktrees/
28
+ playwright-report/
29
+ # The press pack's renders (plan/102): published to R2 by press-publish, never committed.
30
+ apps/press/out/
31
+ # The assistant eval's reports (apps/api: pnpm assistant:eval)
32
+ eval-reports/
33
+ test-results/
34
+ /tmp/
35
+
36
+ # python (packages/python)
37
+ .venv
38
+ __pycache__/
39
+ *.egg-info/
40
+ .pytest_cache/
41
+ .mypy_cache/
42
+ .ruff_cache/
43
+
44
+ # The iOS release build's output: archives and .ipa files, hundreds of MB apiece.
45
+ apps/mobile/build/
46
+
47
+ # Swift Package Manager (packages/ios): the build and Xcode's package state.
48
+ .build/
49
+ .swiftpm/
50
+
51
+ # Wrangler's local state (infra/cloudflare: the edge Worker's deploy).
52
+ .wrangler/
53
+
54
+ # 81-26: sandbox refresh tokens for the live accounting suites (accounting:consent)
55
+ .accounting-sandbox/
56
+
57
+ # CI: the inputs each workspace package was built from (scripts/package-builds.mjs)
58
+ .ci-builds/
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.5
2
+ Name: tuppence
3
+ Version: 0.1.0
4
+ Summary: The Tuppence API for Python — payments for developers and AI agents.
5
+ License-Expression: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Typing :: Typed
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: httpx<1,>=0.27
10
+ Requires-Dist: pydantic<3,>=2.7
11
+ Provides-Extra: dev
12
+ Requires-Dist: black==26.10.1; extra == 'dev'
13
+ Requires-Dist: cryptography>=42; extra == 'dev'
14
+ Requires-Dist: datamodel-code-generator==0.34.0; extra == 'dev'
15
+ Requires-Dist: isort==6.1.0; extra == 'dev'
16
+ Requires-Dist: mypy>=1.10; extra == 'dev'
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Requires-Dist: ruff>=0.6; extra == 'dev'
19
+ Requires-Dist: starlette>=0.37; extra == 'dev'
20
+ Provides-Extra: http402
21
+ Requires-Dist: cryptography>=42; extra == 'http402'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # tuppence
25
+
26
+ The Tuppence API for Python — payments for developers and AI agents.
27
+
28
+ ```bash
29
+ pip install tuppence
30
+ ```
31
+
32
+ ```python
33
+ import os
34
+ from tuppence import Tuppence
35
+
36
+ tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
37
+
38
+ link = tuppence.payment_links.create(name="Research report", amount=2000, currency="gbp")
39
+ print(link.url) # send it to whoever should pay
40
+
41
+ for payment in tuppence.payments.list_all(status="succeeded"):
42
+ print(payment.id, payment.amount, payment.fee, payment.net) # integer pence
43
+ ```
44
+
45
+ Python 3.10+. Money is always an integer number of minor units (pence).
46
+
47
+ ## Async
48
+
49
+ ```python
50
+ from tuppence import AsyncTuppence
51
+
52
+ async with AsyncTuppence(os.environ["TUPPENCE_SECRET_KEY"]) as tuppence:
53
+ payment = await tuppence.payments.create(amount=2000, currency="gbp")
54
+ async for refund in tuppence.refunds.list_all(payment=payment.id):
55
+ ...
56
+ ```
57
+
58
+ ## Retries and idempotency
59
+
60
+ Every POST carries an `Idempotency-Key`: yours, or a fresh one reused by every retry of that
61
+ call — so retrying after a timeout never charges twice. Network errors, timeouts, `5xx`, `429`
62
+ (honouring `Retry-After`) and an idempotent request still in flight are retried twice by default,
63
+ with jittered backoff. Nothing else is.
64
+
65
+ ```python
66
+ tuppence.refunds.create(payment="pay_…", amount=500, idempotency_key="order-42-refund")
67
+ tuppence = Tuppence(key, max_retries=4, timeout=10) # client-wide
68
+ tuppence.balance.retrieve(max_retries=0, timeout=2) # per request
69
+ ```
70
+
71
+ ## Another API address
72
+
73
+ The SDK calls `https://api.tuppence.ai`. To use another Tuppence environment you have been given,
74
+ set `TUPPENCE_API_URL`, or pass `base_url`:
75
+
76
+ ```python
77
+ tuppence = Tuppence(key, base_url="https://…")
78
+ ```
79
+
80
+ ## Errors
81
+
82
+ ```python
83
+ from tuppence import CardError, InvalidRequestError
84
+
85
+ try:
86
+ tuppence.refunds.create(payment="pay_…", amount=10_000)
87
+ except InvalidRequestError as e:
88
+ print(e.code, e.param, e.request_id) # branch on `code`; quote `request_id` to support
89
+ ```
90
+
91
+ `CardError`, `InvalidRequestError`, `AuthenticationError`, `PermissionError`, `RateLimitError`,
92
+ `IdempotencyError`, `APIError` and `ConnectionError` all derive from `TuppenceError`.
93
+
94
+ ## Webhooks
95
+
96
+ ```python
97
+ event = tuppence.webhooks.construct_event(
98
+ request.body, # the RAW body, exactly as received
99
+ request.headers["Tuppence-Signature"],
100
+ os.environ["TUPPENCE_WEBHOOK_SECRET"],
101
+ )
102
+ ```
103
+
104
+ A wrong secret, a tampered body or a timestamp more than five minutes old raises
105
+ `SignatureVerificationError` — never act on that payload.
106
+
107
+ ## Tuppence Connect: acting as your connected accounts
108
+
109
+ A platform's connected accounts are full Tuppence accounts. Make one, send its owner onboarding,
110
+ then act as it with your own key — per call, or through a client bound to it:
111
+
112
+ ```python
113
+ bella = tuppence.accounts.create(email="bella@example.com", business_type="individual")
114
+ link = tuppence.account_links.create(
115
+ account=bella.id,
116
+ type="account_onboarding",
117
+ refresh_url="https://groomly.example/onboarding/refresh",
118
+ return_url="https://groomly.example/onboarding/done",
119
+ )
120
+
121
+ # One call as Bella: the `Tuppence-Account` header, never part of the path.
122
+ tuppence.payments.create(
123
+ amount=4500, currency="gbp", application_fee_amount=100, tuppence_account=bella.id
124
+ )
125
+
126
+ # Or a client bound to her — a new one, on the same connections: `tuppence` is unchanged.
127
+ as_bella = tuppence.as_account(bella.id)
128
+ as_bella.bookings.list()
129
+ ```
130
+
131
+ `accounts.persons`, `.external_accounts` and `.capabilities`, and `files.create(purpose=…,
132
+ file=open("passport.png", "rb"))` are the rest of onboarding through your own forms.
133
+ `application_fees` (and `.refunds`) and `transfers` (and `.reversals`) are the money, and
134
+ `balance.list_transactions(balance="connect")` lists your Connect balance's every movement. The
135
+ async client has the same, method for method. A Connect webhook's event says which account it happened
136
+ on: `event["account"]`.
137
+
138
+ ## HTTP 402: put a price on a route
139
+
140
+ ```python
141
+ from tuppence.http402 import Http402Middleware, PayWall
142
+
143
+ app.add_middleware( # FastAPI, Starlette, any ASGI app
144
+ Http402Middleware,
145
+ routes={"GET /v1/report": PayWall(AsyncTuppence(key), price=50, currency="gbp")},
146
+ )
147
+ ```
148
+
149
+ `pip install "tuppence[http402]"`. Unpaid requests get `402 Payment Required` with where to pay;
150
+ the caller pays and retries with `Authorization: Tuppence-Receipt <token>` — verified offline
151
+ against Tuppence's published keys, and accepted once. Agents with prepaid credit pay with
152
+ `Authorization: Tuppence-Credits <token>` (`credits_meter=`). `paid_tool` does the same for a
153
+ tool on a Python MCP server.
154
+
155
+ ## Payouts and prices
156
+
157
+ `tuppence.payouts.settings()` says when money is paid out and what could change it;
158
+ `.instant_quote()` and `.create(method="instant")` move pending money now, and
159
+ `.set_next_day(enabled=True)` switches next-day payouts (UK) on or off once `next_day.available`
160
+ says so. `tuppence.account.retrieve_pricing()` is every price the account pays — the list's, or
161
+ one agreed with Tuppence, with its dates.
162
+
163
+ ## Models
164
+
165
+ Responses are pydantic models generated from the API's OpenAPI contract
166
+ (`apps/api/openapi.public.json`). Fields the API adds later are kept (`model_extra`), so an older
167
+ SDK still sees everything the server sent. Anything without a method yet:
168
+ `tuppence.request("GET", "/v1/…")` — same auth, retries and idempotency.
@@ -0,0 +1,145 @@
1
+ # tuppence
2
+
3
+ The Tuppence API for Python — payments for developers and AI agents.
4
+
5
+ ```bash
6
+ pip install tuppence
7
+ ```
8
+
9
+ ```python
10
+ import os
11
+ from tuppence import Tuppence
12
+
13
+ tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
14
+
15
+ link = tuppence.payment_links.create(name="Research report", amount=2000, currency="gbp")
16
+ print(link.url) # send it to whoever should pay
17
+
18
+ for payment in tuppence.payments.list_all(status="succeeded"):
19
+ print(payment.id, payment.amount, payment.fee, payment.net) # integer pence
20
+ ```
21
+
22
+ Python 3.10+. Money is always an integer number of minor units (pence).
23
+
24
+ ## Async
25
+
26
+ ```python
27
+ from tuppence import AsyncTuppence
28
+
29
+ async with AsyncTuppence(os.environ["TUPPENCE_SECRET_KEY"]) as tuppence:
30
+ payment = await tuppence.payments.create(amount=2000, currency="gbp")
31
+ async for refund in tuppence.refunds.list_all(payment=payment.id):
32
+ ...
33
+ ```
34
+
35
+ ## Retries and idempotency
36
+
37
+ Every POST carries an `Idempotency-Key`: yours, or a fresh one reused by every retry of that
38
+ call — so retrying after a timeout never charges twice. Network errors, timeouts, `5xx`, `429`
39
+ (honouring `Retry-After`) and an idempotent request still in flight are retried twice by default,
40
+ with jittered backoff. Nothing else is.
41
+
42
+ ```python
43
+ tuppence.refunds.create(payment="pay_…", amount=500, idempotency_key="order-42-refund")
44
+ tuppence = Tuppence(key, max_retries=4, timeout=10) # client-wide
45
+ tuppence.balance.retrieve(max_retries=0, timeout=2) # per request
46
+ ```
47
+
48
+ ## Another API address
49
+
50
+ The SDK calls `https://api.tuppence.ai`. To use another Tuppence environment you have been given,
51
+ set `TUPPENCE_API_URL`, or pass `base_url`:
52
+
53
+ ```python
54
+ tuppence = Tuppence(key, base_url="https://…")
55
+ ```
56
+
57
+ ## Errors
58
+
59
+ ```python
60
+ from tuppence import CardError, InvalidRequestError
61
+
62
+ try:
63
+ tuppence.refunds.create(payment="pay_…", amount=10_000)
64
+ except InvalidRequestError as e:
65
+ print(e.code, e.param, e.request_id) # branch on `code`; quote `request_id` to support
66
+ ```
67
+
68
+ `CardError`, `InvalidRequestError`, `AuthenticationError`, `PermissionError`, `RateLimitError`,
69
+ `IdempotencyError`, `APIError` and `ConnectionError` all derive from `TuppenceError`.
70
+
71
+ ## Webhooks
72
+
73
+ ```python
74
+ event = tuppence.webhooks.construct_event(
75
+ request.body, # the RAW body, exactly as received
76
+ request.headers["Tuppence-Signature"],
77
+ os.environ["TUPPENCE_WEBHOOK_SECRET"],
78
+ )
79
+ ```
80
+
81
+ A wrong secret, a tampered body or a timestamp more than five minutes old raises
82
+ `SignatureVerificationError` — never act on that payload.
83
+
84
+ ## Tuppence Connect: acting as your connected accounts
85
+
86
+ A platform's connected accounts are full Tuppence accounts. Make one, send its owner onboarding,
87
+ then act as it with your own key — per call, or through a client bound to it:
88
+
89
+ ```python
90
+ bella = tuppence.accounts.create(email="bella@example.com", business_type="individual")
91
+ link = tuppence.account_links.create(
92
+ account=bella.id,
93
+ type="account_onboarding",
94
+ refresh_url="https://groomly.example/onboarding/refresh",
95
+ return_url="https://groomly.example/onboarding/done",
96
+ )
97
+
98
+ # One call as Bella: the `Tuppence-Account` header, never part of the path.
99
+ tuppence.payments.create(
100
+ amount=4500, currency="gbp", application_fee_amount=100, tuppence_account=bella.id
101
+ )
102
+
103
+ # Or a client bound to her — a new one, on the same connections: `tuppence` is unchanged.
104
+ as_bella = tuppence.as_account(bella.id)
105
+ as_bella.bookings.list()
106
+ ```
107
+
108
+ `accounts.persons`, `.external_accounts` and `.capabilities`, and `files.create(purpose=…,
109
+ file=open("passport.png", "rb"))` are the rest of onboarding through your own forms.
110
+ `application_fees` (and `.refunds`) and `transfers` (and `.reversals`) are the money, and
111
+ `balance.list_transactions(balance="connect")` lists your Connect balance's every movement. The
112
+ async client has the same, method for method. A Connect webhook's event says which account it happened
113
+ on: `event["account"]`.
114
+
115
+ ## HTTP 402: put a price on a route
116
+
117
+ ```python
118
+ from tuppence.http402 import Http402Middleware, PayWall
119
+
120
+ app.add_middleware( # FastAPI, Starlette, any ASGI app
121
+ Http402Middleware,
122
+ routes={"GET /v1/report": PayWall(AsyncTuppence(key), price=50, currency="gbp")},
123
+ )
124
+ ```
125
+
126
+ `pip install "tuppence[http402]"`. Unpaid requests get `402 Payment Required` with where to pay;
127
+ the caller pays and retries with `Authorization: Tuppence-Receipt <token>` — verified offline
128
+ against Tuppence's published keys, and accepted once. Agents with prepaid credit pay with
129
+ `Authorization: Tuppence-Credits <token>` (`credits_meter=`). `paid_tool` does the same for a
130
+ tool on a Python MCP server.
131
+
132
+ ## Payouts and prices
133
+
134
+ `tuppence.payouts.settings()` says when money is paid out and what could change it;
135
+ `.instant_quote()` and `.create(method="instant")` move pending money now, and
136
+ `.set_next_day(enabled=True)` switches next-day payouts (UK) on or off once `next_day.available`
137
+ says so. `tuppence.account.retrieve_pricing()` is every price the account pays — the list's, or
138
+ one agreed with Tuppence, with its dates.
139
+
140
+ ## Models
141
+
142
+ Responses are pydantic models generated from the API's OpenAPI contract
143
+ (`apps/api/openapi.public.json`). Fields the API adds later are kept (`model_extra`), so an older
144
+ SDK still sees everything the server sent. Anything without a method yet:
145
+ `tuppence.request("GET", "/v1/…")` — same auth, retries and idempotency.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tuppence"
7
+ version = "0.1.0"
8
+ description = "The Tuppence API for Python — payments for developers and AI agents."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["httpx>=0.27,<1", "pydantic>=2.7,<3"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Typing :: Typed",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ # HTTP 402 for Python (tuppence.http402): receipts are Ed25519-signed, verified offline.
20
+ http402 = ["cryptography>=42"]
21
+ dev = [
22
+ "cryptography>=42",
23
+ "starlette>=0.37",
24
+ "pytest>=8",
25
+ "ruff>=0.6",
26
+ "mypy>=1.10",
27
+ # Pinned, with the formatters it runs: the generated models are committed, and a new version
28
+ # of any of the three reformats them (scripts/generate.py --check would then fail).
29
+ "datamodel-code-generator==0.34.0",
30
+ "black==26.10.1",
31
+ "isort==6.1.0",
32
+ ]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/tuppence"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ pythonpath = ["src"]
40
+
41
+ [tool.ruff]
42
+ line-length = 100
43
+ target-version = "py310"
44
+ # Generated (scripts/generate.py). Their sources are linted; mypy still checks both.
45
+ extend-exclude = ["src/tuppence/models.py", "src/tuppence/_sync_resources.py"]
46
+
47
+ [tool.ruff.lint]
48
+ select = ["E", "F", "I", "UP", "B"]
49
+
50
+ [tool.mypy]
51
+ strict = true
52
+ files = ["src/tuppence"]
53
+ plugins = ["pydantic.mypy"]
@@ -0,0 +1,142 @@
1
+ """Generates what the SDK does not write by hand (plan 11-03), and checks it is current.
2
+
3
+ python scripts/generate.py # write
4
+ python scripts/generate.py --check # fail if anything is stale (CI)
5
+
6
+ 1. ``src/tuppence/models.py`` — pydantic models from ``apps/api/openapi.public.json``. The
7
+ server validates what it is SENT; the SDK only reads what comes back, so length, pattern and
8
+ range constraints are dropped before generating — an answer is never refused for being, say,
9
+ an empty string the spec calls ``minLength: 1`` — and so are enums: a status the API adds next
10
+ year must not break parsing in an SDK installed today.
11
+ 2. ``src/tuppence/_sync_resources.py`` — the synchronous resources, from the async ones
12
+ (``_async_resources.py`` is the source of truth), the way ``unasync`` keeps httpcore's two
13
+ halves identical: one set of methods, two clients that cannot drift.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import re
20
+ import sys
21
+ import tempfile
22
+ from pathlib import Path
23
+
24
+ ROOT = Path(__file__).resolve().parent.parent
25
+ SPEC = ROOT.parent.parent / "apps" / "api" / "openapi.public.json"
26
+ MODELS = ROOT / "src" / "tuppence" / "models.py"
27
+ ASYNC = ROOT / "src" / "tuppence" / "_async_resources.py"
28
+ SYNC = ROOT / "src" / "tuppence" / "_sync_resources.py"
29
+
30
+ DROPPED = {
31
+ "minLength",
32
+ "maxLength",
33
+ "pattern",
34
+ "minimum",
35
+ "maximum",
36
+ "minItems",
37
+ "maxItems",
38
+ "multipleOf",
39
+ "enum",
40
+ }
41
+
42
+
43
+ def _loosen(node: object) -> object:
44
+ if isinstance(node, list):
45
+ return [_loosen(item) for item in node]
46
+ if isinstance(node, dict):
47
+ return {k: _loosen(v) for k, v in node.items() if k not in DROPPED}
48
+ return node
49
+
50
+
51
+ def _keep_store_event_types(raw: dict, spec: dict) -> dict:
52
+ """Each store event's ``type`` stays its one value (94-06): ``StoreEvent`` is a union
53
+ discriminated by it, so ``event.type == "order.created"`` narrows ``event.data.object``. A
54
+ new event type is a new schema, never a new value of an old one, so nothing breaks."""
55
+ schemas = raw["components"]["schemas"]
56
+ for item in schemas.get("StoreEvent", {}).get("oneOf", []):
57
+ name = item["$ref"].rsplit("/", 1)[-1]
58
+ spec["components"]["schemas"][name]["properties"]["type"] = schemas[name]["properties"][
59
+ "type"
60
+ ]
61
+ return spec
62
+
63
+
64
+ def models() -> str:
65
+ from datamodel_code_generator import DataModelType, InputFileType, LiteralType, generate
66
+ from datamodel_code_generator.format import PythonVersion
67
+
68
+ raw = json.loads(SPEC.read_text())
69
+ spec = _keep_store_event_types(raw, _loosen(raw))
70
+ with tempfile.TemporaryDirectory() as tmp:
71
+ source = Path(tmp) / "openapi.public.json"
72
+ source.write_text(json.dumps(spec))
73
+ target = Path(tmp) / "models.py"
74
+ generate(
75
+ source,
76
+ input_file_type=InputFileType.OpenAPI,
77
+ output=target,
78
+ output_model_type=DataModelType.PydanticV2BaseModel,
79
+ target_python_version=PythonVersion.PY_310,
80
+ base_class="tuppence._model.TuppenceModel",
81
+ use_standard_collections=True,
82
+ use_union_operator=True,
83
+ use_schema_description=True,
84
+ use_field_description=True,
85
+ strict_nullable=True,
86
+ # Only the store events' one-value `type`s keep an enum (see above): a Literal each.
87
+ enum_field_as_literal=LiteralType.One,
88
+ disable_timestamp=True,
89
+ custom_file_header=(
90
+ "# GENERATED from apps/api/openapi.public.json by scripts/generate.py.\n"
91
+ "# Do not edit: regenerate whenever the API contract changes."
92
+ ),
93
+ )
94
+ return target.read_text()
95
+
96
+
97
+ # async → sync, token by token. Order matters: the longer patterns first.
98
+ UNASYNC = [
99
+ (r"AsyncIterator", "Iterator"),
100
+ (r"AsyncHttpClient", "SyncHttpClient"),
101
+ (r"async def ", "def "),
102
+ (r"async for ", "for "),
103
+ (r"await ", ""),
104
+ (r"\bAsync([A-Z]\w*)", r"\1"),
105
+ (r"_async_resources", "_sync_resources"),
106
+ ]
107
+
108
+
109
+ def sync_resources() -> str:
110
+ text = ASYNC.read_text()
111
+ for pattern, replacement in UNASYNC:
112
+ text = re.sub(pattern, replacement, text)
113
+ header = (
114
+ "# GENERATED from _async_resources.py by scripts/generate.py — do not edit.\n"
115
+ "# Change the async resources and regenerate: the two clients cannot drift.\n"
116
+ )
117
+ return header + text
118
+
119
+
120
+ def main() -> int:
121
+ check = "--check" in sys.argv
122
+ stale = []
123
+ for path, text in ((MODELS, models()), (SYNC, sync_resources())):
124
+ if check:
125
+ if not path.exists() or path.read_text() != text:
126
+ stale.append(path.relative_to(ROOT))
127
+ else:
128
+ path.write_text(text)
129
+ print(f"wrote {path.relative_to(ROOT)}")
130
+ if stale:
131
+ print(
132
+ f"stale: {', '.join(map(str, stale))} — run `python scripts/generate.py` and commit",
133
+ file=sys.stderr,
134
+ )
135
+ return 1
136
+ if check:
137
+ print("generated files are current")
138
+ return 0
139
+
140
+
141
+ if __name__ == "__main__":
142
+ raise SystemExit(main())
@@ -0,0 +1,111 @@
1
+ """The Tuppence API for Python — payments for developers and AI agents.
2
+
3
+ from tuppence import Tuppence
4
+ tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
5
+ payment = tuppence.payments.create(amount=2000, currency="gbp")
6
+ """
7
+
8
+ from . import models
9
+ from ._client import AsyncTuppence, Tuppence
10
+ from ._errors import (
11
+ APIError,
12
+ AuthenticationError,
13
+ CardError,
14
+ ConnectionError,
15
+ IdempotencyError,
16
+ InvalidRequestError,
17
+ PermissionError,
18
+ RateLimitError,
19
+ SignatureVerificationError,
20
+ TuppenceError,
21
+ )
22
+ from ._http import DEFAULT_API_VERSION, VERSION
23
+ from ._model import Page, TuppenceModel
24
+ from ._webhooks import (
25
+ DEFAULT_TOLERANCE_SECONDS,
26
+ SIGNATURE_HEADER,
27
+ construct_event,
28
+ construct_store_event,
29
+ generate_test_header,
30
+ )
31
+
32
+ # The objects by the names a developer uses.
33
+ Account = models.AccountModel
34
+ #: Every price the account pays now — the list's, or one agreed with Tuppence.
35
+ AccountPricing = models.AccountPricingModel
36
+ Payment = models.PaymentModel
37
+ Refund = models.RefundModel
38
+ Customer = models.CustomerModel
39
+ PaymentLink = models.PaymentLinkModel
40
+ Balance = models.BalanceModel
41
+ BalanceTransaction = models.BalanceTransactionModel
42
+ Payout = models.PayoutModel
43
+ #: What an instant move would cost, to the bank or the card balance — or why it can't be made.
44
+ InstantPayoutQuote = models.InstantPayoutQuoteModel
45
+ PayoutSettings = models.PayoutSettingsModel
46
+ Dispute = models.DisputeModel
47
+ Event = models.EventModel
48
+ # Any of the store's webhook events (94-06): `.root` narrows on `type`.
49
+ StoreEvent = models.StoreEvent
50
+ WebhookEndpoint = models.WebhookEndpointModel
51
+ Meter = models.MeterModel
52
+ CreditBalance = models.CreditBalanceModel
53
+ # Tuppence Connect: a platform's connected accounts, and onboarding them.
54
+ ConnectedAccount = models.ConnectAccountModel
55
+ AccountLink = models.AccountLinkModel
56
+ Person = models.PersonModel
57
+ ExternalAccount = models.ExternalAccountModel
58
+ Capability = models.CapabilityModel
59
+ ApplicationFee = models.ApplicationFeeModel
60
+ Transfer = models.TransferModel
61
+
62
+ __version__ = VERSION
63
+
64
+ __all__ = [
65
+ "APIError",
66
+ "Account",
67
+ "AccountLink",
68
+ "AccountPricing",
69
+ "ApplicationFee",
70
+ "AsyncTuppence",
71
+ "AuthenticationError",
72
+ "Balance",
73
+ "BalanceTransaction",
74
+ "Capability",
75
+ "CardError",
76
+ "ConnectedAccount",
77
+ "ConnectionError",
78
+ "CreditBalance",
79
+ "Customer",
80
+ "DEFAULT_API_VERSION",
81
+ "DEFAULT_TOLERANCE_SECONDS",
82
+ "Dispute",
83
+ "Event",
84
+ "ExternalAccount",
85
+ "IdempotencyError",
86
+ "InstantPayoutQuote",
87
+ "InvalidRequestError",
88
+ "Meter",
89
+ "Page",
90
+ "Payment",
91
+ "PaymentLink",
92
+ "Payout",
93
+ "PayoutSettings",
94
+ "PermissionError",
95
+ "Person",
96
+ "RateLimitError",
97
+ "Refund",
98
+ "SIGNATURE_HEADER",
99
+ "SignatureVerificationError",
100
+ "StoreEvent",
101
+ "Transfer",
102
+ "Tuppence",
103
+ "TuppenceError",
104
+ "TuppenceModel",
105
+ "VERSION",
106
+ "WebhookEndpoint",
107
+ "construct_event",
108
+ "construct_store_event",
109
+ "generate_test_header",
110
+ "models",
111
+ ]