pcr-ruby 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pcr-ruby.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 parm289
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Pcr::Ruby
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'pcr-ruby'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install pcr-ruby
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,522 @@
1
+ require 'json'
2
+ require 'open-uri'
3
+ require 'time'
4
+ require 'csv'
5
+
6
+ module PCR
7
+
8
+ #Add some useful String methods
9
+ class ::String
10
+ #Checks if String is valid Penn course code format
11
+ def isValidCourseCode?
12
+ test = self.split('-')
13
+ if test[0].length == 4 and test[1].length == 3
14
+ true
15
+ else
16
+ false
17
+ end
18
+ end
19
+
20
+ #Methods to convert strings to titlecase.
21
+ #Thanks https://github.com/samsouder/titlecase
22
+ def titlecase
23
+ small_words = %w(a an and as at but by en for if in of on or the to v v. via vs vs.)
24
+
25
+ x = split(" ").map do |word|
26
+ # note: word could contain non-word characters!
27
+ # downcase all small_words, capitalize the rest
28
+ small_words.include?(word.gsub(/\W/, "").downcase) ? word.downcase! : word.smart_capitalize!
29
+ word
30
+ end
31
+ # capitalize first and last words
32
+ x.first.smart_capitalize!
33
+ x.last.smart_capitalize!
34
+ # small words after colons are capitalized
35
+ x.join(" ").gsub(/:\s?(\W*#{small_words.join("|")}\W*)\s/) { ": #{$1.smart_capitalize} " }
36
+ end
37
+
38
+ def smart_capitalize
39
+ # ignore any leading crazy characters and capitalize the first real character
40
+ if self =~ /^['"\(\[']*([a-z])/
41
+ i = index($1)
42
+ x = self[i,self.length]
43
+ # word with capitals and periods mid-word are left alone
44
+ self[i,1] = self[i,1].upcase unless x =~ /[A-Z]/ or x =~ /\.\w+/
45
+ end
46
+ self
47
+ end
48
+
49
+ def smart_capitalize!
50
+ replace(smart_capitalize)
51
+ end
52
+
53
+ #Method to compare semesters. Returns true if self is later, false if self is before, 0 if same
54
+ #s should be a string like "2009A"
55
+ def compareSemester(s)
56
+ year = self[0..3]
57
+ season = self[4]
58
+ compYear = s[0..3]
59
+ compSeason = s[4]
60
+
61
+ if year.to_i > compYear.to_i #Later year
62
+ return true
63
+ elsif year.to_i < compYear.to_i #Earlier year
64
+ return false
65
+ elsif year.to_i == compYear.to_i #Same year, so test season
66
+ if season > compSeason #Season is later
67
+ return true
68
+ elsif season = compSeason #Exact same time
69
+ return 0
70
+ elsif season < compSeason #compSeason is later
71
+ return false
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ #Add useful array methods
78
+ class Array
79
+ def binary_search(target)
80
+ self.search_iter(0, self.length-1, target)
81
+ end
82
+
83
+ def search_iter(lower, upper, target)
84
+ return -1 if lower > upper
85
+ mid = (lower+upper)/2
86
+ if (self[mid] == target)
87
+ mid
88
+ elsif (target < self[mid])
89
+ self.search_iter(lower, mid-1, target)
90
+ else
91
+ self.search_iter(mid+1, upper, target)
92
+ end
93
+ end
94
+ end
95
+
96
+ #API class handles token and api url, so both are easily changed
97
+ class API
98
+ attr_accessor :token, :api_endpt
99
+ def initialize()
100
+ @token = File.open('token.dat', &:readline)
101
+ @api_endpt = "http://api.penncoursereview.com/v1/"
102
+ end
103
+ end
104
+
105
+ #These errors serve as more specific exceptions so we know where exactly errors are coming from.
106
+ class CourseError < StandardError
107
+ end
108
+
109
+ class InstructorError < StandardError
110
+ end
111
+
112
+ #Course object matches up with the coursehistory request of the pcr api.
113
+ #A Course essentially is a signle curriculum and course code, and includes all Sections across time (semesters).
114
+ class Course
115
+ attr_accessor :course_code, :sections, :id, :name, :path, :reviews
116
+
117
+ def initialize(args)
118
+ #Set indifferent access for args hash
119
+ args.default_proc = proc do |h, k|
120
+ case k
121
+ when String then sym = k.to_sym; h[sym] if h.key?(sym)
122
+ when Symbol then str = k.to_s; h[str] if h.key?(str)
123
+ end
124
+ end
125
+
126
+ #Initialization actions
127
+ if args[:course_code].is_a? String and args[:course_code].isValidCourseCode?
128
+ @course_code = args[:course_code]
129
+
130
+ #Read JSON from the PCR API
131
+ pcr = PCR::API.new()
132
+ api_url = pcr.api_endpt + "coursehistories/" + self.course_code + "/?token=" + pcr.token
133
+ json = JSON.parse(open(api_url).read)
134
+
135
+ #Create array of Section objects, containing all Sections found in the API JSON for the Course
136
+ @sections = []
137
+ json["result"]["courses"].each do |c|
138
+ @sections << Section.new(:aliases => c["aliases"], :id => c["id"], :name => c["name"], :path => c["path"], :semester => c["semester"], :hit_api => true)
139
+ end
140
+
141
+ #Set variables according to Course JSON data
142
+ @id = json["result"]["id"]
143
+ @name = json["result"]["name"]
144
+ @path = json["result"]["path"]
145
+
146
+ #Get reviews for the Course -- unfortunately this has to be a separate query
147
+ api_url_reviews = pcr.api_endpt + "coursehistories/" + self.id.to_s + "/reviews?token=" + pcr.token
148
+ json_reviews = JSON.parse(open(api_url_reviews).read)
149
+ @reviews = json_reviews["result"]["values"]
150
+
151
+ else
152
+ raise CourseError, "Invalid course code specified. Use format [DEPT-###]."
153
+ end
154
+ end
155
+
156
+ def average(metric)
157
+ #Ensure that we know argument type
158
+ if metric.is_a? Symbol
159
+ metric = metric.to_s
160
+ end
161
+
162
+ if metric.is_a? String
163
+ #Loop vars
164
+ total = 0
165
+ n = 0
166
+
167
+ #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
168
+ self.reviews.each do |review|
169
+ ratings = review["ratings"]
170
+ if ratings.include? metric
171
+ total = total + review["ratings"][metric].to_f
172
+ n = n + 1
173
+ else
174
+ raise CourseError, "No ratings found for \"#{metric}\" in #{self.name}."
175
+ end
176
+ end
177
+
178
+ #Return average score as a float
179
+ return (total/n)
180
+
181
+ else
182
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
183
+ end
184
+ end
185
+
186
+ def recent(metric)
187
+ #Ensure that we know argument type
188
+ if metric.is_a? Symbol
189
+ metric = metric.to_s
190
+ end
191
+
192
+
193
+ if metric.is_a? String
194
+ #Get the most recent section
195
+ section = self.sections[-1]
196
+
197
+ #Iterate through all the section reviews, and if the section review id matches the id of the most recent section, return that rating
198
+ self.reviews.each do |review|
199
+ if review["section"]["id"].to_s[0..4].to_i == section.id
200
+ return review["ratings"][metric]
201
+ end
202
+ end
203
+
204
+ raise CourseError, "No ratings found for #{metric} in #{section.semester}."
205
+
206
+ else
207
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
208
+ end
209
+ end
210
+ end
211
+
212
+ #Section is an individual class under the umbrella of a general Course
213
+ class Section
214
+ attr_accessor :aliases, :id, :name, :path, :semester, :description, :comments, :ratings, :instructor
215
+
216
+ def initialize(args)
217
+ #Set indifferent access for args
218
+ args.default_proc = proc do |h, k|
219
+ case k
220
+ when String then sym = k.to_sym; h[sym] if h.key?(sym)
221
+ when Symbol then str = k.to_s; h[str] if h.key?(str)
222
+ end
223
+ end
224
+
225
+ pcr = PCR::API.new()
226
+ @aliases = args[:aliases] if args[:aliases].is_a? Array
227
+ @id = args[:id] if args[:id].is_a? Integer
228
+ @name = args[:name] if args[:name].is_a? String
229
+ @path = args[:path] if args[:path].is_a? String
230
+ @semester = args[:semester] if args[:semester].is_a? String
231
+ @comments = ""
232
+ @ratings = {}
233
+ @instructor = {}
234
+
235
+ if args[:hit_api]
236
+ if args[:get_reviews]
237
+ self.hit_api(:get_reviews => true)
238
+ else
239
+ self.hit_api(:get_reviews => false)
240
+ end
241
+ end
242
+ end
243
+
244
+ def hit_api(args)
245
+ data = ["aliases", "name", "path", "semester", "description"]
246
+ pcr = PCR::API.new()
247
+ api_url = pcr.api_endpt + "courses/" + self.id.to_s + "?token=" + pcr.token
248
+ json = JSON.parse(open(api_url).read)
249
+
250
+ data.each do |d|
251
+ case d
252
+ when "aliases"
253
+ self.instance_variable_set("@#{d}", json["result"]["aliases"])
254
+ when "name"
255
+ self.instance_variable_set("@#{d}", json["result"]["name"])
256
+ when "path"
257
+ self.instance_variable_set("@#{d}", json["result"]["path"])
258
+ when "semester"
259
+ self.instance_variable_set("@#{d}", json["result"]["semester"])
260
+ when "description"
261
+ self.instance_variable_set("@#{d}", json["result"]["description"])
262
+ end
263
+ end
264
+
265
+ if args[:get_reviews]
266
+ self.reviews()
267
+ end
268
+ end
269
+
270
+ def reviews()
271
+ pcr = PCR::API.new()
272
+ api_url = pcr.api_endpt + "courses/" + self.id.to_s + "/reviews?token=" + pcr.token
273
+ json = JSON.parse(open(api_url).read)
274
+ @comments = []
275
+ @ratings = []
276
+ @instructors = []
277
+ json["result"]["values"].each do |a|
278
+ @comments << {a["instructor"]["id"] => a["comments"]}
279
+ @ratings << {a["instructor"]["id"] => a["ratings"]}
280
+ @instructors << a["instructor"]
281
+ end
282
+ # @comments = json["result"]["values"][0]["comments"]
283
+ # @ratings = json["result"]["values"][0]["ratings"]
284
+ # @instructor = json["result"]["values"][0]["instructor"]
285
+
286
+ return {:comments => @comments, :ratings => @ratings}
287
+ end
288
+
289
+ def after(s)
290
+ if s.is_a? Section
291
+ self.semester.compareSemester(s.semester)
292
+ elsif s.is_a? String
293
+ self.semester.compareSemester(s)
294
+ end
295
+ end
296
+ end
297
+
298
+ #Instructor is a professor. Instructors are not tied to a course or section, but will have to be referenced from Sections.
299
+ class Instructor
300
+ attr_accessor :id, :name, :path, :sections, :reviews
301
+
302
+ def initialize(id, args)
303
+ #Set indifferent access for args
304
+ args.default_proc = proc do |h, k|
305
+ case k
306
+ when String then sym = k.to_sym; h[sym] if h.key?(sym)
307
+ when Symbol then str = k.to_s; h[str] if h.key?(str)
308
+ end
309
+ end
310
+
311
+ #Assign args. ID is necessary because that's how we look up Instructors in the PCR API.
312
+ if id.is_a? String
313
+ @id = id
314
+ else
315
+ raise InstructorError("Invalid Instructor ID specified.")
316
+ end
317
+
318
+ @name = args[:name].downcase.titlecase if args[:name].is_a? String
319
+ @path = args[:path] if args[:path].is_a? String
320
+ @sections = args[:sections] if args[:sections].is_a? Hash
321
+
322
+ #Hit PCR API to get missing info, if requested
323
+ if args[:hit_api] == true
324
+ self.getInfo
325
+ self.getReviews
326
+ end
327
+ end
328
+
329
+ #Hit the PCR API to get all missing info
330
+ #Separate method in case we want to conduct it separately from a class init
331
+ def getInfo
332
+ pcr = PCR::API.new()
333
+ api_url = pcr.api_endpt + "instructors/" + self.id + "?token=" + pcr.token
334
+ json = JSON.parse(open(api_url).read)
335
+
336
+ @name = json["result"]["name"].downcase.titlecase unless @name
337
+ @path = json["result"]["path"] unless @path
338
+ @sections = json["result"]["reviews"]["values"] unless @sections #Mislabeled reviews in PCR API
339
+ end
340
+
341
+ #Separate method for getting review data in case we don't want to make an extra API hit each init
342
+ def getReviews
343
+ if not self.reviews #make sure we don't already have reviews
344
+ pcr = PCR::API.new()
345
+ api_url = pcr.api_endpt + "instructors/" + self.id + "/reviews?token=" + pcr.token
346
+ json = JSON.parse(open(api_url).read)
347
+
348
+ @reviews = json["result"]["values"] #gets array
349
+ end
350
+ end
351
+
352
+ #Get average value of a certain rating for Instructor
353
+ def average(metric)
354
+ #Ensure that we know argument type
355
+ if metric.is_a? Symbol
356
+ metric = metric.to_s
357
+ end
358
+
359
+ if metric.is_a? String
360
+ #Loop vars
361
+ total = 0
362
+ n = 0
363
+
364
+ #For each section, check if ratings include metric arg -- if so, add metric rating to total and increment counting variable
365
+ self.getReviews
366
+ self.reviews.each do |review|
367
+ ratings = review["ratings"]
368
+ if ratings.include? metric
369
+ total = total + review["ratings"][metric].to_f
370
+ n = n + 1
371
+ else
372
+ raise CourseError, "No ratings found for \"#{metric}\" for #{self.name}."
373
+ end
374
+ end
375
+
376
+ #Return average score as a float
377
+ return (total/n)
378
+
379
+ else
380
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
381
+ end
382
+ end
383
+
384
+ #Get most recent value of a certain rating for Instructor
385
+ def recent(metric)
386
+ #Ensure that we know argument type
387
+ if metric.is_a? Symbol
388
+ metric = metric.to_s
389
+ end
390
+
391
+ if metric.is_a? String
392
+ #Iterate through reviews and create Section for each section reviewed, presented in an array
393
+ sections = []
394
+ section_ids = []
395
+ self.getReviews
396
+ self.reviews.each do |review|
397
+ if section_ids.index(review["section"]["id"].to_i).nil?
398
+ s = PCR::Section.new(:id => review["section"]["id"].to_i, :hit_api => false)
399
+ sections << s
400
+ section_ids << s.id
401
+ end
402
+ end
403
+
404
+ #Get only most recent Section(s) in the array
405
+ sections.reverse! #Newest first
406
+ targets = []
407
+ sections.each do |s|
408
+ s.hit_api(:get_reviews => true)
409
+ if sections.index(s) == 0
410
+ targets << s
411
+ elsif s.semester == sections[0].semester and s.id != sections[0].id
412
+ targets << s
413
+ else
414
+ break
415
+ end
416
+ end
417
+
418
+ #Calculate recent rating
419
+ total = 0
420
+ num = 0
421
+ targets.each do |section|
422
+ #Make sure we get the rating for the right Instructor
423
+ section.ratings.each do |rating|
424
+ if rating.key?(self.id)
425
+ if rating[self.id][metric].nil?
426
+ raise InstructorError, "No ratings found for #{metric} for #{self.name}."
427
+ else
428
+ total = total + rating[self.id][metric].to_f
429
+ num += 1
430
+ end
431
+ end
432
+ end
433
+ end
434
+
435
+ return total / num
436
+
437
+ else
438
+ raise CourseError, "Invalid metric format. Metric must be a string or symbol."
439
+ end
440
+ end
441
+ end
442
+
443
+ end #module
444
+
445
+ #Some instance methods to handle instructor searching
446
+ def downloadInstructors(instructors_db)
447
+ pcr = PCR::API.new()
448
+ api_url = pcr.api_endpt + "instructors/" + "?token=" + pcr.token
449
+ #puts "Downloading instructors json..."
450
+ json = JSON.parse(open(api_url).read)
451
+
452
+ #Parse api data, writing to file
453
+ begin
454
+ File.open(instructors_db, 'w') do |f|
455
+ instructor_hashes = json["result"]["values"]
456
+ file_lines = []
457
+ #puts "Constructing instructor file_lines"
458
+ instructor_hashes.each do |instructor|
459
+ n = instructor["name"].split(" ")
460
+ file_lines << ["#{n[2]} #{n[1]} #{n[0]}",instructor["id"]] if n.length == 3 #if instructor has middle name
461
+ file_lines << ["#{n[1]} #{n[0]}",instructor["id"]] if n.length == 2 #if instructor does not have middle name
462
+ end
463
+
464
+ #Sort lines alphabetically
465
+ #puts "sorting file lines alphabetically..."
466
+ file_lines.sort! { |a,b| a[0] <=> b[0] }
467
+
468
+ #Write lines to csv file
469
+ #puts "writing file lines to csv file..."
470
+ file_lines.each { |line| f.write("#{line[0]},#{line[1]}\n") }
471
+ end
472
+ rescue IOError => e
473
+ puts "Could not write to instructors file"
474
+ rescue Errno::ENOENT => e
475
+ puts "Could not open instructors file"
476
+ end
477
+ end
478
+
479
+ def instructorSearch(args)
480
+ #Set indifferent access for args
481
+ args.default_proc = proc do |h, k|
482
+ case k
483
+ when String then sym = k.to_sym; h[sym] if h.key?(sym)
484
+ when Symbol then str = k.to_s; h[str] if h.key?(str)
485
+ end
486
+ end
487
+
488
+ #Set args
489
+ first_name = args[:first_name]
490
+ middle_initial = args[:middle_initial]
491
+ last_name = args[:last_name]
492
+
493
+ #Check if we've downloaded instructors in last week
494
+ begin
495
+ last_dl_time = Time.local(File.mtime("instructors.txt").tv_sec).tv_sec
496
+ #puts last_dl_time
497
+ rescue Errno::ENOENT => e
498
+ downloadInstructors("instructors.txt") #instructors file doesn't exist, so download
499
+ else
500
+ current_time = Time.local(Time.now().tv_sec).tv_sec
501
+ #puts current_time
502
+ if current_time - last_dl_time <= 604800 #1 week in seconds
503
+ downloadInstructors("instructors.txt")
504
+ end
505
+ end
506
+
507
+ #Check if instructors file exists
508
+ # begin
509
+ # f = File.open("instructors.txt", "rb")
510
+ # rescue Errno::ENOENT => e
511
+ # downloadInstructors("instructors.txt")
512
+ # end
513
+
514
+ #Search for instructor name in instructors file and get corresponding ids, in an array
515
+ #puts "searching instructors file..."
516
+ results = []
517
+ CSV.foreach("instructors.txt") do |line|
518
+ results << {line[0] => line[1]} if line[0].include? last_name.upcase
519
+ end
520
+
521
+ return results
522
+ end
@@ -0,0 +1,5 @@
1
+ module Pcr
2
+ module Ruby
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/pcr-ruby/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = "Matt Parmett"
6
+ gem.email = "parm289@yahoo.com"
7
+ gem.description = %q{Ruby wrapper for the Penn Course Review API}
8
+ gem.summary = %q{Ruby wrapper for the Penn Course Review API}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "pcr-ruby"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Pcr::Ruby::VERSION
17
+
18
+ gem.add_dependency "json"
19
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pcr-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matt Parmett
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Ruby wrapper for the Penn Course Review API
31
+ email: parm289@yahoo.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - .gitignore
37
+ - Gemfile
38
+ - LICENSE
39
+ - README.md
40
+ - Rakefile
41
+ - lib/pcr-ruby.rb
42
+ - lib/pcr-ruby/version.rb
43
+ - pcr-ruby.gemspec
44
+ homepage: ''
45
+ licenses: []
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubyforge_project:
64
+ rubygems_version: 1.8.23
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Ruby wrapper for the Penn Course Review API
68
+ test_files: []