itunes-connect 0.9.0 → 0.10.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.
- 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
|