sklik-api 0.0.16 → 0.1.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.
@@ -8,7 +8,7 @@ class SklikApi
8
8
  :premiseMode, :premiseID
9
9
  ]
10
10
 
11
- include Object
11
+ include SklikObject
12
12
  =begin
13
13
  Example of input hash
14
14
  {
@@ -22,10 +22,23 @@ Example of input hash
22
22
 
23
23
  =end
24
24
 
25
- def initialize adgroup, args
25
+ def initialize args, deprecated_args = {}
26
+
27
+ #deprecated way to set up new adgroup!
28
+ if args.is_a?(SklikApi::Adgroup)
29
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Adtext.new(adgroup, args) to SklikApi::Adtext.new(args = {}) possible to add parent adgroup by adding :adgroup => your adgroup"
30
+ #set adgroup owner campaign
31
+ @adgroup = args
32
+ args = deprecated_args
33
+ #new way to set adgroups!
34
+ else
35
+ #set adgroup owner campaign
36
+ #if in input args there is pointer to parent campaign!
37
+ @adgroup = args.delete(:adgroup)
38
+ end
39
+ @args = args
40
+
26
41
  @adtext_data = nil
27
- #set adtext owner adgroup
28
- @adgroup = adgroup
29
42
 
30
43
  super args
31
44
  end
@@ -35,50 +48,106 @@ Example of input hash
35
48
  end
36
49
 
37
50
  def create_args
38
- raise ArgumentError, "Adtexts need's to know adgroup_id" unless @adgroup.args[:adgroup_id]
51
+ raise ArgumentError, "Adtexts need's to know adgroup_id" unless @args[:adgroup_id] || @adgroup.args[:adgroup_id]
39
52
  out = []
40
53
  #add campaign id to know where to create adgroup
41
- out << @adgroup.args[:adgroup_id]
54
+ out << @args[:adgroup_id] || @adgroup.args[:adgroup_id]
42
55
 
43
56
  #add adtext struct
44
- args = {}
45
- args[:creative1] = @args[:headline]
46
- args[:creative2] = @args[:description1]
47
- args[:creative3] = @args[:description2]
48
- args[:clickthruText] = @args[:display_url]
49
- args[:clickthruUrl] = @args[:url]
57
+ c_args = {}
58
+ c_args[:creative1] = @args[:headline]
59
+ c_args[:creative2] = @args[:description1]
60
+ c_args[:creative3] = @args[:description2]
61
+ c_args[:clickthruText] = @args[:display_url]
62
+ c_args[:clickthruUrl] = @args[:url]
63
+ c_args[:status] = status_for_update if status_for_update
50
64
 
51
65
  ADDITIONAL_FIELDS.each do |add_info|
52
66
  field_name = add_info.to_s.underscore.to_sym
53
- args[add_info] = @args[field_name] if @args[field_name]
67
+ c_args[add_info] = @args[field_name] if @args[field_name]
54
68
  end
55
69
 
56
- out << args
70
+ out << c_args
57
71
 
58
72
  #return output
59
73
  out
60
74
  end
61
75
 
62
- def self.find adgroup, args = {}
63
- out = []
64
- super(NAME, adgroup.args[:adgroup_id]).each do |adtext|
76
+ def update_args
77
+ out = []
78
+
79
+ #add campaign id on which will be performed update
80
+ out << @args[:adtext_id]
81
+
82
+ #prepare campaign struct
83
+ u_args = {}
84
+ u_args[:status] = status_for_update if status_for_update
85
+
86
+ out << u_args
87
+ ADDITIONAL_FIELDS.each do |add_info|
88
+ field_name = add_info.to_s.underscore.to_sym
89
+ u_args[add_info] = @args[field_name] if @args[field_name]
90
+ end
91
+
92
+ out
93
+ end
94
+
95
+ def self.get id
96
+ if adtext = super(NAME, id)
97
+ SklikApi::Adtext.new(process_sklik_data adtext)
98
+ else
99
+ nil
100
+ end
101
+ end
102
+
103
+ def self.find args, deprecated_args = {}
104
+ out = []
105
+
106
+ if args.is_a?(Integer)
107
+ return get args
108
+
109
+ #asking for adgroup deprecated way!
110
+ elsif args.is_a?(SklikApi::Adgroup)
111
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Adtext.find(adgroup, args) to SklikApi::Adtext.find(adgroup_id: 1234) possible to add parent adgroup by adding :adgroup => your adgroup"
112
+ adgroup_id = args.args[:adgroup_id]
113
+ args = deprecated_args
114
+
115
+ #asking for adgroup by hash with keyword_id
116
+ elsif args.is_a?(Hash) && args[:adtext_id]
117
+ if adtext = get(args[:adtext_id])
118
+ return [adtext]
119
+ else
120
+ return []
121
+ end
122
+
123
+ #asking for keyword by hash
124
+ else
125
+ adgroup_id = args[:adgroup_id]
126
+ end
127
+
128
+ return [] unless adgroup_id
129
+
130
+ super(NAME, adgroup_id).each do |adtext|
65
131
  if args[:adtext_id].nil? || (args[:adtext_id] && args[:adtext_id].to_i == adtext[:id].to_i)
66
- out << SklikApi::Adtext.new(
67
- adgroup,
68
- :adtext_id => adtext[:id],
69
- :headline => adtext[:creative1],
70
- :description1 => adtext[:creative2],
71
- :description2 => adtext[:creative3],
72
- :display_url =>adtext[:clickthruText],
73
- :url => adtext[:clickthruUrl],
74
- :name => adtext[:name],
75
- :status => fix_status(adtext)
76
- )
132
+ out << SklikApi::Adtext.new(process_sklik_data adtext)
77
133
  end
78
134
  end
79
135
  out
80
136
  end
81
137
 
138
+ def self.process_sklik_data adtext = {}
139
+ {
140
+ :adgroup_id => adtext[:groupId],
141
+ :adtext_id => adtext[:id],
142
+ :headline => adtext[:creative1],
143
+ :description1 => adtext[:creative2],
144
+ :description2 => adtext[:creative3],
145
+ :display_url =>adtext[:clickthruText],
146
+ :url => adtext[:clickthruUrl],
147
+ :status => fix_status(adtext)
148
+ }
149
+ end
150
+
82
151
  def self.fix_status adtext
83
152
  if adtext[:removed] == true
84
153
  return :stopped
@@ -99,14 +168,79 @@ Example of input hash
99
168
  end
100
169
  end
101
170
 
171
+ def self.get_current_status args = {}
172
+ raise ArgumentError, "Adtext_id is required" unless args[:adtext_id]
173
+ if adgroup = self.get(args[:adtext_id])
174
+ adgroup.args[:status]
175
+ else
176
+ raise ArgumentError, "Adtext by #{args.inspect} couldn't be found!"
177
+ end
178
+ end
179
+
180
+ def get_current_status
181
+ self.class.get_current_status :adtext_id => @args[:adtext_id]
182
+ end
183
+
184
+ def valid?
185
+ clear_errors
186
+ log_error "headline is required or too long" unless args[:headline] && args[:headline].size > 0 && args[:headline].size <= 35
187
+ log_error "description1 is required or too long" unless args[:description1] && args[:description1].size > 0 && args[:description1].size <= 45
188
+ log_error "description2 is required or too long" unless args[:description2] && args[:description2].size > 0 && args[:description2].size <= 45
189
+ log_error "display_url is required or too long" unless args[:display_url] && args[:display_url].size > 0 && args[:display_url].size <= 45
190
+ log_error "url is required" unless args[:url] && args[:url].size > 0 && args[:url].size <= 45
191
+ log_error "adgroup_id is required" unless args[:adgroup_id] || (@adgroup && @adgroup.args[:adgroup_id])
192
+ !errors.any?
193
+ end
194
+
195
+ def update args = {}
196
+ @args.merge!(args)
197
+ save
198
+ end
199
+
102
200
  def save
201
+ clear_errors
103
202
  if @args[:adtext_id] #do update
104
203
 
105
- else #do save
106
- #create adtext
107
- create
204
+ #get current status of campaign
205
+ before_status = get_current_status
206
+
207
+ #restore campaign before update
208
+ restore if before_status == :stopped
209
+
210
+ begin
211
+ update_object
212
+
213
+ rescue Exception => e
214
+ log_error e.message
215
+ end
216
+
217
+ #remove it if new status is stopped or status doesn't changed and before it was stopped
218
+ remove if (@args[:status] == :stopped) || (@args[:status].nil? && before_status == :stopped)
219
+
220
+ else #do save
221
+ @args[:adgroup_id] = @adgroup.args[:adgroup_id] if !@args[:adgroup_id] && @adgroup.args[:adgroup_id]
222
+
223
+ begin
224
+ #create adtext
225
+ create
226
+ rescue Exception => e
227
+ log_error e.message
228
+ #don't continue with creating campaign!
229
+ return false
230
+ end
231
+
232
+ #remove it if new status is stopped
233
+ remove if @args[:status] && @args[:status].to_s.to_sym == :stopped
108
234
  end
235
+
236
+ !errors.any?
109
237
  end
238
+
239
+ def log_error message
240
+ @adgroup.log_error "Adtext: #{@args[:headline]} -> #{message}" if @adgroup
241
+ errors << message
242
+ end
243
+
110
244
  end
111
245
  end
112
246
 
@@ -4,52 +4,109 @@ class SklikApi
4
4
 
5
5
  NAME = "keyword"
6
6
 
7
- include Object
7
+ ADDITIONAL_FIELDS = [
8
+ :cpc, :url
9
+ ]
10
+ ADDITIONAL_READ_FIELDS = [
11
+ :disabled, :cpc, :url, :minCpc
12
+ ]
13
+
14
+ include SklikObject
8
15
  =begin
9
16
  Example of input hash
10
17
  {
11
18
  :keyword => "\"some funny keyword\""
12
19
  }
13
20
  =end
14
-
15
- def initialize adgroup, args
21
+
22
+ def initialize args, deprecated_args = {}
23
+
24
+ #deprecated way to set up new adgroup!
25
+ if args.is_a?(SklikApi::Adgroup)
26
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Keyword.new(adgroup, args) to SklikApi::Keyword.new(args = {}) possible to add parent adgroup by adding :adgroup => your adgroup"
27
+ #set adgroup owner campaign
28
+ @adgroup = args
29
+ args = deprecated_args
30
+
31
+ #new way to set adgroups!
32
+ else
33
+ #set adgroup owner campaign
34
+ #if in input args there is pointer to parent campaign!
35
+ @adgroup = args.delete(:adgroup)
36
+ end
37
+ @args = args
38
+
16
39
  @keyword_data = nil
17
- #set keyword owner adgroup
18
- @adgroup = adgroup
19
40
 
20
41
  super args
21
42
  end
22
-
43
+
23
44
  def create_args
24
- raise ArgumentError, "Keyword need's to know adgroup_id" unless @adgroup.args[:adgroup_id]
45
+ raise ArgumentError, "Keyword need's to know adgroup_id" unless @args[:adgroup_id] || @adgroup.args[:adgroup_id]
25
46
  out = []
26
47
  #add campaign id to know where to create adgroup
27
- out << @adgroup.args[:adgroup_id]
28
-
48
+ out << @args[:adgroup_id] || @adgroup.args[:adgroup_id]
49
+
29
50
  #add adtext struct
30
- args = {}
31
- args[:name] = strip_match_type @args[:keyword]
32
- args[:matchType] = get_math_type @args[:keyword]
33
- out << args
34
-
51
+ c_args = {}
52
+ c_args[:name] = strip_match_type @args[:keyword]
53
+ c_args[:matchType] = get_math_type @args[:keyword]
54
+ #Currently not working :(
55
+ c_args[:status] = status_for_update if status_for_update
56
+
57
+ ADDITIONAL_FIELDS.each do |add_info|
58
+ field_name = add_info.to_s.underscore.to_sym
59
+ c_args[add_info] = @args[field_name] if @args[field_name]
60
+ end
61
+
62
+ if @args[:cpc]
63
+ c_args[:cpc] = (@args[:cpc] * 100).to_i
64
+ end
65
+
66
+
67
+ out << c_args
68
+
35
69
  #return output
36
70
  out
37
71
  end
38
-
72
+
73
+ def update_args
74
+ out = []
75
+
76
+ #add campaign id on which will be performed update
77
+ out << @args[:keyword_id]
78
+
79
+ #prepare campaign struct
80
+ u_args = {}
81
+ u_args[:status] = status_for_update if status_for_update
82
+
83
+ ADDITIONAL_FIELDS.each do |add_info|
84
+ field_name = add_info.to_s.underscore.to_sym
85
+ u_args[add_info] = @args[field_name] if @args[field_name]
86
+ end
87
+ if @args[:cpc]
88
+ u_args[:cpc] = (@args[:cpc] * 100).to_i
89
+ end
90
+ out << u_args
91
+
92
+ #return output
93
+ out
94
+ end
95
+
39
96
  def strip_match_type keyword
40
97
  keyword.gsub(/(\[|\]|\")/, "").gsub(/^-/, "")
41
98
  end
42
-
99
+
43
100
  def get_math_type keyword
44
- if /^-\[.*\]$/ =~ keyword
101
+ if /^-\[.*\]$/ =~ keyword
45
102
  return "negativeExact"
46
- elsif /^\[.*\]$/ =~ keyword
103
+ elsif /^\[.*\]$/ =~ keyword
47
104
  return "exact"
48
- elsif /^-\".*\"$/ =~ keyword
105
+ elsif /^-\".*\"$/ =~ keyword
49
106
  return "negativePhrase"
50
- elsif /^\".*\"$/ =~ keyword
107
+ elsif /^\".*\"$/ =~ keyword
51
108
  return "phrase"
52
- elsif /^-.*$/ =~ keyword
109
+ elsif /^-.*$/ =~ keyword
53
110
  return "negativeBroad"
54
111
  else
55
112
  return "broad"
@@ -67,22 +124,66 @@ Example of input hash
67
124
  else keyword
68
125
  end
69
126
  end
70
-
71
- def self.find adgroup, args = {}
127
+
128
+ def self.get id
129
+ if keyword = super(NAME, id)
130
+ SklikApi::Keyword.new(process_sklik_data keyword)
131
+ else
132
+ nil
133
+ end
134
+ end
135
+
136
+ def self.find args, deprecated_args = {}
72
137
  out = []
73
- super(NAME, adgroup.args[:adgroup_id]).each do |keyword|
138
+
139
+ if args.is_a?(Integer)
140
+ return get args
141
+
142
+ #asking for adgroup deprecated way!
143
+ elsif args.is_a?(SklikApi::Adgroup)
144
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Keyword.find(adgroup, args) to SklikApi::Keyword.find(adgroup_id: 1234) possible to add parent adgroup by adding :adgroup => your adgroup"
145
+ adgroup_id = args.args[:adgroup_id]
146
+ args = deprecated_args
147
+
148
+ #asking for adgroup by hash with keyword_id
149
+ elsif args.is_a?(Hash) && args[:keyword_id]
150
+ if keyword = get(args[:keyword_id])
151
+ return [keyword]
152
+ else
153
+ return []
154
+ end
155
+
156
+ #asking for keyword by hash
157
+ else
158
+ adgroup_id = args[:adgroup_id]
159
+ end
160
+
161
+ return [] unless adgroup_id
162
+
163
+ super(NAME, adgroup_id).each do |keyword|
74
164
  if args[:keyword_id].nil? || (args[:keyword_id] && args[:keyword_id].to_i == keyword[:id].to_i)
75
- out << SklikApi::Keyword.new(
76
- adgroup,
77
- :keyword_id => keyword[:id],
78
- :keyword => apply_math_type(keyword[:name], keyword[:matchType] ),
79
- :status => fix_status(keyword)
80
- )
165
+ out << SklikApi::Keyword.new(process_sklik_data keyword)
81
166
  end
82
167
  end
83
168
  out
84
169
  end
85
170
 
171
+ def self.process_sklik_data keyword = {}
172
+ out = {
173
+ :adgroup_id => keyword[:groupId],
174
+ :keyword_id => keyword[:id],
175
+ :keyword => apply_math_type(keyword[:name], keyword[:matchType] ),
176
+ :status => fix_status(keyword)
177
+ }
178
+ ADDITIONAL_READ_FIELDS.each do |add_info|
179
+ field_name = add_info.to_s.underscore.to_sym
180
+ out[field_name] = keyword[add_info] if keyword[add_info]
181
+ end
182
+ out[:cpc] = keyword[:cpc].to_f/100.0 if keyword[:cpc]
183
+
184
+ out
185
+ end
186
+
86
187
  def self.fix_status keyword
87
188
  if keyword[:removed] == true
88
189
  return :stopped
@@ -101,18 +202,71 @@ Example of input hash
101
202
  if @keyword_data
102
203
  @keyword_data
103
204
  else
104
- @keyword_data = {:keyword => @args[:keyword], :status => @args[:status]}
205
+ @keyword_data = @args end
206
+ end
207
+
208
+
209
+ def self.get_current_status args = {}
210
+ raise ArgumentError, "Keyword_id is required" unless args[:keyword_id]
211
+ if adgroup = self.get(args[:keyword_id])
212
+ adgroup.args[:status]
213
+ else
214
+ raise ArgumentError, "Keyword by #{args.inspect} couldn't be found!"
105
215
  end
106
216
  end
107
-
108
- def save
217
+
218
+ def get_current_status
219
+ self.class.get_current_status :keyword_id => @args[:keyword_id]
220
+ end
221
+
222
+ def update args = {}
223
+ @args.merge!(args)
224
+ save
225
+ end
226
+
227
+ def save
228
+ @args[:adgroup_id] = @adgroup.args[:adgroup_id] if !@args[:adgroup_id] && @adgroup.args[:adgroup_id]
229
+
109
230
  if @args[:keyword_id] #do update
110
-
231
+ #get current status of campaign
232
+ before_status = get_current_status
233
+
234
+ #restore campaign before update
235
+ restore if before_status == :stopped
236
+
237
+ begin
238
+ update_object
239
+
240
+ rescue Exception => e
241
+ log_error e.message
242
+ end
243
+
244
+ #remove it if new status is stopped or status doesn't changed and before it was stopped
245
+ remove if (@args[:status] == :stopped) || (@args[:status].nil? && before_status == :stopped)
111
246
  else #do save
112
- #create adtext
113
- create
247
+ #create keyword
248
+ begin
249
+ create
250
+
251
+ rescue Exception => e
252
+ log_error e.message
253
+
254
+ return false
255
+ end
256
+
257
+ #remove it if new status is stopped
258
+ remove if @args[:status] && @args[:status].to_s.to_sym == :stopped
114
259
  end
115
- end
260
+
261
+ !errors.any?
262
+ end
263
+
264
+ def log_error message
265
+ puts message
266
+ @adgroup.log_error "Keyword: #{@args[:keyword]} -> #{message}" if @adgroup
267
+ errors << message
268
+ end
269
+
116
270
  end
117
271
  end
118
-
272
+
@@ -4,15 +4,15 @@ class SklikApi
4
4
 
5
5
  NAME = "client"
6
6
 
7
- include Object
8
-
7
+ include SklikObject
8
+
9
9
  def initialize args = {}
10
10
  super args
11
11
  end
12
-
12
+
13
13
  def self.find args = {}
14
14
  out = connection.call("client.getAttributes") { |param|
15
- ([param[:user]]|param[:foreignAccounts]).collect{|u|
15
+ ([param[:user]]|param[:foreignAccounts]).collect{|u|
16
16
  u.symbolize_keys!
17
17
  SklikApi::Client.new(
18
18
  :customer_id => u[:userId],
@@ -1,21 +1,21 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  class SklikApi
3
3
  class Connection
4
-
4
+
5
5
  MAX_RETRIES = 3
6
6
  DEFAULTS = {
7
7
  :debug => false,
8
8
  :timeout => 100
9
9
  }
10
-
10
+
11
11
  def initialize args = {}
12
12
  @args = DEFAULTS.merge(args)
13
13
  end
14
-
14
+
15
15
  def self.connection
16
16
  @connection ||= SklikApi::Connection.new(:debug => false)
17
17
  end
18
-
18
+
19
19
  #prepare connection to sklik
20
20
  def connection
21
21
  path = (ENV['RACK_ENV'] || ENV['RAILS']) == "test" ? "/sandbox/RPC2" : "/RPC2"
@@ -24,11 +24,11 @@ class SklikApi
24
24
  #fix of UTF-8 encoding
25
25
  server.extend(XMLRPCWorkAround)
26
26
  #debug mode to see what XMLRPC is doing
27
- server.set_debug(File.open("log/xmlrpc-#{Time.now.strftime("%Y_%m_%d-%H_%M_%S")}.log","a:UTF-8")) if @args[:debug]
28
-
27
+ server.set_debug(File.open("log/xmlrpc-#{Time.now.strftime("%Y_%m_%d-%H_%M_%S")}.log","a:UTF-8")) if @args[:debug]
28
+
29
29
  server
30
30
  end
31
-
31
+
32
32
  #Get session is method for login into sklik
33
33
  #save session for other requests until it expires
34
34
  #every taxonomy has its own session!
@@ -38,7 +38,10 @@ class SklikApi
38
38
  @session[SklikApi::Access.uniq_identifier]
39
39
  else
40
40
  begin
41
+ SklikApi.log(:debug, "Getting session for #{SklikApi::Access.email}")
41
42
  param = connection.call("client.login", SklikApi::Access.email, SklikApi::Access.password).symbolize_keys
43
+ SklikApi.log(:debug, "Session received: #{param.inspect}")
44
+
42
45
  if param[:status] == 401
43
46
  raise ArgumentError, "Invalid login for: #{SklikApi::Access.email}"
44
47
  elsif param[:status] == 200
@@ -53,32 +56,52 @@ class SklikApi
53
56
  end
54
57
  end
55
58
  end
56
-
59
+
57
60
  # method to wrap method call to sklik -> allow retry and problem with session expiration
58
61
  def call method, *args
62
+
63
+ SklikApi.log(:debug, "Calling api: #{method} [#{args}]") unless method == "client.login"
59
64
  retry_count = MAX_RETRIES
60
- begin
65
+ begin
61
66
  #get response from sklik
62
67
  param = connection.call( method, get_session, *args ).symbolize_keys
63
- if [200,404].include?(param[:status])
68
+ SklikApi.log(:debug, "Response from api: #{param.inspect}") unless method == "client.login"
69
+ if [200].include?(param[:status])
64
70
  return yield(param)
71
+ elsif param[:status] == 400
72
+ raise SklikApi::InvalidArguments, "Calling method: #{method} with invalid arguments: #{args}, #{param.inspect}"
73
+ elsif param[:status] == 404
74
+ raise SklikApi::NotFound, "Calling method: #{method} with params: #{args} was not found"
65
75
  elsif param[:status] == 406
66
- raise ArgumentError, "Sklik returned: #{method}: #{param[:statusMessage]} #{args.inspect}"
76
+ raise SklikApi::InvalidData, print_errors(param[:errors])
67
77
  elsif param[:statusMessage] == "Session has expired or is malformed."
68
78
  raise ArgumentError, "session has expired"
69
79
  else
70
- raise ArgumentError, "There is error from sklik #{method}: #{param[:statusMessage]}: #{args.inspect}"
80
+ raise ArgumentError, "There is error from sklik #{method}: #{param.inspect}: #{args.inspect}"
71
81
  end
72
82
  rescue Exception => e
83
+ #when know exception which is not fault of sklik don't retry!
84
+ raise e if e.class.name =~ /SklikApi::/
85
+
73
86
  retry_count -= 1
74
87
  pp "Rescuing from request by: #{e.class} - #{e.message}"
75
88
  #if session expired then get new one! and retry
76
89
  get_session(true) if e.message == "session has expired"
77
90
  #don't retry if there is problem with Invalid paramaters od Data
78
- retry_count = 0 if e.message.include?("Invalid")
91
+ retry_count = 0 if e.message.include?("Invalid")
79
92
  retry if retry_count > 0
80
93
  raise e
81
94
  end
82
95
  end
96
+
97
+ def print_errors error_hash
98
+ error_hash.collect do |one_error|
99
+ out = "#{one_error.delete("id").humanize} ("
100
+ out += one_error.to_a.collect do |key, value|
101
+ "#{key} = #{value}"
102
+ end.join(", ")
103
+ out + ")"
104
+ end.join("; ")
105
+ end
83
106
  end
84
107
  end
@@ -0,0 +1,6 @@
1
+ class SklikApi
2
+
3
+ class NotFound < Exception; end
4
+ class InvalidArguments < Exception; end
5
+ class InvalidData < Exception; end
6
+ end