buddo-py 0.1.0a1__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.
- buddo_py-0.1.0a1/.gitignore +26 -0
- buddo_py-0.1.0a1/LICENSE +21 -0
- buddo_py-0.1.0a1/PKG-INFO +493 -0
- buddo_py-0.1.0a1/README.md +447 -0
- buddo_py-0.1.0a1/buddo/__init__.py +74 -0
- buddo_py-0.1.0a1/buddo/_auth_interceptor.py +267 -0
- buddo_py-0.1.0a1/buddo/_http.py +450 -0
- buddo_py-0.1.0a1/buddo/ads.py +841 -0
- buddo_py-0.1.0a1/buddo/auth.py +238 -0
- buddo_py-0.1.0a1/buddo/client.py +732 -0
- buddo_py-0.1.0a1/buddo/deploy.py +619 -0
- buddo_py-0.1.0a1/buddo/exceptions.py +276 -0
- buddo_py-0.1.0a1/buddo/games.py +130 -0
- buddo_py-0.1.0a1/buddo/models/__init__.py +85 -0
- buddo_py-0.1.0a1/buddo/models/ad.py +569 -0
- buddo_py-0.1.0a1/buddo/models/auth.py +90 -0
- buddo_py-0.1.0a1/buddo/models/base.py +63 -0
- buddo_py-0.1.0a1/buddo/models/deploy.py +234 -0
- buddo_py-0.1.0a1/buddo/models/game.py +279 -0
- buddo_py-0.1.0a1/buddo/models/operator.py +222 -0
- buddo_py-0.1.0a1/buddo/models/social.py +241 -0
- buddo_py-0.1.0a1/buddo/models/user.py +202 -0
- buddo_py-0.1.0a1/buddo/operator.py +267 -0
- buddo_py-0.1.0a1/buddo/py.typed +0 -0
- buddo_py-0.1.0a1/buddo/social.py +428 -0
- buddo_py-0.1.0a1/buddo/users.py +272 -0
- buddo_py-0.1.0a1/pyproject.toml +98 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.claude/worktrees/
|
|
2
|
+
system/production-credentials.md
|
|
3
|
+
node_modules/
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.pyc
|
|
6
|
+
.env
|
|
7
|
+
.env.local
|
|
8
|
+
.env.production
|
|
9
|
+
.env.staging
|
|
10
|
+
*.salvaged
|
|
11
|
+
tmp/
|
|
12
|
+
projects/simple-sites/builder/output/
|
|
13
|
+
response.json
|
|
14
|
+
design/
|
|
15
|
+
|
|
16
|
+
# Key and cert files
|
|
17
|
+
*.pem
|
|
18
|
+
*.key
|
|
19
|
+
id_rsa
|
|
20
|
+
id_ed25519
|
|
21
|
+
*.ppk
|
|
22
|
+
|
|
23
|
+
# Credential-containing handoff/archive directories
|
|
24
|
+
briefing/handoff/security/
|
|
25
|
+
briefing/handoff/archive/
|
|
26
|
+
briefing/checkpoints/archive-*/
|
buddo_py-0.1.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Garybots
|
|
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,493 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buddo-py
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Python SDK for the Buddo loyalty platform API
|
|
5
|
+
Project-URL: Homepage, https://buddo.xyz
|
|
6
|
+
Project-URL: Documentation, https://docs.buddo.xyz
|
|
7
|
+
Project-URL: Repository, https://github.com/garybots/buddo-py
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/garybots/buddo-py/issues
|
|
9
|
+
Author-email: Gary Le Gear <garylegear@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,async,buddo,loyalty,sdk
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Requires-Dist: httpx>=0.27.2
|
|
30
|
+
Requires-Dist: pydantic>=2.7
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: numpy>=1.26; extra == 'all'
|
|
33
|
+
Requires-Dist: pandas>=2.0; extra == 'all'
|
|
34
|
+
Requires-Dist: tenacity>=8.0; extra == 'all'
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
41
|
+
Provides-Extra: trading
|
|
42
|
+
Requires-Dist: numpy>=1.26; extra == 'trading'
|
|
43
|
+
Requires-Dist: pandas>=2.0; extra == 'trading'
|
|
44
|
+
Requires-Dist: tenacity>=8.0; extra == 'trading'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# buddo-py
|
|
48
|
+
|
|
49
|
+
[](https://pypi.org/project/buddo-py/)
|
|
50
|
+
[](https://pypi.org/project/buddo-py/)
|
|
51
|
+
[](LICENSE)
|
|
52
|
+
[](https://mypy.readthedocs.io/)
|
|
53
|
+
|
|
54
|
+
**Async Python SDK for the [Buddo](https://buddo.xyz) loyalty platform.**
|
|
55
|
+
|
|
56
|
+
Buddo is a Bitcoin-backed loyalty system for operators — games, apps, and websites.
|
|
57
|
+
Operators earn ad-share revenue when their users view ads; users earn Buddo points
|
|
58
|
+
redeemable for in-app value. This SDK wraps the Buddo REST API with typed responses,
|
|
59
|
+
automatic token refresh, retry/backoff, and rate-limit compliance built in.
|
|
60
|
+
|
|
61
|
+
> **Status:** Alpha (v0.1.0a1) — API is stable within the `0.1.x` line but may change
|
|
62
|
+
> before v1.0. Pin your dependency to a minor version in production.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install buddo-py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**With optional extras:**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Trading bot extras (pandas, numpy, tenacity for strategy work)
|
|
76
|
+
pip install "buddo-py[trading]"
|
|
77
|
+
|
|
78
|
+
# Development tools (pytest, mypy, ruff, respx)
|
|
79
|
+
pip install "buddo-py[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Recommended: use a virtual environment**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m venv .venv
|
|
86
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
87
|
+
pip install buddo-py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Verify the install:**
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -c "import buddo; print(buddo.__version__)"
|
|
94
|
+
# 0.1.0a1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Requires Python 3.9+. Core dependencies (`httpx`, `pydantic`) are installed automatically.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Quick Example
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import asyncio
|
|
105
|
+
import os
|
|
106
|
+
from buddo import BuddoClient
|
|
107
|
+
|
|
108
|
+
async def main() -> None:
|
|
109
|
+
async with BuddoClient(api_key=os.environ["BUDDO_API_KEY"]) as client:
|
|
110
|
+
# Check operator treasury balance
|
|
111
|
+
balance = await client.users.get_balance()
|
|
112
|
+
print(f"Treasury: {balance.buddo_points} pts (tier: {balance.tier})")
|
|
113
|
+
|
|
114
|
+
# Award 100 points to a user (operator-scoped, requires points:award scope)
|
|
115
|
+
txn = await client.users.award_points(
|
|
116
|
+
user_id="usr_abc123",
|
|
117
|
+
amount=100,
|
|
118
|
+
reason="ad_view",
|
|
119
|
+
idempotency_key="award_001", # Required — enables safe retries
|
|
120
|
+
)
|
|
121
|
+
print(f"Awarded {txn.amount} pts — transaction: {txn.id}")
|
|
122
|
+
|
|
123
|
+
asyncio.run(main())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
See [docs/quickstart.md](docs/quickstart.md) for the full step-by-step guide.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
### Getting an API Key
|
|
133
|
+
|
|
134
|
+
1. Sign in to the [Buddo operator dashboard](https://buddocloud.com)
|
|
135
|
+
2. Navigate to **Settings → API Keys**
|
|
136
|
+
3. Create a key — it will be prefixed `bdk_live_` (production) or `bdk_test_` (sandbox)
|
|
137
|
+
|
|
138
|
+
| Prefix | Environment | Use |
|
|
139
|
+
|--------|-------------|-----|
|
|
140
|
+
| `bdk_live_` | Production | Real transactions, real points |
|
|
141
|
+
| `bdk_test_` | Sandbox | Safe for development, no real treasury impact |
|
|
142
|
+
|
|
143
|
+
### Setting the Environment Variable
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# macOS / Linux
|
|
147
|
+
export BUDDO_API_KEY="bdk_test_your_key_here"
|
|
148
|
+
|
|
149
|
+
# Windows PowerShell
|
|
150
|
+
$env:BUDDO_API_KEY = "bdk_test_your_key_here"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For local development, use a `.env` file with [python-dotenv](https://pypi.org/project/python-dotenv/):
|
|
154
|
+
|
|
155
|
+
```ini
|
|
156
|
+
# .env — never commit to version control
|
|
157
|
+
BUDDO_API_KEY=bdk_test_your_key_here
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from dotenv import load_dotenv
|
|
162
|
+
load_dotenv()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Production:** store the key in a secrets manager (AWS Secrets Manager, HashiCorp
|
|
166
|
+
Vault, etc.). Never commit API keys to version control — add `.env` to your `.gitignore`.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Resources
|
|
171
|
+
|
|
172
|
+
`BuddoClient` exposes six resource objects. Each resource maps directly to an
|
|
173
|
+
API endpoint group:
|
|
174
|
+
|
|
175
|
+
| Resource | Description |
|
|
176
|
+
|----------|-------------|
|
|
177
|
+
| `client.auth` | OAuth2 token management: login, refresh, revoke, client credentials flow |
|
|
178
|
+
| `client.users` | User profiles, Buddo point balances, transaction history, and point awards |
|
|
179
|
+
| `client.social` | Friends list, friend requests, presence status, and lobby chat |
|
|
180
|
+
| `client.games` | Game catalogue, session creation, and session settlement |
|
|
181
|
+
| `client.ads` | Ad campaign management, impression recording, and campaign analytics |
|
|
182
|
+
| `client.operator` | Operator stats, registered app management, and monthly billing |
|
|
183
|
+
|
|
184
|
+
### Key Methods
|
|
185
|
+
|
|
186
|
+
**`client.users`**
|
|
187
|
+
- `get_profile()` → `UserProfile` — authenticated user's profile
|
|
188
|
+
- `get_balance()` → `UserBalance` — Buddo points balance and loyalty tier
|
|
189
|
+
- `get_points_history(limit, cursor)` → `PagedResult[PointsTransaction]`
|
|
190
|
+
- `award_points(user_id, amount, reason, idempotency_key)` → `PointsTransaction`
|
|
191
|
+
- `get(user_id)` → `UserProfile` — look up any user (operator scope)
|
|
192
|
+
|
|
193
|
+
**`client.ads`**
|
|
194
|
+
- `list_campaigns(status)` → `List[Campaign]`
|
|
195
|
+
- `create_campaign(name, budget, cpm, start_date, end_date, ...)` → `Campaign`
|
|
196
|
+
- `record_impression(campaign_id, user_id, ad_type, duration_seconds, idempotency_key)` → `Impression`
|
|
197
|
+
- `get_campaign_stats(campaign_id)` → `CampaignStats`
|
|
198
|
+
|
|
199
|
+
**`client.operator`**
|
|
200
|
+
- `get_stats()` → `OperatorStats`
|
|
201
|
+
- `create_app(name, redirect_uris, scopes)` → `OperatorApp`
|
|
202
|
+
- `rotate_secret(app_id)` → `OperatorApp`
|
|
203
|
+
- `get_billing(month)` → `BillingRecord`
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Error Handling
|
|
208
|
+
|
|
209
|
+
All SDK errors subclass `BuddoError`. Catch specific subclasses for fine-grained
|
|
210
|
+
control, or catch the base class as a fallback:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
import asyncio
|
|
214
|
+
import os
|
|
215
|
+
from buddo import BuddoClient
|
|
216
|
+
from buddo.exceptions import (
|
|
217
|
+
BuddoRateLimitError,
|
|
218
|
+
BuddoInsufficientBalanceError,
|
|
219
|
+
BuddoNotFoundError,
|
|
220
|
+
BuddoConflictError,
|
|
221
|
+
BuddoAuthError,
|
|
222
|
+
BuddoError,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def award_points_safe(
|
|
227
|
+
client: BuddoClient,
|
|
228
|
+
user_id: str,
|
|
229
|
+
amount: int,
|
|
230
|
+
idempotency_key: str,
|
|
231
|
+
) -> None:
|
|
232
|
+
try:
|
|
233
|
+
txn = await client.users.award_points(
|
|
234
|
+
user_id=user_id,
|
|
235
|
+
amount=amount,
|
|
236
|
+
reason="ad_view",
|
|
237
|
+
idempotency_key=idempotency_key,
|
|
238
|
+
)
|
|
239
|
+
print(f"Awarded {txn.amount} pts — txn: {txn.id}, balance after: {txn.balance_after}")
|
|
240
|
+
|
|
241
|
+
except BuddoRateLimitError as exc:
|
|
242
|
+
# Raised only after the SDK's built-in retries (default: 3) are exhausted.
|
|
243
|
+
# exc.retry_after: seconds to wait before the next attempt is safe.
|
|
244
|
+
print(f"Rate limited — retry after {exc.retry_after}s")
|
|
245
|
+
await asyncio.sleep(exc.retry_after)
|
|
246
|
+
|
|
247
|
+
except BuddoInsufficientBalanceError as exc:
|
|
248
|
+
# exc.required: points needed; exc.available: current treasury balance.
|
|
249
|
+
print(f"Treasury low: need {exc.required} pts, have {exc.available} pts")
|
|
250
|
+
|
|
251
|
+
except BuddoNotFoundError:
|
|
252
|
+
print(f"User {user_id!r} does not exist")
|
|
253
|
+
|
|
254
|
+
except BuddoConflictError:
|
|
255
|
+
# Same idempotency_key used with a *different* payload — this is a bug.
|
|
256
|
+
# Same key + same payload → original response returned (no exception).
|
|
257
|
+
print("Idempotency key conflict — check for duplicate requests with different params")
|
|
258
|
+
|
|
259
|
+
except BuddoAuthError:
|
|
260
|
+
print("Authentication failed — check your API key or re-authenticate")
|
|
261
|
+
raise # Re-raise; the bot should not continue with a bad credential
|
|
262
|
+
|
|
263
|
+
except BuddoError as exc:
|
|
264
|
+
# Catch-all: includes BuddoServerError, BuddoForbiddenError, etc.
|
|
265
|
+
print(f"SDK error [{exc.status_code}] {exc.error_code}: {exc}")
|
|
266
|
+
if exc.retryable:
|
|
267
|
+
print(" (transient — safe to retry after a short delay)")
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Exception hierarchy:**
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
BuddoError
|
|
274
|
+
├── BuddoAuthError 401 — invalid/expired token
|
|
275
|
+
├── BuddoForbiddenError 403 — valid token, insufficient scope
|
|
276
|
+
├── BuddoNotFoundError 404 — resource not found
|
|
277
|
+
├── BuddoConflictError 409 — duplicate idempotency key with different payload
|
|
278
|
+
├── BuddoRateLimitError 429 — rate limit (.retry_after seconds)
|
|
279
|
+
├── BuddoServerError 5xx — server error (502/503/504 are retryable)
|
|
280
|
+
├── BuddoValidationError 422 — request validation failed (.field_errors dict)
|
|
281
|
+
├── BuddoInsufficientBalanceError 402 — not enough points (.required, .available)
|
|
282
|
+
├── BuddoTimeoutError Network timeout (retryable)
|
|
283
|
+
└── BuddoConnectionError Network connection error (retryable)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Async Usage
|
|
289
|
+
|
|
290
|
+
The SDK is async-only: every resource method is a coroutine. The HTTP layer
|
|
291
|
+
uses `httpx.AsyncClient` for non-blocking I/O, making it suitable for high-throughput
|
|
292
|
+
operator bots that need to award points or record impressions without blocking
|
|
293
|
+
the event loop.
|
|
294
|
+
|
|
295
|
+
**Recommended: async context manager**
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
async with BuddoClient(api_key=os.environ["BUDDO_API_KEY"]) as client:
|
|
299
|
+
balance = await client.users.get_balance()
|
|
300
|
+
# Connection pool is released automatically on exit
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**For long-lived bots: manual lifecycle**
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
client = BuddoClient(api_key=os.environ["BUDDO_API_KEY"])
|
|
307
|
+
await client.open()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
while True:
|
|
311
|
+
await run_bot_cycle(client)
|
|
312
|
+
await asyncio.sleep(60)
|
|
313
|
+
finally:
|
|
314
|
+
await client.aclose() # Always close — releases the connection pool
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Calling from synchronous code:**
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
import asyncio
|
|
321
|
+
|
|
322
|
+
result = asyncio.run(some_async_function())
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Pagination
|
|
328
|
+
|
|
329
|
+
List endpoints that may return many items use **cursor-based pagination** via
|
|
330
|
+
`PagedResult[T]`:
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
from buddo.models import PagedResult
|
|
334
|
+
from buddo.models.user import PointsTransaction
|
|
335
|
+
|
|
336
|
+
async def fetch_all_history(client: BuddoClient) -> list[PointsTransaction]:
|
|
337
|
+
all_txns: list[PointsTransaction] = []
|
|
338
|
+
cursor: str | None = None
|
|
339
|
+
|
|
340
|
+
while True:
|
|
341
|
+
page = await client.users.get_points_history(limit=50, cursor=cursor)
|
|
342
|
+
all_txns.extend(page.data)
|
|
343
|
+
|
|
344
|
+
if not page.has_next:
|
|
345
|
+
break
|
|
346
|
+
cursor = page.cursor
|
|
347
|
+
|
|
348
|
+
return all_txns
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`PagedResult` fields:
|
|
352
|
+
- `.data` — list of items on this page
|
|
353
|
+
- `.cursor` — opaque string to pass as `cursor=` on the next request; `None` when no more pages
|
|
354
|
+
- `.has_next` — `True` when more pages remain
|
|
355
|
+
- `.total` — total count when the server provides it (not always available)
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Trading Bot Example
|
|
360
|
+
|
|
361
|
+
For bots that manage Buddo balances alongside trading activity:
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
import asyncio
|
|
365
|
+
import logging
|
|
366
|
+
import os
|
|
367
|
+
from buddo import BuddoClient
|
|
368
|
+
from buddo.exceptions import BuddoRateLimitError, BuddoInsufficientBalanceError, BuddoError
|
|
369
|
+
|
|
370
|
+
logger = logging.getLogger(__name__)
|
|
371
|
+
|
|
372
|
+
MIN_TREASURY_BALANCE = 1_000 # Don't trade if treasury is critically low
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def run_trading_cycle(client: BuddoClient, user_id: str) -> None:
|
|
376
|
+
"""Check balance, execute a trade, then award loyalty points."""
|
|
377
|
+
try:
|
|
378
|
+
balance = await client.users.get_balance()
|
|
379
|
+
if balance.buddo_points < MIN_TREASURY_BALANCE:
|
|
380
|
+
logger.warning(
|
|
381
|
+
"Treasury low (%d pts) — skipping award this cycle",
|
|
382
|
+
balance.buddo_points,
|
|
383
|
+
)
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
# --- your trading logic here ---
|
|
387
|
+
trade_session_id = "session_example_001"
|
|
388
|
+
logger.info("Treasury OK (%d pts) — executing trade", balance.buddo_points)
|
|
389
|
+
|
|
390
|
+
# Award points as loyalty reward for the trade
|
|
391
|
+
txn = await client.users.award_points(
|
|
392
|
+
user_id=user_id,
|
|
393
|
+
amount=50,
|
|
394
|
+
reason="trade_bonus",
|
|
395
|
+
idempotency_key=f"trade_{trade_session_id}_{user_id}",
|
|
396
|
+
)
|
|
397
|
+
logger.info("Loyalty award sent — txn: %s, balance after: %d", txn.id, txn.balance_after)
|
|
398
|
+
|
|
399
|
+
except BuddoRateLimitError as exc:
|
|
400
|
+
logger.warning("Rate limited — sleeping %ds", exc.retry_after)
|
|
401
|
+
await asyncio.sleep(exc.retry_after)
|
|
402
|
+
|
|
403
|
+
except BuddoInsufficientBalanceError as exc:
|
|
404
|
+
logger.error(
|
|
405
|
+
"Treasury exhausted (need %d, have %d) — top up via operator dashboard",
|
|
406
|
+
exc.required,
|
|
407
|
+
exc.available,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
except BuddoError as exc:
|
|
411
|
+
logger.error("Buddo API error [%s]: %s", exc.status_code, exc)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
async def main() -> None:
|
|
415
|
+
client = BuddoClient(api_key=os.environ["BUDDO_API_KEY"])
|
|
416
|
+
await client.open()
|
|
417
|
+
try:
|
|
418
|
+
while True:
|
|
419
|
+
await run_trading_cycle(client, user_id="usr_abc123")
|
|
420
|
+
await asyncio.sleep(30)
|
|
421
|
+
finally:
|
|
422
|
+
await client.aclose()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
logging.basicConfig(level=logging.INFO)
|
|
427
|
+
asyncio.run(main())
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Advanced: OAuth2 Client Credentials
|
|
433
|
+
|
|
434
|
+
For machine-to-machine flows with scoped tokens:
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
async with BuddoClient(
|
|
438
|
+
client_id=os.environ["BUDDO_CLIENT_ID"],
|
|
439
|
+
client_secret=os.environ["BUDDO_CLIENT_SECRET"],
|
|
440
|
+
) as client:
|
|
441
|
+
token = await client.auth.client_credentials(scope="operator:read operator:write")
|
|
442
|
+
# The client uses this token for subsequent requests
|
|
443
|
+
stats = await client.operator.get_stats()
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Links
|
|
449
|
+
|
|
450
|
+
- [Quickstart Guide](docs/quickstart.md) — award points in 5 minutes
|
|
451
|
+
- [AGENTS.md](AGENTS.md) — LLM agent integration reference
|
|
452
|
+
- [Full API Reference](https://docs.buddo.xyz/api-reference)
|
|
453
|
+
- [OpenAPI Spec](https://docs.buddo.xyz/openapi.yaml)
|
|
454
|
+
- [Buddo Platform](https://buddo.xyz)
|
|
455
|
+
- [Issue Tracker](https://github.com/garybots/buddo-py/issues)
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Contributing
|
|
460
|
+
|
|
461
|
+
Contributions are welcome. The project uses `hatchling` as its build backend
|
|
462
|
+
and requires Python 3.9+.
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# 1. Fork and clone
|
|
466
|
+
git clone https://github.com/garybots/buddo-py.git
|
|
467
|
+
cd buddo-py
|
|
468
|
+
|
|
469
|
+
# 2. Install with dev extras
|
|
470
|
+
pip install -e ".[dev]"
|
|
471
|
+
|
|
472
|
+
# 3. Run the test suite
|
|
473
|
+
pytest
|
|
474
|
+
|
|
475
|
+
# 4. Type-check
|
|
476
|
+
mypy buddo/
|
|
477
|
+
|
|
478
|
+
# 5. Lint
|
|
479
|
+
ruff check buddo/
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Tests use `pytest-asyncio` with `asyncio_mode = "auto"` — no `@pytest.mark.asyncio`
|
|
483
|
+
decorator needed. HTTP calls are mocked with `respx`; no live API access required
|
|
484
|
+
to run the test suite.
|
|
485
|
+
|
|
486
|
+
Please open an issue before submitting a large PR. For bug reports, include
|
|
487
|
+
the SDK version (`buddo.__version__`), Python version, and a minimal reproduction.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## License
|
|
492
|
+
|
|
493
|
+
MIT License — see [LICENSE](LICENSE) for details.
|