kappelas-sdk 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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .eggs/
10
+ *.tgz
11
+ .env
12
+ .DS_Store
@@ -0,0 +1,490 @@
1
+ Metadata-Version: 2.4
2
+ Name: kappelas-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Kappela SDK for Python — build bots and personal automations
5
+ Project-URL: Homepage, https://github.com/Arnel7/kappelas-sdk-python
6
+ Project-URL: Repository, https://github.com/Arnel7/kappelas-sdk-python
7
+ Project-URL: Bug Tracker, https://github.com/Arnel7/kappelas-sdk-python/issues
8
+ Author-email: Arnel LAWSON <arnellawson7@gmail.com>
9
+ License: MIT
10
+ Keywords: api,bot,kappela,messaging,sdk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Communications :: Chat
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: websockets>=12
22
+ Description-Content-Type: text/markdown
23
+
24
+ # kappelas-sdk
25
+
26
+ [![PyPI version](https://img.shields.io/pypi/v/kappelas-sdk.svg)](https://pypi.org/project/kappelas-sdk/)
27
+ [![Python version](https://img.shields.io/pypi/pyversions/kappelas-sdk.svg)](https://pypi.org/project/kappelas-sdk/)
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
29
+
30
+ **Official Python SDK for the [Kappela](https://kappelas.com) messaging platform.**
31
+ Build bots and personal automations — send messages, handle events, manage chats.
32
+
33
+ ---
34
+
35
+ ## Table of contents
36
+
37
+ - [Prerequisites](#prerequisites)
38
+ - [Install](#install)
39
+ - [Quick start](#quick-start)
40
+ - [Async-first design](#async-first-design)
41
+ - [Events — WebSocket vs Webhook](#events--websocket-vs-webhook)
42
+ - [API reference](#api-reference)
43
+ - [messages](#messages)
44
+ - [chats](#chats)
45
+ - [webhooks](#webhooks)
46
+ - [profile](#profile)
47
+ - [Keyboards](#keyboards)
48
+ - [Error handling](#error-handling)
49
+ - [File input](#file-input)
50
+
51
+ ---
52
+
53
+ ## Prerequisites
54
+
55
+ You need a bot token from **BotMother**, the official Kappela bot manager.
56
+
57
+ 1. Open Kappela and start a conversation with **BotMother** at `kappelas.com/bot/botmother_bot`
58
+ 2. Follow the instructions to create a bot
59
+ 3. BotMother gives you a token — keep it secret, it gives full control over your bot
60
+
61
+ For personal automation (sending messages as yourself), generate an API key from your Kappela account settings (`sk_...`).
62
+
63
+ ---
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ pip install kappelas-sdk
69
+ ```
70
+
71
+ Requires **Python 3.11+**.
72
+
73
+ ---
74
+
75
+ ## Quick start
76
+
77
+ ### Bot
78
+
79
+ ```python
80
+ import asyncio
81
+ from kappelas import KappelaBot
82
+
83
+ bot = KappelaBot('YOUR_BOT_TOKEN')
84
+
85
+ @bot.on('message')
86
+ async def on_message(msg):
87
+ await bot.messages.send(msg.chat_id, f'Echo: {msg.text}')
88
+
89
+ @bot.on('callback_query')
90
+ async def on_callback(cb):
91
+ await bot.messages.send(cb.chat_id, f'You clicked: {cb.callback_data}')
92
+
93
+ asyncio.run(bot.run())
94
+ ```
95
+
96
+ ### Personal automation
97
+
98
+ ```python
99
+ import asyncio
100
+ from kappelas import KappelaUser
101
+
102
+ me = KappelaUser('sk_your_api_key')
103
+
104
+ @me.on('message')
105
+ async def on_message(msg):
106
+ print(f'[{msg.chat_id}] {msg.sender_name}: {msg.text}')
107
+
108
+ asyncio.run(me.run())
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Async-first design
114
+
115
+ Every method that touches the network is a coroutine — `await` it:
116
+
117
+ ```python
118
+ result = await bot.messages.send(chat_id, 'Hello!')
119
+ ```
120
+
121
+ Use `asyncio.run()` as the entry point for standalone scripts, or integrate into any async framework (FastAPI, aiohttp, etc.).
122
+
123
+ Both `KappelaBot` and `KappelaUser` support `async with`, which automatically closes the WebSocket and HTTP client on exit:
124
+
125
+ ```python
126
+ async def main():
127
+ async with KappelaBot('YOUR_BOT_TOKEN') as bot:
128
+ @bot.on('message')
129
+ async def on_message(msg):
130
+ await bot.messages.send(msg.chat_id, f'Echo: {msg.text}')
131
+
132
+ await bot.run()
133
+
134
+ asyncio.run(main())
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Events — WebSocket vs Webhook
140
+
141
+ | Mode | Method | Best for |
142
+ |------|--------|----------|
143
+ | **WebSocket** | `await bot.run()` | Development, local scripts |
144
+ | **Webhook** | `await bot.webhooks.set()` + `bot.handle_webhook()` | Production servers |
145
+
146
+ The same `on('message')` and `on('callback_query')` handlers work in both modes — no code change needed when switching.
147
+
148
+ ### WebSocket (development)
149
+
150
+ ```python
151
+ bot = KappelaBot('YOUR_BOT_TOKEN')
152
+
153
+ @bot.on('message')
154
+ async def on_message(msg): ...
155
+
156
+ @bot.on('callback_query')
157
+ async def on_callback(cb): ...
158
+
159
+ asyncio.run(bot.run()) # blocks, auto-reconnects on disconnect
160
+ ```
161
+
162
+ `run()` blocks until `stop()` is called. Use `start()` if you need to connect in the background inside an already-running event loop.
163
+
164
+ ### Webhook (production)
165
+
166
+ ```python
167
+ from fastapi import FastAPI, Request
168
+ from kappelas import KappelaBot
169
+
170
+ app = FastAPI()
171
+ bot = KappelaBot('YOUR_BOT_TOKEN')
172
+
173
+ @bot.on('message')
174
+ async def on_message(msg):
175
+ await bot.messages.send(msg.chat_id, f'Echo: {msg.text}')
176
+
177
+ @bot.on('callback_query')
178
+ async def on_callback(cb):
179
+ await bot.messages.send(cb.chat_id, f'Clicked: {cb.callback_data}')
180
+
181
+ @app.on_event('startup')
182
+ async def register_webhook():
183
+ await bot.webhooks.set('https://your-server.com/kappela-webhook')
184
+
185
+ @app.post('/kappela-webhook')
186
+ async def webhook(request: Request):
187
+ bot.handle_webhook(await request.json())
188
+ return {'ok': True}
189
+ ```
190
+
191
+ > Do **not** call `bot.run()` in webhook mode.
192
+
193
+ ### Event reference
194
+
195
+ | Event | Handler signature | Description |
196
+ |-------|-------------------|-------------|
197
+ | `message` | `async def handler(msg: Message)` | Incoming message of any type |
198
+ | `callback_query` | `async def handler(cb: CallbackQuery)` | Inline button clicked by a user |
199
+ | `connected` | `async def handler()` | WebSocket connected or reconnected |
200
+ | `disconnected` | `async def handler(code, reason)` | WebSocket disconnected |
201
+ | `error` | `async def handler(exc: Exception)` | Connection or handler error |
202
+ | `raw` | `async def handler(event: dict)` | Raw `{ type, data }` wire event |
203
+
204
+ ### `CallbackQuery` fields
205
+
206
+ ```python
207
+ @bot.on('callback_query')
208
+ async def on_callback(cb):
209
+ cb.chat_id # int — chat where the button was clicked
210
+ cb.sender_id # str — UUID of the user who clicked
211
+ cb.sender_nom # str | None — display name (e.g. "Arnel LAWSON")
212
+ cb.sender_username # str | None — username (e.g. "arnell")
213
+ cb.callback_data # str — value set on the button
214
+ cb.sent_at # int — Unix timestamp (seconds)
215
+ ```
216
+
217
+ > Clicks are deduplicated server-side — your handler fires exactly once per click.
218
+
219
+ ### Decorator vs method usage
220
+
221
+ ```python
222
+ # Decorator (persistent)
223
+ @bot.on('message')
224
+ async def on_message(msg): ...
225
+
226
+ # Method call (persistent)
227
+ async def on_message(msg): ...
228
+ bot.on('message', on_message)
229
+ bot.off('message', on_message) # remove handler
230
+
231
+ # fires once then auto-removes
232
+ @bot.once('connected')
233
+ async def on_first_connect(): ...
234
+ ```
235
+
236
+ ---
237
+
238
+ ## API reference
239
+
240
+ ### Constructor
241
+
242
+ #### `KappelaBot(token, *, base_url, max_retries, timeout, ws_max_retries)`
243
+
244
+ | Parameter | Type | Default | Description |
245
+ |-----------|------|---------|-------------|
246
+ | `token` | `str` | — | Bot token from BotMother (required) |
247
+ | `base_url` | `str` | `'https://api.kappelas.com'` | Override API base URL |
248
+ | `max_retries` | `int` | `2` | HTTP retry count on 429 / 5xx |
249
+ | `timeout` | `float` | `30.0` | Per-request timeout (seconds) |
250
+ | `ws_max_retries` | `int` | `12` | Max WebSocket reconnect attempts |
251
+
252
+ #### `KappelaUser(api_key, *, base_url, max_retries, timeout, ws_max_retries)`
253
+
254
+ | Parameter | Type | Default | Description |
255
+ |-----------|------|---------|-------------|
256
+ | `api_key` | `str` | — | Personal API key `sk_...` (required) |
257
+ | `base_url` | `str` | `'https://api.kappelas.com'` | Override API base URL |
258
+ | `max_retries` | `int` | `2` | HTTP retry count on 429 / 5xx |
259
+ | `timeout` | `float` | `30.0` | Per-request timeout (seconds) |
260
+ | `ws_max_retries` | `int` | `12` | Max WebSocket reconnect attempts |
261
+
262
+ ---
263
+
264
+ ### `messages`
265
+
266
+ #### `messages.send(chat_id, text, *, reply_markup, reply_to_id, delete_previous)` → `SendResult`
267
+
268
+ ```python
269
+ result = await bot.messages.send(
270
+ chat_id = 42,
271
+ text = 'Hello!',
272
+ reply_to_id = 123, # optional — reply to a message
273
+ delete_previous = False, # optional
274
+ reply_markup = InlineKeyboard(inline_keyboard=[[
275
+ InlineKeyboardButton(text='Yes', callback_data='yes'),
276
+ InlineKeyboardButton(text='No', callback_data='no'),
277
+ ]]),
278
+ )
279
+ # → SendResult(message_id=..., created_at=...)
280
+ ```
281
+
282
+ #### `messages.send_photo(chat_id, photo, *, caption, reply_to_id, delete_previous, reply_markup)` → `SendMediaResult`
283
+
284
+ ```python
285
+ with open('banner.png', 'rb') as f:
286
+ await bot.messages.send_photo(chat_id, f, caption='Check this out!')
287
+ # → SendMediaResult(message_id=..., created_at=..., media_id=...)
288
+ ```
289
+
290
+ #### `messages.send_video` / `send_document` / `send_audio` → `SendMediaResult`
291
+
292
+ Same signature — replace the file parameter (`video`, `document`, `audio`) with your file.
293
+
294
+ #### `messages.send_carousel(chat_id, carousel, *, text, quick_reply_buttons)` → `SendCarouselResult`
295
+
296
+ ```python
297
+ from kappelas import CarouselCard
298
+
299
+ await bot.messages.send_carousel(
300
+ chat_id = 42,
301
+ text = 'Pick a product:',
302
+ carousel = [
303
+ CarouselCard(id='p1', title='Widget A', subtitle='$9.99', button_text='Buy'),
304
+ CarouselCard(id='p2', title='Widget B', subtitle='$19.99', button_text='Buy'),
305
+ ],
306
+ quick_reply_buttons=['See more', 'Cancel'],
307
+ )
308
+ ```
309
+
310
+ #### `messages.edit(chat_id, message_id, *, new_text, new_extra_data)` → `EditMessageResult`
311
+
312
+ ```python
313
+ # Edit text
314
+ await bot.messages.edit(42, 123, new_text='Updated!')
315
+
316
+ # Edit inline keyboard only (no text change)
317
+ await bot.messages.edit(42, 123, new_extra_data={
318
+ 'inline_keyboard': [[{'text': 'Done ✅', 'callback_data': 'done'}]]
319
+ })
320
+ # → EditMessageResult(edited=True, message_id=...)
321
+ ```
322
+
323
+ #### `messages.send_typing(chat_id, *, is_typing)` → `TypingResult`
324
+
325
+ ```python
326
+ await bot.messages.send_typing(42) # show
327
+ await bot.messages.send_typing(42, is_typing=False) # hide
328
+ ```
329
+
330
+ #### `messages.delete(chat_id, message_id)` → `DeleteResult`
331
+
332
+ ```python
333
+ await bot.messages.delete(42, 123)
334
+ # → DeleteResult(deleted=True)
335
+ ```
336
+
337
+ ---
338
+
339
+ ### `chats`
340
+
341
+ #### `chats.list(*, limit, offset)` → `ChatsResult`
342
+
343
+ ```python
344
+ page = await bot.chats.list(limit=20, offset=0)
345
+ print(page.chats, page.has_more)
346
+ ```
347
+
348
+ #### `chats.iterate(page_size?)` → `AsyncGenerator[Chat]`
349
+
350
+ ```python
351
+ async for chat in bot.chats.iterate():
352
+ print(chat.chat_id, chat.title, chat.type)
353
+ ```
354
+
355
+ ---
356
+
357
+ ### `webhooks`
358
+
359
+ #### `webhooks.set(url, *, secret)` → `WebhookSetResult`
360
+
361
+ ```python
362
+ await bot.webhooks.set('https://your-server.com/kappela-webhook')
363
+ ```
364
+
365
+ #### `webhooks.get_info()` → `WebhookInfo`
366
+
367
+ ```python
368
+ info = await bot.webhooks.get_info()
369
+ # → WebhookInfo(active=True, url='https://...', created_at=...)
370
+ ```
371
+
372
+ #### `webhooks.delete()` → `WebhookDeleteResult`
373
+
374
+ ```python
375
+ await bot.webhooks.delete()
376
+ # → WebhookDeleteResult(active=False)
377
+ ```
378
+
379
+ ---
380
+
381
+ ### `profile`
382
+
383
+ #### `profile.get()` → `BotProfile | UserProfile`
384
+
385
+ ```python
386
+ profile = await bot.profile.get()
387
+ # BotProfile → user_id, username, is_bot=True, about, description, avatar_url
388
+ # UserProfile → id, username, nom, is_bot=False, is_premium, avatar_url, ...
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Keyboards
394
+
395
+ Three types of keyboard can be passed as `reply_markup` on any `send*` call:
396
+
397
+ ```python
398
+ from kappelas import InlineKeyboard, InlineKeyboardButton, ReplyKeyboard, ScrollKeyboard
399
+
400
+ # Inline buttons — attached to the message
401
+ inline = InlineKeyboard(inline_keyboard=[
402
+ [
403
+ InlineKeyboardButton(text='Yes', callback_data='yes'),
404
+ InlineKeyboardButton(text='No', callback_data='no'),
405
+ ],
406
+ [
407
+ InlineKeyboardButton(text='Open website', url='https://kappelas.com'),
408
+ ],
409
+ ])
410
+
411
+ # Reply keyboard — shown below the input bar
412
+ reply = ReplyKeyboard(keyboard=[
413
+ ['Option A', 'Option B'],
414
+ ['Cancel'],
415
+ ])
416
+
417
+ # Scroll keyboard — horizontal scrollable chips
418
+ scroll = ScrollKeyboard(scroll_keyboard=['Small', 'Medium', 'Large'])
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Error handling
424
+
425
+ All API errors raise `KappelaError` with structured fields:
426
+
427
+ ```python
428
+ from kappelas import KappelaError
429
+
430
+ try:
431
+ await bot.messages.send(999, 'Hi')
432
+ except KappelaError as e:
433
+ e.error_code # 'NOT_FOUND'
434
+ e.status # 404
435
+ e.hint # 'The requested resource does not exist.'
436
+ e.solutions # ['Check the ID is correct', ...]
437
+ e.request_id # include when contacting support
438
+ print(e) # full formatted block
439
+ ```
440
+
441
+ ### Error codes
442
+
443
+ | Code | HTTP | Meaning |
444
+ |------|------|---------|
445
+ | `UNAUTHORIZED` | 401 | Token or API key invalid / expired |
446
+ | `FORBIDDEN` | 403 | Missing permission or role |
447
+ | `NOT_FOUND` | 404 | Resource does not exist |
448
+ | `MISSING_FIELD` | 400 | Required parameter missing |
449
+ | `INVALID_FIELD` | 400 | Parameter has wrong type or format |
450
+ | `CONFLICT` | 409 | Resource already exists |
451
+ | `METHOD_NOT_ALLOWED` | 405 | Wrong HTTP method |
452
+ | `INVALID_PATH` | 404 | API path does not exist |
453
+ | `INTERNAL_ERROR` | 500 | Unexpected server error |
454
+ | `SERVICE_UNAVAILABLE` | 503 | Service temporarily down |
455
+ | `UPSTREAM_ERROR` | 502 | Upstream service error |
456
+
457
+ ---
458
+
459
+ ## File input
460
+
461
+ Media methods accept files in several forms:
462
+
463
+ | Type | Example |
464
+ |------|---------|
465
+ | `bytes` | `open('img.jpg', 'rb').read()` |
466
+ | `IO[bytes]` | `open('img.jpg', 'rb')` |
467
+ | `FileData` | `FileData(data=b'...', filename='img.jpg', content_type='image/jpeg')` |
468
+
469
+ ```python
470
+ from kappelas import FileData
471
+
472
+ # bytes
473
+ await bot.messages.send_photo(chat_id, open('photo.jpg', 'rb').read())
474
+
475
+ # file object
476
+ with open('photo.jpg', 'rb') as f:
477
+ await bot.messages.send_photo(chat_id, f)
478
+
479
+ # explicit metadata
480
+ await bot.messages.send_document(
481
+ chat_id,
482
+ FileData(data=pdf_bytes, filename='report.pdf', content_type='application/pdf'),
483
+ )
484
+ ```
485
+
486
+ ---
487
+
488
+ ## License
489
+
490
+ MIT © Arnel LAWSON