xapixctl 1.1.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'xapixctl/base_cli'
4
+
5
+ module Xapixctl
6
+ class TitanCli < BaseCli
7
+ DEFAULT_METHODS = {
8
+ "predict" => :predict,
9
+ "performance" => :performance,
10
+ }.freeze
11
+
12
+ option :schema_import, desc: "Resource id of an existing Schema import"
13
+ option :data, desc: "JSON encoded data for predict API", default: "[[1,2,3]]"
14
+ desc "service URL", "build a service for the deployed model"
15
+ long_desc <<-LONGDESC
16
+ `xapixctl titan service URL` will build a ML service around a deployed ML model.
17
+
18
+ We expect the following of the deployed ML model:
19
+
20
+ - There should be a "POST /predict" endpoint. Use --data="JSON" to specify an example dataset the model expects.
21
+
22
+ - If there is a "GET /performance" endpoint, it'll be made available. It's expected to return a JSON object with the properties 'accuracy', 'precision', 'recall', and 'min_acc_threshold'.
23
+
24
+ Examples:
25
+ \x5> $ xapixctl titan service https://services.demo.akoios.com/ai-model-name -p xapix/ml-project
26
+ LONGDESC
27
+ def service(akoios_url)
28
+ url = URI.parse(akoios_url)
29
+ schema = JSON.parse(RestClient.get(File.join(url.to_s, 'spec'), params: { host: url.hostname }, headers: { accept: :json }))
30
+ patch_schema(schema)
31
+ connector_refs = import_swagger(File.basename(url.path), schema)
32
+ say "\n== Onboarding Connectors", :bold
33
+ connectors = match_connectors_to_action(connector_refs)
34
+ if connectors.empty?
35
+ warn "\nNo valid connectors for ML service detected, not building service."
36
+ exit 1
37
+ end
38
+ say "\n== Building Service", :bold
39
+ service_doc = build_service(schema.dig('info', 'title'), connectors)
40
+ res = prj_connection.apply(service_doc)
41
+ say "\ncreated / updated service #{res.first}"
42
+ end
43
+
44
+ private
45
+
46
+ def patch_schema(schema)
47
+ predict_schema = schema.dig('paths', '/predict', 'post')
48
+ if predict_schema
49
+ predict_data = JSON.parse(options[:data]) rescue {}
50
+ predict_schema['operationId'] = 'predict'
51
+ predict_schema['parameters'].each do |param|
52
+ if param['name'] == 'json' && param['in'] == 'body'
53
+ param['schema']['properties'] = { "data" => extract_schema(predict_data) }
54
+ param['schema']['example'] = { "data" => predict_data }
55
+ end
56
+ end
57
+ end
58
+
59
+ performane_schema = schema.dig('paths', '/performance', 'get')
60
+ if performane_schema
61
+ performane_schema['operationId'] = 'performance'
62
+ end
63
+ end
64
+
65
+ def import_swagger(filename, schema)
66
+ Tempfile.create([filename, '.json']) do |f|
67
+ f.write(schema.to_json)
68
+ f.rewind
69
+
70
+ if options[:schema_import]
71
+ result = prj_connection.update_schema_import(options[:schema_import], f)
72
+ say "updated #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
73
+ else
74
+ result = prj_connection.add_schema_import(f)
75
+ say "created #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
76
+ end
77
+ result.dig('schema_import', 'updated_resources')
78
+ end
79
+ end
80
+
81
+ def match_connectors_to_action(connector_refs)
82
+ connector_refs.map do |connector_ref|
83
+ connector = prj_connection.resource(connector_ref['kind'], connector_ref['id'])
84
+ action = DEFAULT_METHODS[connector.dig('definition', 'name')]
85
+ say "\n#{connector['kind']} #{connector.dig('definition', 'name')} -> "
86
+ if action
87
+ say "#{action} action"
88
+ updated_connector = update_connector_with_preview(connector)
89
+ [action, updated_connector] if updated_connector
90
+ else
91
+ say "no action type detected, ignoring"
92
+ nil
93
+ end
94
+ end.compact
95
+ end
96
+
97
+ def update_connector_with_preview(connector)
98
+ say "fetching preview for #{connector['kind']} #{connector.dig('definition', 'name')}..."
99
+ preview_details = prj_connection.data_source_preview(connector.dig('metadata', 'id'))
100
+ preview = preview_details.dig('preview', 'sample')
101
+ say "got a #{preview['status']} response: #{preview['body']}"
102
+ if preview['status'] != 200
103
+ say "unexpected status, please check data or model"
104
+ elsif yes?("Does this look alright?", :bold)
105
+ res = prj_connection.accept_data_source_preview(connector.dig('metadata', 'id'))
106
+ return res.dig('data_source', 'resource_description')
107
+ end
108
+ nil
109
+ end
110
+
111
+ def extract_schema(data_sample)
112
+ case data_sample
113
+ when Array
114
+ { type: 'array', items: extract_schema(data_sample[0]) }
115
+ when Hash
116
+ { type: 'object', properties: data_sample.transform_values { |v| extract_schema(v) } }
117
+ when Numeric
118
+ { type: 'number' }
119
+ else
120
+ {}
121
+ end
122
+ end
123
+
124
+ def build_service(title, connectors)
125
+ {
126
+ version: 'v1',
127
+ kind: 'Service',
128
+ metadata: { id: title.parameterize },
129
+ definition: {
130
+ name: title.humanize,
131
+ actions: connectors.map { |action, connector| build_service_action(action, connector) }
132
+ }
133
+ }
134
+ end
135
+
136
+ def build_service_action(action_type, connector)
137
+ {
138
+ name: action_type,
139
+ parameter_schema: parameter_schema(action_type, connector),
140
+ result_schema: result_schema(action_type),
141
+ pipeline: { units: pipeline_units(action_type, connector) }
142
+ }
143
+ end
144
+
145
+ def parameter_schema(action_type, connector)
146
+ case action_type
147
+ when :predict
148
+ { type: 'object', properties: {
149
+ data: connector.dig('definition', 'parameter_schema', 'properties', 'body', 'properties', 'data'),
150
+ } }
151
+ when :performance
152
+ { type: 'object', properties: {} }
153
+ end
154
+ end
155
+
156
+ def result_schema(action_type)
157
+ case action_type
158
+ when :predict
159
+ { type: 'object', properties: {
160
+ prediction: { type: 'object', properties: { percent: { type: 'number' }, raw: { type: 'number' } } },
161
+ success: { type: 'boolean' },
162
+ error: { type: 'string' }
163
+ } }
164
+ when :performance
165
+ { type: 'object', properties: {
166
+ performance: {
167
+ type: 'object', properties: {
168
+ accuracy: { type: 'number' },
169
+ precision: { type: 'number' },
170
+ recall: { type: 'number' },
171
+ min_acc_threshold: { type: 'number' }
172
+ }
173
+ }
174
+ } }
175
+ end
176
+ end
177
+
178
+ def pipeline_units(action_type, connector)
179
+ case action_type
180
+ when :predict
181
+ [entry_unit, predict_unit(connector), predict_result_unit]
182
+ when :performance
183
+ [entry_unit, performance_unit(connector), performance_result_unit]
184
+ end
185
+ end
186
+
187
+ def entry_unit
188
+ { version: 'v2', kind: 'Unit/Entry',
189
+ metadata: { id: 'entry' },
190
+ definition: { name: 'Entry' } }
191
+ end
192
+
193
+ def predict_unit(connector)
194
+ leafs = extract_leafs(connector.dig('definition', 'parameter_schema', 'properties', 'body'))
195
+ { version: 'v2', kind: 'Unit/DataSource',
196
+ metadata: { id: 'predict' },
197
+ definition: {
198
+ name: 'Predict',
199
+ inputs: ['entry'],
200
+ data_source: connector.dig('metadata', 'id'),
201
+ formulas: leafs.map { |leaf|
202
+ { ref: leaf[:node]['$id'], formula: (['', 'entry'] + leaf[:path]).join('.') }
203
+ }
204
+ } }
205
+ end
206
+
207
+ def predict_result_unit
208
+ { version: 'v2', kind: 'Unit/Result',
209
+ metadata: { id: 'result' },
210
+ definition: {
211
+ name: 'Result',
212
+ inputs: ['predict'],
213
+ formulas: [{
214
+ ref: "#prediction.percent",
215
+ formula: "IF(.predict.status = 200, ROUND(100*coerce.to-float(.predict.body), 2))"
216
+ }, {
217
+ ref: "#prediction.raw",
218
+ formula: "IF(.predict.status = 200, coerce.to-float(.predict.body))"
219
+ }, {
220
+ ref: "#success",
221
+ formula: ".predict.status = 200"
222
+ }, {
223
+ ref: "#error",
224
+ formula: "IF(.predict.status <> 200, 'Model not trained!')"
225
+ }],
226
+ parameter_sample: {
227
+ "prediction" => { "percent" => 51.12, "raw" => 0.5112131 },
228
+ "success" => true,
229
+ "error" => nil
230
+ }
231
+ } }
232
+ end
233
+
234
+ def performance_unit(connector)
235
+ { version: 'v2', kind: 'Unit/DataSource',
236
+ metadata: { id: 'performance' },
237
+ definition: {
238
+ name: 'Performance',
239
+ inputs: ['entry'],
240
+ data_source: connector.dig('metadata', 'id')
241
+ } }
242
+ end
243
+
244
+ def performance_result_unit
245
+ { version: 'v2', kind: 'Unit/Result',
246
+ metadata: { id: 'result' },
247
+ definition: {
248
+ name: 'Result',
249
+ inputs: ['performance'],
250
+ formulas: [{
251
+ ref: "#performance.recall",
252
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.recall)"
253
+ }, {
254
+ ref: "#performance.accuracy",
255
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.accuracy)"
256
+ }, {
257
+ ref: "#performance.precision",
258
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.precision)"
259
+ }, {
260
+ ref: "#performance.min_acc_threshold",
261
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.min_acc_threshold)"
262
+ }],
263
+ parameter_sample: {
264
+ "performance" => {
265
+ "recall" => 0.8184713375796179,
266
+ "accuracy" => 0.9807847896440129,
267
+ "precision" => 0.8711864406779661,
268
+ "min_acc_threshold" => 0.84,
269
+ }
270
+ }
271
+ } }
272
+ end
273
+
274
+ def extract_leafs(schema, current_path = [])
275
+ return unless schema
276
+ case schema['type']
277
+ when 'object'
278
+ schema['properties'].flat_map { |key, sub_schema| extract_leafs(sub_schema, current_path + [key]) }.compact
279
+ when 'array'
280
+ extract_leafs(schema['items'], current_path + [:[]])
281
+ else
282
+ { path: current_path, node: schema }
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Xapixctl
6
+ module Util
7
+ extend self
8
+
9
+ class InvalidDocumentStructureError < StandardError
10
+ def initialize(file)
11
+ super("#{file} has invalid document structure")
12
+ end
13
+ end
14
+
15
+ DOCUMENT_STRUCTURE = %w[version kind metadata definition].freeze
16
+
17
+ def resources_from_file(filename, ignore_missing: false)
18
+ load_files(filename, ignore_missing) do |actual_file, yaml_string|
19
+ yaml_string.split(/^---\s*\n/).map { |yml| Psych.safe_load(yml) }.compact.each do |doc|
20
+ raise InvalidDocumentStructureError, actual_file unless (DOCUMENT_STRUCTURE - doc.keys.map(&:to_s)).empty?
21
+ yield doc
22
+ end
23
+ end
24
+ end
25
+
26
+ def load_files(filename, ignore_missing)
27
+ if filename == '-'
28
+ yield 'STDIN', $stdin.read
29
+ else
30
+ pn = filename.is_a?(Pathname) ? filename : Pathname.new(filename)
31
+ if pn.directory?
32
+ pn.glob(["**/*.yaml", "**/*.yml"]).sort.each { |dpn| yield dpn.to_s, dpn.read }
33
+ elsif pn.exist?
34
+ yield pn.to_s, pn.read
35
+ elsif !ignore_missing
36
+ warn "file not found: #{filename}"
37
+ exit 1
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xapixctl
4
- VERSION = "1.1.2"
4
+ VERSION = "1.2.4"
5
5
  end
data/xapixctl.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path("lib", __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require "xapixctl/version"
@@ -24,13 +26,19 @@ Gem::Specification.new do |spec|
24
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
27
  spec.require_paths = ["lib"]
26
28
 
29
+ spec.required_ruby_version = '>= 2.6'
30
+
27
31
  spec.add_dependency "activesupport", ">= 5.2.3", "< 6.0.0"
32
+ spec.add_dependency "hashdiff", ">= 1.0.1", "< 1.2.0"
28
33
  spec.add_dependency "rest-client", ">= 2.1.0", "< 3.0.0"
29
- spec.add_dependency "thor", ">= 1.0.0", "< 1.1.0"
34
+ spec.add_dependency "thor", ">= 1.0.0", "< 1.2.0"
30
35
 
31
- spec.add_development_dependency "bundler", "~> 1.17.3"
36
+ spec.add_development_dependency "bundler", "~> 2.1.4"
32
37
  spec.add_development_dependency "rake", "~> 13.0"
33
38
  spec.add_development_dependency "relaxed-rubocop", "~> 2.5"
34
- spec.add_development_dependency "rspec", "~> 3.0"
35
- spec.add_development_dependency "rubocop", "~> 0.89"
39
+ spec.add_development_dependency "rspec", "~> 3.10.0"
40
+ spec.add_development_dependency "rubocop", "~> 1.11.0"
41
+ spec.add_development_dependency "rubocop-rake", "~> 0.5.1"
42
+ spec.add_development_dependency "rubocop-rspec", "~> 2.2.0"
43
+ spec.add_development_dependency "webmock", "~> 3.11.0"
36
44
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xapixctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Reinsch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-25 00:00:00.000000000 Z
11
+ date: 2021-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,6 +30,26 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 6.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: hashdiff
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.0.1
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: 1.2.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 1.0.1
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: 1.2.0
33
53
  - !ruby/object:Gem::Dependency
34
54
  name: rest-client
35
55
  requirement: !ruby/object:Gem::Requirement
@@ -59,7 +79,7 @@ dependencies:
59
79
  version: 1.0.0
60
80
  - - "<"
61
81
  - !ruby/object:Gem::Version
62
- version: 1.1.0
82
+ version: 1.2.0
63
83
  type: :runtime
64
84
  prerelease: false
65
85
  version_requirements: !ruby/object:Gem::Requirement
@@ -69,21 +89,21 @@ dependencies:
69
89
  version: 1.0.0
70
90
  - - "<"
71
91
  - !ruby/object:Gem::Version
72
- version: 1.1.0
92
+ version: 1.2.0
73
93
  - !ruby/object:Gem::Dependency
74
94
  name: bundler
75
95
  requirement: !ruby/object:Gem::Requirement
76
96
  requirements:
77
97
  - - "~>"
78
98
  - !ruby/object:Gem::Version
79
- version: 1.17.3
99
+ version: 2.1.4
80
100
  type: :development
81
101
  prerelease: false
82
102
  version_requirements: !ruby/object:Gem::Requirement
83
103
  requirements:
84
104
  - - "~>"
85
105
  - !ruby/object:Gem::Version
86
- version: 1.17.3
106
+ version: 2.1.4
87
107
  - !ruby/object:Gem::Dependency
88
108
  name: rake
89
109
  requirement: !ruby/object:Gem::Requirement
@@ -118,28 +138,70 @@ dependencies:
118
138
  requirements:
119
139
  - - "~>"
120
140
  - !ruby/object:Gem::Version
121
- version: '3.0'
141
+ version: 3.10.0
122
142
  type: :development
123
143
  prerelease: false
124
144
  version_requirements: !ruby/object:Gem::Requirement
125
145
  requirements:
126
146
  - - "~>"
127
147
  - !ruby/object:Gem::Version
128
- version: '3.0'
148
+ version: 3.10.0
129
149
  - !ruby/object:Gem::Dependency
130
150
  name: rubocop
131
151
  requirement: !ruby/object:Gem::Requirement
132
152
  requirements:
133
153
  - - "~>"
134
154
  - !ruby/object:Gem::Version
135
- version: '0.89'
155
+ version: 1.11.0
136
156
  type: :development
137
157
  prerelease: false
138
158
  version_requirements: !ruby/object:Gem::Requirement
139
159
  requirements:
140
160
  - - "~>"
141
161
  - !ruby/object:Gem::Version
142
- version: '0.89'
162
+ version: 1.11.0
163
+ - !ruby/object:Gem::Dependency
164
+ name: rubocop-rake
165
+ requirement: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - "~>"
168
+ - !ruby/object:Gem::Version
169
+ version: 0.5.1
170
+ type: :development
171
+ prerelease: false
172
+ version_requirements: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - "~>"
175
+ - !ruby/object:Gem::Version
176
+ version: 0.5.1
177
+ - !ruby/object:Gem::Dependency
178
+ name: rubocop-rspec
179
+ requirement: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - "~>"
182
+ - !ruby/object:Gem::Version
183
+ version: 2.2.0
184
+ type: :development
185
+ prerelease: false
186
+ version_requirements: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - "~>"
189
+ - !ruby/object:Gem::Version
190
+ version: 2.2.0
191
+ - !ruby/object:Gem::Dependency
192
+ name: webmock
193
+ requirement: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - "~>"
196
+ - !ruby/object:Gem::Version
197
+ version: 3.11.0
198
+ type: :development
199
+ prerelease: false
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - "~>"
203
+ - !ruby/object:Gem::Version
204
+ version: 3.11.0
143
205
  description:
144
206
  email:
145
207
  - michael@xapix.io
@@ -148,6 +210,7 @@ executables:
148
210
  extensions: []
149
211
  extra_rdoc_files: []
150
212
  files:
213
+ - ".github/workflows/cd.yaml"
151
214
  - ".gitignore"
152
215
  - ".rspec"
153
216
  - ".rubocop.yml"
@@ -163,8 +226,16 @@ files:
163
226
  - lib/xapixctl.rb
164
227
  - lib/xapixctl/base_cli.rb
165
228
  - lib/xapixctl/cli.rb
229
+ - lib/xapixctl/connector_cli.rb
166
230
  - lib/xapixctl/phoenix_client.rb
231
+ - lib/xapixctl/phoenix_client/connection.rb
232
+ - lib/xapixctl/phoenix_client/organization_connection.rb
233
+ - lib/xapixctl/phoenix_client/project_connection.rb
234
+ - lib/xapixctl/phoenix_client/result_handler.rb
167
235
  - lib/xapixctl/preview_cli.rb
236
+ - lib/xapixctl/sync_cli.rb
237
+ - lib/xapixctl/titan_cli.rb
238
+ - lib/xapixctl/util.rb
168
239
  - lib/xapixctl/version.rb
169
240
  - xapixctl.gemspec
170
241
  homepage: https://github.com/xapix-io/xapixctl
@@ -181,14 +252,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
181
252
  requirements:
182
253
  - - ">="
183
254
  - !ruby/object:Gem::Version
184
- version: '0'
255
+ version: '2.6'
185
256
  required_rubygems_version: !ruby/object:Gem::Requirement
186
257
  requirements:
187
258
  - - ">="
188
259
  - !ruby/object:Gem::Version
189
260
  version: '0'
190
261
  requirements: []
191
- rubygems_version: 3.0.8
262
+ rubygems_version: 3.0.9
192
263
  signing_key:
193
264
  specification_version: 4
194
265
  summary: xapix client library and command line tool