k9o 1.3.0 → 1.4.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -1
- data/README.md +285 -32
- data/lib/k9o/ai.rb +15 -0
- data/lib/k9o/approvals.rb +159 -0
- data/lib/k9o/client.rb +8 -2
- data/lib/k9o/data.rb +67 -1
- data/lib/k9o/query.rb +174 -0
- data/lib/k9o/schema.rb +35 -0
- data/lib/k9o/streams.rb +94 -0
- data/lib/k9o/version.rb +1 -1
- data/lib/k9o.rb +4 -0
- data/sig/k9o/ai.rbs +4 -0
- data/sig/k9o/approvals.rbs +49 -0
- data/sig/k9o/query.rbs +26 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eaf2bcfb047371f56983f311783294b81d482812171a893b3e57ea5361d9a5e1
|
|
4
|
+
data.tar.gz: 1afe1668983a800d3c061f11e5f6117bda5eff6459b1f7aab210b091f7f8eaaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4595a0da65edfee744aac3adac7f8f96b4aa67352bc013dca3c3b8bcb534182992f83387bd554e17c9da50c07275a0172fb8ab4488dab6a23b81570fbcefaf2
|
|
7
|
+
data.tar.gz: 9686288bd9ea43dbabb7148e6033d78a45124b16f72774856ac26d893638f1794dc64948e4778fb27237d9a310c62c20f17de4786bc4aa528a3c727e807be45a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog — k9o (Ruby)
|
|
2
2
|
|
|
3
|
+
## 1.4.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `query(sql, params)`: one read-only SQL statement; an array binds `$1, $2, ...`, a hash binds `:name` placeholders.
|
|
8
|
+
- `search(collection, column:, q:)`: full-text search over one column, plus `text_search` and `vector_search` on `select`.
|
|
9
|
+
- `ai.generate`: text generation from a prompt (verb sugar over `compute.llms.invoke`; `model` defaults to the org's default LLM).
|
|
10
|
+
- `streams` namespace for user data streams: `create`, `list`, `get`, `delete`, `publish`, `replay` and `subscribe`.
|
|
11
|
+
`subscribe` tails live events over SSE and reconnects from the last sequence when the server closes the connection.
|
|
12
|
+
|
|
13
|
+
- `approvals` namespace for the pending-change queue: `list`, `get`, `approve`, `reject` and `bulk`.
|
|
14
|
+
- `approval_policies` namespace: `list`, `create` and `delete`.
|
|
15
|
+
- `schema.collections.list`: the project's collections with row counts and last migration, paged by cursor.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The README description now covers streams and realtime, and the platform overview moved to an "About Kilimanjaro" appendix.
|
|
20
|
+
- Function deploy examples use the sandbox tier.
|
|
21
|
+
|
|
3
22
|
## 1.3.0 — 2026-10-26
|
|
4
23
|
|
|
5
24
|
Every method now matches the gateway.
|
|
@@ -37,7 +56,7 @@ Every method now matches the gateway.
|
|
|
37
56
|
|
|
38
57
|
## 1.2.0 — 2026-10-25
|
|
39
58
|
|
|
40
|
-
- The README now introduces the Kilimanjaro
|
|
59
|
+
- The README now introduces the Kilimanjaro building blocks (Keep, Pulse, Gate, Kibo). No change to the public API.
|
|
41
60
|
|
|
42
61
|
## 1.1.1 — 2026-10-25
|
|
43
62
|
|
data/README.md
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
# k9o
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Kilimanjaro is the AI-native foundation for building critical applications.
|
|
6
|
-
Its products are:
|
|
7
|
-
|
|
8
|
-
- Keep: AI Security and Governance
|
|
9
|
-
- Pulse: Data Intelligence
|
|
10
|
-
- Gate: Customer Identity and Access Management
|
|
11
|
-
- Kibo: Keys and Secrets Management
|
|
3
|
+
Client for Kilimanjaro. Read and write your data, store files, manage policies and migrations,
|
|
4
|
+
deploy and invoke agents or functions, and publish to and subscribe to streams and realtime events.
|
|
12
5
|
|
|
13
6
|
## Contents
|
|
14
7
|
|
|
@@ -17,8 +10,15 @@ Its products are:
|
|
|
17
10
|
- [Quick start](#quick-start)
|
|
18
11
|
- [Authentication and project context](#authentication-and-project-context)
|
|
19
12
|
- [Running queries](#running-queries)
|
|
13
|
+
- [Query with SQL](#query-with-sql)
|
|
14
|
+
- [Search](#search)
|
|
15
|
+
- [Streams](#streams)
|
|
16
|
+
- [Approvals](#approvals)
|
|
17
|
+
- [Approval policies](#approval-policies)
|
|
18
|
+
- [Schema collections](#schema-collections)
|
|
20
19
|
- [Handling errors](#handling-errors)
|
|
21
20
|
- [Appendix](#appendix)
|
|
21
|
+
- [About Kilimanjaro](#about-kilimanjaro)
|
|
22
22
|
- [License](#license)
|
|
23
23
|
|
|
24
24
|
## Links
|
|
@@ -31,13 +31,13 @@ Its products are:
|
|
|
31
31
|
## Install
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
gem install k9o
|
|
34
|
+
gem install k9o -v 1.4.0
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Or add it to your `Gemfile`:
|
|
38
38
|
|
|
39
39
|
```ruby
|
|
40
|
-
gem "k9o"
|
|
40
|
+
gem "k9o", "1.4.0"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Requires Ruby 3.2 or newer. See [requirements](#requirements) for details.
|
|
@@ -63,11 +63,11 @@ k9o = K9o.create_client(
|
|
|
63
63
|
```ruby
|
|
64
64
|
new_task = { "title" => "Write the README", "done" => false }
|
|
65
65
|
created = k9o.from("tasks").insert(new_task)
|
|
66
|
-
puts "
|
|
66
|
+
puts "Output: #{created.data['title']}"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
```text
|
|
70
|
-
|
|
70
|
+
Output: Write the README
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
### List tasks
|
|
@@ -75,12 +75,12 @@ created: Write the README
|
|
|
75
75
|
```ruby
|
|
76
76
|
response = k9o.from("tasks").select.eq("done", false).limit(10).execute
|
|
77
77
|
response.data.each do |task|
|
|
78
|
-
puts "#{task['title']} #{task['done']}"
|
|
78
|
+
puts "Output: #{task['title']} #{task['done']}"
|
|
79
79
|
end
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
```text
|
|
83
|
-
Write the README false
|
|
83
|
+
Output: Write the README false
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
### Update and delete a task
|
|
@@ -143,13 +143,193 @@ A sub-object the server did not send is `nil`, so use `&.` when it may be absent
|
|
|
143
143
|
```ruby
|
|
144
144
|
request_id = response.meta.context.request_id
|
|
145
145
|
total = response.meta.cursor.total_count
|
|
146
|
-
puts "#{request_id} #{total}"
|
|
146
|
+
puts "Output: #{request_id} #{total}"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
Output: req_01H... 1
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Query with SQL
|
|
154
|
+
|
|
155
|
+
`k9o.query(sql, params)` runs one read-only SQL statement and returns the rows in `data`.
|
|
156
|
+
It is read-only (a write is refused with `K9O_RAW_SQL_READ_ONLY`; change data through `from`), takes one statement per call,
|
|
157
|
+
applies row-level security, accepts at most 100000 characters of SQL and 1000 parameters, and needs the project editor role.
|
|
158
|
+
A reference to `dataset.<name>` reads an analytics dataset.
|
|
159
|
+
|
|
160
|
+
Pass an array to bind `$1`, `$2`, and so on, or a hash to bind `:name` placeholders. Values are bound by the database and can be a string, number, boolean or `nil`; serialise anything else (for example as JSON text) first.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
rows = k9o.query("SELECT data->>'title' AS title FROM posts WHERE data->>'status' = :status", status: "published")
|
|
164
|
+
puts "Output: #{rows.data.first['title']}"
|
|
165
|
+
|
|
166
|
+
same = k9o.query("SELECT count(*) AS n FROM posts WHERE data->>'status' = $1", ["published"])
|
|
167
|
+
puts "Output: #{same.data.first['n']}"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
Output: Hello
|
|
172
|
+
Output: 12
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A `:name` with no key in the hash raises `K9o::KError` with code `K9O_INVALID_FIELD_VALUE` before any request is sent.
|
|
176
|
+
|
|
177
|
+
## Search
|
|
178
|
+
|
|
179
|
+
`k9o.search(collection, column:, q:)` runs a full-text search over one text column, best matches first.
|
|
180
|
+
`language:` defaults to `english`; `type:` is `plain` (default), `phrase` or `websearch`. `limit:` and `cursor:` page the result.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
found = k9o.search("articles", column: "body", q: "climate resilience", limit: 5)
|
|
184
|
+
puts "Output: #{found.data.length} #{found.data.first['title']}"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
Output: 5 Adapting to heat
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The same search is available on a query as `text_search(column, query, language: "english", type: "plain")`.
|
|
192
|
+
`vector_search(column, embedding, metric: "cosine", limit: nil)` finds the closest rows to an embedding, closest first;
|
|
193
|
+
`metric:` is `cosine`, `euclidean` or `dot`, and `limit:` is 1 to 1000.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
vector = k9o.ai.embed(input: "heat waves").data["embeddings"].first
|
|
197
|
+
near = k9o.from("articles").select.vector_search("embedding", vector, limit: 3).execute
|
|
198
|
+
puts "Output: #{near.data.length}"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
Output: 3
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Streams
|
|
206
|
+
|
|
207
|
+
Publish events to a stream and tail them live. Publishing needs the project admin role; subscribing and replaying need admin or auditor.
|
|
208
|
+
`subscribe` reconnects from the last sequence when the server closes the connection, so no event is repeated or skipped.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
stream = k9o.streams.create("Orders feed", 1_073_741_824)
|
|
212
|
+
slug = stream.data["slug"]
|
|
213
|
+
puts "Output: #{slug}"
|
|
214
|
+
k9o.streams.publish(slug, { "order" => 1001 }, subject: "eu.new")
|
|
215
|
+
|
|
216
|
+
k9o.streams.subscribe(slug).each do |event|
|
|
217
|
+
puts "Output: #{event["sequence"]} #{event["data"]["order"]}"
|
|
218
|
+
break
|
|
219
|
+
end
|
|
147
220
|
```
|
|
148
221
|
|
|
149
222
|
```text
|
|
150
|
-
|
|
223
|
+
Output: orders-feed
|
|
224
|
+
Output: 1 1001
|
|
151
225
|
```
|
|
152
226
|
|
|
227
|
+
`replay` reads a bounded range of past events.
|
|
228
|
+
Failures raise `K9o::KError`: `K9O_STREAM_UNAVAILABLE` while streams are not available, and `K9O_STREAM_INVALID` for a bad subject.
|
|
229
|
+
|
|
230
|
+
## Approvals
|
|
231
|
+
|
|
232
|
+
Writes that need approval wait in a queue for a second person. `k9o.approvals` reads that queue and decides its changes.
|
|
233
|
+
Project admins and auditors see every change; other members see only the ones they proposed.
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
queue = k9o.approvals.list(status: "pending_approval", limit: 20)
|
|
237
|
+
change = queue.data.first
|
|
238
|
+
|
|
239
|
+
puts "Output: #{change["id"]} #{change["operation"]} #{change["collection"]}"
|
|
240
|
+
|
|
241
|
+
decided = k9o.approvals.approve(change["id"], reason: "checked the amount")
|
|
242
|
+
|
|
243
|
+
puts "Output: #{decided.data["status"]}"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
Output: apr_01j9x3 update orders
|
|
248
|
+
Output: redeemed
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- A change is `pending_approval`, `approved`, `rejected`, `expired` or `redeemed` (applied). An `expired` change can still read `pending_approval` until someone decides it.
|
|
252
|
+
- `approve` can answer 200 with `status` still `pending_approval`. That means your approval is recorded and more approvers are needed; the change is not applied yet.
|
|
253
|
+
- `reject(id, reason:)` needs a non-empty `reason`; the SDK checks it before sending.
|
|
254
|
+
- `list` is not paginated: it returns the newest `limit` changes (1 to 200, default 50). `get(id)` reads one.
|
|
255
|
+
- Deciding needs the queue-admin role (project admin or auditor).
|
|
256
|
+
|
|
257
|
+
Decide many at once with `bulk`. `criteria` is a predicate over each pending change (see [Approval policies](#approval-policies)):
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
run = k9o.approvals.bulk(
|
|
261
|
+
action: "reject",
|
|
262
|
+
criteria: { "eq" => ["change.table", "orders"] },
|
|
263
|
+
reason: "stale"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
puts "Output: #{run.data["affectedCount"]} #{run.data["affectedIds"].join(",")}"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```text
|
|
270
|
+
Output: 2 apr_01j9x3,apr_01j9x4
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
- A row that fails (a conflict, an apply error) is skipped silently and left out of `affectedIds`. Compare `affectedIds` with the set you intended.
|
|
274
|
+
- `reject` needs a `reason`; `criteria` must be a predicate hash. `attestationId` identifies the batch, even when nothing matched.
|
|
275
|
+
- It considers the newest 10000 pending changes. Bulk approve is not guaranteed to apply every matched row.
|
|
276
|
+
|
|
277
|
+
## Approval policies
|
|
278
|
+
|
|
279
|
+
A policy decides what happens to writes on one collection that match a predicate: `queue` them for approval, or `auto_approve` them.
|
|
280
|
+
For a write the first matching policy, oldest first, decides. Policies are evaluated for writes sent with the `requireApproval=true` query parameter.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
policy = k9o.approval_policies.create(
|
|
284
|
+
collection: "orders",
|
|
285
|
+
operation: "update",
|
|
286
|
+
action: "queue",
|
|
287
|
+
predicate: { "gt" => ["change.row.amount", 1000] }
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
puts "Output: #{policy.data["id"]} #{policy.data["action"]}"
|
|
291
|
+
|
|
292
|
+
removed = k9o.approval_policies.delete(policy.data["id"])
|
|
293
|
+
|
|
294
|
+
puts "Output: #{removed.data["id"]}"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
Output: apo_01j9x5 queue
|
|
299
|
+
Output: apo_01j9x5
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- `operation` is `insert`, `update` or `delete`; `action` is `auto_approve` or `queue`.
|
|
303
|
+
- `collection` matches `^[a-zA-Z_][a-zA-Z0-9_]{0,62}$` and `predicate` is a non-null hash. The SDK checks both before sending.
|
|
304
|
+
- A predicate has one operator key: `and`, `or`, `not`, `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `in_cidr`, `not_in_cidr` or `contains`.
|
|
305
|
+
`and` and `or` take an array of predicates, `not` takes one, and a comparison takes `[left, right]`.
|
|
306
|
+
- Operands start with `maker.` or `change.` (for example `change.row.amount`, `change.table`, `change.operation`, `maker.id`).
|
|
307
|
+
- `list` returns all policies. Identical policies are not de-duplicated. Creating and deleting need project admin or author.
|
|
308
|
+
|
|
309
|
+
## Schema collections
|
|
310
|
+
|
|
311
|
+
`k9o.schema.collections.list` lists the project's collections by name, with their size and last migration.
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
page = k9o.schema.collections.list(limit: 2)
|
|
315
|
+
|
|
316
|
+
page.data.each do |collection|
|
|
317
|
+
puts "Output: #{collection["name"]} #{collection["rowCount"]} rows, #{collection["columnCount"]} columns"
|
|
318
|
+
end
|
|
319
|
+
puts "Output: next #{page.meta.cursor.next}"
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```text
|
|
323
|
+
Output: orders 120 rows, 8 columns
|
|
324
|
+
Output: tasks 14 rows, 4 columns
|
|
325
|
+
Output: next dGFza3M
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- Pass `meta.cursor.next` as `cursor` for the next page; it is `nil` on the last page. `limit` is 1 to 200 (default 50).
|
|
329
|
+
- A row is `name`, `rowCount`, `rowCountIsExact`, `columnCount`, `indexCount`, `lastMigrationSlug` and `lastMigrationAppliedAt`.
|
|
330
|
+
- `rowCount` is a live count that skips soft-deleted rows and ignores row-level security. Without an `org_id` claim on the token, `meta.warnings` says so.
|
|
331
|
+
- Project admins, auditors, editors, members and viewers can read it.
|
|
332
|
+
|
|
153
333
|
## Handling errors
|
|
154
334
|
|
|
155
335
|
Failures raise `K9o::KError`, a subclass of `StandardError`. Branch on `code`.
|
|
@@ -158,14 +338,14 @@ Failures raise `K9o::KError`, a subclass of `StandardError`. Branch on `code`.
|
|
|
158
338
|
begin
|
|
159
339
|
k9o.from("missing").select.execute
|
|
160
340
|
rescue K9o::KError => error
|
|
161
|
-
puts "#{error.code} #{error.status} #{error.request_id}"
|
|
162
|
-
puts error.message
|
|
341
|
+
puts "Output: #{error.code} #{error.status} #{error.request_id}"
|
|
342
|
+
puts "Output: #{error.message}"
|
|
163
343
|
end
|
|
164
344
|
```
|
|
165
345
|
|
|
166
346
|
```text
|
|
167
|
-
K9O_NOT_FOUND 404 req_01H...
|
|
168
|
-
Collection not found
|
|
347
|
+
Output: K9O_NOT_FOUND 404 req_01H...
|
|
348
|
+
Output: Collection not found
|
|
169
349
|
```
|
|
170
350
|
|
|
171
351
|
- See [error attributes](#error-attributes) for every field.
|
|
@@ -235,6 +415,8 @@ Chain filters before `execute`. Values are matched against one column.
|
|
|
235
415
|
| `limit(n)` | Maximum rows to return. |
|
|
236
416
|
| `cursor(token)` | Continue from `meta.cursor.next`. |
|
|
237
417
|
| `count(mode)` | Row count mode: `none`, `exact` or `estimated`. |
|
|
418
|
+
| `text_search(column, query, language: "english", type: "plain")` | Full-text search over one column. |
|
|
419
|
+
| `vector_search(column, embedding, metric: "cosine", limit: nil)` | Nearest rows to an embedding, closest first. |
|
|
238
420
|
|
|
239
421
|
#### Projects
|
|
240
422
|
|
|
@@ -249,11 +431,11 @@ Chain filters before `execute`. Values are matched against one column.
|
|
|
249
431
|
|
|
250
432
|
```ruby
|
|
251
433
|
project = k9o.projects.create(display_name: "Task app", slug: "task-app")
|
|
252
|
-
puts project.data["slug"]
|
|
434
|
+
puts "Output: #{project.data["slug"]}"
|
|
253
435
|
```
|
|
254
436
|
|
|
255
437
|
```text
|
|
256
|
-
task-app
|
|
438
|
+
Output: task-app
|
|
257
439
|
```
|
|
258
440
|
|
|
259
441
|
#### Gate (end-user sign-in)
|
|
@@ -271,11 +453,11 @@ task-app
|
|
|
271
453
|
```ruby
|
|
272
454
|
session = k9o.gate.sign_in(email: "ada@example.com", password: "correct-horse")
|
|
273
455
|
user = session.data["user"]
|
|
274
|
-
puts user["email"]
|
|
456
|
+
puts "Output: #{user["email"]}"
|
|
275
457
|
```
|
|
276
458
|
|
|
277
459
|
```text
|
|
278
|
-
ada@example.com
|
|
460
|
+
Output: ada@example.com
|
|
279
461
|
```
|
|
280
462
|
|
|
281
463
|
#### Storage
|
|
@@ -324,24 +506,24 @@ signed = avatars.create_signed_url("ada.png", expires_in: 300)
|
|
|
324
506
|
| `k9o.functions.delete` | Remove a function. |
|
|
325
507
|
| `k9o.functions.invoke` | Run a function. The `payload` argument is sent as `{payload}`. |
|
|
326
508
|
|
|
327
|
-
`tier:` is `"
|
|
509
|
+
`tier:` is `"sandbox"` (an OCI image archive, `"tar"`, up to 256 MiB). The `gateway` tier is for functions owned by platform operators and is not available to most projects. Sandbox functions run in a [Firelock](https://github.com/firelock-sandbox/firelock) microVM sandbox.
|
|
328
510
|
|
|
329
511
|
```ruby
|
|
330
|
-
artifact = k9o.functions.upload_artifact(File.binread("hello.
|
|
512
|
+
artifact = k9o.functions.upload_artifact(File.binread("hello.tar"), "tar")
|
|
331
513
|
|
|
332
514
|
k9o.functions.deploy(
|
|
333
515
|
name: "hello",
|
|
334
516
|
runtime: "bun",
|
|
335
|
-
tier: "
|
|
517
|
+
tier: "sandbox",
|
|
336
518
|
artifact_key: artifact.data["artifactKey"],
|
|
337
519
|
version: "1.0.0"
|
|
338
520
|
)
|
|
339
521
|
result = k9o.functions.invoke("hello", { "name" => "Ada" })
|
|
340
|
-
puts result.data["greeting"]
|
|
522
|
+
puts "Output: #{result.data["greeting"]}"
|
|
341
523
|
```
|
|
342
524
|
|
|
343
525
|
```text
|
|
344
|
-
Hello, Ada
|
|
526
|
+
Output: Hello, Ada
|
|
345
527
|
```
|
|
346
528
|
|
|
347
529
|
#### Migrations
|
|
@@ -391,6 +573,21 @@ Hello, Ada
|
|
|
391
573
|
| Method | What it does |
|
|
392
574
|
|---|---|
|
|
393
575
|
| `k9o.ai.embed(input:, model: nil)` | Embed one string or a list of strings. |
|
|
576
|
+
| `k9o.ai.generate(prompt, model: nil, max_tokens: nil, temperature: nil, usage_type: nil)` | Generate text from a prompt. |
|
|
577
|
+
|
|
578
|
+
##### Generate text
|
|
579
|
+
|
|
580
|
+
`model` is optional: without it the org's default model is used. Models from outside providers answer 501 until built.
|
|
581
|
+
The result `data` is the same as `compute.llms.invoke`.
|
|
582
|
+
|
|
583
|
+
```ruby
|
|
584
|
+
result = k9o.ai.generate("Say hi in three words", max_tokens: 20)
|
|
585
|
+
puts "Output: #{result.data['content']}"
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
```text
|
|
589
|
+
Output: Hi there, friend!
|
|
590
|
+
```
|
|
394
591
|
|
|
395
592
|
#### Language models
|
|
396
593
|
|
|
@@ -407,11 +604,11 @@ Pass settings in `params:` (`max_tokens`, `temperature`, `usage_type`). The resu
|
|
|
407
604
|
|
|
408
605
|
```ruby
|
|
409
606
|
reply = k9o.compute.llms.invoke("llama-3-8b", prompt: "Say hi", params: { max_tokens: 20 })
|
|
410
|
-
puts "#{reply.data['content']} #{reply.data['usage']['totalTokens']}"
|
|
607
|
+
puts "Output: #{reply.data['content']} #{reply.data['usage']['totalTokens']}"
|
|
411
608
|
```
|
|
412
609
|
|
|
413
610
|
```text
|
|
414
|
-
Hi there! 14
|
|
611
|
+
Output: Hi there! 14
|
|
415
612
|
```
|
|
416
613
|
|
|
417
614
|
#### Realtime
|
|
@@ -424,6 +621,48 @@ Hi there! 14
|
|
|
424
621
|
|
|
425
622
|
`K9o::Realtime::Topic` builds topic names, for example `Topic.collection("tasks")`.
|
|
426
623
|
|
|
624
|
+
#### Streams
|
|
625
|
+
|
|
626
|
+
`k9o.streams` publishes to and subscribes to the project's user streams. It needs `project_slug`.
|
|
627
|
+
|
|
628
|
+
| Method | What it does |
|
|
629
|
+
|---|---|
|
|
630
|
+
| `k9o.streams.create(display_name, max_bytes, retention_seconds: nil)` | Create a stream. The slug comes from the name. |
|
|
631
|
+
| `k9o.streams.list` | List the project's streams. |
|
|
632
|
+
| `k9o.streams.get(slug)` | Read one stream. |
|
|
633
|
+
| `k9o.streams.delete(slug)` | Retire a stream. |
|
|
634
|
+
| `k9o.streams.publish(slug, data, subject: nil)` | Append an event. `subject` is a dot-separated routing suffix. |
|
|
635
|
+
| `k9o.streams.replay(slug, from_sequence: nil, until_sequence: nil, limit: nil)` | Read past events: `events`, `nextCursor`, `hasMore`. |
|
|
636
|
+
| `k9o.streams.subscribe(slug, from_sequence: nil)` | Tail live events over SSE (an `Enumerator`). Reconnects from the last sequence. |
|
|
637
|
+
|
|
638
|
+
#### Approvals
|
|
639
|
+
|
|
640
|
+
`k9o.approvals` reads and decides the queue of changes waiting for approval. It needs `project_slug`.
|
|
641
|
+
|
|
642
|
+
| Method | What it does |
|
|
643
|
+
|---|---|
|
|
644
|
+
| `k9o.approvals.list(status: nil, limit: nil)` | Newest pending changes first. Not paginated; `limit` is 1 to 200. |
|
|
645
|
+
| `k9o.approvals.get(id)` | Read one change. |
|
|
646
|
+
| `k9o.approvals.approve(id, reason: nil)` | Approve. `status` can stay `pending_approval` while more approvers are needed. |
|
|
647
|
+
| `k9o.approvals.reject(id, reason:)` | Reject. `reason` is required. |
|
|
648
|
+
| `k9o.approvals.bulk(action:, criteria:, reason: nil)` | Approve or reject every change matching a predicate. `reason` is required to reject. Failed rows are skipped. |
|
|
649
|
+
|
|
650
|
+
#### Approval policies
|
|
651
|
+
|
|
652
|
+
`k9o.approval_policies` holds the rules that decide which writes are queued. It needs `project_slug`.
|
|
653
|
+
|
|
654
|
+
| Method | What it does |
|
|
655
|
+
|---|---|
|
|
656
|
+
| `k9o.approval_policies.list` | All policies, oldest first. |
|
|
657
|
+
| `k9o.approval_policies.create(collection:, operation:, action:, predicate:)` | Create a policy. `action` is `auto_approve` or `queue`. |
|
|
658
|
+
| `k9o.approval_policies.delete(id)` | Delete a policy. |
|
|
659
|
+
|
|
660
|
+
#### Schema
|
|
661
|
+
|
|
662
|
+
| Method | What it does |
|
|
663
|
+
|---|---|
|
|
664
|
+
| `k9o.schema.collections.list(limit: nil, cursor: nil)` | List the project's collections by name, with `meta.cursor.next` paging. |
|
|
665
|
+
|
|
427
666
|
#### Studio
|
|
428
667
|
|
|
429
668
|
`k9o.studio` manages workflows and dashboards.
|
|
@@ -584,6 +823,20 @@ The server may send codes not listed here, so keep a fallback branch.
|
|
|
584
823
|
| `@k9o/core` | TypeScript SDK |
|
|
585
824
|
| `@k9o/cli` | Command-line tool |
|
|
586
825
|
|
|
826
|
+
### About Kilimanjaro
|
|
827
|
+
|
|
828
|
+
Kilimanjaro is a modular developer platform for building secure enterprise applications.
|
|
829
|
+
You define AI agents, functions and data pipelines on the platform.
|
|
830
|
+
The SDKs let you deploy and invoke them, read and write your data, publish to and subscribe to streams,
|
|
831
|
+
sign users in, and encrypt with keys you hold.
|
|
832
|
+
|
|
833
|
+
It provides four building blocks:
|
|
834
|
+
|
|
835
|
+
- **Keep**: AI security and governance. Guardrails and policy for the agents you run.
|
|
836
|
+
- **Pulse**: data intelligence and real-time stream processing. Streams, analytics and live feeds.
|
|
837
|
+
- **Gate**: customer identity and access management. Sign-in and sessions, and automatic data scoping so a signed-in user reaches only their own data.
|
|
838
|
+
- **Kibo**: cryptographic keys and secrets management. Client-side encryption with keys you hold, and a secrets vault.
|
|
839
|
+
|
|
587
840
|
## License
|
|
588
841
|
|
|
589
842
|
Apache-2.0 © Formelo Inc. See `LICENSE` and `NOTICE`.
|
data/lib/k9o/ai.rb
CHANGED
|
@@ -7,6 +7,8 @@ module K9o
|
|
|
7
7
|
# Wire (project-plane):
|
|
8
8
|
# POST /@<slug>/ai/embeddings body { model?, input: String | [String] }
|
|
9
9
|
# -> { data: { embeddings: [[Float]], model:, dimensions: }, meta }
|
|
10
|
+
# POST /@<slug>/ai/generate body { prompt:, model?, maxTokens?, temperature?, usageType? }
|
|
11
|
+
# -> { data: { modelId:, content:, finishReason:, usage:, usageType: }, meta }
|
|
10
12
|
#
|
|
11
13
|
# Bounds are enforced by the gateway, not here. The SDK sends `input`
|
|
12
14
|
# exactly as given and never puts it in an error.
|
|
@@ -24,5 +26,18 @@ module K9o
|
|
|
24
26
|
HttpTransport.request(config: @config, method: 'POST',
|
|
25
27
|
path: UrlGrammar.project_path(slug, 'ai/embeddings'), body: body)
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
# POST /@<slug>/ai/generate — one synchronous text generation (verb sugar over compute.llms.invoke).
|
|
31
|
+
# Only the fields given are sent; without `model` the gateway uses the org's default LLM.
|
|
32
|
+
def generate(prompt, model: nil, max_tokens: nil, temperature: nil, usage_type: nil)
|
|
33
|
+
slug = Studio.require_project_slug(@config, 'ai')
|
|
34
|
+
body = { 'prompt' => prompt }
|
|
35
|
+
body['model'] = model unless model.nil?
|
|
36
|
+
body['maxTokens'] = max_tokens unless max_tokens.nil?
|
|
37
|
+
body['temperature'] = temperature unless temperature.nil?
|
|
38
|
+
body['usageType'] = usage_type unless usage_type.nil?
|
|
39
|
+
HttpTransport.request(config: @config, method: 'POST',
|
|
40
|
+
path: UrlGrammar.project_path(slug, 'ai/generate'), body: body)
|
|
41
|
+
end
|
|
27
42
|
end
|
|
28
43
|
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Shared argument checks for the approvals, approval policy and schema namespaces. A bad value raises
|
|
5
|
+
# K9o::KError K9O_INVALID_FIELD_VALUE before any request is sent.
|
|
6
|
+
module ApprovalChecks
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def invalid(message, context = {})
|
|
10
|
+
KError.new(code: 'K9O_INVALID_FIELD_VALUE', message: message, context: context)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def check_limit(limit)
|
|
14
|
+
return if limit.nil?
|
|
15
|
+
return if limit.is_a?(Integer) && limit >= 1 && limit <= 200
|
|
16
|
+
|
|
17
|
+
raise invalid('limit must be a whole number from 1 to 200', limit: limit)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def id_segment(value, what)
|
|
21
|
+
raise invalid("#{what} id must be a non-empty string") unless value.is_a?(String) && !value.empty?
|
|
22
|
+
|
|
23
|
+
::URI.encode_www_form_component(value).gsub('+', '%20')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reason!(reason)
|
|
27
|
+
raise invalid('reason is required and must be a non-empty string') unless reason.is_a?(String) && !reason.strip.empty?
|
|
28
|
+
|
|
29
|
+
reason
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def predicate!(value, field)
|
|
33
|
+
raise invalid("#{field} must be a predicate object") unless value.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def enum!(value, allowed, field)
|
|
39
|
+
text = value.is_a?(Symbol) ? value.to_s : value
|
|
40
|
+
return text if allowed.include?(text)
|
|
41
|
+
|
|
42
|
+
raise invalid("#{field} must be #{allowed.map { |a| "\"#{a}\"" }.join(', ')}", field.to_sym => value)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Pending-change queue: k9o.approvals.*
|
|
47
|
+
#
|
|
48
|
+
# list(status:, limit:) GET /@<slug>/pending-approvals
|
|
49
|
+
# get(id) GET /@<slug>/pending-approvals/<id>
|
|
50
|
+
# approve(id, reason:) POST /@<slug>/pending-approvals/<id>/approve
|
|
51
|
+
# reject(id, reason:) POST /@<slug>/pending-approvals/<id>/reject
|
|
52
|
+
# bulk(action:, criteria:, reason:) POST /@<slug>/pending-approvals/bulk
|
|
53
|
+
#
|
|
54
|
+
# +list+ is not paginated: it returns the newest +limit+ changes (1 to 200, default 50). +approve+ can answer 200
|
|
55
|
+
# with +status+ still +pending_approval+: the approval is recorded and more approvers are needed. +bulk+ skips rows
|
|
56
|
+
# that fail without a report, so compare +affectedIds+ with what you intended.
|
|
57
|
+
class Approvals
|
|
58
|
+
def initialize(config)
|
|
59
|
+
@config = config
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Pending changes, newest first. Only +limit+ caps the result.
|
|
63
|
+
def list(status: nil, limit: nil)
|
|
64
|
+
ApprovalChecks.check_limit(limit)
|
|
65
|
+
query = {}
|
|
66
|
+
query['status'] = status.to_s unless status.nil?
|
|
67
|
+
query['limit'] = limit unless limit.nil?
|
|
68
|
+
call('GET', path, query: query)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# One pending change. Members who are not queue admins can read only their own.
|
|
72
|
+
def get(id)
|
|
73
|
+
call('GET', path("/#{ApprovalChecks.id_segment(id, 'approval')}"))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Approve a pending change. Read +status+ on the answer: +pending_approval+ means more approvers are needed.
|
|
77
|
+
def approve(id, reason: nil)
|
|
78
|
+
seg = ApprovalChecks.id_segment(id, 'approval')
|
|
79
|
+
call('POST', path("/#{seg}/approve"), body: reason.nil? ? {} : { 'reason' => reason })
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Reject a pending change. +reason+ is required and must not be empty.
|
|
83
|
+
def reject(id, reason:)
|
|
84
|
+
seg = ApprovalChecks.id_segment(id, 'approval')
|
|
85
|
+
call('POST', path("/#{seg}/reject"), body: { 'reason' => ApprovalChecks.reason!(reason) })
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Approve or reject every pending change matching +criteria+ (a predicate Hash). +reason+ is required to reject.
|
|
89
|
+
# Rows that fail are skipped without a report: compare +affectedIds+ with what you intended.
|
|
90
|
+
def bulk(action:, criteria:, reason: nil)
|
|
91
|
+
act = ApprovalChecks.enum!(action, %w[approve reject], 'action')
|
|
92
|
+
body = { 'action' => act, 'criteria' => ApprovalChecks.predicate!(criteria, 'criteria') }
|
|
93
|
+
if act == 'reject'
|
|
94
|
+
body['reason'] = ApprovalChecks.reason!(reason)
|
|
95
|
+
elsif !reason.nil?
|
|
96
|
+
body['reason'] = reason
|
|
97
|
+
end
|
|
98
|
+
call('POST', path('/bulk'), body: body)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def call(method, route, body: nil, query: nil)
|
|
104
|
+
HttpTransport.request(config: @config, method: method, path: route, body: body, query: query)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def path(tail = '')
|
|
108
|
+
UrlGrammar.project_path(Studio.require_project_slug(@config, 'approvals'), "pending-approvals#{tail}")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Approval policies: k9o.approval_policies.*
|
|
113
|
+
#
|
|
114
|
+
# list GET /@<slug>/approval-policies
|
|
115
|
+
# create(collection:, operation:, action:, predicate:) POST /@<slug>/approval-policies
|
|
116
|
+
# delete(id) DELETE /@<slug>/approval-policies/<id>
|
|
117
|
+
class ApprovalPolicies
|
|
118
|
+
COLLECTION_RE = /\A[a-zA-Z_][a-zA-Z0-9_]{0,62}\z/.freeze
|
|
119
|
+
|
|
120
|
+
def initialize(config)
|
|
121
|
+
@config = config
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# All policies, oldest first. The first matching policy for a write decides.
|
|
125
|
+
def list
|
|
126
|
+
call('GET', path)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Create a policy. Identical policies are not de-duplicated.
|
|
130
|
+
def create(collection:, operation:, action:, predicate:)
|
|
131
|
+
unless collection.is_a?(String) && COLLECTION_RE.match?(collection)
|
|
132
|
+
raise ApprovalChecks.invalid('collection must match ^[a-zA-Z_][a-zA-Z0-9_]{0,62}$', collection: collection)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
body = {
|
|
136
|
+
'collection' => collection,
|
|
137
|
+
'operation' => ApprovalChecks.enum!(operation, %w[insert update delete], 'operation'),
|
|
138
|
+
'action' => ApprovalChecks.enum!(action, %w[auto_approve queue], 'action'),
|
|
139
|
+
'predicate' => ApprovalChecks.predicate!(predicate, 'predicate')
|
|
140
|
+
}
|
|
141
|
+
call('POST', path, body: body)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Delete a policy; the answer is { 'id' => ... }.
|
|
145
|
+
def delete(id)
|
|
146
|
+
call('DELETE', path("/#{ApprovalChecks.id_segment(id, 'policy')}"))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def call(method, route, body: nil)
|
|
152
|
+
HttpTransport.request(config: @config, method: method, path: route, body: body)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def path(tail = '')
|
|
156
|
+
UrlGrammar.project_path(Studio.require_project_slug(@config, 'approval_policies'), "approval-policies#{tail}")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/k9o/client.rb
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
module K9o
|
|
4
4
|
# The customer-facing client returned by K9o.create_client.
|
|
5
5
|
class K9oClient
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
include QueryMethods
|
|
7
|
+
|
|
8
|
+
attr_reader :config, :projects, :gate, :studio, :compute, :realtime, :streams, :org,
|
|
9
|
+
:approvals, :approval_policies, :schema, :orgs, :migrations, :functions, :policies, :audit, :me, :kibo, :ai, :storage
|
|
8
10
|
|
|
9
11
|
def initialize(config)
|
|
10
12
|
@config = config
|
|
@@ -13,6 +15,10 @@ module K9o
|
|
|
13
15
|
@studio = Studio.new(config)
|
|
14
16
|
@compute = Compute.new(config)
|
|
15
17
|
@realtime = Realtime.new(config)
|
|
18
|
+
@streams = Streams.new(config)
|
|
19
|
+
@approvals = Approvals.new(config)
|
|
20
|
+
@approval_policies = ApprovalPolicies.new(config)
|
|
21
|
+
@schema = Schema.new(config)
|
|
16
22
|
@org = Org.new(config)
|
|
17
23
|
@orgs = Orgs.new(config)
|
|
18
24
|
@migrations = Migrations.new(config)
|
data/lib/k9o/data.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
3
5
|
module K9o
|
|
4
6
|
# Data-plane DSL.
|
|
5
7
|
#
|
|
@@ -113,13 +115,77 @@ module K9o
|
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
class SelectBuilder < BaseBuilder
|
|
118
|
+
SEARCH_COLUMN = /\A[a-zA-Z_][a-zA-Z0-9_]{0,62}\z/.freeze
|
|
119
|
+
SEARCH_LANGUAGE = /\A[a-zA-Z_]+\z/.freeze
|
|
120
|
+
TEXT_TYPES = %w[plain phrase websearch].freeze
|
|
121
|
+
VECTOR_METRICS = %w[cosine euclidean dot].freeze
|
|
122
|
+
|
|
123
|
+
def initialize(config, collection)
|
|
124
|
+
super
|
|
125
|
+
@search = {}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Full-text search over one text column. +type+ is "plain", "phrase" or "websearch".
|
|
129
|
+
def text_search(column, query, language: 'english', type: 'plain')
|
|
130
|
+
check_search_column(column)
|
|
131
|
+
raise search_error('text search query must be a non-empty string') unless query.is_a?(String) && !query.empty?
|
|
132
|
+
unless language.is_a?(String) && language.match?(SEARCH_LANGUAGE)
|
|
133
|
+
raise search_error('text search language must be letters and underscores only')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
type = type.to_s
|
|
137
|
+
raise search_error("text search type must be one of #{TEXT_TYPES.join(', ')}") unless TEXT_TYPES.include?(type)
|
|
138
|
+
|
|
139
|
+
@search.delete('textSearchLanguage')
|
|
140
|
+
@search.delete('textSearchType')
|
|
141
|
+
@search['textSearch'] = "#{column}:#{query}"
|
|
142
|
+
@search['textSearchLanguage'] = language unless language == 'english'
|
|
143
|
+
@search['textSearchType'] = type unless type == 'plain'
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Nearest-neighbour search over one vector column; results come back closest first.
|
|
148
|
+
# +metric+ is "cosine", "euclidean" or "dot"; +limit+ is 1 to 1000 (server default 10).
|
|
149
|
+
def vector_search(column, embedding, metric: 'cosine', limit: nil)
|
|
150
|
+
check_search_column(column)
|
|
151
|
+
unless embedding.is_a?(Array) && !embedding.empty? &&
|
|
152
|
+
embedding.all? { |v| v.is_a?(Integer) || (v.is_a?(Float) && v.finite?) }
|
|
153
|
+
raise search_error('embedding must be a non-empty array of finite numbers')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
metric = metric.to_s
|
|
157
|
+
raise search_error("vector metric must be one of #{VECTOR_METRICS.join(', ')}") unless VECTOR_METRICS.include?(metric)
|
|
158
|
+
unless limit.nil? || (limit.is_a?(Integer) && limit >= 1 && limit <= 1000)
|
|
159
|
+
raise search_error('vector limit must be an integer from 1 to 1000')
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
@search.delete('vectorMetric')
|
|
163
|
+
@search.delete('vectorLimit')
|
|
164
|
+
@search['vectorSearch'] = "#{column}:#{JSON.generate(embedding)}"
|
|
165
|
+
@search['vectorMetric'] = metric unless metric == 'cosine'
|
|
166
|
+
@search['vectorLimit'] = limit.to_s unless limit.nil?
|
|
167
|
+
self
|
|
168
|
+
end
|
|
169
|
+
|
|
116
170
|
def execute
|
|
117
171
|
HttpTransport.request(
|
|
118
172
|
config: @config, method: 'GET',
|
|
119
173
|
path: FromBuilder.collection_path(@config, @collection),
|
|
120
|
-
query: to_query
|
|
174
|
+
query: to_query.merge(@search)
|
|
121
175
|
)
|
|
122
176
|
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def search_error(message)
|
|
181
|
+
KError.new(code: 'K9O_INVALID_FIELD_VALUE', message: message, context: { collection: @collection })
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def check_search_column(column)
|
|
185
|
+
return if column.is_a?(String) && column.match?(SEARCH_COLUMN)
|
|
186
|
+
|
|
187
|
+
raise search_error('search column must be a column name: letters, digits and underscores, starting with a letter or underscore')
|
|
188
|
+
end
|
|
123
189
|
end
|
|
124
190
|
|
|
125
191
|
class UpdateBuilder < BaseBuilder
|
data/lib/k9o/query.rb
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# `client.query(sql, params)` and `client.search(collection, column:, q:)`.
|
|
5
|
+
#
|
|
6
|
+
# Wire (project-plane):
|
|
7
|
+
# POST /@<slug>/query body { sql:, params? } -> { data: [row, ...], meta }
|
|
8
|
+
# GET /@<slug>/data/<collection>?textSearch=<column>:<q>... (the select builder's +text_search+)
|
|
9
|
+
#
|
|
10
|
+
# +params+ is an Array (positional, bound to $1, $2, ...; the SQL is sent as written) or a Hash of named values
|
|
11
|
+
# (+:name+ placeholders are rewritten to $n here and the values become the positional list). Values are bound by
|
|
12
|
+
# the database, never interpolated into the SQL text.
|
|
13
|
+
module Query
|
|
14
|
+
IDENT_START = /[A-Za-z_]/.freeze
|
|
15
|
+
IDENT_CHAR = /[A-Za-z0-9_]/.freeze
|
|
16
|
+
DOLLAR_TAG = /\A\$([A-Za-z_][A-Za-z0-9_]*)?\$/.freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def invalid(message, context = {})
|
|
21
|
+
KError.new(code: 'K9O_INVALID_FIELD_VALUE', message: message, context: context)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_value(value, where)
|
|
25
|
+
return value if value.nil? || value == true || value == false || value.is_a?(String)
|
|
26
|
+
return value if value.is_a?(Integer)
|
|
27
|
+
return value if value.is_a?(Float) && value.finite?
|
|
28
|
+
|
|
29
|
+
raise invalid("#{where} must be a string, number, boolean or nil; serialise anything else (for example as JSON text) first")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def lookup(named, name)
|
|
33
|
+
return [true, named[name]] if named.key?(name)
|
|
34
|
+
return [true, named[name.to_sym]] if named.key?(name.to_sym)
|
|
35
|
+
|
|
36
|
+
[false, nil]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Rewrite +:name+ placeholders to $n. Skips quoted text, comments, dollar-quoted strings and +::+ casts.
|
|
40
|
+
# Returns [sql, params].
|
|
41
|
+
def translate_named_params(sql, named)
|
|
42
|
+
order = []
|
|
43
|
+
out = +''
|
|
44
|
+
i = 0
|
|
45
|
+
n = sql.length
|
|
46
|
+
while i < n
|
|
47
|
+
c = sql[i]
|
|
48
|
+
nxt = i + 1 < n ? sql[i + 1] : ''
|
|
49
|
+
if c == "'" || ((c == 'E' || c == 'e') && nxt == "'" && !(i > 0 && sql[i - 1] =~ IDENT_CHAR))
|
|
50
|
+
escapes = c != "'"
|
|
51
|
+
j = escapes ? i + 2 : i + 1
|
|
52
|
+
while j < n
|
|
53
|
+
if escapes && sql[j] == '\\'
|
|
54
|
+
j += 2
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
if sql[j] == "'"
|
|
58
|
+
if sql[j + 1] == "'"
|
|
59
|
+
j += 2
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
break
|
|
63
|
+
end
|
|
64
|
+
j += 1
|
|
65
|
+
end
|
|
66
|
+
out << sql[i...[j + 1, n].min]
|
|
67
|
+
i = j + 1
|
|
68
|
+
elsif c == '"'
|
|
69
|
+
j = i + 1
|
|
70
|
+
while j < n
|
|
71
|
+
if sql[j] == '"'
|
|
72
|
+
if sql[j + 1] == '"'
|
|
73
|
+
j += 2
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
break
|
|
77
|
+
end
|
|
78
|
+
j += 1
|
|
79
|
+
end
|
|
80
|
+
out << sql[i...[j + 1, n].min]
|
|
81
|
+
i = j + 1
|
|
82
|
+
elsif c == '-' && nxt == '-'
|
|
83
|
+
j = sql.index("\n", i) || n
|
|
84
|
+
out << sql[i...j]
|
|
85
|
+
i = j
|
|
86
|
+
elsif c == '/' && nxt == '*'
|
|
87
|
+
depth = 1
|
|
88
|
+
j = i + 2
|
|
89
|
+
while j < n && depth > 0
|
|
90
|
+
if sql[j, 2] == '/*'
|
|
91
|
+
depth += 1
|
|
92
|
+
j += 2
|
|
93
|
+
elsif sql[j, 2] == '*/'
|
|
94
|
+
depth -= 1
|
|
95
|
+
j += 2
|
|
96
|
+
else
|
|
97
|
+
j += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
out << sql[i...j]
|
|
101
|
+
i = j
|
|
102
|
+
elsif c == '$'
|
|
103
|
+
m = DOLLAR_TAG.match(sql[i..-1])
|
|
104
|
+
if m
|
|
105
|
+
close = sql.index(m[0], i + m[0].length)
|
|
106
|
+
stop = close.nil? ? n : close + m[0].length
|
|
107
|
+
out << sql[i...stop]
|
|
108
|
+
i = stop
|
|
109
|
+
else
|
|
110
|
+
out << c
|
|
111
|
+
i += 1
|
|
112
|
+
end
|
|
113
|
+
elsif c == ':' && nxt == ':'
|
|
114
|
+
out << '::'
|
|
115
|
+
i += 2
|
|
116
|
+
elsif c == ':' && nxt != '' && nxt =~ IDENT_START && !(i > 0 && sql[i - 1] =~ IDENT_CHAR)
|
|
117
|
+
j = i + 2
|
|
118
|
+
j += 1 while j < n && sql[j] =~ IDENT_CHAR
|
|
119
|
+
name = sql[(i + 1)...j]
|
|
120
|
+
unless order.include?(name)
|
|
121
|
+
found, = lookup(named, name)
|
|
122
|
+
raise invalid("no value for :#{name}", name: name) unless found
|
|
123
|
+
|
|
124
|
+
order << name
|
|
125
|
+
end
|
|
126
|
+
out << "$#{order.index(name) + 1}"
|
|
127
|
+
i = j
|
|
128
|
+
else
|
|
129
|
+
out << c
|
|
130
|
+
i += 1
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
values = order.map { |name| check_value(lookup(named, name)[1], "the value for :#{name}") }
|
|
134
|
+
[out, values]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Builds [path, body] for POST /@<slug>/query.
|
|
138
|
+
def prepare(config, sql, params)
|
|
139
|
+
slug = Studio.require_project_slug(config, 'query')
|
|
140
|
+
text = sql
|
|
141
|
+
values = []
|
|
142
|
+
if params.is_a?(Hash)
|
|
143
|
+
text, values = translate_named_params(sql, params)
|
|
144
|
+
elsif !params.nil?
|
|
145
|
+
values = params.each_with_index.map { |v, i| check_value(v, "params[#{i}]") }
|
|
146
|
+
end
|
|
147
|
+
body = { 'sql' => text }
|
|
148
|
+
body['params'] = values unless values.empty?
|
|
149
|
+
[UrlGrammar.project_path(slug, 'query'), body]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Client-root methods mixed into K9oClient.
|
|
154
|
+
module QueryMethods
|
|
155
|
+
# POST /@<slug>/query — run one read-only SQL statement; +data+ is the array of rows.
|
|
156
|
+
# An Array binds positionally to $1, $2, ...; a Hash binds +:name+ placeholders.
|
|
157
|
+
def query(sql, params = nil)
|
|
158
|
+
path, body = Query.prepare(@config, sql, params)
|
|
159
|
+
HttpTransport.request(config: @config, method: 'POST', path: path, body: body)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Full-text search over one text column of a collection, best matches first. Sugar over
|
|
163
|
+
# +from(collection).select.text_search(column, q, ...)+; +limit+ and +cursor+ page the result.
|
|
164
|
+
def search(collection, column:, q:, language: nil, type: nil, limit: nil, cursor: nil)
|
|
165
|
+
opts = {}
|
|
166
|
+
opts[:language] = language unless language.nil?
|
|
167
|
+
opts[:type] = type unless type.nil?
|
|
168
|
+
builder = from(collection).select.text_search(column, q, **opts)
|
|
169
|
+
builder.limit(limit) unless limit.nil?
|
|
170
|
+
builder.cursor(cursor) unless cursor.nil?
|
|
171
|
+
builder.execute
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
data/lib/k9o/schema.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Schema: k9o.schema.*
|
|
5
|
+
#
|
|
6
|
+
# collections.list(limit:, cursor:) GET /@<slug>/schema/collections
|
|
7
|
+
#
|
|
8
|
+
# The list is cursor-paginated and ordered by collection name: pass +meta.cursor.next+ of one page as +cursor+ of
|
|
9
|
+
# the next, until it is nil.
|
|
10
|
+
class Schema
|
|
11
|
+
attr_reader :collections
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@collections = SchemaCollections.new(config)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class SchemaCollections
|
|
19
|
+
def initialize(config)
|
|
20
|
+
@config = config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The project's collections by name, one page at a time. +limit+ is 1 to 200 (default 50).
|
|
24
|
+
def list(limit: nil, cursor: nil)
|
|
25
|
+
route = UrlGrammar.project_path(Studio.require_project_slug(@config, 'schema'), 'schema/collections')
|
|
26
|
+
ApprovalChecks.check_limit(limit)
|
|
27
|
+
raise ApprovalChecks.invalid('cursor must be a string', cursor: cursor) unless cursor.nil? || cursor.is_a?(String)
|
|
28
|
+
|
|
29
|
+
query = {}
|
|
30
|
+
query['limit'] = limit unless limit.nil?
|
|
31
|
+
query['cursor'] = cursor unless cursor.nil?
|
|
32
|
+
HttpTransport.request(config: @config, method: 'GET', path: route, query: query)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/k9o/streams.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# User data streams namespace: k9o.streams.*.
|
|
5
|
+
#
|
|
6
|
+
# Routes under /@<slug>/data-streams/user. Publish needs project admin; subscribe and replay need admin or
|
|
7
|
+
# auditor. While the stream data plane is unavailable the gateway answers K9O_STREAM_UNAVAILABLE (503); a bad
|
|
8
|
+
# subject answers K9O_STREAM_INVALID. Both raise K9o::KError.
|
|
9
|
+
class Streams
|
|
10
|
+
@reconnect_delay = 0.25
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Seconds to wait before reconnecting after the server closes a subscribe connection.
|
|
14
|
+
attr_accessor :reconnect_delay
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(config)
|
|
18
|
+
@config = config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# POST /@<slug>/data-streams/user — +max_bytes+ is the storage ceiling; the slug is derived from +display_name+.
|
|
22
|
+
def create(display_name, max_bytes, retention_seconds: nil)
|
|
23
|
+
body = { 'displayName' => display_name, 'maxBytes' => max_bytes }
|
|
24
|
+
body['retentionSeconds'] = retention_seconds unless retention_seconds.nil?
|
|
25
|
+
call('POST', path, body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# GET /@<slug>/data-streams/user — the project's active user streams.
|
|
29
|
+
def list
|
|
30
|
+
call('GET', path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# GET /@<slug>/data-streams/user/<slug>
|
|
34
|
+
def get(slug)
|
|
35
|
+
call('GET', path(slug))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# DELETE /@<slug>/data-streams/user/<slug> — retire the stream.
|
|
39
|
+
def delete(slug)
|
|
40
|
+
call('DELETE', path(slug))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# POST .../<slug>/publish — +data+ is any JSON value; +subject+ is an optional dot-separated routing suffix.
|
|
44
|
+
def publish(slug, data, subject: nil)
|
|
45
|
+
body = { 'data' => data }
|
|
46
|
+
body['subject'] = subject unless subject.nil?
|
|
47
|
+
call('POST', path(slug, 'publish'), body)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# POST .../<slug>/replay — a bounded range of past events: data['events'], data['nextCursor'], data['hasMore'].
|
|
51
|
+
def replay(slug, from_sequence: nil, until_sequence: nil, limit: nil)
|
|
52
|
+
body = {}
|
|
53
|
+
body['fromSequence'] = from_sequence unless from_sequence.nil?
|
|
54
|
+
body['untilSequence'] = until_sequence unless until_sequence.nil?
|
|
55
|
+
body['limit'] = limit unless limit.nil?
|
|
56
|
+
call('POST', path(slug, 'replay'), body)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# GET .../<slug>/subscribe — an Enumerator tailing live events { 'sequence', 'subject', 'data', 'publishedAt' }.
|
|
60
|
+
# The gateway bounds each connection and closes it; the enumerator reconnects from the last sequence it yielded.
|
|
61
|
+
# Stop by leaving the +each+ block (break) or by taking a finite number (+first(n)+).
|
|
62
|
+
def subscribe(slug, from_sequence: nil)
|
|
63
|
+
route = path(slug, 'subscribe')
|
|
64
|
+
Enumerator.new do |yielder|
|
|
65
|
+
cursor = from_sequence
|
|
66
|
+
loop do
|
|
67
|
+
query = cursor.nil? ? nil : { 'fromSequence' => cursor.to_s }
|
|
68
|
+
HttpTransport.stream_sse(config: @config, method: 'GET', path: route, query: query).each do |event|
|
|
69
|
+
next unless event.is_a?(Hash) && event.key?('sequence')
|
|
70
|
+
|
|
71
|
+
cursor = event['sequence']
|
|
72
|
+
yielder << event
|
|
73
|
+
end
|
|
74
|
+
delay = self.class.reconnect_delay
|
|
75
|
+
sleep(delay) if delay.positive?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def call(method, route, body = nil)
|
|
83
|
+
HttpTransport.request(config: @config, method: method, path: route, body: body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def path(stream_slug = nil, suffix = nil)
|
|
87
|
+
project = Studio.require_project_slug(@config, 'streams')
|
|
88
|
+
sub = +'data-streams/user'
|
|
89
|
+
sub << "/#{::URI.encode_www_form_component(stream_slug)}" if stream_slug
|
|
90
|
+
sub << "/#{suffix}" if suffix
|
|
91
|
+
UrlGrammar.project_path(project, sub)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/k9o/version.rb
CHANGED
data/lib/k9o.rb
CHANGED
|
@@ -29,6 +29,7 @@ require_relative 'k9o/url_grammar'
|
|
|
29
29
|
require_relative 'k9o/region'
|
|
30
30
|
require_relative 'k9o/http_transport'
|
|
31
31
|
require_relative 'k9o/data'
|
|
32
|
+
require_relative 'k9o/query'
|
|
32
33
|
require_relative 'k9o/projects'
|
|
33
34
|
require_relative 'k9o/gate'
|
|
34
35
|
require_relative 'k9o/studio'
|
|
@@ -36,6 +37,9 @@ require_relative 'k9o/compute'
|
|
|
36
37
|
require_relative 'k9o/ai'
|
|
37
38
|
require_relative 'k9o/storage'
|
|
38
39
|
require_relative 'k9o/realtime'
|
|
40
|
+
require_relative 'k9o/streams'
|
|
41
|
+
require_relative 'k9o/approvals'
|
|
42
|
+
require_relative 'k9o/schema'
|
|
39
43
|
require_relative 'k9o/migrations'
|
|
40
44
|
require_relative 'k9o/functions'
|
|
41
45
|
require_relative 'k9o/policies'
|
data/sig/k9o/ai.rbs
CHANGED
|
@@ -8,6 +8,10 @@ module K9o
|
|
|
8
8
|
# { "embeddings" => Array[Array[Float]], "model" => String, "dimensions" => Integer }.
|
|
9
9
|
def initialize: (untyped config) -> void
|
|
10
10
|
def embed: (input: (String | Array[String]), ?model: String?) -> Envelope
|
|
11
|
+
|
|
12
|
+
# Only the fields given are sent. Returns an Envelope whose `data` is
|
|
13
|
+
# { "modelId" => String, "content" => String, "finishReason" => String, "usage" => Hash, "usageType" => String }.
|
|
14
|
+
def generate: (String prompt, ?model: String?, ?max_tokens: Integer?, ?temperature: Numeric?, ?usage_type: String?) -> Envelope
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
class K9oClient
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# RBS signatures for `client.approvals`, `client.approval_policies` and `client.schema`.
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
module ApprovalChecks
|
|
5
|
+
def self.invalid: (String message, ?Hash[Symbol, untyped] context) -> KError
|
|
6
|
+
def self.check_limit: (untyped limit) -> void
|
|
7
|
+
def self.id_segment: (untyped value, String what) -> String
|
|
8
|
+
def self.reason!: (untyped reason) -> String
|
|
9
|
+
def self.predicate!: (untyped value, String field) -> Hash[untyped, untyped]
|
|
10
|
+
def self.enum!: (untyped value, Array[String] allowed, String field) -> String
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Approvals
|
|
14
|
+
def initialize: (untyped config) -> void
|
|
15
|
+
# Not paginated: only `limit` (1 to 200, default 50) caps the result.
|
|
16
|
+
def list: (?status: (String | Symbol)?, ?limit: Integer?) -> Envelope
|
|
17
|
+
def get: (String id) -> Envelope
|
|
18
|
+
# `status` on the answer stays "pending_approval" while more approvers are needed.
|
|
19
|
+
def approve: (String id, ?reason: String?) -> Envelope
|
|
20
|
+
def reject: (String id, reason: String) -> Envelope
|
|
21
|
+
# Failed rows are skipped without a report: compare `affectedIds` with what you intended.
|
|
22
|
+
def bulk: (action: (String | Symbol), criteria: Hash[(String | Symbol), untyped], ?reason: String?) -> Envelope
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ApprovalPolicies
|
|
26
|
+
COLLECTION_RE: Regexp
|
|
27
|
+
|
|
28
|
+
def initialize: (untyped config) -> void
|
|
29
|
+
def list: () -> Envelope
|
|
30
|
+
def create: (collection: String, operation: (String | Symbol), action: (String | Symbol), predicate: Hash[(String | Symbol), untyped]) -> Envelope
|
|
31
|
+
def delete: (String id) -> Envelope
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class SchemaCollections
|
|
35
|
+
def initialize: (untyped config) -> void
|
|
36
|
+
def list: (?limit: Integer?, ?cursor: String?) -> Envelope
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Schema
|
|
40
|
+
attr_reader collections: SchemaCollections
|
|
41
|
+
def initialize: (untyped config) -> void
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class K9oClient
|
|
45
|
+
attr_reader approvals: Approvals
|
|
46
|
+
attr_reader approval_policies: ApprovalPolicies
|
|
47
|
+
attr_reader schema: Schema
|
|
48
|
+
end
|
|
49
|
+
end
|
data/sig/k9o/query.rbs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# RBS signatures for `client.query`, `client.search` and the select builder's search methods.
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
module Query
|
|
5
|
+
type value = String | Integer | Float | bool | nil
|
|
6
|
+
|
|
7
|
+
# Rewrites :name placeholders to $n; returns [sql, positional values].
|
|
8
|
+
def self.translate_named_params: (String sql, Hash[(String | Symbol), untyped] named) -> [String, Array[value]]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module QueryMethods
|
|
12
|
+
# An Array binds positionally to $1, $2, ...; a Hash binds :name placeholders. `data` is the array of rows.
|
|
13
|
+
def query: (String sql, ?(Array[untyped] | Hash[(String | Symbol), untyped])? params) -> Envelope
|
|
14
|
+
|
|
15
|
+
def search: (String collection, column: String, q: String, ?language: String?, ?type: String?, ?limit: Integer?, ?cursor: String?) -> Envelope
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class K9oClient
|
|
19
|
+
include QueryMethods
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class SelectBuilder
|
|
23
|
+
def text_search: (String column, String query, ?language: String, ?type: (String | Symbol)) -> self
|
|
24
|
+
def vector_search: (String column, Array[Numeric] embedding, ?metric: (String | Symbol), ?limit: Integer?) -> self
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: k9o
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kilimanjaro
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- k9o.gemspec
|
|
40
40
|
- lib/k9o.rb
|
|
41
41
|
- lib/k9o/ai.rb
|
|
42
|
+
- lib/k9o/approvals.rb
|
|
42
43
|
- lib/k9o/audit.rb
|
|
43
44
|
- lib/k9o/barbican.rb
|
|
44
45
|
- lib/k9o/client.rb
|
|
@@ -57,16 +58,21 @@ files:
|
|
|
57
58
|
- lib/k9o/orgs.rb
|
|
58
59
|
- lib/k9o/policies.rb
|
|
59
60
|
- lib/k9o/projects.rb
|
|
61
|
+
- lib/k9o/query.rb
|
|
60
62
|
- lib/k9o/realtime.rb
|
|
61
63
|
- lib/k9o/region.rb
|
|
64
|
+
- lib/k9o/schema.rb
|
|
62
65
|
- lib/k9o/storage.rb
|
|
66
|
+
- lib/k9o/streams.rb
|
|
63
67
|
- lib/k9o/studio.rb
|
|
64
68
|
- lib/k9o/url_grammar.rb
|
|
65
69
|
- lib/k9o/vault.rb
|
|
66
70
|
- lib/k9o/version.rb
|
|
67
71
|
- sig/k9o/ai.rbs
|
|
72
|
+
- sig/k9o/approvals.rbs
|
|
68
73
|
- sig/k9o/gate.rbs
|
|
69
74
|
- sig/k9o/migrations.rbs
|
|
75
|
+
- sig/k9o/query.rbs
|
|
70
76
|
- sig/k9o/region.rbs
|
|
71
77
|
- sig/k9o/storage.rbs
|
|
72
78
|
homepage: https://kilimanjaro.dev
|