neuron-client 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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