kryten-api-gate 0.2.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.
- kryten_api_gate-0.2.0/.github/prompts/plan-krytenApiGate.prompt.md +288 -0
- kryten_api_gate-0.2.0/.github/workflows/python-publish.yml +79 -0
- kryten_api_gate-0.2.0/.github/workflows/release.yml +125 -0
- kryten_api_gate-0.2.0/.gitignore +71 -0
- kryten_api_gate-0.2.0/PKG-INFO +113 -0
- kryten_api_gate-0.2.0/README.md +93 -0
- kryten_api_gate-0.2.0/config.example.json +14 -0
- kryten_api_gate-0.2.0/kryten-api-gate.code-workspace +16 -0
- kryten_api_gate-0.2.0/pyproject.toml +50 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/__init__.py +8 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/__main__.py +126 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/app.py +47 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/auth.py +36 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/config.py +62 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/deps.py +16 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/__init__.py +1 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/admin.py +187 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/chat.py +42 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/emotes.py +71 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/filters.py +77 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/kv.py +82 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/library.py +33 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/moderation.py +108 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/playback.py +61 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/playlist.py +105 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/polls.py +58 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/state.py +46 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/routes/system.py +64 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/schemas/__init__.py +1 -0
- kryten_api_gate-0.2.0/src/kryten_api_gate/schemas/responses.py +24 -0
- kryten_api_gate-0.2.0/systemd/kryten-api-gate.service +19 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Plan: kryten-api-gate — HTTP REST Gateway for kryten-py
|
|
2
|
+
|
|
3
|
+
A FastAPI microservice that exposes the full `KrytenClient` API surface over HTTP REST. One instance per kryten-robot/NATS instance, single channel. API-key authenticated for service-to-service use. Upstream consumer web apps talk to one or more instances.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Phase 0: Fix kryten-robot Routing Gap (Prerequisite, Blocking)
|
|
8
|
+
|
|
9
|
+
**Problem:** kryten-py's `__send_command()` publishes to `kryten.robot.command` (handled by `RobotCommandHandler`), but only basic commands (chat, playlist, playback, moderation) are in its dispatch table. Phase 2/3 admin commands (`setChannelCSS`, `setMotd`, `updateEmote`, filters, polls, ranks, banlist, chanlog, library) are silently dropped.
|
|
10
|
+
|
|
11
|
+
**Fix:** Add ~20 handler entries to `Kryten-Robot/kryten/robot_command_handler.py` routing them to `self.sender` (the `CytubeEventSender`), matching what `CommandSubscriber` already does.
|
|
12
|
+
|
|
13
|
+
**Commands to add:**
|
|
14
|
+
- `setMotd` / `set_motd`
|
|
15
|
+
- `setChannelCSS` / `set_channel_css`
|
|
16
|
+
- `setChannelJS` / `set_channel_js`
|
|
17
|
+
- `setOptions` / `set_options`
|
|
18
|
+
- `setPermissions` / `set_permissions`
|
|
19
|
+
- `updateEmote` / `update_emote`
|
|
20
|
+
- `removeEmote` / `remove_emote`
|
|
21
|
+
- `addFilter` / `add_filter`
|
|
22
|
+
- `updateFilter` / `update_filter`
|
|
23
|
+
- `removeFilter` / `remove_filter`
|
|
24
|
+
- `newPoll` / `new_poll`
|
|
25
|
+
- `vote`
|
|
26
|
+
- `closePoll` / `close_poll`
|
|
27
|
+
- `setChannelRank` / `set_channel_rank`
|
|
28
|
+
- `requestChannelRanks` / `request_channel_ranks`
|
|
29
|
+
- `requestBanlist` / `request_banlist`
|
|
30
|
+
- `unban`
|
|
31
|
+
- `readChanLog` / `read_chan_log`
|
|
32
|
+
- `searchLibrary` / `search_library`
|
|
33
|
+
- `deleteFromLibrary` / `delete_from_library`
|
|
34
|
+
- `playNext` / `play_next`
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Phase 1: Project Scaffold
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
kryten-api-gate/
|
|
42
|
+
├── pyproject.toml
|
|
43
|
+
├── config.example.json
|
|
44
|
+
├── systemd/kryten-api-gate.service
|
|
45
|
+
└── src/kryten_api_gate/
|
|
46
|
+
├── __init__.py
|
|
47
|
+
├── __main__.py # Entry point
|
|
48
|
+
├── config.py # Pydantic config model
|
|
49
|
+
├── app.py # FastAPI app factory
|
|
50
|
+
├── deps.py # DI (get_client, get_config)
|
|
51
|
+
├── auth.py # API key dependency
|
|
52
|
+
├── routes/ # One file per domain
|
|
53
|
+
│ ├── chat.py
|
|
54
|
+
│ ├── playlist.py
|
|
55
|
+
│ ├── playback.py
|
|
56
|
+
│ ├── moderation.py
|
|
57
|
+
│ ├── admin.py
|
|
58
|
+
│ ├── emotes.py
|
|
59
|
+
│ ├── filters.py
|
|
60
|
+
│ ├── polls.py
|
|
61
|
+
│ ├── library.py
|
|
62
|
+
│ ├── kv.py
|
|
63
|
+
│ ├── state.py
|
|
64
|
+
│ └── system.py
|
|
65
|
+
└── schemas/
|
|
66
|
+
├── __init__.py
|
|
67
|
+
├── requests.py # Pydantic request models per route group
|
|
68
|
+
└── responses.py # Pydantic response models
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Dependencies:** `kryten-py>=0.10.5`, `fastapi>=0.115`, `uvicorn>=0.30`, `pydantic>=2.0`
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Phase 2: Core Infrastructure
|
|
76
|
+
|
|
77
|
+
### 2.1 Config (`config.py`)
|
|
78
|
+
|
|
79
|
+
Pydantic model loading JSON:
|
|
80
|
+
|
|
81
|
+
| Field | Type | Default | Notes |
|
|
82
|
+
|-------|------|---------|-------|
|
|
83
|
+
| `nats_url` | `str` | `nats://localhost:4222` | |
|
|
84
|
+
| `channel` | `str` | required | CyTube channel name |
|
|
85
|
+
| `domain` | `str` | `cytu.be` | |
|
|
86
|
+
| `http_host` | `str` | `127.0.0.1` | |
|
|
87
|
+
| `http_port` | `int` | `28288` | |
|
|
88
|
+
| `api_keys` | `list[str]` | required | Bearer tokens |
|
|
89
|
+
| `kv_read_only` | `bool` | `false` | When true, KV PUT/DELETE return 403 |
|
|
90
|
+
| `service_name` | `str` | `api-gate` | |
|
|
91
|
+
| `log_level` | `str` | `INFO` | |
|
|
92
|
+
|
|
93
|
+
### 2.2 Entry Point (`__main__.py`)
|
|
94
|
+
|
|
95
|
+
Standard kryten pattern:
|
|
96
|
+
1. Parse args (`--config`, `--log-level`)
|
|
97
|
+
2. Load config (json or yaml, check cwd, then `/etc/kryten/`, then `/opt/kryten/api-gate/` with precedence)
|
|
98
|
+
3. Create `KrytenClient` with config
|
|
99
|
+
4. `await client.connect()`
|
|
100
|
+
5. Create FastAPI app, inject client + config into `app.state`
|
|
101
|
+
6. Run uvicorn server
|
|
102
|
+
7. Signal handler for graceful shutdown → `client.disconnect()`
|
|
103
|
+
|
|
104
|
+
### 2.3 Auth (`auth.py`)
|
|
105
|
+
|
|
106
|
+
- FastAPI dependency extracting `Authorization: Bearer <key>`
|
|
107
|
+
- Validates against `config.api_keys`
|
|
108
|
+
- Returns 401 on mismatch
|
|
109
|
+
- Health endpoint exempted (public for monitoring)
|
|
110
|
+
|
|
111
|
+
### 2.4 Dependencies (`deps.py`)
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
def get_client(request: Request) -> KrytenClient:
|
|
115
|
+
return request.app.state.client
|
|
116
|
+
|
|
117
|
+
def get_config(request: Request) -> Config:
|
|
118
|
+
return request.app.state.config
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Phase 3: Route Implementation
|
|
124
|
+
|
|
125
|
+
All routes under `/api/v1/`. Channel is fixed from config (not passed per-request).
|
|
126
|
+
|
|
127
|
+
### Chat (`/api/v1/chat`)
|
|
128
|
+
|
|
129
|
+
| Method | Endpoint | KrytenClient method |
|
|
130
|
+
|--------|----------|---------------------|
|
|
131
|
+
| POST | `/send` | `send_chat(channel, message)` |
|
|
132
|
+
| POST | `/pm` | `send_pm(channel, username, message)` |
|
|
133
|
+
|
|
134
|
+
### Playlist (`/api/v1/playlist`)
|
|
135
|
+
|
|
136
|
+
| Method | Endpoint | KrytenClient method |
|
|
137
|
+
|--------|----------|---------------------|
|
|
138
|
+
| POST | `/add` | `add_media(channel, type, id, position, temp)` |
|
|
139
|
+
| DELETE | `/{uid}` | `delete_media(channel, uid)` |
|
|
140
|
+
| PUT | `/{uid}/move` | `move_media(channel, uid, position)` |
|
|
141
|
+
| POST | `/{uid}/jump` | `jump_to(channel, uid)` |
|
|
142
|
+
| DELETE | `/` | `clear_playlist(channel)` |
|
|
143
|
+
| POST | `/shuffle` | `shuffle_playlist(channel)` |
|
|
144
|
+
| PUT | `/{uid}/temp` | `set_temp(channel, uid, is_temp)` |
|
|
145
|
+
|
|
146
|
+
### Playback (`/api/v1/playback`)
|
|
147
|
+
|
|
148
|
+
| Method | Endpoint | KrytenClient method |
|
|
149
|
+
|--------|----------|---------------------|
|
|
150
|
+
| POST | `/pause` | `pause(channel)` | (disableable by config)
|
|
151
|
+
| POST | `/play` | `play(channel)` |
|
|
152
|
+
| POST | `/seek` | `seek(channel, time_seconds)` | (disableable by config)
|
|
153
|
+
| POST | `/voteskip` | `voteskip(channel)` | (disableable by config)
|
|
154
|
+
| POST | `/next` | `play_next(channel)` |
|
|
155
|
+
|
|
156
|
+
### Moderation (`/api/v1/moderation`)
|
|
157
|
+
|
|
158
|
+
| Method | Endpoint | KrytenClient method |
|
|
159
|
+
|--------|----------|---------------------|
|
|
160
|
+
| POST | `/kick` | `kick_user(channel, username, reason)` |
|
|
161
|
+
| POST | `/ban` | `ban_user(channel, username, reason)` |
|
|
162
|
+
| POST | `/mute` | `mute_user(channel, username)` |
|
|
163
|
+
| POST | `/shadow-mute` | `shadow_mute_user(channel, username)` |
|
|
164
|
+
| POST | `/unmute` | `unmute_user(channel, username)` |
|
|
165
|
+
| POST | `/assign-leader` | `assign_leader(channel, username)` |
|
|
166
|
+
| GET | `/banlist` | `request_banlist(channel)` |
|
|
167
|
+
| DELETE | `/ban/{ban_id}` | `unban(channel, ban_id)` |
|
|
168
|
+
|
|
169
|
+
### Admin (`/api/v1/admin`)
|
|
170
|
+
|
|
171
|
+
| Method | Endpoint | KrytenClient method |
|
|
172
|
+
|--------|----------|---------------------|
|
|
173
|
+
| PUT | `/motd` | `set_motd(channel, motd)` |
|
|
174
|
+
| GET | `/motd` | `request_motd(channel)` |
|
|
175
|
+
| PUT | `/css` | `set_channel_css(channel, css)` |
|
|
176
|
+
| GET | `/css` | `request_channel_css(channel)` |
|
|
177
|
+
| PUT | `/js` | `set_channel_js(channel, js)` |
|
|
178
|
+
| GET | `/js` | `request_channel_js(channel)` |
|
|
179
|
+
| PUT | `/options` | `set_options(channel, options)` |
|
|
180
|
+
| GET | `/options` | `request_options(channel)` |
|
|
181
|
+
| PUT | `/permissions` | `set_permissions(channel, permissions)` |
|
|
182
|
+
| GET | `/permissions` | `request_permissions(channel)` |
|
|
183
|
+
| GET | `/ranks` | `request_channel_ranks(channel)` |
|
|
184
|
+
| PUT | `/rank` | `set_channel_rank(channel, username, rank)` |
|
|
185
|
+
| GET | `/log` | `read_chan_log(channel, count)` |
|
|
186
|
+
|
|
187
|
+
### Emotes (`/api/v1/emotes`)
|
|
188
|
+
## (Note: There is a way in the cytube UI to bulk import emotes via JSON, and to bulk export the JSON as well. No corresponding kryten-py methods exist, so we NEED to add `export_emotes(channel)` and `import_emotes(channel, emotes_json)` methods to KrytenClient, and route them here as POST `/import` and GET `/export`. The single-emote CRUD routes below are still needed for fine-grained updates.)
|
|
189
|
+
| Method | Endpoint | KrytenClient method |
|
|
190
|
+
|--------|----------|---------------------|
|
|
191
|
+
| PUT | `/{name}` | `update_emote(channel, name, image, source)` |
|
|
192
|
+
| DELETE | `/{name}` | `remove_emote(channel, name)` |
|
|
193
|
+
|
|
194
|
+
### Filters (`/api/v1/filters`)
|
|
195
|
+
|
|
196
|
+
| Method | Endpoint | KrytenClient method |
|
|
197
|
+
|--------|----------|---------------------|
|
|
198
|
+
| POST | `/` | `add_filter(channel, name, source, flags, replace, ...)` |
|
|
199
|
+
| PUT | `/{name}` | `update_filter(channel, ...)` |
|
|
200
|
+
| DELETE | `/{name}` | `remove_filter(channel, name)` |
|
|
201
|
+
|
|
202
|
+
### Polls (`/api/v1/polls`)
|
|
203
|
+
|
|
204
|
+
| Method | Endpoint | KrytenClient method |
|
|
205
|
+
|--------|----------|---------------------|
|
|
206
|
+
| POST | `/` | `new_poll(channel, title, options, obscured, timeout)` |
|
|
207
|
+
| POST | `/vote` | `vote(channel, option)` |
|
|
208
|
+
| POST | `/close` | `close_poll(channel)` |
|
|
209
|
+
|
|
210
|
+
### Library (`/api/v1/library`)
|
|
211
|
+
|
|
212
|
+
| Method | Endpoint | KrytenClient method |
|
|
213
|
+
|--------|----------|---------------------|
|
|
214
|
+
| GET | `/search` | `search_library(channel, query, source)` |
|
|
215
|
+
| DELETE | `/{media_id}` | `delete_from_library(channel, media_id)` |
|
|
216
|
+
|
|
217
|
+
### KV Store (`/api/v1/kv`)
|
|
218
|
+
|
|
219
|
+
| Method | Endpoint | KrytenClient method | Notes |
|
|
220
|
+
|--------|----------|---------------------|-------|
|
|
221
|
+
| GET | `/buckets/{bucket}/keys` | `kv_keys(bucket)` | |
|
|
222
|
+
| GET | `/buckets/{bucket}/keys/{key}` | `kv_get(bucket, key)` | |
|
|
223
|
+
| GET | `/buckets/{bucket}` | `kv_get_all(bucket)` | |
|
|
224
|
+
| PUT | `/buckets/{bucket}/keys/{key}` | `kv_put(bucket, key, value)` | Gated by `kv_read_only` |
|
|
225
|
+
| DELETE | `/buckets/{bucket}/keys/{key}` | `kv_delete(bucket, key)` | Gated by `kv_read_only` | (Delete whole bucket supported via `kv_delete(bucket, '*')`)
|
|
226
|
+
|
|
227
|
+
### State (`/api/v1/state`)
|
|
228
|
+
|
|
229
|
+
| Method | Endpoint | KrytenClient method |
|
|
230
|
+
|--------|----------|---------------------|
|
|
231
|
+
| GET | `/user/{username}` | `get_user(channel, username)` |
|
|
232
|
+
| GET | `/channels` | `get_channels()` |
|
|
233
|
+
| GET | `/playlist` | `get_state_playlist_items(channel)` |
|
|
234
|
+
| GET | `/now-playing` | `get_state_current_media(channel)` |
|
|
235
|
+
|
|
236
|
+
### System (`/api/v1/system`)
|
|
237
|
+
|
|
238
|
+
| Method | Endpoint | KrytenClient method | Auth |
|
|
239
|
+
|--------|----------|---------------------|------|
|
|
240
|
+
| GET | `/health` | local health check | **Public** |
|
|
241
|
+
| GET | `/version` | `get_version()` | Required |
|
|
242
|
+
| GET | `/stats` | `get_stats()` | Required |
|
|
243
|
+
| GET | `/services` | `get_services()` | Required |
|
|
244
|
+
| GET | `/config` | `get_config()` | Required |
|
|
245
|
+
| GET | `/ping` | `ping()` | Required |
|
|
246
|
+
| POST | `/reload` | `reload_config()` | Required |
|
|
247
|
+
| POST | `/shutdown` | `shutdown()` | Required |
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Phase 4: Packaging & Deployment
|
|
252
|
+
|
|
253
|
+
- `pyproject.toml` with console script `kryten-api-gate = kryten_api_gate.__main__:main`
|
|
254
|
+
- `config.example.json` with documented defaults
|
|
255
|
+
- `systemd/kryten-api-gate.service` — standard kryten pattern (port 28288, user `kryten`, `/opt/kryten/api-gate/`)
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Verification
|
|
260
|
+
|
|
261
|
+
1. Start NATS + kryten-robot → start kryten-api-gate
|
|
262
|
+
2. `GET /api/v1/system/ping` with API key → success response
|
|
263
|
+
3. `POST /api/v1/chat/send` → message appears on CyTube
|
|
264
|
+
4. `GET /api/v1/system/health` without auth → 200 (public)
|
|
265
|
+
5. Any protected route without auth → 401
|
|
266
|
+
6. KV PUT with `kv_read_only: true` → 403
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Key Decisions
|
|
271
|
+
|
|
272
|
+
- **Channel from config, not per-request** — matches single-channel-per-instance model across all kryten services
|
|
273
|
+
- **API keys only** — machine-to-machine; user auth lives in upstream consumer app
|
|
274
|
+
- **No event streaming** — REST only; upstream app can poll state endpoints
|
|
275
|
+
- **Health is unauthenticated** — standard for monitoring/load balancers
|
|
276
|
+
- **KV write gating via config** — allows deployment in read-only mode
|
|
277
|
+
- **Phase 0 is blocking** — API gate depends on robot dispatching all commands properly
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Excluded from Scope
|
|
282
|
+
|
|
283
|
+
- WebSocket/SSE streaming
|
|
284
|
+
- OTP/session user auth
|
|
285
|
+
- Frontend/UI
|
|
286
|
+
- Multi-channel or multi-NATS routing
|
|
287
|
+
- Docker
|
|
288
|
+
- Bulk emote import (consumer can loop PUT calls)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
name: Publish Python Package to PyPI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
workflow_call: # Called directly from release.yml to bypass GITHUB_TOKEN cross-workflow trigger limitation
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
push:
|
|
11
|
+
tags:
|
|
12
|
+
- 'kryten-api-gate-v*'
|
|
13
|
+
- 'v*'
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
id-token: write # Required for OIDC trusted publishing
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
release-build:
|
|
21
|
+
name: Build distribution packages
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout repository
|
|
26
|
+
uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v4
|
|
30
|
+
with:
|
|
31
|
+
version: "latest"
|
|
32
|
+
|
|
33
|
+
- name: Set up Python
|
|
34
|
+
uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.11"
|
|
37
|
+
|
|
38
|
+
- name: Build release distributions
|
|
39
|
+
run: |
|
|
40
|
+
uv build
|
|
41
|
+
echo "📦 Built packages:"
|
|
42
|
+
ls -lh dist/
|
|
43
|
+
|
|
44
|
+
- name: Upload distributions
|
|
45
|
+
uses: actions/upload-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: release-dists
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
pypi-publish:
|
|
51
|
+
name: Publish to PyPI
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
needs:
|
|
54
|
+
- release-build
|
|
55
|
+
environment:
|
|
56
|
+
name: pypi
|
|
57
|
+
permissions:
|
|
58
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
59
|
+
id-token: write
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- name: Retrieve release distributions
|
|
63
|
+
uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: release-dists
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Publish release distributions to PyPI
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
packages-dir: dist/
|
|
72
|
+
skip-existing: true
|
|
73
|
+
attestations: false # workflow_call sets workflow_ref=release.yml but attestations check uses that not job_workflow_ref, causing a mismatch with the trusted publisher config
|
|
74
|
+
|
|
75
|
+
- name: Success notification
|
|
76
|
+
if: success()
|
|
77
|
+
run: |
|
|
78
|
+
echo "✅ Successfully published to PyPI!"
|
|
79
|
+
echo "🔗 Package URL: https://pypi.org/project/kryten-api-gate/"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
name: Release Automation
|
|
2
|
+
|
|
3
|
+
# Automatically create GitHub releases when version in pyproject.toml changes
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
paths:
|
|
9
|
+
- 'pyproject.toml'
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
14
|
+
pull-requests: read
|
|
15
|
+
id-token: write
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
create-release:
|
|
19
|
+
name: Create Release
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0 # Full history for changelog
|
|
26
|
+
|
|
27
|
+
- name: Read version from pyproject.toml
|
|
28
|
+
id: version
|
|
29
|
+
run: |
|
|
30
|
+
VERSION=$(grep -m1 'version = "' pyproject.toml | cut -d '"' -f 2)
|
|
31
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
32
|
+
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
|
|
33
|
+
echo "📦 Version: $VERSION"
|
|
34
|
+
|
|
35
|
+
- name: Check if tag exists
|
|
36
|
+
id: check_tag
|
|
37
|
+
run: |
|
|
38
|
+
git fetch --tags
|
|
39
|
+
if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
|
|
40
|
+
echo "exists=true" >> $GITHUB_OUTPUT
|
|
41
|
+
echo "ℹ️ Tag ${{ steps.version.outputs.tag }} already exists"
|
|
42
|
+
else
|
|
43
|
+
echo "exists=false" >> $GITHUB_OUTPUT
|
|
44
|
+
echo "✨ Tag ${{ steps.version.outputs.tag }} will be created"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
- name: Generate changelog
|
|
48
|
+
id: changelog
|
|
49
|
+
if: steps.check_tag.outputs.exists == 'false'
|
|
50
|
+
run: |
|
|
51
|
+
echo "Generating changelog..."
|
|
52
|
+
|
|
53
|
+
# Get previous tag
|
|
54
|
+
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
|
55
|
+
|
|
56
|
+
if [ -z "$PREV_TAG" ]; then
|
|
57
|
+
echo "📝 First release - generating full changelog"
|
|
58
|
+
COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges)
|
|
59
|
+
else
|
|
60
|
+
echo "📝 Generating changelog since $PREV_TAG"
|
|
61
|
+
COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges)
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Save changelog to file
|
|
65
|
+
cat > changelog.md << EOF
|
|
66
|
+
## What's Changed
|
|
67
|
+
|
|
68
|
+
${COMMITS}
|
|
69
|
+
|
|
70
|
+
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${{ steps.version.outputs.tag }}
|
|
71
|
+
EOF
|
|
72
|
+
|
|
73
|
+
cat changelog.md
|
|
74
|
+
|
|
75
|
+
- name: Create Git tag
|
|
76
|
+
if: steps.check_tag.outputs.exists == 'false'
|
|
77
|
+
run: |
|
|
78
|
+
git config user.name "github-actions[bot]"
|
|
79
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
80
|
+
git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"
|
|
81
|
+
git push origin "${{ steps.version.outputs.tag }}"
|
|
82
|
+
|
|
83
|
+
- name: Create GitHub Release
|
|
84
|
+
if: steps.check_tag.outputs.exists == 'false'
|
|
85
|
+
env:
|
|
86
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
87
|
+
run: |
|
|
88
|
+
gh release create "${{ steps.version.outputs.tag }}" \
|
|
89
|
+
--title "Release ${{ steps.version.outputs.tag }}" \
|
|
90
|
+
--notes-file changelog.md
|
|
91
|
+
|
|
92
|
+
- name: Skip release creation
|
|
93
|
+
if: steps.check_tag.outputs.exists == 'true'
|
|
94
|
+
run: |
|
|
95
|
+
echo "ℹ️ Release ${{ steps.version.outputs.tag }} already exists - skipping"
|
|
96
|
+
|
|
97
|
+
publish-to-pypi:
|
|
98
|
+
name: Publish to PyPI
|
|
99
|
+
needs: [create-release]
|
|
100
|
+
uses: ./.github/workflows/python-publish.yml
|
|
101
|
+
permissions:
|
|
102
|
+
id-token: write
|
|
103
|
+
contents: read
|
|
104
|
+
|
|
105
|
+
notify-release:
|
|
106
|
+
name: Notify Release Created
|
|
107
|
+
runs-on: ubuntu-latest
|
|
108
|
+
needs: [create-release, publish-to-pypi]
|
|
109
|
+
|
|
110
|
+
steps:
|
|
111
|
+
- uses: actions/checkout@v4
|
|
112
|
+
|
|
113
|
+
- name: Read version from pyproject.toml
|
|
114
|
+
id: version
|
|
115
|
+
run: |
|
|
116
|
+
VERSION=$(grep -m1 'version = "' pyproject.toml | cut -d '"' -f 2)
|
|
117
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
118
|
+
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
|
|
119
|
+
|
|
120
|
+
- name: Send notification
|
|
121
|
+
run: |
|
|
122
|
+
echo "📢 Release notification"
|
|
123
|
+
echo "Version: ${{ steps.version.outputs.tag }}"
|
|
124
|
+
echo "Repository: ${{ github.repository }}"
|
|
125
|
+
echo "Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
pip-wheel-metadata/
|
|
20
|
+
share/python-wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
|
|
26
|
+
# Virtual environments
|
|
27
|
+
.venv/
|
|
28
|
+
venv/
|
|
29
|
+
ENV/
|
|
30
|
+
env/
|
|
31
|
+
|
|
32
|
+
# PyCharm
|
|
33
|
+
.idea/
|
|
34
|
+
|
|
35
|
+
# VSCode
|
|
36
|
+
.vscode/
|
|
37
|
+
|
|
38
|
+
# pytest
|
|
39
|
+
.pytest_cache/
|
|
40
|
+
htmlcov/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
.hypothesis/
|
|
46
|
+
|
|
47
|
+
# mypy
|
|
48
|
+
.mypy_cache/
|
|
49
|
+
.dmypy.json
|
|
50
|
+
dmypy.json
|
|
51
|
+
|
|
52
|
+
# Logs
|
|
53
|
+
*.log
|
|
54
|
+
logs/
|
|
55
|
+
|
|
56
|
+
# Configuration files (keep examples only)
|
|
57
|
+
config.json
|
|
58
|
+
config-*.json
|
|
59
|
+
!config.example.json
|
|
60
|
+
|
|
61
|
+
# OS
|
|
62
|
+
.DS_Store
|
|
63
|
+
Thumbs.db
|
|
64
|
+
|
|
65
|
+
# Backup files
|
|
66
|
+
*~
|
|
67
|
+
*.bak
|
|
68
|
+
*.swp
|
|
69
|
+
|
|
70
|
+
# Test output
|
|
71
|
+
test-output/
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kryten-api-gate
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: HTTP REST gateway for the Kryten ecosystem — exposes KrytenClient via FastAPI
|
|
5
|
+
Author: grobertson
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: fastapi>=0.115
|
|
9
|
+
Requires-Dist: kryten-py>=0.10.5
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: black>=24.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
15
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# kryten-api-gate
|
|
22
|
+
|
|
23
|
+
HTTP REST gateway for the Kryten ecosystem — exposes [`kryten-py`](https://github.com/grobertson/kryten-py) via FastAPI.
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
`kryten-api-gate` sits between HTTP clients and the Kryten NATS message bus, translating REST calls into `KrytenClient` commands dispatched to [Kryten-Robot](https://github.com/grobertson/Kryten-Robot).
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
HTTP Client → kryten-api-gate → KrytenClient → NATS → Kryten-Robot → CyTube
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install kryten-api-gate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
1. Copy `config.example.json` to `config.json` and fill in your values:
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"nats_url": "nats://localhost:4222",
|
|
45
|
+
"channel": "your-channel",
|
|
46
|
+
"api_key": "your-secret-api-key",
|
|
47
|
+
"host": "0.0.0.0",
|
|
48
|
+
"port": 8080
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
2. Run the server:
|
|
53
|
+
```bash
|
|
54
|
+
kryten-api-gate
|
|
55
|
+
# or
|
|
56
|
+
python -m kryten_api_gate
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. Interactive API docs at `http://localhost:8080/docs`
|
|
60
|
+
|
|
61
|
+
## Authentication
|
|
62
|
+
|
|
63
|
+
All endpoints (except `/api/v1/system/health` and `/api/v1/system/version`) require a Bearer token:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Authorization: Bearer <api_key>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API Routes
|
|
70
|
+
|
|
71
|
+
| Prefix | Description |
|
|
72
|
+
|--------|-------------|
|
|
73
|
+
| `GET /api/v1/system/health` | Health check (unauthenticated) |
|
|
74
|
+
| `GET /api/v1/system/version` | Version info (unauthenticated) |
|
|
75
|
+
| `POST /api/v1/chat/send` | Send a chat message |
|
|
76
|
+
| `GET/POST /api/v1/playlist/` | Playlist management |
|
|
77
|
+
| `POST /api/v1/playback/` | Playback control |
|
|
78
|
+
| `POST /api/v1/moderation/` | Moderation actions |
|
|
79
|
+
| `GET/POST /api/v1/admin/` | Admin channel settings |
|
|
80
|
+
| `GET/POST /api/v1/emotes/` | Emote management |
|
|
81
|
+
| `GET/POST /api/v1/filters/` | Chat filter management |
|
|
82
|
+
| `GET/POST /api/v1/polls/` | Poll management |
|
|
83
|
+
| `GET /api/v1/library/` | Media library search |
|
|
84
|
+
| `GET/PUT/DELETE /api/v1/kv/` | Key-value store |
|
|
85
|
+
| `GET /api/v1/state/` | Channel state queries |
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
| Key | Description | Default |
|
|
90
|
+
|-----|-------------|---------|
|
|
91
|
+
| `nats_url` | NATS server URL | `nats://localhost:4222` |
|
|
92
|
+
| `channel` | CyTube channel name | required |
|
|
93
|
+
| `api_key` | API authentication key | required |
|
|
94
|
+
| `host` | Listen address | `0.0.0.0` |
|
|
95
|
+
| `port` | Listen port | `8080` |
|
|
96
|
+
| `log_level` | Logging level | `info` |
|
|
97
|
+
|
|
98
|
+
## Systemd Service
|
|
99
|
+
|
|
100
|
+
A systemd unit file is provided at `systemd/kryten-api-gate.service`.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git clone https://github.com/grobertson/kryten-api-gate
|
|
106
|
+
cd kryten-api-gate
|
|
107
|
+
pip install -e ".[dev]"
|
|
108
|
+
uvicorn kryten_api_gate.app:create_app --factory --reload
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|