tgbp-kit 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.
@@ -0,0 +1,18 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ .claude/
5
+ .tgbp-source/
6
+ .api-spec/
7
+ test/fixtures/out/
8
+ test/fixtures/cache/
9
+ *.tsbuildinfo
10
+ .wrangler/
11
+ .env
12
+ python/dist/
13
+ python/.venv/
14
+ __pycache__/
15
+ *.egg-info/
16
+ .mypy_cache/
17
+ .pytest_cache/
18
+ .ruff_cache/
tgbp_kit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BCP Markets
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.5
2
+ Name: tgbp-kit
3
+ Version: 0.1.0
4
+ Summary: tGBP, the tokenised British pound, for Python: typed clients for the tGBP mint, redeem and swap API.
5
+ Project-URL: Homepage, https://dev.tokenisedgbp.com
6
+ Project-URL: Source, https://github.com/bcp-markets/tgbp-devkit/tree/main/python
7
+ Project-URL: Issues, https://github.com/bcp-markets/tgbp-devkit/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: gbp,mint,redeem,stablecoin,swap,tgbp
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Office/Business :: Financial
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: pydantic<3,>=2.7
16
+ Description-Content-Type: text/markdown
17
+
18
+ # tgbp-kit for Python
19
+
20
+ Typed Python clients for the tGBP mint, redeem and swap API. tGBP is the tokenised British pound,
21
+ issued by BCP Technologies Ltd. This package covers the same API operations as the TypeScript
22
+ client in [`@bcptechnologies/tgbp-kit`](https://www.npmjs.com/package/@bcptechnologies/tgbp-kit).
23
+ The request and response models are generated from the tGBP API spec.
24
+
25
+ ```sh
26
+ pip install tgbp-kit
27
+ ```
28
+
29
+ It needs Python 3.10 or later and depends only on pydantic. Run it on your server: API keys carry
30
+ no scopes, so never ship one to a client device.
31
+
32
+ ## Mint and redeem
33
+
34
+ ```python
35
+ import os
36
+ from decimal import Decimal
37
+
38
+ from tgbp_kit import Created, TgbpClient
39
+
40
+ tgbp = TgbpClient(api_key=os.environ["TGBP_API_KEY"], environment="sandbox")
41
+
42
+ mint = tgbp.mints.create(
43
+ {
44
+ "customerId": "customer_…",
45
+ "destinationAddress": "0x…",
46
+ "chain": "sepolia",
47
+ "amount": {"currency": "GBP", "value": Decimal("100.00")},
48
+ }
49
+ )
50
+ if isinstance(mint, Created):
51
+ print(mint.data.bank_transfer_details) # pay this account, with this reference
52
+ tgbp.mints.simulate_payin(mint.data.id) # sandbox only
53
+
54
+ redemption = tgbp.redemptions.create(
55
+ {"chain": "sepolia", "amount": {"currency": "GBP", "value": 50}, "bankId": "bank_…"}
56
+ )
57
+ # Then send the tGBP to redemption.data.burnAddress; GBP is paid out once the burn confirms.
58
+ ```
59
+
60
+ `environment="sandbox"` calls `https://sandbox.tgbp.io`, which takes staging keys, runs on
61
+ testnets and simulates bank transfers. `environment="production"` calls `https://api.tgbp.io`,
62
+ and `base_url=` takes any other host.
63
+
64
+ | `TgbpClient` | Does |
65
+ |---|---|
66
+ | `mints.create`, `mints.get`, `mints.list`, `mints.cancel` | Create, read, list and cancel mints |
67
+ | `mints.simulate_payin` | Sandbox only: simulate the bank transfer for a pending mint |
68
+ | `redemptions.create`, `redemptions.get`, `redemptions.list`, `redemptions.cancel` | Create, read, list and cancel redemptions |
69
+ | `redemptions.quote` | Screen the address that will send the burn, before you burn |
70
+ | `redemptions.confirm_burn` | Report the burn when it is not detected on its own |
71
+
72
+ A request body is a model from `tgbp_kit.models`, such as `MintCreateRequest`, or a dict the
73
+ client validates against that model. A list takes its query as a model:
74
+
75
+ ```python
76
+ from tgbp_kit.models import ListMintsQuery
77
+
78
+ page = tgbp.mints.list(ListMintsQuery(status=["pending"], per_page=50))
79
+ ```
80
+
81
+ ## Swap
82
+
83
+ `TgbpSwapClient` converts between USDC and tGBP. It defaults to production; for staging, pass
84
+ the swap host from your onboarding pack as `base_url`.
85
+
86
+ ```python
87
+ from tgbp_kit import TgbpSwapClient
88
+ from tgbp_kit.swap_models import GetQuoteQuery
89
+
90
+ swap = TgbpSwapClient(api_key=os.environ["TGBP_API_KEY"])
91
+ quote = swap.get_quote(GetQuoteQuery(direction="USDC_TO_TGBP", usdcAmount="100"))
92
+ ```
93
+
94
+ Its methods are `get_quote`, `get_limits`, `get_supported_networks`, `get_deposit_session`,
95
+ `create_deposit_address`, `list_deposits`, `get_deposit_balance`, `create_swap_intent`,
96
+ `get_swap_intent`, `cancel_swap_intent` and `create_conversion`. Each returns the record inside
97
+ the API's `data` field.
98
+
99
+ ## Amounts
100
+
101
+ Pass amounts as `Decimal`, a string or an int. A float raises `TypeError`, because a float cannot
102
+ hold most decimal amounts exactly. The client reads every JSON number with a fraction as
103
+ `Decimal`, and writes a `Decimal` to the API digit for digit.
104
+
105
+ ## Idempotency and retries
106
+
107
+ Every call that creates a mint, redemption, swap intent or conversion sends an idempotency key.
108
+ The client generates one unless you pass `idempotency_key=`, and returns it on the result. A retry
109
+ with the same key returns the first record and never creates a second one. Keys are 1 to 18
110
+ letters, digits, `_` or `-`, and last 24 hours. A swap intent's key is its `id`.
111
+
112
+ The client retries a network error or a 5xx response up to 3 times, with backoff, and resends the
113
+ same key each time. It never retries a 4xx, including 429. It never retries a call that is unsafe
114
+ to repeat and has no key, such as `simulate_payin` or `cancel_swap_intent`. Set `max_retries=`,
115
+ `base_delay=` (seconds) and `timeout=` (seconds, for the connect and each read) to change this.
116
+
117
+ A create returns `Created`, whose `status` is `"created"` for a new record and `"replayed"` for an
118
+ earlier one with the same key. It returns `PendingApproval` when your account needs a second
119
+ person to approve the action. Nothing exists until then, so do not retry.
120
+
121
+ ## Errors
122
+
123
+ A failed call raises `TgbpKitError`, with a `code` and a `details` dict:
124
+
125
+ | `code` | Meaning |
126
+ |---|---|
127
+ | `API_ERROR` | The API answered with an error, or with a success body that does not match the spec. `details` carries `status`, the API's `apiCode` and `requestId` (quote it to support). |
128
+ | `API_UNREACHABLE` | No response, after every retry. |
129
+
130
+ When `details["mayHaveTakenEffect"]` is true, the call may have created or changed a record.
131
+ Retry only with `details["idempotencyKey"]` (or `details["swapIntentId"]`), or read the record
132
+ before you send anything new. An invalid argument raises `ValueError` or `TypeError` before any
133
+ request is sent.
134
+
135
+ ## Development
136
+
137
+ From the repo root:
138
+
139
+ ```sh
140
+ python -m venv python/.venv && . python/.venv/bin/activate
141
+ pip install --group python/pyproject.toml:dev -e python
142
+ ruff check python && mypy --strict --config-file python/pyproject.toml python/ && pytest python
143
+ ```
144
+
145
+ `python/spec/*.json` holds the API spec, pruned to the operations the kit publishes. The tGBP team
146
+ regenerates it from the private spec with `pnpm generate:api`.
147
+ `python/scripts/generate_models.py` then writes `models.py` and `swap_models.py`; CI fails when
148
+ they drift from the spec. `pnpm check:python-parity` checks that this client has every operation
149
+ the TypeScript client has.
150
+
151
+ `tests/test_sandbox_live.py` creates, replays and cancels a mint on the sandbox. It runs when
152
+ `TGBP_SANDBOX_API_KEY`, `TGBP_SANDBOX_CUSTOMER_ID` and `TGBP_SANDBOX_ADDRESS` are set.
153
+
154
+ ## Releases
155
+
156
+ Publishing a GitHub release tagged `python-v<version>` publishes this package to PyPI through
157
+ trusted publishing. The npm package releases on `v<version>` tags.
158
+
159
+ ## Licence
160
+
161
+ MIT
@@ -0,0 +1,144 @@
1
+ # tgbp-kit for Python
2
+
3
+ Typed Python clients for the tGBP mint, redeem and swap API. tGBP is the tokenised British pound,
4
+ issued by BCP Technologies Ltd. This package covers the same API operations as the TypeScript
5
+ client in [`@bcptechnologies/tgbp-kit`](https://www.npmjs.com/package/@bcptechnologies/tgbp-kit).
6
+ The request and response models are generated from the tGBP API spec.
7
+
8
+ ```sh
9
+ pip install tgbp-kit
10
+ ```
11
+
12
+ It needs Python 3.10 or later and depends only on pydantic. Run it on your server: API keys carry
13
+ no scopes, so never ship one to a client device.
14
+
15
+ ## Mint and redeem
16
+
17
+ ```python
18
+ import os
19
+ from decimal import Decimal
20
+
21
+ from tgbp_kit import Created, TgbpClient
22
+
23
+ tgbp = TgbpClient(api_key=os.environ["TGBP_API_KEY"], environment="sandbox")
24
+
25
+ mint = tgbp.mints.create(
26
+ {
27
+ "customerId": "customer_…",
28
+ "destinationAddress": "0x…",
29
+ "chain": "sepolia",
30
+ "amount": {"currency": "GBP", "value": Decimal("100.00")},
31
+ }
32
+ )
33
+ if isinstance(mint, Created):
34
+ print(mint.data.bank_transfer_details) # pay this account, with this reference
35
+ tgbp.mints.simulate_payin(mint.data.id) # sandbox only
36
+
37
+ redemption = tgbp.redemptions.create(
38
+ {"chain": "sepolia", "amount": {"currency": "GBP", "value": 50}, "bankId": "bank_…"}
39
+ )
40
+ # Then send the tGBP to redemption.data.burnAddress; GBP is paid out once the burn confirms.
41
+ ```
42
+
43
+ `environment="sandbox"` calls `https://sandbox.tgbp.io`, which takes staging keys, runs on
44
+ testnets and simulates bank transfers. `environment="production"` calls `https://api.tgbp.io`,
45
+ and `base_url=` takes any other host.
46
+
47
+ | `TgbpClient` | Does |
48
+ |---|---|
49
+ | `mints.create`, `mints.get`, `mints.list`, `mints.cancel` | Create, read, list and cancel mints |
50
+ | `mints.simulate_payin` | Sandbox only: simulate the bank transfer for a pending mint |
51
+ | `redemptions.create`, `redemptions.get`, `redemptions.list`, `redemptions.cancel` | Create, read, list and cancel redemptions |
52
+ | `redemptions.quote` | Screen the address that will send the burn, before you burn |
53
+ | `redemptions.confirm_burn` | Report the burn when it is not detected on its own |
54
+
55
+ A request body is a model from `tgbp_kit.models`, such as `MintCreateRequest`, or a dict the
56
+ client validates against that model. A list takes its query as a model:
57
+
58
+ ```python
59
+ from tgbp_kit.models import ListMintsQuery
60
+
61
+ page = tgbp.mints.list(ListMintsQuery(status=["pending"], per_page=50))
62
+ ```
63
+
64
+ ## Swap
65
+
66
+ `TgbpSwapClient` converts between USDC and tGBP. It defaults to production; for staging, pass
67
+ the swap host from your onboarding pack as `base_url`.
68
+
69
+ ```python
70
+ from tgbp_kit import TgbpSwapClient
71
+ from tgbp_kit.swap_models import GetQuoteQuery
72
+
73
+ swap = TgbpSwapClient(api_key=os.environ["TGBP_API_KEY"])
74
+ quote = swap.get_quote(GetQuoteQuery(direction="USDC_TO_TGBP", usdcAmount="100"))
75
+ ```
76
+
77
+ Its methods are `get_quote`, `get_limits`, `get_supported_networks`, `get_deposit_session`,
78
+ `create_deposit_address`, `list_deposits`, `get_deposit_balance`, `create_swap_intent`,
79
+ `get_swap_intent`, `cancel_swap_intent` and `create_conversion`. Each returns the record inside
80
+ the API's `data` field.
81
+
82
+ ## Amounts
83
+
84
+ Pass amounts as `Decimal`, a string or an int. A float raises `TypeError`, because a float cannot
85
+ hold most decimal amounts exactly. The client reads every JSON number with a fraction as
86
+ `Decimal`, and writes a `Decimal` to the API digit for digit.
87
+
88
+ ## Idempotency and retries
89
+
90
+ Every call that creates a mint, redemption, swap intent or conversion sends an idempotency key.
91
+ The client generates one unless you pass `idempotency_key=`, and returns it on the result. A retry
92
+ with the same key returns the first record and never creates a second one. Keys are 1 to 18
93
+ letters, digits, `_` or `-`, and last 24 hours. A swap intent's key is its `id`.
94
+
95
+ The client retries a network error or a 5xx response up to 3 times, with backoff, and resends the
96
+ same key each time. It never retries a 4xx, including 429. It never retries a call that is unsafe
97
+ to repeat and has no key, such as `simulate_payin` or `cancel_swap_intent`. Set `max_retries=`,
98
+ `base_delay=` (seconds) and `timeout=` (seconds, for the connect and each read) to change this.
99
+
100
+ A create returns `Created`, whose `status` is `"created"` for a new record and `"replayed"` for an
101
+ earlier one with the same key. It returns `PendingApproval` when your account needs a second
102
+ person to approve the action. Nothing exists until then, so do not retry.
103
+
104
+ ## Errors
105
+
106
+ A failed call raises `TgbpKitError`, with a `code` and a `details` dict:
107
+
108
+ | `code` | Meaning |
109
+ |---|---|
110
+ | `API_ERROR` | The API answered with an error, or with a success body that does not match the spec. `details` carries `status`, the API's `apiCode` and `requestId` (quote it to support). |
111
+ | `API_UNREACHABLE` | No response, after every retry. |
112
+
113
+ When `details["mayHaveTakenEffect"]` is true, the call may have created or changed a record.
114
+ Retry only with `details["idempotencyKey"]` (or `details["swapIntentId"]`), or read the record
115
+ before you send anything new. An invalid argument raises `ValueError` or `TypeError` before any
116
+ request is sent.
117
+
118
+ ## Development
119
+
120
+ From the repo root:
121
+
122
+ ```sh
123
+ python -m venv python/.venv && . python/.venv/bin/activate
124
+ pip install --group python/pyproject.toml:dev -e python
125
+ ruff check python && mypy --strict --config-file python/pyproject.toml python/ && pytest python
126
+ ```
127
+
128
+ `python/spec/*.json` holds the API spec, pruned to the operations the kit publishes. The tGBP team
129
+ regenerates it from the private spec with `pnpm generate:api`.
130
+ `python/scripts/generate_models.py` then writes `models.py` and `swap_models.py`; CI fails when
131
+ they drift from the spec. `pnpm check:python-parity` checks that this client has every operation
132
+ the TypeScript client has.
133
+
134
+ `tests/test_sandbox_live.py` creates, replays and cancels a mint on the sandbox. It runs when
135
+ `TGBP_SANDBOX_API_KEY`, `TGBP_SANDBOX_CUSTOMER_ID` and `TGBP_SANDBOX_ADDRESS` are set.
136
+
137
+ ## Releases
138
+
139
+ Publishing a GitHub release tagged `python-v<version>` publishes this package to PyPI through
140
+ trusted publishing. The npm package releases on `v<version>` tags.
141
+
142
+ ## Licence
143
+
144
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ # Pinned: the build runs in the release workflow.
3
+ requires = ["hatchling==1.32.4"]
4
+ build-backend = "hatchling.build"
5
+
6
+ [project]
7
+ name = "tgbp-kit"
8
+ dynamic = ["version"]
9
+ description = "tGBP, the tokenised British pound, for Python: typed clients for the tGBP mint, redeem and swap API."
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["pydantic>=2.7,<3"]
15
+ keywords = ["tgbp", "stablecoin", "gbp", "mint", "redeem", "swap"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Typing :: Typed",
19
+ "Topic :: Office/Business :: Financial",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://dev.tokenisedgbp.com"
24
+ Source = "https://github.com/bcp-markets/tgbp-devkit/tree/main/python"
25
+ Issues = "https://github.com/bcp-markets/tgbp-devkit/issues"
26
+
27
+ [dependency-groups]
28
+ dev = ["mypy==2.4.0", "pytest==9.1.1", "ruff==0.17.3"]
29
+
30
+ [tool.hatch.version]
31
+ path = "src/tgbp_kit/_version.py"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/tgbp_kit"]
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ include = ["src/tgbp_kit", "tests", "README.md", "LICENSE"]
38
+
39
+ [tool.mypy]
40
+ strict = true
41
+ python_version = "3.10"
42
+ plugins = ["pydantic.mypy"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ addopts = "--strict-markers"
47
+
48
+ [tool.ruff]
49
+ line-length = 110
50
+ target-version = "py310"
51
+ extend-exclude = ["src/tgbp_kit/models.py", "src/tgbp_kit/swap_models.py"]
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I", "B", "UP", "PL"]
55
+ ignore = ["PLR0911", "PLR0912", "PLR0913", "PLR0917", "PLR2004"]
@@ -0,0 +1,25 @@
1
+ """tGBP, the tokenised British pound, for Python: typed clients for the mint, redeem and swap API."""
2
+
3
+ from ._http import HttpRequest, HttpResponse, Transport, generate_idempotency_key
4
+ from ._version import __version__
5
+ from .client import API_BASE_URLS, Created, PendingApproval, TgbpClient, TgbpEnvironment
6
+ from .errors import TgbpErrorCode, TgbpKitError
7
+ from .swap import SWAP_PRODUCTION_URL, SwapCreateResult, TgbpSwapClient
8
+
9
+ __all__ = [
10
+ "API_BASE_URLS",
11
+ "SWAP_PRODUCTION_URL",
12
+ "Created",
13
+ "HttpRequest",
14
+ "HttpResponse",
15
+ "PendingApproval",
16
+ "SwapCreateResult",
17
+ "TgbpClient",
18
+ "TgbpEnvironment",
19
+ "TgbpErrorCode",
20
+ "TgbpKitError",
21
+ "TgbpSwapClient",
22
+ "Transport",
23
+ "__version__",
24
+ "generate_idempotency_key",
25
+ ]
@@ -0,0 +1,35 @@
1
+ """The base class every generated model extends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, ConfigDict, model_validator
8
+
9
+
10
+ def reject_floats(value: Any, where: str = "value") -> None:
11
+ """Raises TypeError if a float appears anywhere in `value`.
12
+
13
+ A float cannot hold most decimal amounts exactly, so 0.1 + 0.2 would reach
14
+ the API as 0.30000000000000004. Pass a `Decimal`, a string or an int.
15
+ """
16
+ if isinstance(value, float):
17
+ raise TypeError(f"{where} is a float ({value!r}); pass a Decimal, a str or an int")
18
+ if isinstance(value, dict):
19
+ for key, item in value.items():
20
+ reject_floats(item, f"{where}.{key}")
21
+ elif isinstance(value, (list, tuple)):
22
+ for index, item in enumerate(value):
23
+ reject_floats(item, f"{where}[{index}]")
24
+
25
+
26
+ class Model(BaseModel):
27
+ # Fields the pinned spec does not know yet are kept, not dropped.
28
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
29
+
30
+ # A before-validator on the whole input also covers the extra fields.
31
+ @model_validator(mode="before")
32
+ @classmethod
33
+ def _no_floats(cls, data: Any) -> Any:
34
+ reject_floats(data, cls.__name__)
35
+ return data