itunes-connect 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/itunes_connect +2 -1
- data/lib/itunes_connect/connection.rb +227 -98
- data/lib/itunes_connect/store.rb +19 -12
- metadata +63 -36
data/bin/itunes_connect
CHANGED
@@ -3,20 +3,50 @@ require "tempfile"
|
|
3
3
|
require "yaml"
|
4
4
|
require "zlib"
|
5
5
|
require "rubygems"
|
6
|
-
|
7
|
-
require "
|
6
|
+
gem 'mechanize'
|
7
|
+
require "mechanize"
|
8
|
+
|
9
|
+
# mechanize monkey patch
|
10
|
+
# handle Content-Encoding of 'agzip'
|
11
|
+
begin
|
12
|
+
require 'mechanize/chain/body_decoding_handler'
|
13
|
+
|
14
|
+
class Mechanize
|
15
|
+
class Chain
|
16
|
+
class BodyDecodingHandler
|
17
|
+
alias :orig_handle :handle
|
18
|
+
|
19
|
+
def handle(ctx, options = {})
|
20
|
+
response = options[:response]
|
21
|
+
encoding = response['Content-Encoding'] || ''
|
22
|
+
response['Content-Encoding'] = 'gzip' if encoding.downcase == 'agzip'
|
23
|
+
orig_handle(ctx, options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
8
29
|
|
9
30
|
module ItunesConnect
|
10
31
|
|
32
|
+
NETWORK_TIMEOUT = 60 # seconds
|
33
|
+
|
11
34
|
# Abstracts the iTunes Connect website.
|
12
35
|
# Implementation inspired by
|
13
36
|
# http://code.google.com/p/itunes-connect-scraper/
|
14
37
|
class Connection
|
15
38
|
|
16
|
-
REPORT_PERIODS = ["
|
39
|
+
REPORT_PERIODS = ["Weekly", "Daily"]
|
40
|
+
|
41
|
+
BASE_URL = 'https://itunesconnect.apple.com' # login base
|
42
|
+
REPORT_URL = 'https://reportingitc.apple.com/sales.faces'
|
43
|
+
LOGIN_URL = 'https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa'
|
17
44
|
|
18
|
-
|
19
|
-
|
45
|
+
# select ids:
|
46
|
+
# theForm:datePickerSourceSelectElementSales (daily)
|
47
|
+
# theForm:weekPickerSourceSelectElement (weekly)
|
48
|
+
ID_SELECT_DAILY = "theForm:datePickerSourceSelectElementSales"
|
49
|
+
ID_SELECT_WEEKLY = "theForm:weekPickerSourceSelectElement"
|
20
50
|
|
21
51
|
# Create a new instance with the username and password used to sign
|
22
52
|
# in to the iTunes Connect website
|
@@ -24,6 +54,7 @@ module ItunesConnect
|
|
24
54
|
@username, @password = username, password
|
25
55
|
@verbose = verbose
|
26
56
|
@debug = debug
|
57
|
+
@current_period = "Daily" # default period in reportingitc interface
|
27
58
|
end
|
28
59
|
|
29
60
|
def verbose? # :nodoc:
|
@@ -34,6 +65,10 @@ module ItunesConnect
|
|
34
65
|
!!@debug
|
35
66
|
end
|
36
67
|
|
68
|
+
def logged_in? # :nodoc:
|
69
|
+
!!@logged_in
|
70
|
+
end
|
71
|
+
|
37
72
|
# Retrieve a report from iTunes Connect. This method will return the
|
38
73
|
# raw report file as a String. If specified, the <tt>date</tt>
|
39
74
|
# parameter should be a <tt>Date</tt> instance, and the
|
@@ -44,118 +79,212 @@ module ItunesConnect
|
|
44
79
|
# Any dates given that equal the current date or newer will cause
|
45
80
|
# this method to raise an <tt>ArgumentError</tt>.
|
46
81
|
#
|
47
|
-
def get_report(date, out, period='Daily')
|
82
|
+
def get_report(date, out, period = 'Daily')
|
48
83
|
date = Date.parse(date) if String === date
|
49
84
|
if date >= Date.today
|
50
85
|
raise ArgumentError, "You must specify a date before today"
|
51
86
|
end
|
52
87
|
|
53
|
-
period = 'Monthly Free' if period == 'Monthly'
|
54
88
|
unless REPORT_PERIODS.member?(period)
|
55
89
|
raise ArgumentError, "'period' must be one of #{REPORT_PERIODS.join(', ')}"
|
56
90
|
end
|
57
91
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
raise ArgumentError, "No reports are available for that date" unless date_str
|
99
|
-
|
100
|
-
report = get_content(report_url, {
|
101
|
-
report_type_name => 'Summary',
|
102
|
-
date_type_name => period,
|
103
|
-
date_name => date_str,
|
104
|
-
'download' => 'Download',
|
105
|
-
'hiddenDayOrWeekSelection' => date_str,
|
106
|
-
'hiddenSubmitTypeName' => 'Download'
|
107
|
-
})
|
108
|
-
|
109
|
-
begin
|
110
|
-
gunzip = Zlib::GzipReader.new(StringIO.new(report))
|
111
|
-
out << gunzip.read
|
112
|
-
rescue => e
|
113
|
-
doc = Nokogiri::HTML(report)
|
114
|
-
msg = (doc / "//font[@id='iddownloadmsg']").text.strip
|
115
|
-
$stderr.puts "Unable to download the report, reason:"
|
116
|
-
$stderr.puts msg.strip
|
92
|
+
login unless self.logged_in?
|
93
|
+
|
94
|
+
# fetch report
|
95
|
+
# (cache report page)
|
96
|
+
fetch_report_page unless @report_page
|
97
|
+
|
98
|
+
# requested download date
|
99
|
+
date_str = date.strftime("%m/%d/%Y")
|
100
|
+
debug_msg("download date: #{date_str}")
|
101
|
+
|
102
|
+
# determine available download options
|
103
|
+
@select_name = period == 'Daily' ? ID_SELECT_DAILY : ID_SELECT_WEEKLY
|
104
|
+
options = @report_page.search(".//select[@id='#{@select_name}']/option")
|
105
|
+
options = options.collect { |i| i ? i['value'] : nil } if options
|
106
|
+
raise "unable to determine daily report options" unless options
|
107
|
+
|
108
|
+
debug_msg("options: #{options.inspect}")
|
109
|
+
|
110
|
+
# constrain download to available reports
|
111
|
+
available = options.find { |i| i <= date_str } ? true : false
|
112
|
+
|
113
|
+
unless available
|
114
|
+
raise ArgumentError, "No #{period} reports are available for #{date_str}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# get ajax parameter name for Daily/Weekly (<a> id)
|
118
|
+
report_period_link = @report_page.link_with(:text => /#{period}/)
|
119
|
+
@report_period_id = report_period_link.node['id']
|
120
|
+
raise "could not determine form period AJAX parameter" unless @report_period_id
|
121
|
+
|
122
|
+
# get ajax parameter name from <select> onchange attribute
|
123
|
+
# 'parameters':{'theForm:j_id_jsp_4933398_30':'theForm:j_id_jsp_4933398_30'}
|
124
|
+
report_date_select = @report_page.search(".//select[@id='#{@select_name}']")
|
125
|
+
@report_date_id = report_date_select[0]['onchange'].match(/parameters':\{'(.*?)'/)[1] rescue nil
|
126
|
+
raise "could not determine form date AJAX parameter" unless @report_date_id
|
127
|
+
|
128
|
+
# select report period to download (Weekly/Daily)
|
129
|
+
if @current_period != period
|
130
|
+
change_report(@report_page, date_str, @report_period_id => @report_period_id)
|
131
|
+
@current_period = period
|
117
132
|
end
|
133
|
+
|
134
|
+
# select report date
|
135
|
+
page = change_report(@report_page, date_str, @report_date_id => @report_date_id)
|
136
|
+
|
137
|
+
# after selecting report type, recheck if selection is available.
|
138
|
+
# (selection options exist even when the report isn't available, so
|
139
|
+
# we need to do another check here)
|
140
|
+
dump(client, page)
|
141
|
+
available = !page.body.match(/There is no report available for this selection/)
|
142
|
+
unless available
|
143
|
+
raise ArgumentError, "No #{period} reports are available for #{date_str}"
|
144
|
+
end
|
145
|
+
|
146
|
+
# download the report
|
147
|
+
page = @report_page.form_with(:name => 'theForm') do |form|
|
148
|
+
form['theForm:xyz'] = 'notnormal'
|
149
|
+
form['theForm:downloadLabel2'] = 'theForm:downloadLabel2'
|
150
|
+
form[@select_name] = date_str
|
151
|
+
|
152
|
+
form.delete_field!('AJAXREQUEST')
|
153
|
+
form.delete_field!(@report_period_id)
|
154
|
+
form.delete_field!(@report_date_id)
|
155
|
+
|
156
|
+
debug_form(form)
|
157
|
+
end.submit
|
158
|
+
|
159
|
+
dump(client, page)
|
160
|
+
report = page.body.to_s
|
161
|
+
debug_msg("report is #{report.length} bytes")
|
162
|
+
out << report
|
163
|
+
report
|
118
164
|
end
|
119
165
|
|
120
166
|
private
|
121
167
|
|
122
|
-
def
|
123
|
-
|
168
|
+
def debug_msg(message)
|
169
|
+
return unless self.debug?
|
170
|
+
puts message
|
124
171
|
end
|
125
172
|
|
126
|
-
def
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
}
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
173
|
+
def change_report(report_page, date_str, params = {})
|
174
|
+
page = report_page.form_with(:name => 'theForm') do |form|
|
175
|
+
form.delete_field!(@report_period_id)
|
176
|
+
form.delete_field!(@report_date_id)
|
177
|
+
form.delete_field!('theForm:downloadLabel2')
|
178
|
+
|
179
|
+
params.each { |k,v| form[k] = v }
|
180
|
+
|
181
|
+
form['AJAXREQUEST'] = @ajax_id
|
182
|
+
form['theForm:xyz'] = 'notnormal'
|
183
|
+
form[@select_name] = date_str
|
184
|
+
|
185
|
+
debug_form(form)
|
186
|
+
end.submit
|
187
|
+
end
|
188
|
+
|
189
|
+
# fetch main report page (sales.faces)
|
190
|
+
def fetch_report_page
|
191
|
+
@report_page = client.get(REPORT_URL)
|
192
|
+
dump(client, @report_page)
|
193
|
+
|
194
|
+
# get ajax parameter name for AJAXREQUEST (<a> id)
|
195
|
+
# AJAX.Submit('theForm:j_id_jsp_4933398_2'
|
196
|
+
@ajax_id = @report_page.body.match(/AJAX\.Submit\('([^\']+)'/)[1] rescue nil
|
197
|
+
raise "could not determine form AJAX id" unless @ajax_id
|
198
|
+
|
199
|
+
@report_page
|
200
|
+
end
|
201
|
+
|
202
|
+
# log in and navigate to the reporting interface
|
203
|
+
def login
|
204
|
+
debug_msg("getting login page")
|
205
|
+
page = client.get(LOGIN_URL)
|
206
|
+
|
207
|
+
while true do
|
208
|
+
debug_msg("logging in")
|
209
|
+
page = page.form_with(:name => 'appleConnectForm') do |form|
|
210
|
+
raise "login form not found" unless form
|
211
|
+
|
212
|
+
form['theAccountName'] = @username
|
213
|
+
form['theAccountPW'] = @password
|
214
|
+
form['1.Continue.x'] = '35'
|
215
|
+
form['1.Continue.y'] = '16'
|
216
|
+
form['theAuxValue'] = ''
|
217
|
+
end.submit
|
218
|
+
|
219
|
+
dump(client, page)
|
220
|
+
|
221
|
+
# 'session expired' message sometimes appears after logging in. weird.
|
222
|
+
expired = page.body.match(/Your session has expired.*?href\="(.*?)"/)
|
223
|
+
if expired
|
224
|
+
debug_msg("expired session detected, retrying login")
|
225
|
+
page = client.get(expired[1])
|
226
|
+
next # retry login
|
153
227
|
end
|
154
|
-
|
228
|
+
|
229
|
+
break # done logging in
|
230
|
+
end
|
231
|
+
|
232
|
+
# skip past new license available notifications
|
233
|
+
new_license = page.body.match(/License Agreement Update/)
|
234
|
+
if new_license
|
235
|
+
debug_msg("new license detected, skipping")
|
236
|
+
submit_parameter = page.body.match(/input.*?type\="image".*?name="(.*?)"/)[1] rescue nil
|
237
|
+
raise "could not determine how to skip new license agreement" unless submit_parameter
|
238
|
+
page = page.form_with(:name => 'mainForm') do |form|
|
239
|
+
form["#{submit_parameter}.x"] = 40
|
240
|
+
form["#{submit_parameter}.y"] = 18
|
241
|
+
end.submit
|
155
242
|
end
|
156
243
|
|
157
|
-
|
158
|
-
|
244
|
+
# Click the sales and trends link
|
245
|
+
sales_link = page.link_with(:text => /Sales and Trends/)
|
246
|
+
raise "Sales and Trends link not found" unless sales_link
|
247
|
+
page2 = client.click(sales_link)
|
248
|
+
dump(client, page2)
|
249
|
+
|
250
|
+
@report_page = nil # clear any cached report page
|
251
|
+
@logged_in = true
|
252
|
+
end
|
253
|
+
|
254
|
+
def client
|
255
|
+
return @client if @client
|
256
|
+
@client = Mechanize.new
|
257
|
+
@client.read_timeout = NETWORK_TIMEOUT
|
258
|
+
@client.open_timeout = NETWORK_TIMEOUT
|
259
|
+
@client.user_agent_alias = 'Mac FireFox'
|
260
|
+
@client
|
261
|
+
end
|
262
|
+
|
263
|
+
# dump state information to a file (debugging)
|
264
|
+
def dump(a, page)
|
265
|
+
return unless self.debug?
|
266
|
+
|
267
|
+
url = a.current_page.uri.request_uri
|
268
|
+
puts "current page: #{url}"
|
269
|
+
|
270
|
+
md5 = Digest::MD5.new; md5 << url; md5 << Time.now.to_s
|
271
|
+
path = File.join(Dir.tmpdir, md5.to_s + ".html")
|
272
|
+
out = open(path, "w") do |f|
|
273
|
+
f << "Current page: #{url}"
|
274
|
+
f << "Headers: #{page.header}"
|
275
|
+
f << page.body
|
276
|
+
end
|
277
|
+
|
278
|
+
puts "#{url} -> #{path}"
|
279
|
+
end
|
280
|
+
|
281
|
+
def debug_form(form)
|
282
|
+
return unless self.debug?
|
283
|
+
|
284
|
+
puts "\nsubmitting form:"
|
285
|
+
form.keys.each do |key|
|
286
|
+
puts "#{key}: #{form[key]}"
|
287
|
+
end
|
159
288
|
end
|
160
289
|
|
161
290
|
end
|
data/lib/itunes_connect/store.rb
CHANGED
@@ -11,7 +11,8 @@ module ItunesConnect
|
|
11
11
|
@db = SQLite3::Database.new(file)
|
12
12
|
if @db.table_info("reports").empty?
|
13
13
|
@db.execute("CREATE TABLE reports (id INTEGER PRIMARY KEY, " +
|
14
|
-
"report_date DATE, country TEXT
|
14
|
+
"report_date DATE NOT NULL, country TEXT NOT NULL, " +
|
15
|
+
"install_count INTEGER, " +
|
15
16
|
"update_count INTEGER)")
|
16
17
|
@db.execute("CREATE UNIQUE INDEX u_reports_idx ON reports " +
|
17
18
|
"(report_date, country)")
|
@@ -25,11 +26,11 @@ module ItunesConnect
|
|
25
26
|
|
26
27
|
# Add a record to this instance
|
27
28
|
def add(date, country, install_count, update_count)
|
28
|
-
@db.execute("INSERT INTO reports (report_date, country, " +
|
29
|
-
|
30
|
-
|
29
|
+
ret = @db.execute("INSERT INTO reports (report_date, country, " +
|
30
|
+
"install_count, update_count) VALUES (?, ?, ?, ?)",
|
31
|
+
[format_date(date), country, install_count, update_count])
|
31
32
|
true
|
32
|
-
rescue SQLite3::
|
33
|
+
rescue SQLite3::ConstraintException => e
|
33
34
|
if e.message =~ /columns .* are not unique/
|
34
35
|
$stdout.puts "Skipping existing row for #{country} on #{date}" if verbose?
|
35
36
|
false
|
@@ -56,12 +57,12 @@ module ItunesConnect
|
|
56
57
|
|
57
58
|
if opts[:from]
|
58
59
|
clauses << "report_date >= ?"
|
59
|
-
params << opts[:from]
|
60
|
+
params << format_date(opts[:from])
|
60
61
|
end
|
61
62
|
|
62
63
|
if opts[:to]
|
63
64
|
clauses << "report_date <= ?"
|
64
|
-
params << opts[:to]
|
65
|
+
params << format_date(opts[:to])
|
65
66
|
end
|
66
67
|
|
67
68
|
if opts[:country]
|
@@ -73,9 +74,9 @@ module ItunesConnect
|
|
73
74
|
sql << clauses.join(" AND ") unless params.empty?
|
74
75
|
sql << " ORDER BY report_date DESC"
|
75
76
|
|
76
|
-
@db.execute(sql,
|
77
|
+
@db.execute(sql, params).map do |row|
|
77
78
|
OpenStruct.new({
|
78
|
-
:report_date => Date.parse(row[1]),
|
79
|
+
:report_date => row[1] ? Date.parse(row[1]) : nil,
|
79
80
|
:country => row[2],
|
80
81
|
:install_count => row[3].to_i,
|
81
82
|
:update_count => row[4].to_i
|
@@ -100,12 +101,12 @@ module ItunesConnect
|
|
100
101
|
|
101
102
|
if opts[:from]
|
102
103
|
clauses << "report_date >= ?"
|
103
|
-
params << opts[:from]
|
104
|
+
params << format_date(opts[:from])
|
104
105
|
end
|
105
106
|
|
106
107
|
if opts[:to]
|
107
108
|
clauses << "report_date <= ?"
|
108
|
-
params << opts[:to]
|
109
|
+
params << format_date(opts[:to])
|
109
110
|
end
|
110
111
|
|
111
112
|
if opts[:country]
|
@@ -117,7 +118,7 @@ module ItunesConnect
|
|
117
118
|
sql << clauses.join(" AND ") unless params.empty?
|
118
119
|
sql << " GROUP BY country ORDER BY country"
|
119
120
|
|
120
|
-
@db.execute(sql,
|
121
|
+
@db.execute(sql, params).map do |row|
|
121
122
|
OpenStruct.new({
|
122
123
|
:country => row[0],
|
123
124
|
:install_count => row[1].to_i,
|
@@ -125,5 +126,11 @@ module ItunesConnect
|
|
125
126
|
})
|
126
127
|
end
|
127
128
|
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def format_date(date)
|
133
|
+
date.is_a?(Date) ? date.strftime("%Y-%m-%d") : date
|
134
|
+
end
|
128
135
|
end
|
129
136
|
end
|
metadata
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: itunes-connect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 55
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 10
|
9
|
+
- 0
|
10
|
+
version: 0.10.0
|
5
11
|
platform: ruby
|
6
12
|
authors:
|
7
13
|
- Alex Vollmer
|
@@ -9,69 +15,84 @@ autorequire:
|
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date:
|
18
|
+
date: 2010-09-22 00:00:00 +09:30
|
13
19
|
default_executable: itunes_connect
|
14
20
|
dependencies:
|
15
21
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
name: mechanize
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
20
26
|
requirements:
|
21
27
|
- - ~>
|
22
28
|
- !ruby/object:Gem::Version
|
23
|
-
|
24
|
-
|
25
|
-
-
|
26
|
-
|
29
|
+
hash: 23
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
- 0
|
34
|
+
version: 1.0.0
|
27
35
|
type: :runtime
|
28
|
-
|
29
|
-
version_requirements: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ~>
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: "1.3"
|
34
|
-
version:
|
36
|
+
version_requirements: *id001
|
35
37
|
- !ruby/object:Gem::Dependency
|
36
38
|
name: clip
|
37
|
-
|
38
|
-
|
39
|
-
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
40
42
|
requirements:
|
41
43
|
- - ">="
|
42
44
|
- !ruby/object:Gem::Version
|
45
|
+
hash: 21
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 0
|
49
|
+
- 1
|
43
50
|
version: 1.0.1
|
44
|
-
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
45
53
|
- !ruby/object:Gem::Dependency
|
46
54
|
name: sqlite3-ruby
|
47
|
-
|
48
|
-
|
49
|
-
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
50
58
|
requirements:
|
51
59
|
- - ~>
|
52
60
|
- !ruby/object:Gem::Version
|
61
|
+
hash: 11
|
62
|
+
segments:
|
63
|
+
- 1
|
64
|
+
- 2
|
53
65
|
version: "1.2"
|
54
|
-
|
66
|
+
type: :runtime
|
67
|
+
version_requirements: *id003
|
55
68
|
- !ruby/object:Gem::Dependency
|
56
69
|
name: rspec
|
57
|
-
|
58
|
-
|
59
|
-
|
70
|
+
prerelease: false
|
71
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
60
73
|
requirements:
|
61
74
|
- - ">="
|
62
75
|
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
63
79
|
version: "0"
|
64
|
-
|
80
|
+
type: :development
|
81
|
+
version_requirements: *id004
|
65
82
|
- !ruby/object:Gem::Dependency
|
66
83
|
name: fakeweb
|
67
|
-
|
68
|
-
|
69
|
-
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
70
87
|
requirements:
|
71
88
|
- - ">="
|
72
89
|
- !ruby/object:Gem::Version
|
90
|
+
hash: 3
|
91
|
+
segments:
|
92
|
+
- 0
|
73
93
|
version: "0"
|
74
|
-
|
94
|
+
type: :development
|
95
|
+
version_requirements: *id005
|
75
96
|
description: Programmatic and command-line access to iTunes Connect Reports
|
76
97
|
email: alex.vollmer@gmail.com
|
77
98
|
executables:
|
@@ -116,21 +137,27 @@ rdoc_options:
|
|
116
137
|
require_paths:
|
117
138
|
- lib
|
118
139
|
required_ruby_version: !ruby/object:Gem::Requirement
|
140
|
+
none: false
|
119
141
|
requirements:
|
120
142
|
- - ">="
|
121
143
|
- !ruby/object:Gem::Version
|
144
|
+
hash: 3
|
145
|
+
segments:
|
146
|
+
- 0
|
122
147
|
version: "0"
|
123
|
-
version:
|
124
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
125
150
|
requirements:
|
126
151
|
- - ">="
|
127
152
|
- !ruby/object:Gem::Version
|
153
|
+
hash: 3
|
154
|
+
segments:
|
155
|
+
- 0
|
128
156
|
version: "0"
|
129
|
-
version:
|
130
157
|
requirements: []
|
131
158
|
|
132
159
|
rubyforge_project:
|
133
|
-
rubygems_version: 1.3.
|
160
|
+
rubygems_version: 1.3.7
|
134
161
|
signing_key:
|
135
162
|
specification_version: 3
|
136
163
|
summary: Get your iTunes Connect Reports
|