k9o 1.0.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.

Potentially problematic release.


This version of k9o might be problematic. Click here for more details.

data/lib/k9o/client.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # The customer-facing client returned by K9o.create_client.
5
+ class K9oClient
6
+ attr_reader :config, :projects, :gate, :studio, :compute, :realtime, :org,
7
+ :orgs, :migrations, :functions, :policies, :audit, :me, :kibo, :ai, :storage
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @projects = Projects.new(config)
12
+ @gate = Gate.new(config)
13
+ @studio = Studio.new(config)
14
+ @compute = Compute.new(config)
15
+ @realtime = Realtime.new(config)
16
+ @org = Org.new(config)
17
+ @orgs = Orgs.new(config)
18
+ @migrations = Migrations.new(config)
19
+ @functions = Functions.new(config)
20
+ @policies = Policies.new(config)
21
+ @audit = Audit.new(config)
22
+ @me = Me.new(config)
23
+ @kibo = KiboNamespace.new(config)
24
+ @ai = Ai.new(config)
25
+ @storage = Storage.new(config)
26
+ end
27
+
28
+ def from(collection)
29
+ FromBuilder.new(@config, collection)
30
+ end
31
+
32
+ def base_url
33
+ @config.base_url
34
+ end
35
+ end
36
+
37
+ # Factory — construct a K9oClient bound to the supplied options.
38
+ #
39
+ # Per ch13 §2.1.8 spec sample:
40
+ # k9o = K9o.create_client(
41
+ # api_key: ENV["K9O_API_KEY"],
42
+ # org_slug: "acme",
43
+ # project_slug: "member-portal",
44
+ # )
45
+ #
46
+ # base_url is OPTIONAL. When omitted it
47
+ # defaults to the platform apex https://api.k9o.io; when the host is the
48
+ # bare apex and org_slug is set, the canonical org-subdomain wire root
49
+ # https://<org_slug>.api.k9o.io is composed. Any other host passes through.
50
+ def self.create_client(base_url: nil, api_key: nil, org_slug: nil, project_slug: nil, default_headers: {}, http_client: nil)
51
+ normalised = UrlGrammar.resolve_base_url(base_url, org_slug)
52
+ config = HttpClientConfig.new(
53
+ base_url: normalised,
54
+ api_key: api_key, org_slug: org_slug, project_slug: project_slug,
55
+ default_headers: default_headers, http_client: http_client
56
+ )
57
+ K9oClient.new(config)
58
+ end
59
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Compute namespace (Compute LLMs registry).
5
+ # k9o.compute.llms.*
6
+ class Compute
7
+ # Curated 9-model closed enum per ch07 §10.1 (extensible v2.x).
8
+ LLM_MODEL_IDS = %w[
9
+ mistral-7b mistral-8x7b llama-3-8b llama-3-70b
10
+ claude-3-5-sonnet gpt-4o gpt-4o-mini
11
+ text-embedding-3-small text-embedding-3-large
12
+ ].freeze
13
+
14
+ attr_reader :llms
15
+
16
+ def initialize(config)
17
+ @llms = Llms.new(config)
18
+ end
19
+
20
+ # k9o.compute.llms.* per ch07 §10.
21
+ class Llms
22
+ def initialize(config)
23
+ @config = config
24
+ end
25
+
26
+ def list(modality: nil)
27
+ slug = Studio.require_project_slug(@config, 'compute.llms')
28
+ q = modality ? { 'modality' => modality } : nil
29
+ HttpTransport.request(config: @config, method: 'GET',
30
+ path: UrlGrammar.project_path(slug, 'compute/llms'), query: q)
31
+ end
32
+
33
+ def read(model_id)
34
+ slug = Studio.require_project_slug(@config, 'compute.llms')
35
+ HttpTransport.request(config: @config, method: 'GET',
36
+ path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}"))
37
+ end
38
+
39
+ # Synchronous inference per ch07 §10.2 (PROJECT_AUTHOR/ADMIN).
40
+ def invoke(model_id, prompt:, params: nil)
41
+ slug = Studio.require_project_slug(@config, 'compute.llms')
42
+ body = { 'prompt' => prompt }
43
+ body['params'] = params if params
44
+ HttpTransport.request(config: @config, method: 'POST',
45
+ path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}/invoke"), body: body)
46
+ end
47
+
48
+ # Streaming inference (SSE per-token) per ch07 §10.2. Returns an
49
+ # Enumerator of chunk hashes (ch13 §8 Ruby idiom). Consume with
50
+ # `.each { |chunk| ... }`.
51
+ def invoke_stream(model_id, prompt:, params: nil)
52
+ slug = Studio.require_project_slug(@config, 'compute.llms')
53
+ body = { 'prompt' => prompt }
54
+ body['params'] = params if params
55
+ HttpTransport.stream_sse(config: @config, method: 'POST',
56
+ path: UrlGrammar.project_path(slug, "compute/llms/#{enc(model_id)}/invoke-stream"),
57
+ body: body)
58
+ end
59
+
60
+ private
61
+
62
+ def enc(s)
63
+ ::URI.encode_www_form_component(s)
64
+ end
65
+ end
66
+ end
67
+ end
data/lib/k9o/data.rb ADDED
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Data-plane DSL.
5
+ #
6
+ # Usage:
7
+ #
8
+ # response = k9o.from('posts').select.eq('status', 'published').limit(10).execute
9
+ # response.data.each { |row| puts row }
10
+ class FromBuilder
11
+ def initialize(config, collection)
12
+ @config = config
13
+ @collection = collection
14
+ end
15
+
16
+ def select
17
+ SelectBuilder.new(@config, @collection)
18
+ end
19
+
20
+ def insert(record)
21
+ HttpTransport.request(
22
+ config: @config, method: 'POST', path: FromBuilder.collection_path(@config, @collection), body: record
23
+ )
24
+ end
25
+
26
+ def update(patch)
27
+ UpdateBuilder.new(@config, @collection, patch)
28
+ end
29
+
30
+ def delete
31
+ DeleteBuilder.new(@config, @collection)
32
+ end
33
+
34
+ def self.collection_path(config, collection)
35
+ raise KError.new(
36
+ code: 'K9O_MISSING_PROJECT_CONTEXT',
37
+ message: 'data-plane operations require project_slug to be bound on the client',
38
+ context: { collection: collection },
39
+ recommendation: 'Pass project_slug: in K9o.create_client(...).'
40
+ ) if config.project_slug.nil? || config.project_slug.empty?
41
+
42
+ unless collection.is_a?(String) && collection.match?(/\A[a-z][a-z0-9_-]{0,62}\z/)
43
+ raise KError.new(
44
+ code: 'K9O_INVALID_FIELD_VALUE',
45
+ message: "collection name #{collection.inspect} does not match required format",
46
+ context: { collection: collection }
47
+ )
48
+ end
49
+ UrlGrammar.project_path(config.project_slug, "data/#{collection}")
50
+ end
51
+ end
52
+
53
+ # Shared filter/pagination state.
54
+ class BaseBuilder
55
+ def initialize(config, collection)
56
+ @config = config
57
+ @collection = collection
58
+ @filters = []
59
+ @limit_val = nil
60
+ @cursor_val = nil
61
+ @count_mode = nil
62
+ end
63
+
64
+ %i[eq neq gt gte lt lte like ilike is].each do |op|
65
+ define_method(op) do |column, value|
66
+ @filters << [column, op.to_s, value.to_s]
67
+ self
68
+ end
69
+ end
70
+
71
+ def in_(column, values)
72
+ @filters << [column, 'in', "(#{values.map(&:to_s).join(',')})"]
73
+ self
74
+ end
75
+
76
+ def not_in(column, values)
77
+ @filters << [column, 'notIn', "(#{values.map(&:to_s).join(',')})"]
78
+ self
79
+ end
80
+
81
+ def filter(column, op, value)
82
+ @filters << [column, op.to_s, value.to_s]
83
+ self
84
+ end
85
+
86
+ def limit(n)
87
+ @limit_val = n
88
+ self
89
+ end
90
+
91
+ def cursor(opaque)
92
+ @cursor_val = opaque
93
+ self
94
+ end
95
+
96
+ def count(mode)
97
+ @count_mode = mode
98
+ self
99
+ end
100
+
101
+ protected
102
+
103
+ def to_query
104
+ q = {}
105
+ @filters.each do |col, op, val|
106
+ q[col] = val.start_with?('(') ? "#{op}.#{val}" : "#{op}.#{val}"
107
+ end
108
+ q['limit'] = @limit_val.to_s if @limit_val
109
+ q['cursor'] = @cursor_val if @cursor_val
110
+ q['count'] = @count_mode if @count_mode
111
+ q
112
+ end
113
+ end
114
+
115
+ class SelectBuilder < BaseBuilder
116
+ def execute
117
+ HttpTransport.request(
118
+ config: @config, method: 'GET',
119
+ path: FromBuilder.collection_path(@config, @collection),
120
+ query: to_query
121
+ )
122
+ end
123
+ end
124
+
125
+ class UpdateBuilder < BaseBuilder
126
+ def initialize(config, collection, patch)
127
+ super(config, collection)
128
+ @patch = patch
129
+ end
130
+
131
+ def execute
132
+ id_filter = @filters.find { |col, op, _| col == 'id' && op == 'eq' }
133
+ raise KError.new(
134
+ code: 'K9O_INVALID_FIELD_VALUE',
135
+ message: 'update() requires .eq("id", ...) filter; row-mass update is not supported at v0.1.0',
136
+ context: { collection: @collection }
137
+ ) unless id_filter
138
+
139
+ path = "#{FromBuilder.collection_path(@config, @collection)}/#{id_filter[2]}"
140
+ HttpTransport.request(config: @config, method: 'PATCH', path: path, body: @patch)
141
+ end
142
+ end
143
+
144
+ class DeleteBuilder < BaseBuilder
145
+ def execute
146
+ id_filter = @filters.find { |col, op, _| col == 'id' && op == 'eq' }
147
+ raise KError.new(
148
+ code: 'K9O_INVALID_FIELD_VALUE',
149
+ message: 'delete() requires .eq("id", ...) filter; row-mass delete is not supported at v0.1.0',
150
+ context: { collection: @collection }
151
+ ) unless id_filter
152
+
153
+ path = "#{FromBuilder.collection_path(@config, @collection)}/#{id_filter[2]}"
154
+ HttpTransport.request(config: @config, method: 'DELETE', path: path)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Standard ch01 §1.1 envelope returned by every k9o surface on success.
5
+ class Envelope
6
+ attr_reader :data, :meta
7
+
8
+ def initialize(data:, meta:)
9
+ @data = data
10
+ @meta = meta
11
+ end
12
+
13
+ def self.from_wire(wire)
14
+ Envelope.new(
15
+ data: wire['data'],
16
+ meta: Meta.from_wire(wire['meta'] || {})
17
+ )
18
+ end
19
+ end
20
+
21
+ # Meta accessor sugar per ch13 §5.
22
+ class Meta
23
+ attr_reader :cursor, :subscription, :idempotency, :warnings, :cost, :timing, :context
24
+
25
+ def initialize(cursor: nil, subscription: nil, idempotency: nil, warnings: [], cost: {}, timing: nil, context: nil)
26
+ @cursor = cursor
27
+ @subscription = subscription
28
+ @idempotency = idempotency
29
+ @warnings = warnings
30
+ @cost = cost
31
+ @timing = timing
32
+ @context = context
33
+ end
34
+
35
+ def self.from_wire(wire)
36
+ cursor = wire['cursor'] ? MetaCursor.new(next_: wire['cursor']['next'], total_count: wire['cursor']['totalCount']) : nil
37
+ sub = wire['subscription'] ? MetaSubscription.new(is_initial: !!wire['subscription']['isInitial']) : nil
38
+ idem = wire['idempotency'] ? MetaIdempotency.new(replayed: !!wire['idempotency']['replayed']) : nil
39
+ warnings = (wire['warnings'] || []).map { |w| MetaWarning.new(code: w['code'] || '', message: w['message'] || '') }
40
+ timing = wire['timing'] ? MetaTiming.new(total_ms: wire['timing']['totalMs'].to_i) : nil
41
+ context = wire['context'] && wire['context']['requestId'] ? MetaContext.new(request_id: wire['context']['requestId']) : nil
42
+ Meta.new(
43
+ cursor: cursor, subscription: sub, idempotency: idem,
44
+ warnings: warnings, cost: wire['cost'] || {},
45
+ timing: timing, context: context
46
+ )
47
+ end
48
+ end
49
+
50
+ class MetaCursor
51
+ attr_reader :next, :total_count
52
+
53
+ def initialize(next_:, total_count:)
54
+ @next = next_
55
+ @total_count = total_count
56
+ end
57
+ end
58
+
59
+ MetaSubscription = Struct.new(:is_initial, keyword_init: true)
60
+ MetaIdempotency = Struct.new(:replayed, keyword_init: true)
61
+ MetaWarning = Struct.new(:code, :message, keyword_init: true)
62
+ MetaTiming = Struct.new(:total_ms, keyword_init: true)
63
+ MetaContext = Struct.new(:request_id, keyword_init: true)
64
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Function lifecycle namespace — k9o.functions.*.
5
+ class Functions
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def deploy(name:, runtime:, code:, tier: nil, config: nil)
11
+ slug = require_project_slug
12
+ body = { 'name' => name, 'runtime' => runtime, 'code' => code }
13
+ body['tier'] = tier if tier
14
+ body['config'] = config if config
15
+ HttpTransport.request(config: @config, method: 'POST', path: UrlGrammar.project_path(slug, 'functions'), body: body)
16
+ end
17
+
18
+ def list(status: nil)
19
+ slug = require_project_slug
20
+ q = status ? { 'status' => status } : nil
21
+ HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, 'functions'), query: q)
22
+ end
23
+
24
+ def get(name)
25
+ slug = require_project_slug
26
+ HttpTransport.request(config: @config, method: 'GET', path: UrlGrammar.project_path(slug, "functions/#{enc(name)}"))
27
+ end
28
+
29
+ def delete(name)
30
+ slug = require_project_slug
31
+ HttpTransport.request(config: @config, method: 'DELETE', path: UrlGrammar.project_path(slug, "functions/#{enc(name)}"))
32
+ end
33
+
34
+ # Invoke a function; payload passed to the runtime verbatim.
35
+ def invoke(name, payload)
36
+ slug = require_project_slug
37
+ HttpTransport.request(config: @config, method: 'POST',
38
+ path: UrlGrammar.project_path(slug, "functions/#{enc(name)}/invoke"), body: payload)
39
+ end
40
+
41
+ private
42
+
43
+ def require_project_slug
44
+ Studio.require_project_slug(@config, 'functions')
45
+ end
46
+
47
+ def enc(s)
48
+ ::URI.encode_www_form_component(s)
49
+ end
50
+ end
51
+ end
data/lib/k9o/gate.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K9o
4
+ # Gate (Identity-Aware Kernel) namespace. Project-scoped.
5
+ class Gate
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def sign_up(email:, password:, display_name: nil)
11
+ body = { 'email' => email, 'password' => password }
12
+ body['displayName'] = display_name if display_name
13
+ HttpTransport.request(config: @config, method: 'POST', path: gate_path('signup'), body: body)
14
+ end
15
+
16
+ def sign_in(email:, password:)
17
+ HttpTransport.request(
18
+ config: @config, method: 'POST', path: gate_path('signin'),
19
+ body: { 'email' => email, 'password' => password }
20
+ )
21
+ end
22
+
23
+ def sign_out(refresh_token: nil)
24
+ body = refresh_token ? { 'refreshToken' => refresh_token } : {}
25
+ HttpTransport.request(config: @config, method: 'POST', path: gate_path('signout'), body: body)
26
+ end
27
+
28
+ def whoami
29
+ HttpTransport.request(config: @config, method: 'GET', path: gate_path('whoami'))
30
+ end
31
+
32
+ def refresh_token(refresh_token:)
33
+ HttpTransport.request(
34
+ config: @config, method: 'POST', path: gate_path('refresh'),
35
+ body: { 'refreshToken' => refresh_token }
36
+ )
37
+ end
38
+
39
+ def verify_session
40
+ HttpTransport.request(config: @config, method: 'GET', path: gate_path('verify'))
41
+ end
42
+
43
+ # The project's hosted sign-in URL and WebAuthn rpId:
44
+ # GET /@<slug>/gate/hosted. Unauthenticated and cacheable; no credential is
45
+ # sent even when an api_key is configured. Returns an Envelope whose `data`
46
+ # is { 'signInUrl' => String, 'rpId' => String }. Take both values as given:
47
+ # an unknown org, a deleted project and a non-Gate project all answer the
48
+ # same 404 K9O_PROJECT_NOT_FOUND.
49
+ def hosted
50
+ HttpTransport.request(config: @config, method: 'GET', path: gate_path('hosted'), authenticated: false)
51
+ end
52
+
53
+ private
54
+
55
+ def gate_path(action)
56
+ raise KError.new(
57
+ code: 'K9O_MISSING_PROJECT_CONTEXT',
58
+ message: 'k9o.gate operations require project_slug bound on the client',
59
+ context: {}
60
+ ) if @config.project_slug.nil? || @config.project_slug.empty?
61
+ UrlGrammar.project_path(@config.project_slug, "gate/#{action}")
62
+ end
63
+ end
64
+ end