tmc-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +44 -0
- data/Readme.md +0 -0
- data/bin/tmc-client +42 -0
- data/lib/tmc-client/client.rb +386 -0
- data/lib/tmc-client/config.default.yml +2 -0
- data/lib/tmc-client/config.yml +0 -0
- data/lib/tmc-client/my_config.rb +56 -0
- data/lib/tmc-client/old-client.rb +208 -0
- data/lib/tmc-client.rb +30 -0
- data/specs/download_spec.rb +28 -0
- data/specs/ex.zip +0 -0
- data/specs/list_spec.rb +27 -0
- data/specs/submit_spec.rb +25 -0
- data/specs/update_ex.zip +0 -0
- data/specs/update_spec.rb +43 -0
- data/specs/update_universal_ex.zip +0 -0
- data/tmc +3 -0
- data/tmc-client.gemspec +24 -0
- metadata +212 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
coderay (1.0.9)
|
5
|
+
diff-lcs (1.1.3)
|
6
|
+
faraday (0.8.7)
|
7
|
+
multipart-post (~> 1.1)
|
8
|
+
fileutils (0.7)
|
9
|
+
rmagick (>= 2.13.1)
|
10
|
+
highline (1.6.18)
|
11
|
+
json (1.7.7)
|
12
|
+
metaclass (0.0.1)
|
13
|
+
method_source (0.8.1)
|
14
|
+
mocha (0.13.3)
|
15
|
+
metaclass (~> 0.0.1)
|
16
|
+
multipart-post (1.2.0)
|
17
|
+
pry (0.9.12)
|
18
|
+
coderay (~> 1.0.5)
|
19
|
+
method_source (~> 0.8)
|
20
|
+
slop (~> 3.4)
|
21
|
+
rake (10.0.4)
|
22
|
+
rmagick (2.13.2)
|
23
|
+
rspec (2.12.0)
|
24
|
+
rspec-core (~> 2.12.0)
|
25
|
+
rspec-expectations (~> 2.12.0)
|
26
|
+
rspec-mocks (~> 2.12.0)
|
27
|
+
rspec-core (2.12.2)
|
28
|
+
rspec-expectations (2.12.1)
|
29
|
+
diff-lcs (~> 1.1.3)
|
30
|
+
rspec-mocks (2.12.2)
|
31
|
+
slop (3.4.4)
|
32
|
+
|
33
|
+
PLATFORMS
|
34
|
+
ruby
|
35
|
+
|
36
|
+
DEPENDENCIES
|
37
|
+
faraday
|
38
|
+
fileutils
|
39
|
+
highline
|
40
|
+
json
|
41
|
+
mocha
|
42
|
+
pry
|
43
|
+
rake
|
44
|
+
rspec
|
data/Readme.md
ADDED
File without changes
|
data/bin/tmc-client
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'tmc-client/client'
|
5
|
+
|
6
|
+
@c = Client.new
|
7
|
+
|
8
|
+
commands = {
|
9
|
+
list: :list,
|
10
|
+
download: :download,
|
11
|
+
submit: :submit_exercise,
|
12
|
+
update: :update_exercise,
|
13
|
+
auth: :auth,
|
14
|
+
check: :check,
|
15
|
+
init: :init_course,
|
16
|
+
status: :status,
|
17
|
+
solution: :solution,
|
18
|
+
get: :get,
|
19
|
+
set: :set
|
20
|
+
}
|
21
|
+
|
22
|
+
command = ARGV[0].to_s
|
23
|
+
sub_arguments = ARGV.drop 1
|
24
|
+
|
25
|
+
unless commands[command.to_sym].nil?
|
26
|
+
@c.send(commands[command.to_sym], *sub_arguments) #unless command
|
27
|
+
else
|
28
|
+
puts "Unknown command"
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
rescue LoadError => e
|
33
|
+
$stderr.puts <<-EOS
|
34
|
+
#{'*'*50}
|
35
|
+
puts "An error occurred. Make sure the command was triggered in the right directory. Also make sure you have a valid server address and authentication information. If these do not help, please contact an administrator."
|
36
|
+
#{'*'*50}
|
37
|
+
EOS
|
38
|
+
|
39
|
+
raise e
|
40
|
+
|
41
|
+
exit(1)
|
42
|
+
end
|
@@ -0,0 +1,386 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'highline/import'
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'faraday'
|
6
|
+
require 'yaml'
|
7
|
+
require 'pry'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'tempfile'
|
10
|
+
require 'pp'
|
11
|
+
require_relative 'my_config'
|
12
|
+
|
13
|
+
class Client
|
14
|
+
attr_accessor :courses, :config, :conn, :output, :input
|
15
|
+
def initialize(output=$stdout, input=$stdin)
|
16
|
+
@config = MyConfig.new
|
17
|
+
@output = output
|
18
|
+
@input = input
|
19
|
+
begin
|
20
|
+
@config.server_url ||= request_server_url
|
21
|
+
rescue
|
22
|
+
request_server_url
|
23
|
+
end
|
24
|
+
init_connection()
|
25
|
+
if @config.auth
|
26
|
+
begin
|
27
|
+
@courses = JSON.parse get_courses_json
|
28
|
+
rescue => e
|
29
|
+
auth
|
30
|
+
end
|
31
|
+
else
|
32
|
+
output.puts "No username/password. run tmc auth"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# stupid name - this should create connection, but not fetch courses.json!
|
37
|
+
def init_connection()
|
38
|
+
@conn = Faraday.new(:url => @config.server_url) do |faraday|
|
39
|
+
faraday.request :multipart
|
40
|
+
faraday.request :url_encoded # form-encode POST params
|
41
|
+
#faraday.response :logger # log requests to STDOUT We dont want to do this in production!
|
42
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
43
|
+
end
|
44
|
+
|
45
|
+
if @config.auth
|
46
|
+
@conn.headers[Faraday::Request::Authorization::KEY] = @config.auth
|
47
|
+
else
|
48
|
+
auth
|
49
|
+
@conn.headers[Faraday::Request::Authorization::KEY] = @config.auth
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(*args)
|
55
|
+
target = args.first
|
56
|
+
case target
|
57
|
+
when 'url' then output.puts @config.server_url
|
58
|
+
else output.puts "No property selected"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def set(*args)
|
63
|
+
target = args.first
|
64
|
+
case target
|
65
|
+
when "url"
|
66
|
+
@config.server_url = args[1] if args.count >= 2
|
67
|
+
else output.puts "No property selected"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def check
|
72
|
+
output.puts get_courses_json
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_courses_json
|
76
|
+
data = @conn.get('courses.json', {api_version: 5}).body
|
77
|
+
raise "Error with autentikation" if data['error']
|
78
|
+
data
|
79
|
+
end
|
80
|
+
|
81
|
+
# Path can be relative or absolute
|
82
|
+
def is_universal_project?(path)
|
83
|
+
File.exists? File.join(path, ".universal")
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_password(prompt="Enter Password")
|
87
|
+
ask(prompt) {|q| q.echo = false}
|
88
|
+
end
|
89
|
+
|
90
|
+
def auth
|
91
|
+
output.print "Username: "
|
92
|
+
username = @input.gets.chomp.strip
|
93
|
+
password = get_password("Password (typing is hidden): ")
|
94
|
+
@config.auth = nil
|
95
|
+
@conn.basic_auth(username, password)
|
96
|
+
@config.auth = @conn.headers[Faraday::Request::Authorization::KEY]
|
97
|
+
end
|
98
|
+
|
99
|
+
def request_server_url
|
100
|
+
output.print "Server url: "
|
101
|
+
@config.server_url = @input.gets.chomp.strip
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_real_name(headers)
|
105
|
+
name = headers['content-disposition']
|
106
|
+
name.split("\"").compact[-1]
|
107
|
+
end
|
108
|
+
|
109
|
+
def fetch_zip(zip_url)
|
110
|
+
@conn.get(zip_url)
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_course_name
|
114
|
+
get_my_course['name']
|
115
|
+
end
|
116
|
+
|
117
|
+
def list(mode=:courses)
|
118
|
+
mode = mode.to_sym
|
119
|
+
case mode
|
120
|
+
when :courses
|
121
|
+
list_courses
|
122
|
+
when :exercises
|
123
|
+
list_exercises
|
124
|
+
else
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def list_exercises(course_dir_name=nil)
|
129
|
+
course_dir_name = current_directory_name if course_dir_name.nil?
|
130
|
+
course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
|
131
|
+
raise "Invalid course name" if course.nil?
|
132
|
+
course["exercises"].each do |ex|
|
133
|
+
output.puts "#{ex['name']} #{ex['deadline']}" if ex["returnable"]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def list_courses
|
138
|
+
output.puts "No courses available. Make sure your server url is correct and that you have authenticated." if @courses.nil? or @courses["courses"].nil?
|
139
|
+
@courses['courses'].each do |course|
|
140
|
+
output.puts "#{course['name']}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def current_directory_name
|
145
|
+
File.basename(Dir.getwd)
|
146
|
+
end
|
147
|
+
|
148
|
+
def previous_directory_name
|
149
|
+
path = Dir.getwd
|
150
|
+
directories = path.split("/")
|
151
|
+
directories[directories.count - 2]
|
152
|
+
end
|
153
|
+
|
154
|
+
def init_course(course_name)
|
155
|
+
FileUtils.mkdir_p(course_name)
|
156
|
+
output.print "Would you like to download all available exercises? Yn"
|
157
|
+
if ["", "y", "Y"].include? @input.gets.strip.chomp
|
158
|
+
Dir.chdir(course_name) do
|
159
|
+
course = @courses['courses'].select { |course| course['name'] == course_name }.first
|
160
|
+
download_new_exercises
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def download(*args)
|
166
|
+
if args.include? "all" or args.include? "-a" or args.include? "--all"
|
167
|
+
download_new_exercises
|
168
|
+
else
|
169
|
+
download_new_exercise(*args)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def solution(exercise_dir_name=nil)
|
174
|
+
# Initialize course and exercise names to identify exercise to submit (from json)
|
175
|
+
if exercise_dir_name.nil?
|
176
|
+
exercise_dir_name = current_directory_name
|
177
|
+
course_dir_name = previous_directory_name
|
178
|
+
else
|
179
|
+
course_dir_name = current_directory_name
|
180
|
+
end
|
181
|
+
|
182
|
+
exercise_dir_name.chomp("/")
|
183
|
+
# Find course and exercise ids
|
184
|
+
course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
|
185
|
+
raise "Invalid course name" if course.nil?
|
186
|
+
exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
|
187
|
+
raise "Invalid exercise name" if exercise.nil?
|
188
|
+
|
189
|
+
update_from_zip(exercise['solution_zip_url'], exercise_dir_name, course_dir_name, exercise, course)
|
190
|
+
end
|
191
|
+
|
192
|
+
def update_from_zip(zip_url, exercise_dir_name, course_dir_name, exercise, course)
|
193
|
+
zip = fetch_zip(zip_url)
|
194
|
+
output.puts "URL: #{zip_url}"
|
195
|
+
work_dir = Dir.pwd
|
196
|
+
to_dir = if Dir.pwd.chomp("/").split("/").last == exercise_dir_name
|
197
|
+
work_dir
|
198
|
+
else
|
199
|
+
File.join(work_dir, exercise_dir_name)
|
200
|
+
end
|
201
|
+
Dir.mktmpdir do |tmpdir|
|
202
|
+
Dir.chdir tmpdir do
|
203
|
+
File.open("tmp.zip", 'wb') {|file| file.write(zip.body)}
|
204
|
+
`unzip -n tmp.zip && rm tmp.zip`
|
205
|
+
files = Dir.glob('**/*')
|
206
|
+
all_selected = false
|
207
|
+
files.each do |file|
|
208
|
+
next if file == exercise_dir_name or File.directory? file
|
209
|
+
output.puts "Want to update #{file}? Yn[A]" unless all_selected
|
210
|
+
input = @input.gets.chomp.strip unless all_selected
|
211
|
+
all_selected = true if input == "A" or input == "a"
|
212
|
+
if all_selected or (["", "y", "Y"].include? input)
|
213
|
+
begin
|
214
|
+
to = File.join(to_dir,file.split("/")[1..-1].join("/"))
|
215
|
+
output.puts "copying #{file} to #{to}"
|
216
|
+
unless File.directory? to
|
217
|
+
FileUtils.mkdir_p(to.split("/")[0..-2].join("/"))
|
218
|
+
else
|
219
|
+
FileUtils.mkdir_p(to)
|
220
|
+
end
|
221
|
+
FileUtils.cp_r(file, to)
|
222
|
+
rescue ArgumentError => e
|
223
|
+
output.puts "An error occurred #{e}"
|
224
|
+
end
|
225
|
+
elsif input == "b"
|
226
|
+
binding.pry
|
227
|
+
else
|
228
|
+
output.puts "Skipping file #{file}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def update_automatically_detected_project_from_zip(zip_url, exercise_dir_name, course_dir_name, exercise, course)
|
236
|
+
zip = fetch_zip(exercise['zip_url'])
|
237
|
+
work_dir = Dir.pwd
|
238
|
+
to_dir = if Dir.pwd.chomp("/").split("/").last == exercise_dir_name
|
239
|
+
work_dir
|
240
|
+
else
|
241
|
+
File.join(work_dir, exercise_dir_name)
|
242
|
+
end
|
243
|
+
Dir.mktmpdir do |tmpdir|
|
244
|
+
Dir.chdir tmpdir do
|
245
|
+
File.open("tmp.zip", 'wb') {|file| file.write(zip.body)}
|
246
|
+
`unzip -n tmp.zip && rm tmp.zip`
|
247
|
+
files = Dir.glob('**/*')
|
248
|
+
|
249
|
+
files.each do |file|
|
250
|
+
next if file == exercise_dir_name or file.to_s.include? "src" or File.directory? file
|
251
|
+
begin
|
252
|
+
to = File.join(to_dir,file.split("/")[1..-1].join("/"))
|
253
|
+
output.puts "copying #{file} to #{to}"
|
254
|
+
unless File.directory? to
|
255
|
+
FileUtils.mkdir_p(to.split("/")[0..-2].join("/"))
|
256
|
+
else
|
257
|
+
FileUtils.mkdir_p(to)
|
258
|
+
end
|
259
|
+
FileUtils.cp_r(file, to)
|
260
|
+
rescue ArgumentError => e
|
261
|
+
output.puts "An error occurred #{e}"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def filter_returnable(exercises)
|
269
|
+
exercises.collect { |ex| ex['name'] if ex['returnable'] }.compact
|
270
|
+
end
|
271
|
+
|
272
|
+
def download_new_exercises(course_dir_name=nil)
|
273
|
+
course_dir_name = current_directory_name if course_dir_name.nil?
|
274
|
+
course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
|
275
|
+
raise "Invalid course name" if course.nil?
|
276
|
+
filter_returnable(course['exercises']).each do |ex_name|
|
277
|
+
begin
|
278
|
+
download_new_exercise(ex_name)
|
279
|
+
rescue
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def download_new_exercise(exercise_dir_name=nil)
|
285
|
+
# Initialize course and exercise names to identify exercise to submit (from json)
|
286
|
+
course_dir_name = current_directory_name
|
287
|
+
exercise_dir_name.chomp("/")
|
288
|
+
# Find course and exercise ids
|
289
|
+
course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
|
290
|
+
raise "Invalid course name" if course.nil?
|
291
|
+
exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
|
292
|
+
raise "Invalid exercise name" if exercise.nil?
|
293
|
+
|
294
|
+
raise "Exercise already downloaded" if File.exists? exercise['name']
|
295
|
+
zip = fetch_zip(exercise['zip_url'])
|
296
|
+
File.open("tmp.zip", 'wb') {|file| file.write(zip.body)}
|
297
|
+
`unzip -n tmp.zip && rm tmp.zip`
|
298
|
+
end
|
299
|
+
|
300
|
+
# Filepath can be either relative or absolute
|
301
|
+
def zip_file_content(filepath)
|
302
|
+
`zip -r -q tmp_submit.zip #{filepath}`
|
303
|
+
#`zip -r -q - #{filepath}`
|
304
|
+
end
|
305
|
+
|
306
|
+
# Call in exercise root
|
307
|
+
# Zipping to stdout zip -r -q - tmc
|
308
|
+
def submit_exercise(*args)
|
309
|
+
# Initialize course and exercise names to identify exercise to submit (from json)
|
310
|
+
if args.count == 0 or args.all? { |arg| arg.start_with? "-" }
|
311
|
+
exercise_dir_name = current_directory_name
|
312
|
+
course_dir_name = previous_directory_name
|
313
|
+
zip_file_content(".")
|
314
|
+
else
|
315
|
+
exercise_dir_name = args.first
|
316
|
+
course_dir_name = current_directory_name
|
317
|
+
zip_file_content(exercise_dir_name)
|
318
|
+
end
|
319
|
+
|
320
|
+
exercise_dir_name.chomp("/")
|
321
|
+
exercise_id = 0
|
322
|
+
# Find course and exercise ids
|
323
|
+
course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
|
324
|
+
raise "Invalid course name" if course.nil?
|
325
|
+
exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
|
326
|
+
raise "Invalid exercise name" if exercise.nil?
|
327
|
+
|
328
|
+
# Submit
|
329
|
+
payload={:submission => {}}
|
330
|
+
payload[:request_review] = true if args.include? "--request-review" or args.include? "-r" or args.include? "--review"
|
331
|
+
payload[:paste] = true if args.include? "--paste" or args.include? "-p" or args.include? "--public"
|
332
|
+
payload[:submission][:file] = Faraday::UploadIO.new('tmp_submit.zip', 'application/zip')
|
333
|
+
response = @conn.post "/exercises/#{exercise['id']}/submissions.json?api_version=5&client=netbeans_plugin&client_version=1", payload
|
334
|
+
submission_url = JSON.parse(response.body)['submission_url']
|
335
|
+
puts "Submission url: #{submission_url}"
|
336
|
+
|
337
|
+
if (args & %w{-q --quiet -s --silent}).empty?
|
338
|
+
while status(submission_url) == "processing"
|
339
|
+
sleep(1)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
FileUtils.rm 'tmp_submit.zip'
|
344
|
+
payload
|
345
|
+
end
|
346
|
+
|
347
|
+
def status(submission_id_or_url)
|
348
|
+
url = (submission_id_or_url.include? "submissions") ? submission_id_or_url : "/submissions/#{submission_id_or_url}.json?api_version=5"
|
349
|
+
json = JSON.parse(@conn.get(url).body)
|
350
|
+
if json['status'] != 'processing'
|
351
|
+
puts "Status: #{json['status']}"
|
352
|
+
puts "Points: #{json['points'].inspect}"
|
353
|
+
puts "Tests:"
|
354
|
+
json['test_cases'].each do |test|
|
355
|
+
puts "#{test['name']} : #{(test['successful']) ? 'Ok' : 'Fail'}#{(test['message'].nil?) ? '' : (' : ' + test['message'])}"
|
356
|
+
end
|
357
|
+
end
|
358
|
+
json['status']
|
359
|
+
end
|
360
|
+
|
361
|
+
def update_exercise(exercise_dir_name=nil)
|
362
|
+
# Initialize course and exercise names to identify exercise to submit (from json)
|
363
|
+
is_universal = false
|
364
|
+
if exercise_dir_name.nil?
|
365
|
+
exercise_dir_name = current_directory_name
|
366
|
+
course_dir_name = previous_directory_name
|
367
|
+
is_universal = true if File.exists? ".universal"
|
368
|
+
else
|
369
|
+
course_dir_name = current_directory_name
|
370
|
+
is_universal = true if File.exists?(File.join("#{exercise_dir_name}", ".universal"))
|
371
|
+
end
|
372
|
+
|
373
|
+
exercise_dir_name.chomp("/")
|
374
|
+
# Find course and exercise ids
|
375
|
+
course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
|
376
|
+
raise "Invalid course name" if course.nil?
|
377
|
+
exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
|
378
|
+
raise "Invalid exercise name" if exercise.nil?
|
379
|
+
|
380
|
+
if is_universal
|
381
|
+
update_from_zip(exercise['zip_url'], exercise_dir_name, course_dir_name, exercise, course)
|
382
|
+
else
|
383
|
+
update_automatically_detected_project_from_zip(exercise['zip_url'], exercise_dir_name, course_dir_name, exercise, course)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
File without changes
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class MyConfig
|
2
|
+
attr_accessor :config
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@config = get_config
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_config
|
9
|
+
begin
|
10
|
+
yaml = YAML::load(File.open(File.join(File.dirname(File.expand_path(__FILE__)), "config.yml")))
|
11
|
+
rescue
|
12
|
+
raise "Error loading config.yml"
|
13
|
+
end
|
14
|
+
if yaml then yaml else {} end
|
15
|
+
end
|
16
|
+
|
17
|
+
def save_config
|
18
|
+
File.open(File.open(File.join(File.dirname(File.expand_path(__FILE__)), "config.yml")), "w") {|f| f.write(config.to_yaml) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def server_url
|
22
|
+
@config[:server_url] unless @config.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def server_url=(url)
|
26
|
+
@config[:server_url] = url
|
27
|
+
save_config
|
28
|
+
end
|
29
|
+
|
30
|
+
def course_id
|
31
|
+
@config[:course_id]
|
32
|
+
end
|
33
|
+
|
34
|
+
def course_id=(id)
|
35
|
+
@config[:course_id] = id
|
36
|
+
end
|
37
|
+
|
38
|
+
def save_course_path
|
39
|
+
@config[:save_course_path]
|
40
|
+
end
|
41
|
+
|
42
|
+
def save_course_path=(path)
|
43
|
+
path= "#{path}/" unless path[-1] == "/"
|
44
|
+
@config[:save_course_path]=path
|
45
|
+
save_config
|
46
|
+
end
|
47
|
+
|
48
|
+
def auth=(auth)
|
49
|
+
@config[:auth] = auth
|
50
|
+
save_config
|
51
|
+
end
|
52
|
+
|
53
|
+
def auth
|
54
|
+
@config[:auth] unless @config.nil?
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'json'
|
3
|
+
require 'faraday'
|
4
|
+
require 'yaml'
|
5
|
+
require 'pry'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'tempfile'
|
8
|
+
require 'pp'
|
9
|
+
require_relative 'my_config'
|
10
|
+
|
11
|
+
class Client
|
12
|
+
attr_accessor :courses, :config, :conn
|
13
|
+
def initialize()
|
14
|
+
@config = MyConfig.new
|
15
|
+
response = get_connection(@config.username,@config.password)
|
16
|
+
@courses = JSON.parse response.body
|
17
|
+
end
|
18
|
+
|
19
|
+
# stupid name - this should create connection, but not fetch courses.json!
|
20
|
+
def get_connection(username, password)
|
21
|
+
@conn = Faraday.new(:url => @config.server_url) do |faraday|
|
22
|
+
faraday.request :url_encoded # form-encode POST params
|
23
|
+
faraday.response :logger # log requests to STDOUT We dont want to do this in production!
|
24
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
25
|
+
end
|
26
|
+
@conn.basic_auth(username, password) # )
|
27
|
+
@conn.get 'courses.json', {api_version: 5}
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_real_name(headers)
|
31
|
+
name = headers['content-disposition']
|
32
|
+
name.split("\"").compact[-1]
|
33
|
+
end
|
34
|
+
|
35
|
+
def download_file(path)
|
36
|
+
ask_course_path unless @config.save_course_path
|
37
|
+
loaded_file = @conn.get(path)
|
38
|
+
real_name = get_real_name(loaded_file.headers)
|
39
|
+
fname= path.split("/")[-1]
|
40
|
+
course_name = get_course_name
|
41
|
+
path = "#{config.save_course_path}#{course_name}/"
|
42
|
+
begin
|
43
|
+
FileUtils.mkdir_p(path) unless File.exists? path
|
44
|
+
rescue Errno::EISDIR
|
45
|
+
binding.pry
|
46
|
+
end
|
47
|
+
file_path = "#{path}#{real_name}"
|
48
|
+
begin
|
49
|
+
File.open(file_path, 'wb') {|file| file.write(loaded_file.body)} #unless File.exists? file_path
|
50
|
+
rescue Errno::EISDIR
|
51
|
+
binding.pry
|
52
|
+
end
|
53
|
+
result = `unzip -o #{file_path} -d #{path}`
|
54
|
+
FileUtils.rm file_path
|
55
|
+
end
|
56
|
+
|
57
|
+
def fetch_zip(zip_url)
|
58
|
+
@conn.get(zip_url)
|
59
|
+
end
|
60
|
+
|
61
|
+
def ask_course_path
|
62
|
+
puts "where to download?"
|
63
|
+
@config.save_course_path=gets.chomp
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_my_course(course_name=config.course_id)
|
67
|
+
list = @courses['courses'].select {|course| course['id'] == config.course_id}
|
68
|
+
list[0]
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def list(mode=nil)
|
73
|
+
if mode.nil?
|
74
|
+
puts "What would you like to list? Perhaps exercises with list exercises, or active projects with list active?"
|
75
|
+
return
|
76
|
+
end
|
77
|
+
|
78
|
+
send("list_#{mode}")
|
79
|
+
end
|
80
|
+
|
81
|
+
def list_exercises
|
82
|
+
list = get_my_course['exercises'].map {|ex| ex['name']}
|
83
|
+
print_exercises(list)
|
84
|
+
end
|
85
|
+
|
86
|
+
def print_exercises(hash)
|
87
|
+
list = hash.each {|ex| puts ex['name']}
|
88
|
+
end
|
89
|
+
|
90
|
+
def list_active
|
91
|
+
list = get_my_course['exercises'].select {|ex| ex['returnable'] == true and ex['completed']==false}
|
92
|
+
print_exercises(list)
|
93
|
+
end
|
94
|
+
|
95
|
+
def ask_for_course_id
|
96
|
+
list_courses
|
97
|
+
@config.course_id= gets.chomp
|
98
|
+
end
|
99
|
+
|
100
|
+
def download_all_available
|
101
|
+
list = get_my_course['exercises']
|
102
|
+
ask_course_id if list.empty?
|
103
|
+
list.each do |ex|
|
104
|
+
download_file(ex['zip_url'])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Filepath can be either relative or absolute
|
109
|
+
def zip_file_content(filepath)
|
110
|
+
`zip -r -q - #{filepath}`
|
111
|
+
end
|
112
|
+
|
113
|
+
# Call in exercise root
|
114
|
+
# Zipping to stdout zip -r -q - tmc
|
115
|
+
def update_exercise(exercise_dir_name=nil)
|
116
|
+
# Initialize course and exercise names to identify exercise to submit (from json)
|
117
|
+
if exercise_dir_name.nil?
|
118
|
+
exercise_dir_name = get_current_directory_name
|
119
|
+
course_dir_name = get_previous_directory_name
|
120
|
+
else
|
121
|
+
course_dir_name = get_current_directory_name
|
122
|
+
end
|
123
|
+
|
124
|
+
exercise_dir_name.chomp("/")
|
125
|
+
exercise_id = 0
|
126
|
+
zip_url=""
|
127
|
+
# Find course and exercise ids
|
128
|
+
@courses["courses"].each do |course|
|
129
|
+
if course["name"] == course_dir_name
|
130
|
+
course["exercises"].each do |exercise|
|
131
|
+
if exercise["name"] == exercise_dir_name
|
132
|
+
exercise_id = exercise["id"]
|
133
|
+
zip_url = exercise["zip_url"]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
raise "UrlNotFound" if zip_url.empty?
|
139
|
+
# and now download the zip file and extract it into a tmpdir.
|
140
|
+
# Then copy all files except in src to this dir
|
141
|
+
zip = fetch_zip(zip_url)
|
142
|
+
Dir.mktmpdir do |dir|
|
143
|
+
begin
|
144
|
+
File.open("#{dir}/tmp.zip", 'wb') {|file| file.write(zip.body)} #unless File.exists? file_path
|
145
|
+
rescue Errno::EISDIR
|
146
|
+
binding.pry
|
147
|
+
end
|
148
|
+
result = `unzip -o #{dir}/tmp.zip -d #{dir}/`
|
149
|
+
#now its in #{dir}/#{exercise_dir_name}
|
150
|
+
from_dir = "#{dir}/#{exercise_dir_name}/"
|
151
|
+
to_dir = "#{exercise_dir_name}/"
|
152
|
+
copy_updateable_files(from_dir, to_dir)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def copy_updateable_files(from_dir, to_dir)
|
157
|
+
to_dir = "." if get_current_directory_name == to_dir.chomp("/")
|
158
|
+
do_not_update = %w(src)
|
159
|
+
Dir.glob("#{from_dir}**") do |file|
|
160
|
+
filename = file.split("/")[-1]
|
161
|
+
next if do_not_update.include? filename
|
162
|
+
FileUtils.rm_rf("#{to_dir}/file")
|
163
|
+
FileUtils.cp_r(file,"#{to_dir}/")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Call in exercise root
|
168
|
+
>>>>>>> update-action
|
169
|
+
def submit_exercise(exercise_dir_name=nil)
|
170
|
+
# Initialize course and exercise names to identify exercise to submit (from json)
|
171
|
+
if exercise_dir_name.nil?
|
172
|
+
exercise_dir_name = current_directory_name
|
173
|
+
course_dir_name = previous_directory_name
|
174
|
+
zipped = zip_file_content(".")
|
175
|
+
else
|
176
|
+
#<<<<<<< HEAD
|
177
|
+
# course_dir_name = current_directory_name
|
178
|
+
# zipped = zip_file_content(exercise_dir_name)
|
179
|
+
#=======
|
180
|
+
course_dir_name = get_current_directory_name
|
181
|
+
# Zip folder
|
182
|
+
`zip -r zipped.zip #{exercise_dir_name}`
|
183
|
+
end
|
184
|
+
|
185
|
+
exercise_dir_name.chomp("/")
|
186
|
+
exercise_id = 0
|
187
|
+
# Find course and exercise ids
|
188
|
+
@courses["courses"].each do |course|
|
189
|
+
if course["name"] == course_dir_name
|
190
|
+
course["exercises"].each do |exercise|
|
191
|
+
if exercise["name"] == exercise_dir_name
|
192
|
+
exercise_id = exercise["id"]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Submit
|
199
|
+
payload = {:submission => zipped}
|
200
|
+
@conn.post do |req|
|
201
|
+
req.url "/exercises/#{exercise_id}/submissions"
|
202
|
+
req.headers["Content-Type"] = "application/zip"
|
203
|
+
req.body = "#{payload}"
|
204
|
+
end
|
205
|
+
payload
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
data/lib/tmc-client.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'tmc-client/client'
|
2
|
+
|
3
|
+
@c = Client.new
|
4
|
+
|
5
|
+
commands = {
|
6
|
+
list: :list,
|
7
|
+
download: :download,
|
8
|
+
submit: :submit_exercise,
|
9
|
+
update: :update_exercise,
|
10
|
+
auth: :auth,
|
11
|
+
check: :check,
|
12
|
+
init: :init_course,
|
13
|
+
status: :status,
|
14
|
+
solution: :solution,
|
15
|
+
get: :get,
|
16
|
+
set: :set
|
17
|
+
}
|
18
|
+
|
19
|
+
command = ARGV[0].to_s
|
20
|
+
sub_arguments = ARGV.drop 1
|
21
|
+
|
22
|
+
begin
|
23
|
+
unless commands[command.to_sym].nil?
|
24
|
+
@c.send(commands[command.to_sym], *sub_arguments) #unless command
|
25
|
+
else
|
26
|
+
puts "Unknown command"
|
27
|
+
end
|
28
|
+
rescue
|
29
|
+
puts "An error occurred. Make sure the command was triggered in the right directory. Also make sure you have a valid server address and authentication information. If these do not help, please contact an administrator."
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../lib/tmc-client/client.rb'
|
2
|
+
require 'rspec'
|
3
|
+
require 'mocha/setup'
|
4
|
+
|
5
|
+
describe Client do
|
6
|
+
subject do
|
7
|
+
c = Client.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should be able to download a zip" do
|
11
|
+
received_zip_data = mock("faraday_object")
|
12
|
+
received_zip_data.expects(:body).returns(File.read(File.join(File.dirname(File.expand_path(__FILE__)), "ex.zip")))
|
13
|
+
subject.expects(:fetch_zip).returns(received_zip_data)
|
14
|
+
current_dir = subject.current_directory_name
|
15
|
+
subject.courses = { "courses" => [{ "name" => current_dir, "exercises" => [{ "name" => "ex", "returnable" => true }, {"name" => "old", "returnable" => false}] }] }
|
16
|
+
|
17
|
+
subject.download_new_exercise("ex")
|
18
|
+
file_content = File.read("ex.rb")
|
19
|
+
file_content.include?("def method").should == true
|
20
|
+
`rm ex.rb`
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should not download unreturnable exercises" do
|
24
|
+
filtered = subject.filter_returnable([{ "name" => "true", "returnable" => true}, {"name" => "false", "returnable" => false }])
|
25
|
+
filtered.count.should == 1
|
26
|
+
filtered.first.should == "true"
|
27
|
+
end
|
28
|
+
end
|
data/specs/ex.zip
ADDED
Binary file
|
data/specs/list_spec.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative '../lib/tmc-client/client.rb'
|
2
|
+
require 'rspec'
|
3
|
+
require 'mocha'
|
4
|
+
|
5
|
+
describe Client do
|
6
|
+
subject do
|
7
|
+
Client.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should print all course names when listing courses" do
|
11
|
+
subject.courses = {"courses" => [{ "name" => "test_course1"}, {"name" => "test_course2" }] }
|
12
|
+
output = mock("output")
|
13
|
+
output.expects(:puts).with("test_course1")
|
14
|
+
output.expects(:puts).with("test_course2")
|
15
|
+
subject.output = output
|
16
|
+
subject.list_courses
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should print all exercise names when listing exercises of a course" do
|
20
|
+
subject.courses = {"courses" => [{ "name" => "test_course1", "exercises" => [{"name" => "test_ex1"}, {"name" => "test_ex2"}]}] }
|
21
|
+
output = mock("output")
|
22
|
+
output.expects(:puts).with("test_ex1")
|
23
|
+
output.expects(:puts).with("test_ex2")
|
24
|
+
subject.output = output
|
25
|
+
subject.list_exercises("test_course1")
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative '../lib/tmc-client/client.rb'
|
2
|
+
require 'rspec'
|
3
|
+
require 'mocha'
|
4
|
+
|
5
|
+
describe Client do
|
6
|
+
subject do
|
7
|
+
Client.new
|
8
|
+
end
|
9
|
+
|
10
|
+
its(:current_directory_name) { should == `pwd`.split("/").last.chomp }
|
11
|
+
its(:previous_directory_name) { should == `pwd`.split("/")[-2].chomp }
|
12
|
+
|
13
|
+
it "should be able to produce some zip contents" do
|
14
|
+
file = Tempfile.new("tmp")
|
15
|
+
file.write("content")
|
16
|
+
file.close
|
17
|
+
subject.zip_file_content(file.path)
|
18
|
+
zip_content = File.read("tmp_submit.zip")
|
19
|
+
`rm tmp_submit.zip`
|
20
|
+
file.unlink
|
21
|
+
|
22
|
+
zip_content.should_not be_nil
|
23
|
+
zip_content.should_not == ""
|
24
|
+
end
|
25
|
+
end
|
data/specs/update_ex.zip
ADDED
Binary file
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative '../lib/tmc-client/client.rb'
|
2
|
+
require 'rspec'
|
3
|
+
require 'mocha/setup'
|
4
|
+
|
5
|
+
describe Client do
|
6
|
+
subject do
|
7
|
+
c = Client.new
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
`rm -r update_universal_ex`
|
12
|
+
`rm -r update_ex`
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should download all but contents of src folder with normal projects" do
|
16
|
+
received_zip_data = mock("faraday_object")
|
17
|
+
received_zip_data.expects(:body).returns(File.read(File.join(File.dirname(File.expand_path(__FILE__)), "update_ex.zip")))
|
18
|
+
subject.expects(:fetch_zip).returns(received_zip_data)
|
19
|
+
current_dir = subject.current_directory_name
|
20
|
+
subject.courses = { "courses" => [{ "name" => current_dir, "exercises" => [{ "name" => "update_ex", "returnable" => true }, {"name" => "old", "returnable" => false}] }] }
|
21
|
+
|
22
|
+
subject.update_exercise("update_ex")
|
23
|
+
File.exists?("update_ex/included.txt").should == true
|
24
|
+
File.exists?("update_ex/src/not_included.txt").should == false
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should download all files when universal project" do
|
28
|
+
FileUtils.mkdir_p(File.join("update_universal_ex", ".universal"))
|
29
|
+
|
30
|
+
received_zip_data = mock("faraday_object")
|
31
|
+
received_zip_data.expects(:body).returns(File.read(File.join(File.dirname(File.expand_path(__FILE__)), "update_universal_ex.zip")))
|
32
|
+
input = mock("input")
|
33
|
+
input.expects(:gets).returns("A")
|
34
|
+
subject.input = input
|
35
|
+
subject.expects(:fetch_zip).returns(received_zip_data)
|
36
|
+
current_dir = subject.current_directory_name
|
37
|
+
subject.courses = { "courses" => [{ "name" => current_dir, "exercises" => [{ "name" => "update_universal_ex", "returnable" => true }, {"name" => "old", "returnable" => false}] }] }
|
38
|
+
|
39
|
+
subject.update_exercise("update_universal_ex")
|
40
|
+
File.exists?("update_universal_ex/included.txt").should == true
|
41
|
+
File.exists?("update_universal_ex/src/included.txt").should == true
|
42
|
+
end
|
43
|
+
end
|
Binary file
|
data/tmc-client.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'tmc-client'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.summary = 'TestMyCode Commandline client'
|
6
|
+
s.authors = ['Jarmo Isotalo', 'Tony Kovanen']
|
7
|
+
s.homepage = "https://github.com/TMCee/tmc-client/"
|
8
|
+
s.files = `git ls-files`.split("\n")
|
9
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
10
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
11
|
+
s.require_paths = ["lib"]
|
12
|
+
|
13
|
+
s.add_dependency "json"
|
14
|
+
s.add_dependency "faraday"
|
15
|
+
s.add_dependency "pry"
|
16
|
+
s.add_dependency "fileutils"
|
17
|
+
s.add_dependency "mocha"
|
18
|
+
s.add_dependency "highline"
|
19
|
+
s.add_dependency "rake"
|
20
|
+
s.add_dependency "rspec"
|
21
|
+
|
22
|
+
s.add_development_dependency "rspec", "~> 2.8"
|
23
|
+
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tmc-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jarmo Isotalo
|
9
|
+
- Tony Kovanen
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-05-03 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: json
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: faraday
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: pry
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: fileutils
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: mocha
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: highline
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: rspec
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :runtime
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
- !ruby/object:Gem::Dependency
|
144
|
+
name: rspec
|
145
|
+
requirement: !ruby/object:Gem::Requirement
|
146
|
+
none: false
|
147
|
+
requirements:
|
148
|
+
- - ~>
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '2.8'
|
151
|
+
type: :development
|
152
|
+
prerelease: false
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ~>
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '2.8'
|
159
|
+
description:
|
160
|
+
email:
|
161
|
+
executables:
|
162
|
+
- tmc-client
|
163
|
+
extensions: []
|
164
|
+
extra_rdoc_files: []
|
165
|
+
files:
|
166
|
+
- .gitignore
|
167
|
+
- .travis.yml
|
168
|
+
- Gemfile
|
169
|
+
- Gemfile.lock
|
170
|
+
- Readme.md
|
171
|
+
- bin/tmc-client
|
172
|
+
- lib/tmc-client.rb
|
173
|
+
- lib/tmc-client/client.rb
|
174
|
+
- lib/tmc-client/config.default.yml
|
175
|
+
- lib/tmc-client/config.yml
|
176
|
+
- lib/tmc-client/my_config.rb
|
177
|
+
- lib/tmc-client/old-client.rb
|
178
|
+
- specs/download_spec.rb
|
179
|
+
- specs/ex.zip
|
180
|
+
- specs/list_spec.rb
|
181
|
+
- specs/submit_spec.rb
|
182
|
+
- specs/update_ex.zip
|
183
|
+
- specs/update_spec.rb
|
184
|
+
- specs/update_universal_ex.zip
|
185
|
+
- tmc
|
186
|
+
- tmc-client-0.0.1.gem
|
187
|
+
- tmc-client.gemspec
|
188
|
+
homepage: https://github.com/TMCee/tmc-client/
|
189
|
+
licenses: []
|
190
|
+
post_install_message:
|
191
|
+
rdoc_options: []
|
192
|
+
require_paths:
|
193
|
+
- lib
|
194
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
195
|
+
none: false
|
196
|
+
requirements:
|
197
|
+
- - ! '>='
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: '0'
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
none: false
|
202
|
+
requirements:
|
203
|
+
- - ! '>='
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
requirements: []
|
207
|
+
rubyforge_project:
|
208
|
+
rubygems_version: 1.8.23
|
209
|
+
signing_key:
|
210
|
+
specification_version: 3
|
211
|
+
summary: TestMyCode Commandline client
|
212
|
+
test_files: []
|