beexar 1.0.1__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,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to `beexar`. The version is shared across the Beexar SDKs
4
+ for Node, PHP, Go and Python — the same number always means the same contract
5
+ snapshot.
6
+
7
+ ## 1.0.1 — 2026-09-18
8
+
9
+ No change to the code you consume. The release exists to move publishing onto
10
+ npm's and PyPI's trusted publishing, so no long-lived registry token is stored
11
+ anywhere any more.
12
+
13
+ ## 1.0.0 — 2026-09-18
14
+
15
+ First public release.
16
+
17
+ - `Client` — `launch_real`, `launch_demo`, `list_games`, signed with HMAC-SHA256
18
+ over the exact request bytes.
19
+ - `WalletServer` — the four seamless-wallet callbacks as one pure
20
+ `dispatch(route, raw_body, signature)`.
21
+ - Bindings for ASGI, FastAPI and Flask, each dealing with that framework's own
22
+ way of destroying the raw body.
23
+ - `Money` — decimal amounts that cannot be built from a float.
24
+ - `WalletError` — the full api_code registry, with the balance a required
25
+ argument for codes 100, 105 and 106.
26
+ - No runtime dependencies.
beexar-1.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Beexar
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.
beexar-1.0.1/PKG-INFO ADDED
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.5
2
+ Name: beexar
3
+ Version: 1.0.1
4
+ Summary: Beexar operator SDK — launch game sessions and serve the four seamless-wallet callbacks.
5
+ Project-URL: Documentation, https://docs.beexar.com/tools/sdk-python/
6
+ Project-URL: Source, https://github.com/beexar-games/beexar-python
7
+ Project-URL: Issues, https://github.com/beexar-games/beexar-python/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: beexar,betwin,casino,game-aggregator,rollback,seamless-wallet,softswiss
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest==8.3.4; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # beexar
27
+
28
+ Beexar operator SDK for Python. Launch game sessions, and serve the four
29
+ seamless-wallet callbacks the platform calls during play.
30
+
31
+ **Zero runtime dependencies** — standard library only. Python 3.9+.
32
+
33
+ ```bash
34
+ pip install beexar
35
+ ```
36
+
37
+ Full docs: **https://docs.beexar.com** · OpenAPI: **https://docs.beexar.com/api-reference/**
38
+
39
+ ---
40
+
41
+ ## The integration in one picture
42
+
43
+ There are two halves, and the second one is the work.
44
+
45
+ ```
46
+ you ── POST /api/v1/softswiss/launcher/real ──▶ Beexar (Client)
47
+
48
+ player plays │
49
+
50
+ your wallet ◀── POST /balance /betwin /rollback /finish ── Beexar (WalletServer)
51
+ ```
52
+
53
+ ## Half 1 — launching a game
54
+
55
+ ```python
56
+ from beexar import Client
57
+
58
+ beexar = Client(os.environ["BEEXAR_CASINO_ID"], os.environ["BEEXAR_API_SECRET"])
59
+
60
+ launch_url = beexar.launch_real({
61
+ "game": "dice",
62
+ "account": {"id": "player_123", "currency": "EUR"},
63
+ "locale": "en",
64
+ })
65
+ # put launch_url in an iframe
66
+ ```
67
+
68
+ `launch_demo()` does the same on a virtual balance and makes no wallet calls.
69
+ `list_games()` returns the catalogue enabled for you.
70
+
71
+ ## Half 2 — serving the wallet
72
+
73
+ Implement four methods against your ledger. Everything else — signature
74
+ verification, parsing, validation, the error envelope — is handled.
75
+
76
+ ```python
77
+ from beexar import BetWinResult, BetWinTransactionResult, WalletError, WalletServer
78
+
79
+ class Wallet:
80
+ def bet_win(self, request, context):
81
+ with self.db.transaction(): # ONE transaction. See "The boundary".
82
+ results = []
83
+ for t in request.transactions:
84
+ seen = self.lookup(t.id_provider)
85
+ if seen:
86
+ results.append(BetWinTransactionResult(t.id_provider, seen.id))
87
+ continue
88
+ if self.is_rolled_back(t.id_provider):
89
+ raise WalletError.already_rolled_back()
90
+ if t.type == "bet" and self.balance().compare(t.amount) < 0:
91
+ raise WalletError.insufficient_funds(self.balance())
92
+ results.append(BetWinTransactionResult(t.id_provider, self.apply(t)))
93
+ return BetWinResult(self.round_id(request.round_id), self.balance(), results)
94
+
95
+ # … balance(), rollback(), finish()
96
+
97
+ server = WalletServer(Wallet(), os.environ["BEEXAR_API_SECRET"])
98
+ ```
99
+
100
+ Mount it:
101
+
102
+ ```python
103
+ from beexar.asgi import fastapi_router
104
+ app.include_router(fastapi_router(server, prefix="/wallet")) # FastAPI
105
+
106
+ from beexar.asgi import flask_blueprint
107
+ app.register_blueprint(flask_blueprint(server, url_prefix="/wallet")) # Flask
108
+
109
+ from beexar.asgi import asgi_app
110
+ application = asgi_app(server) # bare ASGI
111
+ ```
112
+
113
+ Then point the four callback URLs in the backoffice at
114
+ `https://your-host/wallet/{balance,betwin,rollback,finish}` and run the
115
+ [Integration Test Game](https://docs.beexar.com/guides/testing/) — 29 scenarios
116
+ against your implementation.
117
+
118
+ A complete, correct wallet you can read in one sitting:
119
+ [`examples/inmemory_wallet.py`](./examples/inmemory_wallet.py). A runnable
120
+ server: [`examples/server.py`](./examples/server.py).
121
+
122
+ ## The raw body — read this one
123
+
124
+ The signature is an HMAC over the **exact bytes** of the request. If anything
125
+ parses the JSON and serialises it again before the SDK sees it, those bytes are
126
+ gone — key order, escaping and number rendering all change — and no signature
127
+ can ever match again.
128
+
129
+ | Framework | The trap | What the binding does |
130
+ |---|---|---|
131
+ | FastAPI | declaring a Pydantic model makes Starlette parse the body and you never see bytes | declares `Request`, calls `await request.body()` |
132
+ | Flask | `request.get_data(as_text=True)` re-decodes the body | `request.get_data(cache=True)` → bytes |
133
+ | ASGI | building the body by concatenating decoded chunks corrupts multi-byte characters | concatenates `bytes` |
134
+
135
+ `dispatch()` refuses a `str` outright — a decode is not always reversible, and
136
+ verifying a lossy copy is worse than failing. When a signature fails and the SDK
137
+ can tell why, the `on_warning` callback receives the reason in plain words.
138
+
139
+ ## Money
140
+
141
+ Amounts and balances are decimal strings in the currency's main unit — `"0.90"`
142
+ is ninety cents. `Money` cannot be built from a float, raises on `float()`, and
143
+ refuses `+` and `-` so an accidental mix with a number is impossible.
144
+
145
+ ```python
146
+ str(Money.parse("100.00").sub(Money.parse("0.30"))) # "99.70"
147
+ str(Money.from_minor_units(9970, 2)) # "99.70" from an integer ledger
148
+ ```
149
+
150
+ `"1E2000000000"` and anything past 16 decimals is rejected on shape, before any
151
+ arithmetic touches it.
152
+
153
+ ## Errors
154
+
155
+ Two HTTP statuses exist on this contract, 400 and 500, and the meaning lives in
156
+ `meta.api_code`. Raise a `WalletError` and the envelope is built for you:
157
+
158
+ ```python
159
+ raise WalletError.insufficient_funds(current_balance) # 400 / api_code 100
160
+ raise WalletError.already_rolled_back() # 400 / api_code 409
161
+ raise WalletError.invalid_player() # 400 / api_code 101
162
+ ```
163
+
164
+ Codes 100, 105 and 106 **must** carry the player's balance, so those
165
+ constructors take it as a required argument — there is no way to build one
166
+ without it. Anything else you raise becomes an opaque 500 and your message never
167
+ leaves the process.
168
+
169
+ ## The boundary
170
+
171
+ The SDK does **not** do idempotency or tombstones for you, and it will not
172
+ pretend to. Both have to happen in the same database transaction as the balance
173
+ update, and no library can join your transaction. What you must do:
174
+
175
+ 1. Store every `id_provider` with a unique index, and check it **inside** the
176
+ transaction that moves the money.
177
+ 2. Store the response you returned — a repeat must return the id and balance you
178
+ gave the first time, not today's.
179
+ 3. On rollback, record a tombstone for `original_id_provider` **whether or not**
180
+ the original exists. Out-of-order delivery is normal; a later `/betwin` for a
181
+ tombstoned id must be refused with `WalletError.already_rolled_back()`.
182
+
183
+ Your handler has **15 seconds**. The platform retries 5xx and timeouts for up to
184
+ 30 seconds with the same `id_provider`; it does not retry insufficient funds,
185
+ bet limits, bad requests or signature failures.
186
+
187
+ ## Types come from the spec
188
+
189
+ The dataclasses track the published OpenAPI documents (`openapi/` in this repo).
190
+ `tests/test_contract.py` reads a normalised description of those specs and fails
191
+ if a property the contract declares never reaches your handler.
192
+
193
+ ## License
194
+
195
+ MIT
beexar-1.0.1/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # beexar
2
+
3
+ Beexar operator SDK for Python. Launch game sessions, and serve the four
4
+ seamless-wallet callbacks the platform calls during play.
5
+
6
+ **Zero runtime dependencies** — standard library only. Python 3.9+.
7
+
8
+ ```bash
9
+ pip install beexar
10
+ ```
11
+
12
+ Full docs: **https://docs.beexar.com** · OpenAPI: **https://docs.beexar.com/api-reference/**
13
+
14
+ ---
15
+
16
+ ## The integration in one picture
17
+
18
+ There are two halves, and the second one is the work.
19
+
20
+ ```
21
+ you ── POST /api/v1/softswiss/launcher/real ──▶ Beexar (Client)
22
+
23
+ player plays │
24
+
25
+ your wallet ◀── POST /balance /betwin /rollback /finish ── Beexar (WalletServer)
26
+ ```
27
+
28
+ ## Half 1 — launching a game
29
+
30
+ ```python
31
+ from beexar import Client
32
+
33
+ beexar = Client(os.environ["BEEXAR_CASINO_ID"], os.environ["BEEXAR_API_SECRET"])
34
+
35
+ launch_url = beexar.launch_real({
36
+ "game": "dice",
37
+ "account": {"id": "player_123", "currency": "EUR"},
38
+ "locale": "en",
39
+ })
40
+ # put launch_url in an iframe
41
+ ```
42
+
43
+ `launch_demo()` does the same on a virtual balance and makes no wallet calls.
44
+ `list_games()` returns the catalogue enabled for you.
45
+
46
+ ## Half 2 — serving the wallet
47
+
48
+ Implement four methods against your ledger. Everything else — signature
49
+ verification, parsing, validation, the error envelope — is handled.
50
+
51
+ ```python
52
+ from beexar import BetWinResult, BetWinTransactionResult, WalletError, WalletServer
53
+
54
+ class Wallet:
55
+ def bet_win(self, request, context):
56
+ with self.db.transaction(): # ONE transaction. See "The boundary".
57
+ results = []
58
+ for t in request.transactions:
59
+ seen = self.lookup(t.id_provider)
60
+ if seen:
61
+ results.append(BetWinTransactionResult(t.id_provider, seen.id))
62
+ continue
63
+ if self.is_rolled_back(t.id_provider):
64
+ raise WalletError.already_rolled_back()
65
+ if t.type == "bet" and self.balance().compare(t.amount) < 0:
66
+ raise WalletError.insufficient_funds(self.balance())
67
+ results.append(BetWinTransactionResult(t.id_provider, self.apply(t)))
68
+ return BetWinResult(self.round_id(request.round_id), self.balance(), results)
69
+
70
+ # … balance(), rollback(), finish()
71
+
72
+ server = WalletServer(Wallet(), os.environ["BEEXAR_API_SECRET"])
73
+ ```
74
+
75
+ Mount it:
76
+
77
+ ```python
78
+ from beexar.asgi import fastapi_router
79
+ app.include_router(fastapi_router(server, prefix="/wallet")) # FastAPI
80
+
81
+ from beexar.asgi import flask_blueprint
82
+ app.register_blueprint(flask_blueprint(server, url_prefix="/wallet")) # Flask
83
+
84
+ from beexar.asgi import asgi_app
85
+ application = asgi_app(server) # bare ASGI
86
+ ```
87
+
88
+ Then point the four callback URLs in the backoffice at
89
+ `https://your-host/wallet/{balance,betwin,rollback,finish}` and run the
90
+ [Integration Test Game](https://docs.beexar.com/guides/testing/) — 29 scenarios
91
+ against your implementation.
92
+
93
+ A complete, correct wallet you can read in one sitting:
94
+ [`examples/inmemory_wallet.py`](./examples/inmemory_wallet.py). A runnable
95
+ server: [`examples/server.py`](./examples/server.py).
96
+
97
+ ## The raw body — read this one
98
+
99
+ The signature is an HMAC over the **exact bytes** of the request. If anything
100
+ parses the JSON and serialises it again before the SDK sees it, those bytes are
101
+ gone — key order, escaping and number rendering all change — and no signature
102
+ can ever match again.
103
+
104
+ | Framework | The trap | What the binding does |
105
+ |---|---|---|
106
+ | FastAPI | declaring a Pydantic model makes Starlette parse the body and you never see bytes | declares `Request`, calls `await request.body()` |
107
+ | Flask | `request.get_data(as_text=True)` re-decodes the body | `request.get_data(cache=True)` → bytes |
108
+ | ASGI | building the body by concatenating decoded chunks corrupts multi-byte characters | concatenates `bytes` |
109
+
110
+ `dispatch()` refuses a `str` outright — a decode is not always reversible, and
111
+ verifying a lossy copy is worse than failing. When a signature fails and the SDK
112
+ can tell why, the `on_warning` callback receives the reason in plain words.
113
+
114
+ ## Money
115
+
116
+ Amounts and balances are decimal strings in the currency's main unit — `"0.90"`
117
+ is ninety cents. `Money` cannot be built from a float, raises on `float()`, and
118
+ refuses `+` and `-` so an accidental mix with a number is impossible.
119
+
120
+ ```python
121
+ str(Money.parse("100.00").sub(Money.parse("0.30"))) # "99.70"
122
+ str(Money.from_minor_units(9970, 2)) # "99.70" from an integer ledger
123
+ ```
124
+
125
+ `"1E2000000000"` and anything past 16 decimals is rejected on shape, before any
126
+ arithmetic touches it.
127
+
128
+ ## Errors
129
+
130
+ Two HTTP statuses exist on this contract, 400 and 500, and the meaning lives in
131
+ `meta.api_code`. Raise a `WalletError` and the envelope is built for you:
132
+
133
+ ```python
134
+ raise WalletError.insufficient_funds(current_balance) # 400 / api_code 100
135
+ raise WalletError.already_rolled_back() # 400 / api_code 409
136
+ raise WalletError.invalid_player() # 400 / api_code 101
137
+ ```
138
+
139
+ Codes 100, 105 and 106 **must** carry the player's balance, so those
140
+ constructors take it as a required argument — there is no way to build one
141
+ without it. Anything else you raise becomes an opaque 500 and your message never
142
+ leaves the process.
143
+
144
+ ## The boundary
145
+
146
+ The SDK does **not** do idempotency or tombstones for you, and it will not
147
+ pretend to. Both have to happen in the same database transaction as the balance
148
+ update, and no library can join your transaction. What you must do:
149
+
150
+ 1. Store every `id_provider` with a unique index, and check it **inside** the
151
+ transaction that moves the money.
152
+ 2. Store the response you returned — a repeat must return the id and balance you
153
+ gave the first time, not today's.
154
+ 3. On rollback, record a tombstone for `original_id_provider` **whether or not**
155
+ the original exists. Out-of-order delivery is normal; a later `/betwin` for a
156
+ tombstoned id must be refused with `WalletError.already_rolled_back()`.
157
+
158
+ Your handler has **15 seconds**. The platform retries 5xx and timeouts for up to
159
+ 30 seconds with the same `id_provider`; it does not retry insufficient funds,
160
+ bet limits, bad requests or signature failures.
161
+
162
+ ## Types come from the spec
163
+
164
+ The dataclasses track the published OpenAPI documents (`openapi/` in this repo).
165
+ `tests/test_contract.py` reads a normalised description of those specs and fails
166
+ if a property the contract declares never reaches your handler.
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,86 @@
1
+ """Beexar operator SDK.
2
+
3
+ Two halves:
4
+
5
+ * :class:`~beexar.launcher.Client` — the calls you send to Beexar.
6
+ * :class:`~beexar.wallet.WalletServer` — the four callbacks Beexar sends you.
7
+
8
+ See https://docs.beexar.com for the full contract.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from .errors import ApiCode, ApiError, TwirpCode, WalletError, is_funds_related_code
14
+ from .launcher import PRODUCTION_BASE_URL, Client
15
+ from .money import (
16
+ MAX_CLIENT_DECIMAL_LEN,
17
+ MAX_SCALE,
18
+ CURRENCY_PATTERN,
19
+ Money,
20
+ MoneyError,
21
+ is_valid_currency,
22
+ )
23
+ from .signature import SIGNATURE_HEADER, sign, verify
24
+ from .types import (
25
+ ROUTES,
26
+ BalanceRequest,
27
+ BalanceResult,
28
+ BetWinRequest,
29
+ BetWinResult,
30
+ BetWinTransaction,
31
+ BetWinTransactionResult,
32
+ FinishRequest,
33
+ FinishResult,
34
+ RequestContext,
35
+ RollbackRequest,
36
+ RollbackResult,
37
+ RollbackTransaction,
38
+ RollbackTransactionResult,
39
+ Route,
40
+ WalletHandler,
41
+ )
42
+ from .wallet import MAX_BODY_BYTES, DispatchResponse, WalletServer, route_from_path
43
+
44
+ #: Stamped at release time from sdk/VERSION. The same number is published for
45
+ #: the Node, PHP, Go and Python SDKs and always means the same contract snapshot.
46
+ __version__ = "1.0.1"
47
+
48
+ __all__ = [
49
+ "__version__",
50
+ "ApiCode",
51
+ "ApiError",
52
+ "TwirpCode",
53
+ "WalletError",
54
+ "is_funds_related_code",
55
+ "Client",
56
+ "PRODUCTION_BASE_URL",
57
+ "Money",
58
+ "MoneyError",
59
+ "MAX_SCALE",
60
+ "MAX_CLIENT_DECIMAL_LEN",
61
+ "CURRENCY_PATTERN",
62
+ "is_valid_currency",
63
+ "sign",
64
+ "verify",
65
+ "SIGNATURE_HEADER",
66
+ "ROUTES",
67
+ "Route",
68
+ "RequestContext",
69
+ "BalanceRequest",
70
+ "BalanceResult",
71
+ "BetWinTransaction",
72
+ "BetWinRequest",
73
+ "BetWinTransactionResult",
74
+ "BetWinResult",
75
+ "RollbackTransaction",
76
+ "RollbackRequest",
77
+ "RollbackTransactionResult",
78
+ "RollbackResult",
79
+ "FinishRequest",
80
+ "FinishResult",
81
+ "WalletHandler",
82
+ "WalletServer",
83
+ "DispatchResponse",
84
+ "MAX_BODY_BYTES",
85
+ "route_from_path",
86
+ ]
@@ -0,0 +1,113 @@
1
+ """Framework bindings: ASGI, FastAPI and Flask.
2
+
3
+ Each one exists to solve the same problem in that framework's own way — getting
4
+ the SDK the **raw bytes** of the request. The signature is an HMAC over those
5
+ bytes; a framework that parses the JSON and hands you a dict has already
6
+ destroyed them, and no care afterwards can bring them back.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Callable, Dict, Optional
12
+
13
+ from .signature import SIGNATURE_HEADER
14
+ from .wallet import WalletServer, route_from_path
15
+
16
+ __all__ = ["asgi_app", "fastapi_router", "flask_blueprint"]
17
+
18
+
19
+ def asgi_app(server: WalletServer) -> Callable[..., Any]:
20
+ """A bare ASGI application serving the four callbacks.
21
+
22
+ Mount it under any prefix; the routes are matched on the path suffix.
23
+ """
24
+
25
+ async def app(scope: Dict[str, Any], receive: Callable[..., Any], send: Callable[..., Any]) -> None:
26
+ if scope.get("type") != "http":
27
+ raise RuntimeError("beexar: the wallet app only handles HTTP scopes")
28
+
29
+ route = route_from_path(scope.get("path", "/"))
30
+ if route is None or scope.get("method") != "POST":
31
+ await _send_json(send, 404, b'{"code":"invalid_argument","msg":"not found",'
32
+ b'"meta":{"api_code":"404","api_message":"not found"}}')
33
+ return
34
+
35
+ # Read the body as bytes. Never decode it here: the bytes are what the
36
+ # signature covers.
37
+ body = b""
38
+ more = True
39
+ while more:
40
+ message = await receive()
41
+ body += message.get("body", b"")
42
+ more = message.get("more_body", False)
43
+
44
+ headers = {k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope.get("headers", [])}
45
+ out = server.dispatch(route, body, headers.get(SIGNATURE_HEADER.lower()), headers)
46
+ await _send_json(send, out.status, out.body)
47
+
48
+ return app
49
+
50
+
51
+ async def _send_json(send: Callable[..., Any], status: int, body: bytes) -> None:
52
+ await send(
53
+ {
54
+ "type": "http.response.start",
55
+ "status": status,
56
+ "headers": [(b"content-type", b"application/json")],
57
+ }
58
+ )
59
+ await send({"type": "http.response.body", "body": body})
60
+
61
+
62
+ def fastapi_router(server: WalletServer, prefix: str = "") -> Any:
63
+ """A FastAPI ``APIRouter`` with the four callbacks.
64
+
65
+ The handlers declare ``Request`` and call ``await request.body()`` rather
66
+ than a typed model. Declaring a model would make Starlette parse the JSON,
67
+ and the bytes the signature covers would be gone before the SDK ever saw
68
+ them. Starlette caches the body, so reading it here is safe.
69
+ """
70
+ from fastapi import APIRouter, Request, Response # imported lazily: FastAPI is optional
71
+
72
+ router = APIRouter(prefix=prefix)
73
+
74
+ def make(route: str) -> Callable[..., Any]:
75
+ async def endpoint(request: Request) -> Response:
76
+ body = await request.body()
77
+ headers = {k.lower(): v for k, v in request.headers.items()}
78
+ out = server.dispatch(route, body, headers.get(SIGNATURE_HEADER.lower()), headers)
79
+ return Response(content=out.body, status_code=out.status, media_type="application/json")
80
+
81
+ return endpoint
82
+
83
+ for route in ("/balance", "/betwin", "/rollback", "/finish"):
84
+ router.add_api_route(route, make(route), methods=["POST"])
85
+
86
+ return router
87
+
88
+
89
+ def flask_blueprint(server: WalletServer, name: str = "beexar_wallet", url_prefix: Optional[str] = None) -> Any:
90
+ """A Flask ``Blueprint`` with the four callbacks.
91
+
92
+ Uses ``request.get_data(cache=True)``, which yields ``bytes``.
93
+ ``get_data(as_text=True)`` re-decodes the body and is the usual way a Flask
94
+ integration loses its signature.
95
+ """
96
+ from flask import Blueprint, Response, request # imported lazily: Flask is optional
97
+
98
+ blueprint = Blueprint(name, __name__, url_prefix=url_prefix)
99
+
100
+ def make(route: str) -> Callable[..., Any]:
101
+ def endpoint() -> Any:
102
+ body = request.get_data(cache=True)
103
+ headers = {k.lower(): v for k, v in request.headers.items()}
104
+ out = server.dispatch(route, body, headers.get(SIGNATURE_HEADER.lower()), headers)
105
+ return Response(out.body, status=out.status, mimetype="application/json")
106
+
107
+ endpoint.__name__ = "beexar" + route.replace("/", "_")
108
+ return endpoint
109
+
110
+ for route in ("/balance", "/betwin", "/rollback", "/finish"):
111
+ blueprint.add_url_rule(route, view_func=make(route), methods=["POST"])
112
+
113
+ return blueprint