down 3.0.0 → 3.1.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.
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