xapixctl 1.2.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 175a3c54493e14738757b5eb95ef1da9724b961f460d1ac5dfb17ea6eb696507
4
- data.tar.gz: 0a79951ab81a2b69d0087161191097cf24b01e2282887d03b12e8f32527d3707
3
+ metadata.gz: 4ba3cb0a95f481dfd6568282c75d0bc4a2db36a03c4b8a177401deac61b04eaa
4
+ data.tar.gz: f5cd613fdcb2c9a204b224b6504348c4840fe45fd3506bea904bb26d5367793a
5
5
  SHA512:
6
- metadata.gz: 040c017fb340670f8751a45c5b9c0c659cd814bb712cc74fb40e368f714fb1b80759d9c4387cbbb19999cfd512fc389a082168cde33f67cc1fd9fa0fb17b42f3
7
- data.tar.gz: 1bdbbbb3da472ade7b520cfebc284ebebdeb871ee77c3bafa5fb22789bf4c0057d4dc7ff116ee3c0d611dbd692d99a96ac168ffa8824be006b22b124c7e98bf7
6
+ metadata.gz: c0635abac3da2424e0109306eec1795dac8ae2cf9cb81aa4f8769ef397f5e70f4b5db7095b2820f0a49f7902d2d3e75df86b2128622fdaa10bb3082dea38ddab
7
+ data.tar.gz: f14855a202d934303acf771987686dcf395c0842a870a309283bbd080c1ebd32c0c1178e1a0df2256352c575920e4116010953a4e6d2a8833e178f27838f5e85
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- xapixctl (1.2.1)
4
+ xapixctl (1.2.2)
5
5
  activesupport (>= 5.2.3, < 6.0.0)
6
6
  rest-client (>= 2.1.0, < 3.0.0)
7
7
  thor (>= 1.0.0, < 1.2.0)
data/lib/xapixctl/cli.rb CHANGED
@@ -1,17 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'xapixctl/base_cli'
4
+ require 'xapixctl/connector_cli'
4
5
  require 'xapixctl/preview_cli'
5
6
  require 'xapixctl/sync_cli'
7
+ require 'xapixctl/titan_cli'
6
8
 
7
9
  module Xapixctl
8
10
  class Cli < BaseCli
11
+ desc "connectors SUBCOMMAND ...ARGS", "Commands for Connector resources"
12
+ subcommand "connectors", ConnectorCli
13
+
9
14
  desc "preview SUBCOMMAND ...ARGS", "Request preview for resources"
10
15
  subcommand "preview", PreviewCli
11
16
 
12
17
  desc "sync SUBCOMMAND ...ARGS", "Sync resources"
13
18
  subcommand "sync", SyncCli
14
19
 
20
+ desc "titan SUBCOMMAND ...ARGS", "Tools for ML model deployments and service generation", hide: true
21
+ subcommand "titan", TitanCli
22
+
15
23
  option :format, aliases: "-f", default: 'text', enum: ['text', 'yaml', 'json'], desc: "Output format"
16
24
  desc "get TYPE [ID]", "retrieve either all resources of given TYPE or just the resource of given TYPE and ID"
17
25
  long_desc <<-LONGDESC
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'xapixctl/base_cli'
4
+
5
+ module Xapixctl
6
+ class ConnectorCli < BaseCli
7
+ option :schema_import, desc: "Resource id of an existing Schema import"
8
+ desc "import SPECFILE", "Create HTTP Connectors from Swagger / OpenAPI or SOAP WSDL files"
9
+ long_desc <<-LONGDESC
10
+ `xapixctl connectors import SPECFILE` will create HTTP Connectors from the given Swagger / OpenAPI or SOAP WSDL file.
11
+
12
+ Examples:
13
+ \x5> $ xapixctl connectors import ./swagger.json -p xapix/some-project
14
+ \x5> $ xapixctl connectors import ./swagger.json -p xapix/some-project --schema-import=existing-schema
15
+ LONGDESC
16
+ def import(spec_filename)
17
+ path = Pathname.new(spec_filename)
18
+ unless path.file? && path.readable?
19
+ warn "Cannot read #{path}, please check file exists and is readable"
20
+ exit 1
21
+ end
22
+ if options[:schema_import]
23
+ puts "uploading to update schema import '#{options[:schema_import]}': #{spec_filename}..."
24
+ result = prj_connection.update_schema_import(options[:schema_import], spec_filename)
25
+ puts "updated #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
26
+ else
27
+ puts "uploading as new import: #{spec_filename}..."
28
+ result = prj_connection.add_schema_import(spec_filename)
29
+ puts "created #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
30
+ end
31
+
32
+ [['issues', 'import'], ['validation_issues', 'validation']].each do |key, name|
33
+ issues = result.dig('schema_import', 'report', key)
34
+ if issues.any?
35
+ puts "\n#{name} issues:"
36
+ issues.each { |issue| puts " - #{issue}" }
37
+ end
38
+ end
39
+
40
+ updated_resources = result.dig('schema_import', 'updated_resources')
41
+ if updated_resources.any?
42
+ puts "\nconnectors:"
43
+ updated_resources.each { |resource| puts " - #{resource['kind']} #{resource['id']}" }
44
+ else
45
+ puts "\nno connectors created/updated."
46
+ end
47
+ end
48
+ end
49
+ end
@@ -76,6 +76,18 @@ module Xapixctl
76
76
  run { @client[data_source_preview_path(data_source_id)].post(preview_data.to_json, content_type: :json) }
77
77
  end
78
78
 
79
+ def add_schema_import(spec_filename, &block)
80
+ spec_data = { schema_import: { file: File.new(spec_filename, 'r') } }
81
+ result_handler(block).
82
+ run { @client[schema_imports_path].post(spec_data) }
83
+ end
84
+
85
+ def update_schema_import(schema_import, spec_filename, &block)
86
+ spec_data = { schema_import: { file: File.new(spec_filename, 'r') } }
87
+ result_handler(block).
88
+ run { @client[schema_import_path(schema_import)].patch(spec_data) }
89
+ end
90
+
79
91
  def pipeline_preview(pipeline_id, format: :hash, &block)
80
92
  result_handler(block).
81
93
  prepare_data(->(data) { data['pipeline_preview'] }).
@@ -128,6 +140,14 @@ module Xapixctl
128
140
  "/projects/#{@org}/#{@project}/onboarding/data_sources/#{id}/preview"
129
141
  end
130
142
 
143
+ def schema_imports_path
144
+ "/projects/#{@org}/#{@project}/onboarding/schema_imports"
145
+ end
146
+
147
+ def schema_import_path(schema_import)
148
+ "/projects/#{@org}/#{@project}/onboarding/schema_imports/#{schema_import}"
149
+ end
150
+
131
151
  def resource_path(type, id)
132
152
  "/projects/#{@org}/#{@project}/#{translate_type(type)}/#{id}"
133
153
  end
@@ -0,0 +1,281 @@
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
+ say "\n== Building Service", :bold
35
+ service_doc = build_service(schema.dig('info', 'title'), connectors)
36
+ res = prj_connection.apply(service_doc)
37
+ say "\ncreated / updated service #{res.first}"
38
+ end
39
+
40
+ private
41
+
42
+ def patch_schema(schema)
43
+ predict_schema = schema.dig('paths', '/predict', 'post')
44
+ if predict_schema
45
+ predict_data = JSON.parse(options[:data]) rescue {}
46
+ predict_schema['operationId'] = 'predict'
47
+ predict_schema['parameters'].each do |param|
48
+ if param['name'] == 'json' && param['in'] == 'body'
49
+ param['schema']['properties'] = { "data" => extract_schema(predict_data) }
50
+ param['schema']['example'] = { "data" => predict_data }
51
+ end
52
+ end
53
+ end
54
+
55
+ performane_schema = schema.dig('paths', '/performance', 'get')
56
+ if performane_schema
57
+ performane_schema['operationId'] = 'performance'
58
+ end
59
+ end
60
+
61
+ def import_swagger(filename, schema)
62
+ Tempfile.create([filename, '.json']) do |f|
63
+ f.write(schema.to_json)
64
+ f.rewind
65
+
66
+ if options[:schema_import]
67
+ result = prj_connection.update_schema_import(options[:schema_import], f)
68
+ say "updated #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
69
+ else
70
+ result = prj_connection.add_schema_import(f)
71
+ say "created #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
72
+ end
73
+ result.dig('schema_import', 'updated_resources')
74
+ end
75
+ end
76
+
77
+ def match_connectors_to_action(connector_refs)
78
+ connector_refs.map do |connector_ref|
79
+ connector = prj_connection.resource(connector_ref['kind'], connector_ref['id'])
80
+ action = DEFAULT_METHODS[connector.dig('definition', 'name')]
81
+ say "\n#{connector['kind']} #{connector.dig('definition', 'name')} -> "
82
+ if action
83
+ say "#{action} action"
84
+ [action, update_connector_with_preview(connector)]
85
+ else
86
+ say "no action type detected, ignoring"
87
+ nil
88
+ end
89
+ end.compact
90
+ end
91
+
92
+ def update_connector_with_preview(connector)
93
+ say "fetching preview for #{connector['kind']} #{connector.dig('definition', 'name')}..."
94
+ preview_details = prj_connection.data_source_preview(connector.dig('metadata', 'id'))
95
+ preview = preview_details.dig('preview', 'sample')
96
+ say "got a #{preview['status']} response: #{preview['body']}"
97
+ if preview['status'] != 200
98
+ say "unexpected status, please check data or model"
99
+ elsif yes?("Does this look alright?", :bold)
100
+ res = prj_connection.accept_data_source_preview(connector.dig('metadata', 'id'))
101
+ return res.dig('data_source', 'resource_description')
102
+ end
103
+ connector
104
+ end
105
+
106
+ def extract_schema(data_sample)
107
+ case data_sample
108
+ when Array
109
+ { type: 'array', items: extract_schema(data_sample[0]) }
110
+ when Hash
111
+ { type: 'object', properties: data_sample.transform_values { |v| extract_schema(v) } }
112
+ when Numeric
113
+ { type: 'number' }
114
+ else
115
+ {}
116
+ end
117
+ end
118
+
119
+ def build_service(title, connectors)
120
+ {
121
+ version: 'v1',
122
+ kind: 'Service',
123
+ metadata: { id: title.parameterize },
124
+ definition: {
125
+ name: title.humanize,
126
+ actions: connectors.map { |action, connector| build_service_action(action, connector) }
127
+ }
128
+ }
129
+ end
130
+
131
+ def build_service_action(action_type, connector)
132
+ {
133
+ name: action_type,
134
+ parameter_schema: parameter_schema(action_type, connector),
135
+ result_schema: result_schema(action_type),
136
+ pipeline: { units: pipeline_units(action_type, connector) }
137
+ }
138
+ end
139
+
140
+ def parameter_schema(action_type, connector)
141
+ case action_type
142
+ when :predict
143
+ { type: 'object', properties: {
144
+ data: connector.dig('definition', 'parameter_schema', 'properties', 'body', 'properties', 'data'),
145
+ } }
146
+ when :performance
147
+ { type: 'object', properties: {} }
148
+ end
149
+ end
150
+
151
+ def result_schema(action_type)
152
+ case action_type
153
+ when :predict
154
+ { type: 'object', properties: {
155
+ prediction: { type: 'object', properties: { percent: { type: 'number' }, raw: { type: 'number' } } },
156
+ success: { type: 'boolean' },
157
+ error: { type: 'string' }
158
+ } }
159
+ when :performance
160
+ { type: 'object', properties: {
161
+ performance: {
162
+ type: 'object', properties: {
163
+ accuracy: { type: 'number' },
164
+ precision: { type: 'number' },
165
+ recall: { type: 'number' },
166
+ min_acc_threshold: { type: 'number' }
167
+ }
168
+ }
169
+ } }
170
+ end
171
+ end
172
+
173
+ def pipeline_units(action_type, connector)
174
+ case action_type
175
+ when :predict
176
+ [entry_unit, predict_unit(connector), predict_result_unit]
177
+ when :performance
178
+ [entry_unit, performance_unit(connector), performance_result_unit]
179
+ end
180
+ end
181
+
182
+ def entry_unit
183
+ { version: 'v2', kind: 'Unit/Entry',
184
+ metadata: { id: 'entry' },
185
+ definition: { name: 'Entry' } }
186
+ end
187
+
188
+ def predict_unit(connector)
189
+ leafs = extract_leafs(connector.dig('definition', 'parameter_schema', 'properties', 'body'))
190
+ { version: 'v2', kind: 'Unit/DataSource',
191
+ metadata: { id: 'predict' },
192
+ definition: {
193
+ name: 'Predict',
194
+ inputs: ['entry'],
195
+ data_source: connector.dig('metadata', 'id'),
196
+ formulas: leafs.map { |leaf|
197
+ { ref: leaf[:node]['$id'], formula: (['', 'entry'] + leaf[:path]).join('.') }
198
+ }
199
+ } }
200
+ end
201
+
202
+ def predict_result_unit
203
+ { version: 'v2', kind: 'Unit/Result',
204
+ metadata: { id: 'result' },
205
+ definition: {
206
+ name: 'Result',
207
+ inputs: ['predict'],
208
+ formulas: [{
209
+ ref: "#prediction.raw",
210
+ formula: "IF(.predict.status = 200, ROUND(100*coerce.to-float(.predict.body), 2))"
211
+ }, {
212
+ ref: "#prediction.percent",
213
+ formula: "IF(.predict.status = 200, coerce.to-float(.predict.body))"
214
+ }, {
215
+ ref: "#success",
216
+ formula: ".predict.status = 200"
217
+ }, {
218
+ ref: "#error",
219
+ formula: "IF(.predict.status <> 200, 'Model not trained!')"
220
+ }],
221
+ parameter_sample: {
222
+ "prediction" => { "percent" => 51, "raw" => 0.5112131 },
223
+ "success" => true,
224
+ "error" => nil
225
+ }
226
+ } }
227
+ end
228
+
229
+ def performance_unit(connector)
230
+ { version: 'v2', kind: 'Unit/DataSource',
231
+ metadata: { id: 'performance' },
232
+ definition: {
233
+ name: 'Performance',
234
+ inputs: ['entry'],
235
+ data_source: connector.dig('metadata', 'id')
236
+ } }
237
+ end
238
+
239
+ def performance_result_unit
240
+ { version: 'v2', kind: 'Unit/Result',
241
+ metadata: { id: 'result' },
242
+ definition: {
243
+ name: 'Result',
244
+ inputs: ['performance'],
245
+ formulas: [{
246
+ ref: "#performance.recall",
247
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.recall)"
248
+ }, {
249
+ ref: "#performance.accuracy",
250
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.accuracy)"
251
+ }, {
252
+ ref: "#performance.precision",
253
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.precision)"
254
+ }, {
255
+ ref: "#performance.min_acc_threshold",
256
+ formula: "WITH(data, JSON.DECODE(REGEXREPLACE(performance.body, \"'\", \"\\\"\")), .data.min_acc_threshold)"
257
+ }],
258
+ parameter_sample: {
259
+ "performance" => {
260
+ "recall" => 0.8184713375796179,
261
+ "accuracy" => 0.9807847896440129,
262
+ "precision" => 0.8711864406779661,
263
+ "min_acc_threshold" => 0.84,
264
+ }
265
+ }
266
+ } }
267
+ end
268
+
269
+ def extract_leafs(schema, current_path = [])
270
+ return unless schema
271
+ case schema['type']
272
+ when 'object'
273
+ schema['properties'].flat_map { |key, sub_schema| extract_leafs(sub_schema, current_path + [key]) }.compact
274
+ when 'array'
275
+ extract_leafs(schema['items'], current_path + [:[]])
276
+ else
277
+ { path: current_path, node: schema }
278
+ end
279
+ end
280
+ end
281
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xapixctl
4
- VERSION = "1.2.1"
4
+ VERSION = "1.2.2"
5
5
  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.2.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Reinsch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-24 00:00:00.000000000 Z
11
+ date: 2021-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -206,6 +206,7 @@ files:
206
206
  - lib/xapixctl.rb
207
207
  - lib/xapixctl/base_cli.rb
208
208
  - lib/xapixctl/cli.rb
209
+ - lib/xapixctl/connector_cli.rb
209
210
  - lib/xapixctl/phoenix_client.rb
210
211
  - lib/xapixctl/phoenix_client/connection.rb
211
212
  - lib/xapixctl/phoenix_client/organization_connection.rb
@@ -213,6 +214,7 @@ files:
213
214
  - lib/xapixctl/phoenix_client/result_handler.rb
214
215
  - lib/xapixctl/preview_cli.rb
215
216
  - lib/xapixctl/sync_cli.rb
217
+ - lib/xapixctl/titan_cli.rb
216
218
  - lib/xapixctl/util.rb
217
219
  - lib/xapixctl/version.rb
218
220
  - xapixctl.gemspec