down 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c16a220821aeb2a11910334c3598ccb5b823473d
4
- data.tar.gz: 21b1426c6169e82627fb445cbe0526456e9c9f19
3
+ metadata.gz: 29dd2b9e7c612e2be576964dc0b35203d4e6b038
4
+ data.tar.gz: 3f9146bd292d11b4bcd82b196b8c59a4125cff2d
5
5
  SHA512:
6
- metadata.gz: 97447da54fba5009a0dca7b3013906dc5c0f707620b2ab98bd031f85e0b02180829188bb9df31daeacdcc49138aba024f630a2d9a3c5170d6f4119add90ab2c8
7
- data.tar.gz: '09498dc3f7ad7256700f42fb536b76293d6f3825e2b686eb091772b22e366116a346130832ea46d28b739fe76e99f477575e3d003efa852696c8fb492128243e'
6
+ metadata.gz: 5dba71a205bb2f2e0353774e8801cf8692e1c0e5605bdb62810f9bd1ea16bc0076e8d17f2edaa7986ad019675a723c91b693bd8c8f9f19a8637f6aa2630ad040
7
+ data.tar.gz: d1d810b2defabd1a460dcd614e65d578b9c27f2ac6008efb5fd3d56c3a080cb12d721a4196383b9eb1349c7939cce9ff07716692a6c803f13eb095d136fd9aa4
data/README.md CHANGED
@@ -57,29 +57,6 @@ Down.download("http://user:password@example.org")
57
57
  Down.open("http://user:password@example.org")
58
58
  ```
59
59
 
60
- ### Download errors
61
-
62
- There are a lot of ways in which a download can fail:
63
-
64
- * Response status was 4xx or 5xx
65
- * Domain was not found
66
- * Timeout occurred
67
- * URL is invalid
68
- * ...
69
-
70
- Down attempts to unify all of these exceptions into one `Down::NotFound` error
71
- (because this is what actually happened from the outside perspective). If you
72
- want to retrieve the original error raised, in Ruby 2.1+ you can use
73
- `Exception#cause`:
74
-
75
- ```rb
76
- begin
77
- Down.download("http://example.com")
78
- rescue Down::Error => exception
79
- exception.cause #=> #<Timeout::Error>
80
- end
81
- ```
82
-
83
60
  ## Streaming
84
61
 
85
62
  Down has the ability to retrieve content of the remote file *as it is being
@@ -162,7 +139,7 @@ Down::ChunkedIO.new(...)
162
139
  * `:on_close` – called when streaming finishes or IO is closed
163
140
  * `:data` - custom data that you want to store (returned by `#data`)
164
141
  * `:rewindable` - whether to cache retrieved data into a file (defaults to `true`)
165
- * `:encoding` - force content to be returned in specified encoding (defaults to ASCII-8BIT)
142
+ * `:encoding` - force content to be returned in specified encoding (defaults to `Encoding::BINARY`)
166
143
 
167
144
  Here is an example of wrapping streaming MongoDB files:
168
145
 
@@ -182,30 +159,78 @@ io = Down::ChunkedIO.new(
182
159
  )
183
160
  ```
184
161
 
185
- ## open-uri + Net::HTTP
162
+ ### Exceptions
163
+
164
+ Down tries to recognize various types of exceptions and re-raise them as one of
165
+ the `Down::Error` subclasses. This is Down's exception hierarchy:
166
+
167
+ * `Down::Error`
168
+ * `Down::TooLarge`
169
+ * `Down::NotFound`
170
+ * `Down::InvalidUrl`
171
+ * `Down::TooManyRedirects`
172
+ * `Down::ResponseError`
173
+ * `Down::ClientError`
174
+ * `Down::ServerError`
175
+ * `Down::ConnectionError`
176
+ * `Down::TimeoutError`
177
+ * `Down::SSLError`
178
+
179
+ ## Backends
180
+
181
+ By default Down implements `Down.download` and `Down.open` using the built-in
182
+ [open-uri] + [Net::HTTP] Ruby standard libraries. However, there are other
183
+ backends as well:
184
+
185
+ ```rb
186
+ require "down/net_http" # uses open-uri + Net::HTTP
187
+ require "down/http" # uses HTTP.rb gem
188
+ ```
189
+
190
+ When a backend is loaded, is overrides `Down.download` and `Down.open` methods,
191
+ but it's recommended you always use the backends explicitly:
186
192
 
187
- Then [open-uri] + Net::HTTP is the default backend, loaded by requiring `down`
188
- or `down/net_http`:
193
+ ```rb
194
+ # not recommended
195
+ Down.download("...")
196
+ Down.open("...")
197
+
198
+ # recommended
199
+ Down::NetHttp.download("...")
200
+ Down::NetHttp.open("...")
201
+ ```
202
+
203
+ ### open-uri + Net::HTTP
189
204
 
190
205
  ```rb
191
- require "down"
192
- # or
206
+ gem "down", ">= 3.0"
207
+ ```
208
+ ```rb
193
209
  require "down/net_http"
210
+
211
+ tempfile = Down::NetHttp.download("http://nature.com/forest.jpg")
212
+ tempfile #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20150925-55456-z7vxqz.jpg>
213
+
214
+ io = Down::NetHttp.open("http://nature.com/forest.jpg")
215
+ io #=> #<Down::ChunkedIO ...>
194
216
  ```
195
217
 
196
- `Down.download` is implemented as a wrapper around open-uri, and fixes some of
197
- open-uri's undesired behaviours:
218
+ `Down::NetHttp.download` is implemented as a wrapper around open-uri, and fixes
219
+ some of open-uri's undesired behaviours:
220
+
221
+ * uses `URI::HTTP#open` or `URI::HTTPS#open` directly for [security](https://sakurity.com/blog/2015/02/28/openuri.html)
222
+ * always returns a `Tempfile` object, whereas open-uri returns `StringIO`
223
+ when file is smaller than 10KB
224
+ * gives the extension to the `Tempfile` object from the URL
225
+ * allows you to limit maximum number of redirects
198
226
 
199
- * open-uri returns `StringIO` for files smaller than 10KB, and `Tempfile`
200
- otherwise, but `Down.download` always returns a `Tempfile`
201
- * open-uri doesn't give any extension to the returned `Tempfile`, but
202
- `Down.download` adds the extension from the URL
203
- * ...
227
+ On the other hand `Down::NetHttp.open` is implemented using Net::HTTP directly,
228
+ as open-uri
204
229
 
205
- Since open-uri doesn't expose support for partial downloads, `Down.open` is
206
- implemented using `Net::HTTP` directly.
230
+ Since open-uri doesn't expose support for partial downloads,
231
+ `Down::NetHttp.open` is implemented using `Net::HTTP` directly.
207
232
 
208
- ### Redirects
233
+ #### Redirects
209
234
 
210
235
  `Down.download` turns off open-uri's following redirects, as open-uri doesn't
211
236
  have a way to limit the maximum number of hops, and implements its own. By
@@ -213,28 +238,38 @@ default maximum of 2 redirects will be followed, but you can change it via the
213
238
  `:max_redirects` option:
214
239
 
215
240
  ```rb
216
- Down.download("http://example.com/image.jpg") # 2 redirects allowed
217
- Down.download("http://example.com/image.jpg", max_redirects: 5) # 5 redirects allowed
218
- Down.download("http://example.com/image.jpg", max_redirects: 0) # 0 redirects allowed
241
+ Down::NetHttp.download("http://example.com/image.jpg") # 2 redirects allowed
242
+ Down::NetHttp.download("http://example.com/image.jpg", max_redirects: 5) # 5 redirects allowed
243
+ Down::NetHttp.download("http://example.com/image.jpg", max_redirects: 0) # 0 redirects allowed
219
244
  ```
220
245
 
221
- ### Proxy
246
+ #### Proxy
222
247
 
223
248
  Both `Down.download` and `Down.open` support a `:proxy` option, where you can
224
249
  specify a URL to an HTTP proxy which should be used when downloading.
225
250
 
226
251
  ```rb
227
- Down.download("http://example.com/image.jpg", proxy: "http://proxy.org")
228
- Down.open("http://example.com/image.jpg", proxy: "http://user:password@proxy.org")
252
+ Down::NetHttp.download("http://example.com/image.jpg", proxy: "http://proxy.org")
253
+ Down::NetHttp.open("http://example.com/image.jpg", proxy: "http://user:password@proxy.org")
229
254
  ```
230
255
 
231
- ### Additional options
256
+ #### Timeouts
257
+
258
+ Both `Down.download` and `Down.open` support `:read_timeout` and `:open_timeout`
259
+ options, which are forwarded to `Net::HTTP`:
260
+
261
+ ```rb
262
+ Down::NetHttp.download("http://example.com/image.jpg", open_timeout: 5)
263
+ Down::NetHttp.open("http://example.com/image.jpg", read_timeout: 10)
264
+ ```
265
+
266
+ #### Additional options
232
267
 
233
268
  Any additional options passed to `Down.download` will be forwarded to
234
269
  [open-uri], so you can for example add basic authentication or a timeout:
235
270
 
236
271
  ```rb
237
- Down.download "http://example.com/image.jpg",
272
+ Down::NetHttp.download "http://example.com/image.jpg",
238
273
  http_basic_authentication: ['john', 'secret'],
239
274
  read_timeout: 5
240
275
  ```
@@ -244,19 +279,23 @@ semantics as in open-uri, and any options with String keys will be interpreted
244
279
  as request headers, like with open-uri.
245
280
 
246
281
  ```rb
247
- Down.open("http://example.com/image.jpg", {"Authorization" => "..."})
282
+ Down::NetHttp.open("http://example.com/image.jpg", {"Authorization" => "..."})
248
283
  ```
249
284
 
250
- ## HTTP.rb
251
-
252
- The [HTTP.rb] backend can be used by requiring `down/http`:
285
+ ### HTTP.rb
253
286
 
254
287
  ```rb
288
+ gem "down", "~> 3.0"
255
289
  gem "http", "~> 2.1"
256
- gem "down"
257
290
  ```
258
291
  ```rb
259
292
  require "down/http"
293
+
294
+ tempfile = Down::Http.download("http://nature.com/forest.jpg")
295
+ tempfile #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20150925-55456-z7vxqz.jpg>
296
+
297
+ io = Down::Http.open("http://nature.com/forest.jpg")
298
+ io #=> #<Down::ChunkedIO ...>
260
299
  ```
261
300
 
262
301
  Some features that give the HTTP.rb backend an advantage over open-uri +
@@ -266,9 +305,7 @@ Net::HTTP include:
266
305
  * Proper support for streaming downloads (`#download` and now reuse `#open`)
267
306
  * Proper support for SSL
268
307
  * Chaninable HTTP client builder API for setting default options
269
- * Persistent connections
270
- * Auto-inflating compressed response bodies
271
- * ...
308
+ * Support for persistent connections
272
309
 
273
310
  ### Default client
274
311
 
@@ -290,13 +327,13 @@ All additional options passed to `Down::Download` and `Down.open` will be
290
327
  forwarded to `HTTP::Client#request`:
291
328
 
292
329
  ```rb
293
- Down.download("http://example.org/image.jpg", headers: {"Accept-Encoding" => "gzip"})
330
+ Down::Http.download("http://example.org/image.jpg", headers: {"Accept-Encoding" => "gzip"})
294
331
  ```
295
332
 
296
333
  If you prefer to add options using the chainable API, you can pass a block:
297
334
 
298
335
  ```rb
299
- Down.open("http://example.org/image.jpg") do |client|
336
+ Down::Http.open("http://example.org/image.jpg") do |client|
300
337
  client.timeout(read: 3)
301
338
  end
302
339
  ```
@@ -315,23 +352,21 @@ backend is thread safe.
315
352
 
316
353
  ## Development
317
354
 
318
- The test suite runs the http://httpbin.org/ server locally, and uses it to test
319
- downloads. Httpbin is a Python package which is run with GUnicorn:
320
-
321
- ```
322
- $ pip install gunicorn httpbin
323
- ```
324
-
325
- Afterwards you can run tests with
355
+ You can run tests with
326
356
 
327
357
  ```
328
358
  $ rake test
329
359
  ```
330
360
 
361
+ The test suite pulls and runs [kennethreitz/httpbin] as a Docker container, so
362
+ you'll need to have Docker installed and running.
363
+
331
364
  ## License
332
365
 
333
366
  [MIT](LICENSE.txt)
334
367
 
335
368
  [open-uri]: http://ruby-doc.org/stdlib-2.3.0/libdoc/open-uri/rdoc/OpenURI.html
369
+ [Net::HTTP]: https://ruby-doc.org/stdlib-2.4.1/libdoc/net/http/rdoc/Net/HTTP.html
336
370
  [HTTP.rb]: https://github.com/httprb/http
337
371
  [Addressable::URI]: https://github.com/sporkmonger/addressable
372
+ [kennethreitz/httpbin]: https://github.com/kennethreitz/httpbin
@@ -18,4 +18,5 @@ Gem::Specification.new do |spec|
18
18
  spec.add_development_dependency "minitest", "~> 5.8"
19
19
  spec.add_development_dependency "mocha"
20
20
  spec.add_development_dependency "http", "~> 2.1"
21
+ spec.add_development_dependency "docker-api"
21
22
  end
@@ -1,16 +1,41 @@
1
1
  module Down
2
- class Error < StandardError
3
- end
2
+ # generic error which is a superclass to all other errors
3
+ class Error < StandardError; end
4
4
 
5
- class TooLarge < Error
6
- end
5
+ # raised when the file is larger than the specified maximum size
6
+ class TooLarge < Error; end
7
+
8
+ # raised when the file failed to be retrieved for whatever reason
9
+ class NotFound < Error; end
10
+
11
+ # raised when the given URL couldn't be parsed
12
+ class InvalidUrl < NotFound; end
13
+
14
+ # raised when the number of redirects was larger than the specified maximum
15
+ class TooManyRedirects < NotFound; end
7
16
 
8
- class NotFound < Error
17
+ # raised when response returned 4xx or 5xx response
18
+ class ResponseError < NotFound
9
19
  attr_reader :response
10
20
 
11
- def initialize(message, response: nil)
21
+ def initialize(message, response:)
12
22
  super(message)
13
23
  @response = response
14
24
  end
15
25
  end
26
+
27
+ # raised when response returned 4xx response
28
+ class ClientError < ResponseError; end
29
+
30
+ # raised when response returned 5xx response
31
+ class ServerError < ResponseError; end
32
+
33
+ # raised when there was an error connecting to the server
34
+ class ConnectionError < NotFound; end
35
+
36
+ # raised when connecting to the server too longer than the specified timeout
37
+ class TimeoutError < ConnectionError; end
38
+
39
+ # raised when an SSL error was raised
40
+ class SSLError < NotFound; end
16
41
  end
@@ -65,28 +65,23 @@ module Down
65
65
  def open(url, **options, &block)
66
66
  rewindable = options.delete(:rewindable)
67
67
 
68
- response = get(url, **options, &block)
69
-
70
- if response.code.between?(400, 599)
71
- raise Down::NotFound.new("file not found", response: response)
68
+ begin
69
+ response = get(url, **options, &block)
70
+ response_error!(response) if !response.status.success?
71
+ rescue => exception
72
+ request_error!(exception)
72
73
  end
73
74
 
74
75
  down_options = {
75
- chunks: response.body.enum_for(:each),
76
- size: response.content_length,
77
- data: { status: response.status, headers: response.headers.to_h, response: response },
76
+ chunks: response.body.enum_for(:each),
77
+ size: response.content_length,
78
+ data: { status: response.code, headers: response.headers.to_h, response: response },
78
79
  }
79
80
  down_options[:encoding] = response.content_type.charset if response.content_type.charset
80
81
  down_options[:on_close] = -> { response.connection.close } unless client.persistent?
81
82
  down_options[:rewindable] = rewindable if rewindable != nil
82
83
 
83
84
  Down::ChunkedIO.new(down_options)
84
- rescue HTTP::ConnectionError,
85
- HTTP::Request::UnsupportedSchemeError,
86
- HTTP::TimeoutError
87
- raise Down::NotFound, "file not found"
88
- rescue HTTP::Redirector::TooManyRedirectsError
89
- raise Down::NotFound, "too many redirects"
90
85
  end
91
86
 
92
87
  def get(url, **options, &block)
@@ -112,6 +107,42 @@ module Down
112
107
  Thread.current[:down_client] = value
113
108
  end
114
109
 
110
+ def response_error!(response)
111
+ args = [response.status.to_s, response: response]
112
+
113
+ case response.code
114
+ when 400..499 then raise Down::ClientError.new(*args)
115
+ when 500..599 then raise Down::ServerError.new(*args)
116
+ else raise Down::ResponseError.new(*args)
117
+ end
118
+ end
119
+
120
+ def request_error!(exception)
121
+ case exception
122
+ when HTTP::Request::UnsupportedSchemeError
123
+ raise Down::InvalidUrl, exception.message
124
+ when Errno::ECONNREFUSED
125
+ raise Down::ConnectionError, "connection was refused"
126
+ when HTTP::ConnectionError,
127
+ Errno::ECONNABORTED,
128
+ Errno::ECONNRESET,
129
+ Errno::EPIPE,
130
+ Errno::EINVAL,
131
+ Errno::EHOSTUNREACH
132
+ raise Down::ConnectionError, exception.message
133
+ when SocketError
134
+ raise Down::ConnectionError, "domain name could not be resolved"
135
+ when HTTP::TimeoutError
136
+ raise Down::TimeoutError, exception.message
137
+ when HTTP::Redirector::TooManyRedirectsError
138
+ raise Down::TooManyRedirects, exception.message
139
+ when defined?(OpenSSL) && OpenSSL::SSL::SSLError
140
+ raise Down::SSLError, exception.message
141
+ else
142
+ raise exception
143
+ end
144
+ end
145
+
115
146
  module DownloadedFile
116
147
  attr_accessor :url, :headers
117
148
 
@@ -73,7 +73,7 @@ module Down
73
73
  uri = URI(uri)
74
74
 
75
75
  if uri.class != URI::HTTP && uri.class != URI::HTTPS
76
- raise URI::InvalidURIError, "url is not http nor https"
76
+ raise Down::InvalidUrl, "URL scheme needs to be http or https"
77
77
  end
78
78
 
79
79
  if uri.user || uri.password
@@ -83,24 +83,29 @@ module Down
83
83
  end
84
84
 
85
85
  downloaded_file = uri.open(open_uri_options)
86
- rescue OpenURI::HTTPRedirect => redirect
86
+ rescue OpenURI::HTTPRedirect => exception
87
87
  if (tries -= 1) > 0
88
- uri = redirect.uri
88
+ uri = exception.uri
89
89
 
90
- if !redirect.io.meta["set-cookie"].to_s.empty?
91
- open_uri_options["Cookie"] = redirect.io.meta["set-cookie"]
90
+ if !exception.io.meta["set-cookie"].to_s.empty?
91
+ open_uri_options["Cookie"] = exception.io.meta["set-cookie"]
92
92
  end
93
93
 
94
94
  retry
95
95
  else
96
- raise Down::NotFound, "too many redirects"
96
+ raise Down::TooManyRedirects, "too many redirects"
97
97
  end
98
- rescue OpenURI::HTTPError,
99
- URI::InvalidURIError,
100
- Errno::ECONNREFUSED,
101
- SocketError,
102
- Timeout::Error
103
- raise Down::NotFound, "file not found"
98
+ rescue OpenURI::HTTPError => exception
99
+ code, message = exception.io.status
100
+ response_class = Net::HTTPResponse::CODE_TO_OBJ.fetch(code)
101
+ response = response_class.new(nil, code, message)
102
+ exception.io.metas.each do |name, values|
103
+ values.each { |value| response.add_field(name, value) }
104
+ end
105
+
106
+ response_error!(response)
107
+ rescue => exception
108
+ request_error!(exception)
104
109
  end
105
110
 
106
111
  # open-uri will return a StringIO instead of a Tempfile if the filesize is
@@ -117,7 +122,15 @@ module Down
117
122
  end
118
123
 
119
124
  def open(uri, options = {})
120
- uri = URI(uri)
125
+ begin
126
+ uri = URI(uri)
127
+ if uri.class != URI::HTTP && uri.class != URI::HTTPS
128
+ raise Down::InvalidUrl, "URL scheme needs to be http or https"
129
+ end
130
+ rescue URI::InvalidURIError
131
+ raise Down::InvalidUrl, "URL was invalid"
132
+ end
133
+
121
134
  http_class = Net::HTTP
122
135
 
123
136
  if options[:proxy]
@@ -143,6 +156,9 @@ module Down
143
156
  http.cert_store = store
144
157
  end
145
158
 
159
+ http.read_timeout = options[:read_timeout] if options.key?(:read_timeout)
160
+ http.open_timeout = options[:open_timeout] if options.key?(:open_timeout)
161
+
146
162
  request_headers = options.select { |key, value| key.is_a?(String) }
147
163
  get = Net::HTTP::Get.new(uri.request_uri, request_headers)
148
164
  get.basic_auth(uri.user, uri.password) if uri.user || uri.password
@@ -156,9 +172,13 @@ module Down
156
172
  end
157
173
  end
158
174
 
159
- response = request.resume
175
+ begin
176
+ response = request.resume
160
177
 
161
- raise Down::NotFound, "request returned status #{response.code} and body:\n#{response.body}" if response.code.to_i.between?(400, 599)
178
+ response_error!(response) unless (200..299).cover?(response.code.to_i)
179
+ rescue => exception
180
+ request_error!(exception)
181
+ end
162
182
 
163
183
  down_params = {
164
184
  chunks: response.enum_for(:read_body),
@@ -193,6 +213,47 @@ module Down
193
213
  tempfile
194
214
  end
195
215
 
216
+ def response_error!(response)
217
+ code = response.code.to_i
218
+ message = response.message.split(" ").map(&:capitalize).join(" ")
219
+
220
+ args = ["#{code} #{message}", response: response]
221
+
222
+ case response.code.to_i
223
+ when 400..499 then raise Down::ClientError.new(*args)
224
+ when 500..599 then raise Down::ServerError.new(*args)
225
+ else raise Down::ResponseError.new(*args)
226
+ end
227
+ end
228
+
229
+ def request_error!(exception)
230
+ case exception
231
+ when URI::InvalidURIError
232
+ raise Down::InvalidUrl, "URL was invalid"
233
+ when Errno::ECONNREFUSED
234
+ raise Down::ConnectionError, "connection was refused"
235
+ when EOFError,
236
+ IOError,
237
+ Errno::ECONNABORTED,
238
+ Errno::ECONNRESET,
239
+ Errno::EPIPE,
240
+ Errno::EINVAL,
241
+ Errno::EHOSTUNREACH
242
+ raise Down::ConnectionError, exception.message
243
+ when SocketError
244
+ raise Down::ConnectionError, "domain name could not be resolved"
245
+ when Errno::ETIMEDOUT,
246
+ Timeout::Error,
247
+ Net::OpenTimeout,
248
+ Net::ReadTimeout
249
+ raise Down::TimeoutError, "request timed out"
250
+ when defined?(OpenSSL) && OpenSSL::SSL::SSLError
251
+ raise Down::SSLError, exception.message
252
+ else
253
+ raise exception
254
+ end
255
+ end
256
+
196
257
  module DownloadedFile
197
258
  def original_filename
198
259
  filename_from_content_disposition || filename_from_uri
@@ -1,3 +1,3 @@
1
1
  module Down
2
- VERSION = "3.0.0"
2
+ VERSION = "3.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: down
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-24 00:00:00.000000000 Z
11
+ date: 2017-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: docker-api
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'
55
69
  description:
56
70
  email:
57
71
  - janko.marohnic@gmail.com