comic_vine 0.1.4 → 1.0.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/lib/comic_vine.rb CHANGED
@@ -1,102 +1,307 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "comic_vine/version"
2
4
  require "net/http"
3
- require "multi_json"
5
+ require "openssl"
6
+ require "json"
4
7
  require "cgi"
5
8
 
9
+ # A simple Ruby interface to the ComicVine API
10
+ # (https://comicvine.gamespot.com/api/).
11
+ #
12
+ # Set {ComicVine::API.key} and call resource methods on {ComicVine::API}:
13
+ #
14
+ # ComicVine::API.key = "your-api-key"
15
+ # ComicVine::API.volume 766
16
+ # ComicVine::API.characters(limit: 5)
17
+ # ComicVine::API.search "volume", "batman"
6
18
  module ComicVine
7
-
19
+ # Base class for all errors raised by this gem.
8
20
  class CVError < StandardError
9
21
  end
10
-
22
+
23
+ # Raised when the ComicVine API answers with a non-success status_code
24
+ # (e.g. invalid API key, object not found).
25
+ class CVAPIError < CVError
26
+ end
27
+
28
+ # Raised when the connection fails or times out after all retries.
29
+ class CVConnectionError < CVError
30
+ end
31
+
32
+ # Raised when the server answers with a non-2xx HTTP status.
33
+ class CVHTTPError < CVError
34
+ # @return [Integer, nil] the HTTP status code, if known
35
+ attr_reader :status
36
+
37
+ # @param message [String] the error message
38
+ # @param status [Integer, nil] the HTTP status code
39
+ def initialize(message, status = nil)
40
+ @status = status
41
+ super(message)
42
+ end
43
+ end
44
+
45
+ # Raised on HTTP 420/429 once retries are exhausted. ComicVine rate-limits
46
+ # to roughly 200 requests per resource per hour.
47
+ class CVRateLimitError < CVHTTPError
48
+ end
49
+
50
+ # Raised when the response body is not valid JSON (e.g. an HTML error page).
51
+ class CVParseError < CVError
52
+ end
53
+
54
+ # Class-level entry point for all ComicVine API calls.
55
+ #
56
+ # Resource methods are resolved dynamically against the API's `/types/`
57
+ # list: plural resources (`characters`, `volumes`, ...) return a
58
+ # {CVObjectList}, singular resources (`character`, `volume`, ...) take an id
59
+ # and return a {CVObject}.
11
60
  class API
12
- @@key = nil
13
-
14
- @@types = nil
15
- @@last_type_check = nil
16
-
17
- @@API_BASE_URL = "http://comicvine.gamespot.com/api/"
61
+ API_BASE_URL = "https://comicvine.gamespot.com/api/"
62
+ TYPES_CACHE_TTL = 4 * 60 * 60
63
+ RETRYABLE_STATUS_CODES = [420, 429, 500, 502, 503, 504].freeze
64
+ RETRYABLE_EXCEPTIONS = [
65
+ Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, Errno::ECONNREFUSED,
66
+ Errno::ETIMEDOUT, SocketError, EOFError, OpenSSL::SSL::SSLError
67
+ ].freeze
68
+
69
+ @key = nil
70
+ @types = nil
71
+ @last_type_check = nil
72
+ @types_mutex = Mutex.new
73
+
74
+ @open_timeout = 10
75
+ @read_timeout = 30
76
+ @max_retries = 3
77
+ @retry_base_delay = 1.0
18
78
 
19
79
  class << self
20
- def search res, query, opts={}
21
- opts[:resources] = res.gsub " ", ""
22
- opts[:query] = CGI::escape query
23
- resp = hit_api(build_base_url("search"), build_query(opts))
24
- ComicVine::CVSearchList.new(resp, res, query)
80
+ # @!attribute key
81
+ # @return [String, nil] your ComicVine API key (required)
82
+ attr_accessor :key
83
+
84
+ # @!attribute open_timeout
85
+ # @return [Numeric] seconds to wait for the connection to open (default 10)
86
+ # @!attribute read_timeout
87
+ # @return [Numeric] seconds to wait for a response (default 30)
88
+ attr_accessor :open_timeout, :read_timeout
89
+
90
+ # @!attribute max_retries
91
+ # @return [Integer] retries on 420/429/5xx and connection errors (default 3)
92
+ # @!attribute retry_base_delay
93
+ # @return [Numeric] base for exponential backoff, in seconds (default 1.0)
94
+ attr_accessor :max_retries, :retry_base_delay
95
+
96
+ attr_writer :user_agent
97
+
98
+ # The User-Agent header sent with every request.
99
+ #
100
+ # @return [String] defaults to `comic_vine gem/x.y.z (Ruby/x.y.z)`
101
+ def user_agent
102
+ @user_agent ||= "comic_vine gem/#{ComicVine::VERSION} (Ruby/#{RUBY_VERSION})"
25
103
  end
26
-
104
+
105
+ # Searches ComicVine.
106
+ #
107
+ # @param res [String] resource type or types, comma-separated (e.g. `"volume,issue"`)
108
+ # @param query [String] the search string
109
+ # @param opts [Hash] extra query options (`:limit`, `:field_list`, ...)
110
+ # @return [CVSearchList]
111
+ # @raise [CVError]
112
+ def search res, query, opts = {}
113
+ query_opts = opts.merge(:resources => res.gsub(" ", ""), :query => query)
114
+ resp = hit_api(build_base_url("search"), build_query(query_opts))
115
+ ComicVine::CVSearchList.new(resp, res, query, opts)
116
+ end
117
+
118
+ # Looks up a plural (list) resource in the `/types/` list.
119
+ #
120
+ # @param type [String] e.g. `"characters"`
121
+ # @return [Hash, nil]
27
122
  def find_list type
28
123
  types.find { |t| t['list_resource_name'] == type }
29
124
  end
30
-
125
+
126
+ # Looks up a singular (detail) resource in the `/types/` list.
127
+ #
128
+ # @param type [String] e.g. `"character"`
129
+ # @return [Hash, nil]
31
130
  def find_detail type
32
131
  types.find { |t| t['detail_resource_name'] == type }
33
132
  end
34
-
35
- def method_missing(method_sym, *arguments, &block)
133
+
134
+ # Resolves resource methods (`ComicVine::API.characters`,
135
+ # `ComicVine::API.volume 766`, ...) against the `/types/` list.
136
+ def method_missing(method_sym, *arguments, &)
36
137
  if find_list(method_sym.to_s)
37
138
  get_list method_sym.to_s, arguments.first
38
139
  elsif find_detail(method_sym.to_s)
39
140
  get_details method_sym.to_s, *arguments
40
- elsif
141
+ else
41
142
  super
42
143
  end
43
144
  end
44
-
45
- def key
46
- @@key
47
- end
48
-
49
- def key= key
50
- @@key = key
145
+
146
+ def respond_to_missing?(method_sym, include_private = false)
147
+ name = method_sym.to_s
148
+ return true if find_list(name) || find_detail(name)
149
+
150
+ super
151
+ rescue CVError
152
+ super
51
153
  end
52
-
154
+
155
+ # The cached `/types/` list, fetched on first use and refreshed every
156
+ # {TYPES_CACHE_TTL} seconds.
157
+ #
158
+ # @return [Array<Hash>]
159
+ # @raise [CVError] if the `/types/` list cannot be fetched
53
160
  def types
54
- if @@types.nil? || (@@last_type_check + (4 *60 *60)) < Time.now
55
- @@last_type_check = Time.now
56
- @@types = hit_api(build_base_url('types'))['results']
161
+ @types_mutex.synchronize do
162
+ if @types.nil? || (@last_type_check + TYPES_CACHE_TTL) < Time.now
163
+ begin
164
+ @types = hit_api(build_base_url('types'))['results']
165
+ @last_type_check = Time.now
166
+ rescue CVHTTPError => e
167
+ raise e.class.new("Could not load the ComicVine /types/ list (needed to resolve API methods): #{e.message}", e.status)
168
+ rescue CVError => e
169
+ raise e.class, "Could not load the ComicVine /types/ list (needed to resolve API methods): #{e.message}"
170
+ end
171
+ end
172
+ @types
173
+ end
174
+ end
175
+
176
+ # Clears the cached `/types/` list (mainly useful in tests).
177
+ #
178
+ # @return [void]
179
+ def reset_types_cache!
180
+ @types_mutex.synchronize do
181
+ @types = nil
182
+ @last_type_check = nil
57
183
  end
58
- @@types
59
184
  end
60
185
 
61
- def get_list list_type, opts=nil
186
+ # Fetches a list resource.
187
+ #
188
+ # @param list_type [String] plural resource name (e.g. `"characters"`)
189
+ # @param opts [Hash, nil] query options (`:limit`, `:offset`, `:filter`,
190
+ # `:sort`, `:field_list`, ...)
191
+ # @return [CVObjectList]
192
+ # @raise [CVError]
193
+ def get_list list_type, opts = nil
62
194
  resp = hit_api(build_base_url(list_type), build_query(opts))
63
- ComicVine::CVObjectList.new(resp, list_type)
195
+ ComicVine::CVObjectList.new(resp, list_type, opts || {})
64
196
  end
65
-
66
- def get_details item_type, id, opts=nil
67
- resp = hit_api(build_base_url("#{item_type}/#{find_detail(item_type)['id']}-#{id}"), build_query(opts))
197
+
198
+ # Fetches a single resource by id.
199
+ #
200
+ # @param item_type [String] singular resource name (e.g. `"volume"`)
201
+ # @param id [Integer, String] the resource id
202
+ # @param opts [Hash, nil] query options (`:field_list`, ...)
203
+ # @return [CVObject]
204
+ # @raise [CVError] if the type is unknown or the request fails
205
+ def get_details item_type, id, opts = nil
206
+ detail = find_detail(item_type)
207
+ raise CVError, "Unknown ComicVine resource type: #{item_type}" if detail.nil?
208
+
209
+ resp = hit_api(build_base_url("#{item_type}/#{detail['id']}-#{id}"), build_query(opts))
68
210
  ComicVine::CVObject.new(resp['results'])
69
211
  end
70
-
212
+
213
+ # Fetches a resource from its `api_detail_url`.
214
+ #
215
+ # @param url [String]
216
+ # @return [CVObject]
217
+ # @raise [CVError]
71
218
  def get_details_by_url url
72
219
  resp = hit_api(url)
73
220
  ComicVine::CVObject.new(resp['results'])
74
221
  end
75
-
222
+
76
223
  private
77
- def hit_api base_url, query=""
78
- url = base_url+"?format=json&api_key=#{@@key}"+query
79
- uri = URI.parse(url)
80
- resp = Net::HTTP.get(uri)
81
- presp = MultiJson.load(resp)
82
- raise CVError, presp['error'] unless presp['status_code'] == 1
83
- presp
84
- end
85
-
86
- def build_base_url action
87
- @@API_BASE_URL+action+"/"
88
- end
89
-
90
- def build_query opts=nil
91
- query = ''
92
- if !opts.nil? && !opts.empty?
93
- opts.each do |k,v|
94
- query << "&#{k.to_s}=#{v}"
224
+
225
+ def hit_api base_url, query = ""
226
+ uri = URI.parse("#{base_url}?format=json&api_key=#{@key}#{query}")
227
+ response = fetch_with_retries(uri)
228
+ parse_body(response.body)
229
+ end
230
+
231
+ def fetch_with_retries uri
232
+ attempt = 0
233
+ loop do
234
+ attempt += 1
235
+ begin
236
+ response = perform_request(uri)
237
+ rescue *RETRYABLE_EXCEPTIONS => e
238
+ if attempt <= max_retries
239
+ sleep retry_delay(attempt)
240
+ next
95
241
  end
242
+ raise CVConnectionError, "Connection to #{uri.host} failed after #{attempt} attempts: #{e.class}: #{e.message}"
243
+ end
244
+
245
+ code = response.code.to_i
246
+ return response if (200..299).cover?(code)
247
+
248
+ if RETRYABLE_STATUS_CODES.include?(code) && attempt <= max_retries
249
+ sleep retry_delay(attempt, response)
250
+ next
251
+ end
252
+
253
+ if [420, 429].include?(code)
254
+ raise CVRateLimitError.new(
255
+ "ComicVine rate limit hit (HTTP #{code}) and still throttled after #{attempt} attempts" \
256
+ "The API allows roughly 200 requests per resource per hour.", code
257
+ )
96
258
  end
97
- query
259
+
260
+ raise CVHTTPError.new("ComicVine API returned HTTP #{code} #{response.message}".strip, code)
261
+ end
262
+ end
263
+
264
+ def perform_request uri
265
+ Net::HTTP.start(uri.host, uri.port,
266
+ :use_ssl => uri.scheme == "https",
267
+ :open_timeout => open_timeout,
268
+ :read_timeout => read_timeout) do |http|
269
+ request = Net::HTTP::Get.new(uri)
270
+ request["User-Agent"] = user_agent
271
+ request["Accept"] = "application/json"
272
+ http.request(request)
273
+ end
274
+ end
275
+
276
+ def retry_delay attempt, response = nil
277
+ if response && response["Retry-After"].to_s =~ /\A\d+\z/
278
+ response["Retry-After"].to_i
279
+ else
280
+ retry_base_delay * (2**(attempt - 1))
281
+ end
282
+ end
283
+
284
+ def parse_body body
285
+ presp = begin
286
+ JSON.parse(body)
287
+ rescue JSON::ParserError => e
288
+ raise CVParseError, "ComicVine returned a response that is not valid JSON: #{e.message}"
98
289
  end
290
+ raise CVParseError, "ComicVine returned unexpected JSON (expected an object, got #{presp.class})" unless presp.is_a?(Hash)
291
+ raise CVAPIError, presp['error'] unless presp['status_code'] == 1
292
+
293
+ presp
294
+ end
99
295
 
296
+ def build_base_url action
297
+ "#{API_BASE_URL}#{action}/"
298
+ end
299
+
300
+ def build_query opts = nil
301
+ return '' if opts.nil? || opts.empty?
302
+
303
+ opts.map { |k, v| "&#{k}=#{CGI.escape(v.to_s)}" }.join
304
+ end
100
305
  end
101
306
  end
102
307
  end
metadata CHANGED
@@ -1,132 +1,52 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: comic_vine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Sharp
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2016-02-12 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: multi_json
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - '>='
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - '>='
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rspec
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - '>='
32
- - !ruby/object:Gem::Version
33
- version: 2.0.0
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - '>='
39
- - !ruby/object:Gem::Version
40
- version: 2.0.0
41
- - !ruby/object:Gem::Dependency
42
- name: webmock
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - '>='
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - '>='
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: simplecov
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - '>='
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - '>='
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - '>='
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- description: Simple api interface to Comic Vine. Allows for searches and returning
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Simple API interface to Comic Vine. Allows for searches and returning
84
13
  specific information on resources.
85
- email: jakanapes@gmail.com
14
+ email:
15
+ - jakanapes@gmail.com
86
16
  executables: []
87
17
  extensions: []
88
18
  extra_rdoc_files: []
89
19
  files:
90
- - .gitignore
91
- - .rspec
92
- - .travis.yml
93
- - Gemfile
20
+ - CHANGELOG.md
94
21
  - LICENSE
95
22
  - README.md
96
- - Rakefile
97
- - changelog
98
- - comic_vine.gemspec
99
23
  - lib/comic_vine.rb
100
24
  - lib/comic_vine/cv_list.rb
101
25
  - lib/comic_vine/cv_object.rb
102
26
  - lib/comic_vine/version.rb
103
- - spec/comic_vine_spec.rb
104
- - spec/spec_helper.rb
105
27
  homepage: https://github.com/Jakanapes/ComicVine
106
28
  licenses:
107
29
  - MIT
108
- metadata: {}
109
- post_install_message:
30
+ metadata:
31
+ source_code_uri: https://github.com/Jakanapes/ComicVine
32
+ changelog_uri: https://github.com/Jakanapes/ComicVine/blob/master/CHANGELOG.md
33
+ bug_tracker_uri: https://github.com/Jakanapes/ComicVine/issues
34
+ rubygems_mfa_required: 'true'
110
35
  rdoc_options: []
111
36
  require_paths:
112
37
  - lib
113
38
  required_ruby_version: !ruby/object:Gem::Requirement
114
39
  requirements:
115
- - - '>='
40
+ - - ">="
116
41
  - !ruby/object:Gem::Version
117
- version: '0'
42
+ version: '3.4'
118
43
  required_rubygems_version: !ruby/object:Gem::Requirement
119
44
  requirements:
120
- - - '>='
45
+ - - ">="
121
46
  - !ruby/object:Gem::Version
122
47
  version: '0'
123
48
  requirements: []
124
- rubyforge_project:
125
- rubygems_version: 2.5.2
126
- signing_key:
49
+ rubygems_version: 3.6.9
127
50
  specification_version: 4
128
- summary: Interface to ComicVine API
129
- test_files:
130
- - spec/comic_vine_spec.rb
131
- - spec/spec_helper.rb
132
- has_rdoc:
51
+ summary: Interface to the ComicVine API
52
+ test_files: []
data/.gitignore DELETED
@@ -1,19 +0,0 @@
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
18
- coverage
19
- .DS_Store
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --format progress
data/.travis.yml DELETED
@@ -1,4 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 1.9.2
4
- - 1.9.3
data/Gemfile DELETED
@@ -1,7 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in comic_vine.gemspec
4
- gemspec
5
-
6
- gem 'gson', platform: :jruby
7
- gem 'oj', platform: :ruby
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
3
-
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new('spec')
7
-
8
- task :default => :spec
data/changelog DELETED
@@ -1,21 +0,0 @@
1
- 0.1.4 - Update comicvine api url
2
-
3
- 0.1.1 - Escape query on search, remove spaces from resources
4
-
5
- 0.1.0 - Fix small bug with types cache, full coverage with rspec
6
-
7
- 0.0.8 - Fix bug exposing api-key
8
-
9
- 0.0.7 - Fix bug with search
10
-
11
- 0.0.6 - Expose the helper methods, add get_details_by_url, allow get_* for ANY of the resource items returned in the body
12
-
13
- 0.0.5 - Behind the scenes cleanup. Change gem to work as a pure ruby implementation. API.key is now manually set as opposed to reading from a config file. Removed generator and railties.
14
-
15
- 0.0.4 - Add CVObjectList to carry count vars from result. Add simple pagination. Include enumerable in the list classes
16
-
17
- 0.0.3 - Add simple associations, remove initializer and do that work in the railtie class, error check on CV response, allow options
18
-
19
- 0.0.2 - Create CVObjects from resource returns
20
-
21
- 0.0.1 - First check-in
data/comic_vine.gemspec DELETED
@@ -1,24 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/comic_vine/version', __FILE__)
3
-
4
- Gem::Specification.new do |gem|
5
- gem.authors = "Patrick Sharp"
6
- gem.email = "jakanapes@gmail.com"
7
- gem.description = %q{Simple api interface to Comic Vine. Allows for searches and returning specific information on resources.}
8
- gem.summary = %q{Interface to ComicVine API}
9
- gem.homepage = "https://github.com/Jakanapes/ComicVine"
10
-
11
- gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
- gem.files = `git ls-files`.split("\n")
13
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
- gem.name = "comic_vine"
15
- gem.require_paths = ["lib"]
16
- gem.version = ComicVine::VERSION
17
- gem.license = 'MIT'
18
-
19
- gem.add_dependency 'multi_json'
20
- gem.add_development_dependency "rspec", ">= 2.0.0"
21
- gem.add_development_dependency "webmock"
22
- gem.add_development_dependency "simplecov"
23
- gem.add_development_dependency "rake"
24
- end