yt-core 0.1.4 → 0.1.5

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: bcf250be4dd09edba92d177ab388d0e6ad9c1c7c
4
- data.tar.gz: 73302bb09f7fa918fb68d040c0c157e4ea171cd4
3
+ metadata.gz: 38fe8ee547f6c51750be3b935de33654304a92bc
4
+ data.tar.gz: ca5b035d73c726d4f9f4a2428b3a4cc6cb573515
5
5
  SHA512:
6
- metadata.gz: 11ad4095e3cba36dac088c13fe60084e3fb3b3a172e83b4a2e0ad893032a86d77afb7e9ffca71145be8a8140b891063eef3b0dbc52f0448a6224fcee5c2e4d58
7
- data.tar.gz: f86ceb9904d7480b89b41a0c4d33d93d9056265381085cd9566a4908b7200de69f869b4317a29eea23163093e0a887537c4b404c663f2135aeef12017b474760
6
+ metadata.gz: 3201de66a86b9df6caf10792426616f01ea177153983090cb32d1bd19f83f99b947b79f9012d4e33f34411930e5f3c978664d6d9c3f7b2c536ccbb4412523a52
7
+ data.tar.gz: 0f1daea896d3ea422837992e8b3f1aeefb04d0576af63e51f783ae00ac5234b45ccf7ce3d77b2fc84578968fc1e69a37befb9ac9d32b3c3cfb7ef2669c0653b1
@@ -1,6 +1,11 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.2
3
+ - 2.2.2
4
4
  script:
5
- - bundle exec rspec
6
- - bundle exec yard stats | grep "100.00% documented"
5
+ - bundle exec rspec
6
+ - bundle exec yard stats | grep "100.00% documented"
7
+ notifications:
8
+ slack:
9
+ secure: q+C+6nmfAg0W3ZqmqCyV7cRJXoJX22WPHodEYI5WVE7PY2Y42phHOIipXeUFta3CF3VO5sk4MLXh08y0WtShi/ZGpUui0ZH8aimplUlPbbmwKukAGAwYJp7q/trU3hPZd2CkQoBBWNmJsx2aU4X39aYpUOExYrSwW4Cu17hgW1y2m3kxNJyOocuaAuMZyWYY8ktH0C5sVfL0LR7U8Ct8B9D72yyXzLrPOlTWkrln7e82tirh318XqWRM8YLLLGkwaOBqptxkNbmbNu/y+SKiqixcSUPDhLAjRXsByhDlR2f9kZsQZJ/1YnQFl/SygiL39JNMWY8aiHkN15N5iWiocalw/r+oSdZApWf7Ts69cgtoPGxNDt8L73FXs5tnhNOEkm/3kc+Vkq7mnrvX1l0Z0wqdcmZekiw31u30j/j6GU2WHC3C4taJ9TJIc7AfNUntg0l1GXh1aKi62pEdVVgJWLPU8MKXn0Z+2Me2ALrFjfGkfbGXD1lbIlQ/UfyiDyzmqN+DpT7UNcUuXKH4N/BoJtqr9upnXZsVIr0+mVD0/rpo7lZtNHsYaP1BtywKcKdXs4TNHSV51cE3ZUtAweGRsgnQsLWtszQPPG98OLFpqJP5wKRaAuIEI0134uf6V+DqWyICPY6oGX1BTbm8LuXCby7I8ErHI4V16tWuiNm6ojQ=
10
+ on_success: never
11
+ on_failure: change
@@ -6,6 +6,12 @@ For more information about changelogs, check
6
6
  [Keep a Changelog](http://keepachangelog.com) and
7
7
  [Vandamme](http://tech-angels.github.io/vandamme).
8
8
 
9
+ ## 0.1.5 - 2017-08-24
10
+
11
+ * [FEATURE] Add `Yt::PlaylistItem.insert` and `Yt::PlaylistItem#delete`
12
+ * [FEATURE] Add `Channel#related_playlists` and `Channel#like_playlists`
13
+ * [FEATURE] Add Channel.mine
14
+
9
15
  ## 0.1.4 - 2017-06-02
10
16
 
11
17
  * [FEATURE] Add CommentThread#comments
data/README.md CHANGED
@@ -57,11 +57,21 @@ To run live-tests against the YouTube API, type:
57
57
  rspec
58
58
  ```
59
59
 
60
- This will fail unless you have set up a test YouTube application with access to
61
- the YouTube Data API v3 and an environment variable:
60
+ Note that some tests actually hit the YouTube API, and therefore require
61
+ either an API key or authentication credentials.
62
62
 
63
- - `YT_SERVER_API_KEY`: API Key of a Google app with access to the YouTube Data API v3 and the YouTube Analytics API
63
+ In order to run tests marked as :server you need to set up a test YouTube
64
+ application with access to the YouTube Data API v3 and an environment variable:
64
65
 
66
+ - `YT_SERVER_API_KEY`: API Key of a Google app with access to the YouTube Data API v3
67
+
68
+ In order to run tests marked as :account you als need to create a client ID
69
+ and secret and then generate a refresh token for the account you want to use
70
+ as test. Make sure this account has a channel with at least one playlist:
71
+
72
+ - `YT_ACCOUNT_CLIENT_ID`: Client ID of a Google app with access to the YouTube Data API v3
73
+ - `YT_ACCOUNT_CLIENT_SECRET`: Client Secret of a Google app with access to the YouTube Data API v3
74
+ - `YT_ACCOUNT_REFRESH_TOKEN`: Refresh token of a YouTube account for the app above
65
75
 
66
76
  How to release new versions
67
77
  ===========================
@@ -32,6 +32,21 @@ channel = Yt::Channel.new id: 'UCwCnUcLcb9-eSrHa_RQGkQQ' ## use any chan
32
32
  channel.title # => "Yt Test"
33
33
  {% endhighlight %}
34
34
 
35
+
36
+ <p>
37
+ Other methods <strong>acts on behalf of YouTube accounts</strong> (e.g.: subscribe to a channel, delete playlists).<br />
38
+ To use these methods (marked with <span class="label label-warning">&nbsp;</span> below), you need to <a href="{{ site.baseurl }}/#api_client">get an API Client ID/Secret from Google</a>, then <a href="{{ site.baseurl }}/#tokens">obtain a refresh token</a> from the account you want to act as, and finally configure the values:
39
+ </p>
40
+
41
+ {% highlight ruby %}
42
+ Yt.configuration.client_id = "<your ID>" ## replace with your client ID
43
+ Yt.configuration.client_secret = "<your secret>" ## replace with your client secret
44
+ Yt.configuration.refresh_token = "<token>" ## use the account’s refresh token
45
+
46
+ channel = Yt::Channel.mine
47
+ # => #<Yt::Channel @id=UCwCnUcLcb9-eSrHa_RQGkQQ>
48
+ {% endhighlight %}
49
+
35
50
  <hr />
36
51
  <h4>List of <code>Yt::Channel</code> data methods</h4>
37
52
  <dl>
@@ -70,6 +85,12 @@ channel.title # => "Yt Test"
70
85
  {% include doc.html instance="Channel#featured_channels_title" %}{% include example.html object='channel' method='featured_channels_title' result='"Featured channels"' %}
71
86
  {% include doc.html instance="Channel#featured_channels_urls" %}{% include example.html object='channel' method='featured_channels_urls' result='["UCxO1tY8h1AhOz0T4ENwmpow"]' %}</pre>
72
87
  </div></dd>
88
+
89
+ {% include dt.html title="Channel’s content details" label="warning" auth="must authenticate as the channel’s account" %}
90
+ <dd><a class="anchor" id="content_details"></a><div class="highlight"><pre>
91
+ {% include doc.html instance="Channel#related_playlists" %}{% include example.html object='channel' method='related_playlists' result='{"likes"=>"LLwCncb9-e", "watchHistory"=>"HL"}' %}
92
+ {% include doc.html instance="Channel#like_playlists" %}{% include example.html object='channel' method='like_playlists' result='#&lt;Yt::Relation [#&lt;Yt::Playlist @id=LLWCncb...&gt;]&gt;' %}</pre>
93
+ </div></dd>
73
94
  </dl>
74
95
  <p>
75
96
  To limit the number of HTTP requests, use <code>select</code> to specify which <a href="https://developers.google.com/youtube/v3/docs/channels/list#part">parts</a> of the channel’s data to load:
@@ -142,3 +163,11 @@ channel.title # => "Yt Test"
142
163
  The previous method returns existing channels that match the provided IDs, skipping any unrecognized ID.<br />
143
164
  As usual, use <code>select</code> to specify which <a href="https://developers.google.com/youtube/v3/docs/channels/list#part">parts</a> of each channels’s data to load before iterating through the list.
144
165
  </p>
166
+
167
+ <dl>
168
+ {% include dt.html title="Authenticated account’s channel" label="warning" auth="must authenticate as the channel’s account" %}
169
+ <dd><a class="anchor" id="mine"></a><div class="highlight"><pre>
170
+ {% include doc.html class="Channel#mine" %}{% include example.html object='<span class="no">Yt</span><span class="o">::</span><span class="no">Channel</span>' method='mine' %}
171
+ {% include example.html result='#&lt;Yt::Channel @id=UCwCnUcLcb9-eSrHa_RQGkQQ&gt;' %}</pre>
172
+ </div></dd>
173
+ </dl>
@@ -27,6 +27,20 @@ item = Yt::PlaylistItem.new id: 'UEwtTGVUdXRjOUdSS0Qze' ## use any playlist item
27
27
  item.position # => 0
28
28
  {% endhighlight %}
29
29
 
30
+ <p>
31
+ Other methods <strong>acts on behalf of YouTube accounts</strong> (e.g.: add an item to a playlist).<br />
32
+ To use these methods (marked with <span class="label label-warning">&nbsp;</span> below), you need to <a href="{{ site.baseurl }}/#api_client">get an API Client ID/Secret from Google</a>, then <a href="{{ site.baseurl }}/#tokens">obtain a refresh token</a> from the account you want to act as, and finally configure the values:
33
+ </p>
34
+
35
+ {% highlight ruby %}
36
+ Yt.configuration.client_id = "<your ID>" ## replace with your client ID
37
+ Yt.configuration.client_secret = "<your secret>" ## replace with your client secret
38
+ Yt.configuration.refresh_token = "<token>" ## use the account’s refresh token
39
+
40
+ Yt::PlaylistItem.insert playlist_id: 'PL-LeTutc9GRKD3DhnRF_y', video_id: 'gknzFj_0vvY'
41
+ # => #<Yt::PlaylistItem:0x... @id=UEwtTGVUdXRjOUdSS0Qze>
42
+ {% endhighlight %}
43
+
30
44
  <hr />
31
45
  <h4>List of <code>Yt::PlaylistItem</code> data methods</h4>
32
46
  <dl>
@@ -63,3 +77,10 @@ item.position # => 0
63
77
  {% include example.html object='fast' method='privacy_status' result='=> no extra HTTP requests' %}</pre>
64
78
  </div></dd>
65
79
  </dl>
80
+ <dl>
81
+ {% include dt.html title="Adding and removing a playlist item" label="warning" auth="must authenticate as the channel’s account" %}
82
+ <dd><a class="anchor" id="insert_remove"></a><div class="highlight"><pre>
83
+ {% include doc.html class="PlaylistItem#insert" %}{% include example.html object='item = <span class="no">Yt</span><span class="o">::</span><span class="no">PlaylistItem</span>' method='insert' params=' <span class="ss">playlist_id:</span> <span class="s1">"PL-..."</span>, <span class="ss">video_id:</span> <span class="s1">"gknzFj_0vvY"</span>' %}
84
+ {% include doc.html instance="PlaylistItem#delete" %}{% include example.html object='item' method='delete' result='true' %}</pre>
85
+ </div></dd>
86
+ </dl>
@@ -91,6 +91,11 @@ module Yt
91
91
  # channels module.
92
92
  has_attribute :featured_channels_urls, in: %i(branding_settings channel), default: []
93
93
 
94
+ # @!attribute [r] related_playlists
95
+ # @return [Hash] the playlists associated with the channel, such as the
96
+ # channel's uploaded videos, liked videos, and watch history.
97
+ has_attribute :related_playlists, in: :content_details
98
+
94
99
  # @return [String] the canonical form of the channel’s URL.
95
100
  def canonical_url
96
101
  "https://www.youtube.com/channel/#{id}"
@@ -122,7 +127,7 @@ module Yt
122
127
  # @see https://developers.google.com/youtube/v3/docs/search/list#channelId
123
128
  def videos
124
129
  @videos ||= Relation.new(Video, channel_id: id, limit: 500) do |options|
125
- items = fetch '/youtube/v3/search', channel_videos_params(options)
130
+ items = get '/youtube/v3/search', channel_videos_params(options)
126
131
  videos_for items, 'id', options
127
132
  end
128
133
  end
@@ -130,8 +135,31 @@ module Yt
130
135
  # @return [Yt::Relation<Yt::Playlist>] the public playlists of the channel.
131
136
  def playlists
132
137
  @playlists ||= Relation.new(Playlist, channel_id: id) do |options|
133
- fetch '/youtube/v3/playlists', channel_playlists_params(options)
138
+ get '/youtube/v3/playlists', channel_playlists_params(options)
134
139
  end
135
140
  end
141
+
142
+ # @return [Yt::Relation<Yt::Playlist>] the playlists associated with
143
+ # liked videos. Includes the deprecated favorites if still present.
144
+ def like_playlists
145
+ @like_lists ||= Relation.new(Playlist, ids: like_list_ids) do |options|
146
+ get '/youtube/v3/playlists', resource_params(options)
147
+ end
148
+ end
149
+
150
+ # @return [Yt::Channel] the channel associated with the YouTube account
151
+ # that provided the authentication token.
152
+ def self.mine
153
+ Relation.new(self) do |options|
154
+ get '/youtube/v3/channels', mine: true, part: 'id'
155
+ end.first
156
+ end
157
+
158
+ private
159
+
160
+ def like_list_ids
161
+ names = %w(likes favorites)
162
+ related_playlists.select{|name,_| names.include? name}.values
163
+ end
136
164
  end
137
165
  end
@@ -20,7 +20,7 @@ module Yt
20
20
  def comments
21
21
  @comments ||= Relation.new(Comment, parent_id: id,
22
22
  initial_items: -> {[top_level_comment]}) do |options|
23
- fetch '/youtube/v3/comments', thread_comments_params(options)
23
+ get '/youtube/v3/comments', thread_comments_params(options)
24
24
  end
25
25
  end
26
26
  end
@@ -1,6 +1,7 @@
1
1
  require 'json' # for JSON.parse
2
2
 
3
3
  require 'yt/config'
4
+ require 'yt/auth'
4
5
  require 'yt/no_items_error'
5
6
  require 'yt/http_request'
6
7
  require 'yt/relation'
@@ -3,6 +3,6 @@ module Yt
3
3
  module Core
4
4
  # @return [String] the SemVer-compatible gem version.
5
5
  # @see http://semver.org
6
- VERSION = '0.1.4'
6
+ VERSION = '0.1.5'
7
7
  end
8
8
  end
@@ -59,7 +59,7 @@ module Yt
59
59
  # @return [Yt::Relation<Yt::PlaylistItem>] the items of the playlist.
60
60
  def items
61
61
  @items ||= Relation.new(PlaylistItem, playlist_id: id) do |options|
62
- fetch '/youtube/v3/playlistItems', playlist_items_params(options)
62
+ get '/youtube/v3/playlistItems', playlist_items_params(options)
63
63
  end
64
64
  end
65
65
 
@@ -67,7 +67,7 @@ module Yt
67
67
  def videos
68
68
  @videos ||= Relation.new(Video, playlist_id: id) do |options|
69
69
  params = playlist_items_params(options.merge parts: [:content_details])
70
- items = fetch '/youtube/v3/playlistItems', params
70
+ items = get '/youtube/v3/playlistItems', params
71
71
  videos_for items, 'contentDetails', options
72
72
  end
73
73
  end
@@ -55,5 +55,27 @@ module Yt
55
55
  def thumbnail_url(size = :default)
56
56
  thumbnails.fetch(size.to_s, {})['url']
57
57
  end
58
+
59
+ # @return [Yt::PlaylistItem] the item created by appending the given
60
+ # video to the given playlist.
61
+ def self.insert(playlist_id:, video_id:)
62
+ parts = %i(id snippet)
63
+ items = -> (body) { [body] } # the response body only includes one item
64
+ resource_id = {kind: 'youtube#video', videoId: video_id}
65
+ snippet = {playlistId: playlist_id, resourceId: resource_id}
66
+
67
+ Relation.new(self, parts: parts, extract_items: items) do |options|
68
+ post '/youtube/v3/playlistItems', {part: 'snippet'}, {snippet: snippet}
69
+ end.first
70
+ end
71
+
72
+ # @return [Boolean] whether the item was removed from the playlist.
73
+ def delete
74
+ items = -> (body) { [{}] } # the response body is empty
75
+
76
+ Relation.new(PlaylistItem, id: id, extract_items: items) do |options|
77
+ delete '/youtube/v3/playlistItems', id: options[:id]
78
+ end.any?
79
+ end
58
80
  end
59
81
  end
@@ -9,7 +9,7 @@ module Yt
9
9
  # @yield [Hash] the options to change which items to iterate through.
10
10
  def initialize(item_class, options = {}, &item_block)
11
11
  @options = {parts: %i(id), limit: Float::INFINITY, item_class: item_class,
12
- initial_items: -> {[]}}
12
+ initial_items: -> {[]}, extract_items: -> (body) {body['items']}}
13
13
  @options.merge! options
14
14
  @item_block = item_block
15
15
  end
@@ -27,10 +27,10 @@ module Yt
27
27
  @items ||= initial_items.dup
28
28
  if @items[@last_index].nil? && more_pages?
29
29
  response = Response.new(@options, &@item_block).run
30
- more_items = response.body['items'].map do |item|
30
+ more_items = @options[:extract_items].call(response.body).map do |item|
31
31
  @options[:item_class].new attributes_for_new_item(item)
32
32
  end
33
- @options.merge! offset: response.body['nextPageToken']
33
+ @options.merge! offset: response.body['nextPageToken'] if response.body
34
34
  @items.concat more_items
35
35
  end
36
36
  @items[(@last_index +=1) -1]
@@ -34,7 +34,7 @@ module Yt
34
34
  def self.where(conditions = {})
35
35
  @where ||= Relation.new(self) do |options|
36
36
  slicing_conditions_every(50) do |slice_options|
37
- fetch resources_path, where_params(slice_options)
37
+ get resources_path, where_params(slice_options)
38
38
  end
39
39
  end
40
40
  @where.where conditions
@@ -50,7 +50,7 @@ module Yt
50
50
  define_method name do
51
51
  keys = Array(options[:in]) + [name]
52
52
  part = keys.shift
53
- value = @data[part] || fetch_part(part)
53
+ value = @data[part] || get_part(part)
54
54
  keys.each{|key| value = value[camelize key]}
55
55
  if value.nil? && options[:default]
56
56
  value = options[:default]
@@ -60,9 +60,9 @@ module Yt
60
60
  end
61
61
  end
62
62
 
63
- def fetch_part(required_part)
63
+ def get_part(required_part)
64
64
  resources = Relation.new(self.class, ids: [id]) do |options|
65
- fetch resources_path, resource_params(options)
65
+ get resources_path, resource_params(options)
66
66
  end
67
67
 
68
68
  parts = (@selected_data_parts + [required_part]).uniq
@@ -12,8 +12,47 @@ module Yt
12
12
 
13
13
  private
14
14
 
15
- def fetch(path, params)
16
- HTTPRequest.new(path: path, params: params).run
15
+ def get(path, params = {})
16
+ request :get, path: path, params: params
17
+ end
18
+
19
+ def post(path, params = {}, body = {})
20
+ request :post, path: path, params: params, body: body
21
+ end
22
+
23
+ def delete(path, params = {})
24
+ request :delete, path: path, params: params
25
+ end
26
+
27
+ def request(method, options = {})
28
+ HTTPRequest.new(request_options options.merge method: method).run
29
+ rescue HTTPError => error
30
+ if unauthorized?(error) && refresh_access_token
31
+ retry
32
+ else
33
+ raise
34
+ end
35
+ end
36
+
37
+ def request_options(options)
38
+ options[:error_message] = ->(body) {JSON(body)['error']['message']}
39
+ if access_token = Yt.configuration.access_token || refresh_access_token
40
+ options[:headers] = {'Authorization' => "Bearer #{access_token}"}
41
+ else
42
+ options[:params] = options[:params].merge key: Yt.configuration.api_key
43
+ end
44
+ options
45
+ end
46
+
47
+ def unauthorized?(error)
48
+ error.response.is_a? Net::HTTPUnauthorized
49
+ end
50
+
51
+ def refresh_access_token
52
+ if Yt.configuration.refresh_token
53
+ auth = Auth.new refresh_token: Yt.configuration.refresh_token
54
+ Yt.configuration.access_token = auth.access_token
55
+ end
17
56
  end
18
57
 
19
58
  def resources_path
@@ -55,7 +94,6 @@ module Yt
55
94
  def default_params(options)
56
95
  {}.tap do |params|
57
96
  params[:max_results] = 50
58
- params[:key] = Yt.configuration.api_key
59
97
  params[:part] = options[:parts].join ','
60
98
  params[:page_token] = options[:offset]
61
99
  end
@@ -90,7 +128,7 @@ module Yt
90
128
  else
91
129
  options[:ids] = items.body['items'].map{|item| item['id']}
92
130
  options[:offset] = nil
93
- fetch('/youtube/v3/videos', resource_params(options)).tap do |response|
131
+ get('/youtube/v3/videos', resource_params(options)).tap do |response|
94
132
  response.body['nextPageToken'] = items.body['nextPageToken']
95
133
  end
96
134
  end
@@ -183,7 +183,7 @@ module Yt
183
183
  # @return [Yt::Relation<Yt::CommentThread>] the threads of the video.
184
184
  def threads
185
185
  @threads ||= Relation.new(CommentThread, video_id: id) do |options|
186
- fetch '/youtube/v3/commentThreads', video_threads_params(options)
186
+ get '/youtube/v3/commentThreads', video_threads_params(options)
187
187
  end
188
188
  end
189
189
 
@@ -23,7 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.add_dependency 'yt-support', '>= 0.1.2'
26
+ spec.add_dependency 'yt-auth', '>= 0.2.3'
27
+ spec.add_dependency 'yt-support', '>= 0.1.3'
27
28
 
28
29
  spec.add_development_dependency 'bundler', '~> 1.14'
29
30
  spec.add_development_dependency 'rspec', '~> 3.5'
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yt-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-06 00:00:00.000000000 Z
11
+ date: 2017-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: yt-auth
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.3
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: yt-support
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ">="
18
32
  - !ruby/object:Gem::Version
19
- version: 0.1.2
33
+ version: 0.1.3
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - ">="
25
39
  - !ruby/object:Gem::Version
26
- version: 0.1.2
40
+ version: 0.1.3
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement