neuron-client 0.1.0 → 0.2.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.
Files changed (85) hide show
  1. data/README.md +34 -8
  2. data/lib/neuron-client.rb +61 -12
  3. data/lib/neuron-client/{connection.rb → admin_connection.rb} +7 -7
  4. data/lib/neuron-client/api.rb +48 -31
  5. data/lib/neuron-client/membase_connection.rb +18 -0
  6. data/lib/neuron-client/model/admin/ad.rb +22 -0
  7. data/lib/neuron-client/model/admin/ad_zone.rb +15 -0
  8. data/lib/neuron-client/model/admin/base.rb +91 -0
  9. data/lib/neuron-client/model/admin/blocked_referer.rb +12 -0
  10. data/lib/neuron-client/model/admin/blocked_user_agent.rb +12 -0
  11. data/lib/neuron-client/model/admin/geo_target.rb +16 -0
  12. data/lib/neuron-client/model/admin/report.rb +15 -0
  13. data/lib/neuron-client/model/admin/s3_file.rb +12 -0
  14. data/lib/neuron-client/model/admin/zone.rb +15 -0
  15. data/lib/neuron-client/model/base.rb +38 -0
  16. data/lib/neuron-client/model/common/ad.rb +40 -0
  17. data/lib/neuron-client/model/common/ad_calculations.rb +329 -0
  18. data/lib/neuron-client/model/common/ad_zone.rb +17 -0
  19. data/lib/neuron-client/model/common/base.rb +67 -0
  20. data/lib/neuron-client/model/common/blocked_referer.rb +16 -0
  21. data/lib/neuron-client/model/common/blocked_user_agent.rb +16 -0
  22. data/lib/neuron-client/model/common/geo_target.rb +16 -0
  23. data/lib/neuron-client/model/common/report.rb +21 -0
  24. data/lib/neuron-client/model/common/s3_file.rb +16 -0
  25. data/lib/neuron-client/model/common/zone.rb +22 -0
  26. data/lib/neuron-client/model/common/zone_calculations.rb +41 -0
  27. data/lib/neuron-client/model/membase/ad.rb +31 -0
  28. data/lib/neuron-client/model/membase/ad_zone.rb +11 -0
  29. data/lib/neuron-client/model/membase/blocked_referer.rb +18 -0
  30. data/lib/neuron-client/model/membase/blocked_user_agent.rb +18 -0
  31. data/lib/neuron-client/model/membase/geo_target.rb +11 -0
  32. data/lib/neuron-client/model/membase/report.rb +11 -0
  33. data/lib/neuron-client/model/membase/s3_file.rb +11 -0
  34. data/lib/neuron-client/model/membase/zone.rb +19 -0
  35. data/lib/neuron-client/model/models.rb +14 -0
  36. data/lib/neuron-client/version.rb +1 -1
  37. data/neuron-client.gemspec +18 -11
  38. data/spec/fixtures/vcr_cassettes/s3_file.yml +186 -4
  39. data/spec/lib/admin_connection_spec.rb +82 -0
  40. data/spec/lib/api_spec.rb +80 -0
  41. data/spec/lib/membase_connection_spec.rb +27 -0
  42. data/spec/lib/model/admin/ad_spec.rb +34 -0
  43. data/spec/lib/model/admin/ad_zone_spec.rb +19 -0
  44. data/spec/lib/model/admin/base_spec.rb +11 -0
  45. data/spec/lib/model/admin/blocked_referer_spec.rb +11 -0
  46. data/spec/lib/model/admin/blocked_user_agent_spec.rb +11 -0
  47. data/spec/lib/model/admin/geo_target_spec.rb +30 -0
  48. data/spec/lib/model/admin/report_spec.rb +21 -0
  49. data/spec/lib/model/admin/s3_spec.rb +11 -0
  50. data/spec/lib/model/admin/zone_spec.rb +21 -0
  51. data/spec/lib/model/base_spec.rb +89 -0
  52. data/spec/lib/model/common/ad_calculations_spec.rb +1148 -0
  53. data/spec/lib/model/common/ad_spec.rb +11 -0
  54. data/spec/lib/model/common/ad_zone_spec.rb +11 -0
  55. data/spec/lib/model/common/base_spec.rb +11 -0
  56. data/spec/lib/model/common/blocked_referer_spec.rb +11 -0
  57. data/spec/lib/model/common/blocked_user_agent_spec.rb +11 -0
  58. data/spec/lib/model/common/geo_target_spec.rb +11 -0
  59. data/spec/lib/model/common/report_spec.rb +11 -0
  60. data/spec/lib/model/common/s3_spec.rb +11 -0
  61. data/spec/lib/model/common/zone_calculations_spec.rb +54 -0
  62. data/spec/lib/model/common/zone_spec.rb +11 -0
  63. data/spec/lib/model/membase/ad_spec.rb +50 -0
  64. data/spec/lib/model/membase/ad_zone_spec.rb +11 -0
  65. data/spec/lib/model/membase/base_spec.rb +11 -0
  66. data/spec/lib/model/membase/blocked_referer_spec.rb +30 -0
  67. data/spec/lib/model/membase/blocked_user_agent_spec.rb +30 -0
  68. data/spec/lib/model/membase/geo_target_spec.rb +11 -0
  69. data/spec/lib/model/membase/report_spec.rb +11 -0
  70. data/spec/lib/model/membase/s3_spec.rb +11 -0
  71. data/spec/lib/model/membase/zone_spec.rb +28 -0
  72. data/spec/lib/old_spec.rb +192 -149
  73. data/spec/lib/s3_file_spec.rb +45 -42
  74. data/spec/spec_helper.rb +2 -1
  75. metadata +296 -57
  76. data/lib/neuron-client/ad.rb +0 -39
  77. data/lib/neuron-client/ad_zone.rb +0 -16
  78. data/lib/neuron-client/blocked_referer.rb +0 -12
  79. data/lib/neuron-client/blocked_user_agent.rb +0 -12
  80. data/lib/neuron-client/connected.rb +0 -138
  81. data/lib/neuron-client/geo_target.rb +0 -16
  82. data/lib/neuron-client/real_time_stats.rb +0 -0
  83. data/lib/neuron-client/report.rb +0 -20
  84. data/lib/neuron-client/s3_file.rb +0 -10
  85. data/lib/neuron-client/zone.rb +0 -16
@@ -0,0 +1,12 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ class BlockedUserAgent < Common::BlockedUserAgent
6
+ include Base
7
+
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ class GeoTarget < Common::GeoTarget
6
+ include Base
7
+
8
+ def self.query(parameters)
9
+ response = self.connection.get("geo_targets", parameters)
10
+ response.map{|hash| self.new(hash[superclass.resource_name])}
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ class Report < Common::Report
6
+ include Base
7
+
8
+ def result
9
+ self.class.connection.get("reports/#{id}/result", :format => "")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ class S3File < Common::S3File
6
+ include Base
7
+
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Admin
5
+ class Zone < Common::Zone
6
+ include Base
7
+
8
+ def unlink(ad_id)
9
+ self.class.connection.delete("zones/#{id}/ads/#{ad_id}")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ class Base
5
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id|^class$)/ }
6
+
7
+ def initialize(attrs=nil)
8
+ @proxied_model = self.class.class_to_proxy.new(attrs)
9
+ end
10
+
11
+ def method_missing(meth, *args, &block)
12
+ (@proxied_model.methods.include?(meth) ? @proxied_model.send(meth, *args, &block) : super)
13
+ end
14
+
15
+ class << self
16
+ attr_accessor :api
17
+ def api
18
+ @api || Neuron::Client::API.default_api
19
+ end
20
+
21
+ def connection
22
+ api.connection
23
+ end
24
+
25
+ def class_to_proxy
26
+ module_to_load = api.connection_type.to_s.titleize
27
+ class_name_to_load = name.split('::').last
28
+ Neuron::Client::Model.const_get(module_to_load).const_get(class_name_to_load)
29
+ end
30
+
31
+ def method_missing(meth, *args, &block)
32
+ (class_to_proxy.methods.include?(meth) ? class_to_proxy.send(meth, *args, &block) : super)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Common
5
+ class Ad
6
+ include Base
7
+ include AdCalculations
8
+
9
+ resource_name("ad")
10
+ resources_name("ads")
11
+
12
+ attr_accessor :name, :approved, :response_type, :parameters, :geo_target_ids, :ad_trackers,
13
+ :ad_trackers_attributes, :trackers,
14
+ # redirect
15
+ :redirect_url,
16
+ # video
17
+ :video_api_url, :video_setup_xml, :video_flv_url,
18
+ :video_clickthru_url, :video_companion_ad_html, :social_links, :social_links_attributes,
19
+ # caps
20
+ :frequency_cap_limit, :frequency_cap_window, :overall_cap,
21
+ :daily_cap, :day_partitions, :ideal_impressions_per_hour,
22
+ # range
23
+ :start_datetime, :end_datetime, :time_zone,
24
+ # timestamps
25
+ :created_at, :updated_at
26
+
27
+ class << self
28
+ def stringify_day_partitions(days)
29
+ result = ""
30
+ 168.times do |i|
31
+ result << (days[i.to_s] || "F")
32
+ end
33
+ result
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,329 @@
1
+ module Neuron
2
+ module Client
3
+ module Model
4
+ module Common
5
+ module AdCalculations
6
+ # This module expects the following methods to be defined:
7
+ # start_datetime (Time, String, or nil)
8
+ # end_datetime (Time, String, or nil)
9
+ # time_zone (String)
10
+ # day_partitions (String, or nil, length = 7*24, matches /^[TF]+$/)
11
+ # daily_cap (FixNum, or nil)
12
+ # overall_cap (FixNum, or nil)
13
+ # ideal_impressions_per_hour (Number, or nil)
14
+ # total_impressed (Integer, >= 0)
15
+ # today_impressed (Integer, >= 0)
16
+
17
+ def active?
18
+ calculate_active?(Time.now, total_impressed, today_impressed)
19
+ end
20
+
21
+ def pressure
22
+ calculate_pressure(Time.now, total_impressed, today_impressed)
23
+ end
24
+
25
+ def calculate_active?(time, total, today)
26
+ time = time.in_time_zone(time_zone)
27
+ return false unless within_date_range?(time)
28
+ return false if partitioned? && !partitioned_hour?(time)
29
+ !cap_met?(total, today)
30
+ end
31
+
32
+ # Set the optional "active" parameter to true or false if you already know
33
+ def calculate_pressure(time, total, today, active=nil)
34
+ return nil if active == false
35
+ return nil unless active || calculate_active?(time,total,today)
36
+ time = time.in_time_zone(time_zone)
37
+ if daily_capped?
38
+ if daily_cap_precludes_overall_cap?(time,total,today)
39
+ calculate_overall_pressure(time, total)
40
+ else
41
+ calculate_today_pressure(time, today)
42
+ end
43
+ elsif overall_pressure_exists?
44
+ calculate_overall_pressure(time, total)
45
+ else
46
+ (ideal_impressions_per_hour || 1.0).to_f
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def in_ad_time_zone(time)
53
+ if time.present?
54
+ (time.is_a?(String) ? Time.parse(time) : time).in_time_zone(time_zone)
55
+ end
56
+ rescue
57
+ nil
58
+ end
59
+
60
+ def start_in_time_zone
61
+ in_ad_time_zone(start_datetime)
62
+ end
63
+
64
+ def end_in_time_zone
65
+ in_ad_time_zone(end_datetime)
66
+ end
67
+
68
+ def daily_capped?
69
+ !daily_cap.nil? && daily_cap >= 0
70
+ end
71
+
72
+ def overall_capped?
73
+ !overall_cap.nil? && overall_cap >= 0
74
+ end
75
+
76
+ def overall_pressure_exists?
77
+ overall_capped? && end_datetime.present?
78
+ end
79
+
80
+ def partitioned?
81
+ !day_partitions.nil? && day_partitions.length == 24*7
82
+ end
83
+
84
+ def within_date_range?(time)
85
+ return false if start_datetime.present? && time < start_in_time_zone
86
+ return false if end_datetime.present? && time >= end_in_time_zone
87
+ true
88
+ end
89
+
90
+ # Assume time is in the time zone of the ad.
91
+ # Doesn't worry about whether or not the time is in the ad's date range.
92
+ # Assumes ad is partitioned.
93
+ def partitioned_hour?(time)
94
+ day_partitions[(time.wday * 24) + time.hour] == 'T'
95
+ end
96
+
97
+ # Assume time is in the time zone of the ad.
98
+ # Doesn't worry about whether or not the time is in the ad's date range.
99
+ # Assumes ad is partitioned.
100
+ # Returns true if there is any active partition the day of the given time.
101
+ def partitioned_day?(time)
102
+ day_partitions[(time.wday) * 24, 24].include?('T')
103
+ end
104
+
105
+ # Assumes time is in the time zone of the ad.
106
+ # Doesn't worry about date range or day partitions.
107
+ def cap_met?(total_impressed, today_impressed)
108
+ return true if daily_capped? && (daily_cap <= today_impressed)
109
+ return true if overall_capped? && (overall_cap <= total_impressed)
110
+ false
111
+ end
112
+
113
+ # Assumes time is in time_zone of the ad.
114
+ # Returns true if there's no way we can hit the overall cap
115
+ # without busting the daily cap.
116
+ def daily_cap_precludes_overall_cap?(time,total_impressed,today_impressed)
117
+ return false unless daily_capped? && overall_capped?
118
+ return false unless self.end_datetime.present?
119
+ a = remaining_impressions_via_daily_cap(time, today_impressed)
120
+ b = remaining_impressions_via_overall_cap(total_impressed)
121
+ a < b
122
+ end
123
+
124
+ def remaining_impressions_via_overall_cap(total_impressed)
125
+ [overall_cap - total_impressed, 0].max
126
+ end
127
+
128
+ def remaining_impressions_today(today_impressed)
129
+ [daily_cap - today_impressed, 0].max
130
+ end
131
+
132
+ def remaining_impressions_via_daily_cap(time, today_impressed)
133
+ days = remaining_days(time) - 1 # exclude today
134
+ daily_cap * days + remaining_impressions_today(today_impressed)
135
+ end
136
+
137
+ # Assumes time is in time_zone of the ad.
138
+ # Assume daily_cap is present.
139
+ def calculate_today_pressure(time, today_impressed)
140
+ hours = remaining_hours_today(time)
141
+ if hours <= 0
142
+ 0.0
143
+ else
144
+ impressions = remaining_impressions_today(today_impressed)
145
+ 2 * impressions / hours.to_f
146
+ end
147
+ end
148
+
149
+ # Assumes time is in time_zone of the ad.
150
+ # Assume overall_cap is present.
151
+ # Assume end_datetime is present.
152
+ def calculate_overall_pressure(time, total_impressed)
153
+ hours = remaining_hours(time)
154
+ if hours <= 0
155
+ 0.0
156
+ else
157
+ impressions = remaining_impressions_via_overall_cap(total_impressed)
158
+ impressions / hours.to_f
159
+ end
160
+ end
161
+
162
+ # remaining_days : the total integer number of days that an ad will run
163
+ # (even for part of the day) between now and the
164
+ # end_datetime.
165
+ # Assume all dates/times are in the time_zone of the ad.
166
+ # Assume end_datetime is present.
167
+ def remaining_days(time)
168
+ ending = end_in_time_zone
169
+ beginning = [time, start_in_time_zone].compact.max
170
+ return 0 unless beginning < ending
171
+ if partitioned?
172
+ end_of_beginning_week = beginning_of_week(beginning) + 7.days
173
+ beginning_of_end_week = beginning_of_week(ending)
174
+
175
+ if end_of_beginning_week < beginning_of_end_week
176
+ head_days = partitioned_days(beginning, end_of_beginning_week)
177
+ tail_days = partitioned_days(beginning_of_end_week, ending)
178
+ whole_weeks = (beginning_of_end_week-end_of_beginning_week) / 7.days
179
+ head_days + (whole_weeks * days_per_week) + tail_days
180
+ else
181
+ partitioned_days(beginning, ending)
182
+ end
183
+ else
184
+ (ending.beginning_of_day.to_datetime -
185
+ beginning.beginning_of_day.to_datetime) + 1
186
+ end
187
+ end
188
+
189
+ # remaining_hours : the total number of hours that an ad will run from now
190
+ # to the end_datetime, taking day parting into account.
191
+ # Assume all dates/times are in the time_zone of the ad.
192
+ def remaining_hours(time)
193
+ ending = end_in_time_zone
194
+ beginning = [time, start_in_time_zone].compact.max
195
+ return 0 unless beginning < ending
196
+ if partitioned?
197
+
198
+ end_of_beginning_week = beginning_of_week(beginning) + 7.days
199
+ beginning_of_end_week = beginning_of_week(ending)
200
+
201
+ if end_of_beginning_week < beginning_of_end_week
202
+ head_hours = partitioned_hours(beginning, end_of_beginning_week)
203
+ tail_hours = partitioned_hours(beginning_of_end_week, ending)
204
+ whole_weeks = (beginning_of_end_week-end_of_beginning_week) / 7.days
205
+ head_hours + (whole_weeks * hours_per_week) + tail_hours
206
+ else
207
+ partitioned_hours(beginning, ending)
208
+ end
209
+ else
210
+ actual_hours(beginning, ending)
211
+ end
212
+ end
213
+
214
+ # remaining_hours_today : the total number of hours that an ad will run
215
+ # between now and the end of the day, taking day
216
+ # parting into account.
217
+ # Assume all dates/times are in the time_zone of the ad.
218
+ def remaining_hours_today(time)
219
+ beginning = [time, start_in_time_zone].compact.max
220
+ ending = [time.beginning_of_day + 1.day, end_in_time_zone].compact.min
221
+ return 0 unless beginning < ending
222
+ if partitioned?
223
+ partitioned_hours(beginning, ending)
224
+ else
225
+ actual_hours(beginning, ending)
226
+ end
227
+ end
228
+
229
+ # Assume time is in the time_zone of the ad.
230
+ # Assume a week begins on Sunday.
231
+ def beginning_of_week(time)
232
+ recent_monday = time.monday
233
+ sunday_before_recent_monday = recent_monday - 1.day
234
+ if (time - sunday_before_recent_monday) < 7.days
235
+ sunday_before_recent_monday
236
+ else
237
+ sunday_before_recent_monday + 7.days
238
+ end
239
+ end
240
+
241
+ def beginning_of_hour(time)
242
+ time.change(:min => 0, :sec => 0, :usec => 0)
243
+ end
244
+
245
+ # Assume beginning and ending are in the ad's time_zone.
246
+ # Assume beginning and ending are within the ad's date range.
247
+ # Assume ad is partitioned.
248
+ def partitioned_hours(beginning, ending)
249
+ return 0 unless beginning < ending
250
+ total = 0.0
251
+ this_hour = beginning_of_hour(beginning)
252
+ last_hour = beginning_of_hour(ending)
253
+ if this_hour.to_i == last_hour.to_i
254
+ if partitioned_hour?(this_hour)
255
+ total = actual_hours(beginning, ending)
256
+ end
257
+ else
258
+ next_hour = this_hour + 1.hour
259
+ if partitioned_hour?(this_hour)
260
+ total += actual_hours(beginning, next_hour)
261
+ end
262
+ each_hour(next_hour, last_hour) do |hour|
263
+ total += 1 if partitioned_hour?(hour)
264
+ end
265
+ if partitioned_hour?(last_hour)
266
+ total += actual_hours(last_hour, ending)
267
+ end
268
+ end
269
+ total
270
+ end
271
+
272
+ # Assume beginning and ending are in the ad's time_zone.
273
+ # Assume beginning and ending are within the ad's date range.
274
+ # Assume ad is partitioned.
275
+ # Assume beginning and ending are within a week of each other.
276
+ # Computes integer number of days where any part of the day is active.
277
+ def partitioned_days(beginning, ending)
278
+ return 0 unless beginning < ending
279
+
280
+ end_of_beginning_day = beginning.beginning_of_day + 1.day
281
+ beginning_of_end_day = ending.beginning_of_day
282
+
283
+ if end_of_beginning_day < beginning_of_end_day
284
+ days = 0
285
+ days += 1 if partitioned_hours(beginning, end_of_beginning_day) > 0
286
+ days += 1 if partitioned_hours(beginning_of_end_day, ending) > 0
287
+ each_day(end_of_beginning_day, beginning_of_end_day) do |day|
288
+ days += 1 if partitioned_day?(day)
289
+ end
290
+ days
291
+ else
292
+ (partitioned_hours(beginning,ending) > 0) ? 1 : 0
293
+ end
294
+ end
295
+
296
+ def each_day(beginning, ending)
297
+ time = beginning.clone
298
+ while (time < ending)
299
+ yield time
300
+ time += 1.day
301
+ end
302
+ end
303
+
304
+ def each_hour(beginning, ending)
305
+ time = beginning.clone
306
+ while(time < ending)
307
+ yield time
308
+ time += 1.hour
309
+ end
310
+ end
311
+
312
+ def actual_hours(beginning, ending)
313
+ (ending.to_f - beginning.to_f) / 3600
314
+ end
315
+
316
+ # Assumes ad is partitioned.
317
+ def days_per_week
318
+ (0..6).map{|d| day_partitions[d*24,24].include?('T') ? 1 : 0 }.sum
319
+ end
320
+
321
+ # Assumes ad is partitioned.
322
+ def hours_per_week
323
+ day_partitions.count("T")
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end