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.
@@ -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-*/
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/buddo-py.svg)](https://pypi.org/project/buddo-py/)
50
+ [![Python versions](https://img.shields.io/pypi/pyversions/buddo-py.svg)](https://pypi.org/project/buddo-py/)
51
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
52
+ [![Typing: strict](https://img.shields.io/badge/typing-strict-green.svg)](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.