markov_twitter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4cf59089020057ea5529984594a60fe7397d58f3
4
+ data.tar.gz: 2b9396221a1a511e0f7a2b324cadcc52595569b0
5
+ SHA512:
6
+ metadata.gz: 5346cb8d500dd6cc45f58bcbb1ff0025803c0f9559c30e0dec4f5c42f1afcc5ff8a009c501706df5010f7504b185d8fac4c5b53486b1873d5dd588bae848b2a3
7
+ data.tar.gz: c29df5a5f48de8089e193a2f884730880b4343fa3bd4682138dcbf9fbf824aa39898d00b18966129bbf406c14746f1d293a59eef282b4cdf37a96da968190b9c
data/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # markov_twitter
2
+
3
+ ## setup: _installation_
4
+
5
+ Either:
6
+
7
+ ```sh
8
+ gem install markov_twitter
9
+ ```
10
+
11
+ or add it to a Gemfile:
12
+
13
+ ```rb
14
+ gem "markov_twitter"
15
+ ```
16
+
17
+ After doing this, require it as usual:
18
+
19
+ ```rb
20
+ require "markov_twitter"
21
+ ```
22
+
23
+ ## setup: _twitter integration_
24
+
25
+ The source code of the gem (available on github [here](http://github.com/maxpleaner/markov_twitter)) includes a `.env.example` file which includes two environment variables. Both of them need to be changed to the values provided by Twitter. To get these credentials, create an application on the Twitter developer console. Then create a file identical to `.env.example` but named `.env` in the root of your project, and add the credentials there. Finally, add the [dotenv](https://github.com/bkeepers/dotenv) gem and call `Dotenv.load` right afterward.
26
+
27
+ The two environment variables that are needed are `TWITTER_API_KEY` and `TWITTER_SECRET_KEY`. They can alternatively be set on a per-invocation basis using the [env](https://ss64.com/bash/env.html) command in bash, e.g.:
28
+
29
+ ```sh
30
+ env TWITTER_API_KEY=foo TWITTER_SECRET_KEY=bar ruby script.rb
31
+ ```
32
+
33
+ Note that the callback URL or any of the OAuth stuff on the Twitter dev console is unnecessary. Specifically this requires only [application-only authentication](https://developer.twitter.com/en/docs/basics/authentication/overview/application-only).
34
+
35
+ ## usage: _TweetReader_
36
+
37
+ First, initialize a [MarkovTwitter::Authenticator](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/Authenticator):
38
+
39
+ ```rb
40
+ authenticator = MarkovTwitter::Authenticator.new(
41
+ api_key: ENV.fetch("TWITTER_API_KEY"),
42
+ secret_key: ENV.fetch("TWITTER_SECRET_KEY")
43
+ )
44
+ ```
45
+
46
+ Then initialize [MarkovTwitter::TweetReader](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/TweetReader):
47
+
48
+ ```rb
49
+ tweet_reader = MarkovTwitter::TweetReader.new(
50
+ client: authenticator.client
51
+ )
52
+ ```
53
+
54
+ Lastly, fetch some tweets for an arbitrary username. Note that the [get_tweets](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/TweetReader:get_tweets) method will return the most recently 20 tweets only. This gem doesn't have a way to fetch more tweets than that.
55
+
56
+ ```rb
57
+ tweets = tweet_reader.get_tweets(username: "@accidental575")
58
+ puts tweets.map(&:text).first # the newest
59
+ # => "Jets fan who stands for /\nnational anthem sits on /\nAmerican flag /\n#accidentalhaiku by @Deadspin \nhttps://t.co/INsLlMB31G"
60
+ ```
61
+
62
+ ## usage: _MarkovBuilder_
63
+
64
+ [MarkovTwitter::MarkovBuilder](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder) gets passed the list of tweet strings to its initialize:
65
+
66
+ ```rb
67
+ chain = MarkovTwitter::MarkovBuilder.new(
68
+ phrases: tweets.map(&:text)
69
+ )
70
+ ```
71
+
72
+ It internally stores the words in a [#nodes](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:nodes) dict where keys are strings and values are [Node](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node) instances. A Node is created from each whitespace-separated entity. Punctuation is treated like any other non-whitespace character.
73
+
74
+ The linkages between words are automatically created ([Node#linkages](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:linkages)) and it's possible to evaluate the chain right away, producing a randomly generated sentence. There are three built in methods to evaluate the chain, but more can be constructed using lower-level methods. There are two ways these methods differ:
75
+
76
+ 1. Do they build the result by walking along the :next or :prev nodes (forward or backward)?
77
+
78
+ 2. How do they pick the first node, and how do they choose a node when there are no more linkages along the given direction (:prev or :next)?
79
+
80
+ Here are those three methods:
81
+
82
+ 1. [evaluate](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:evaluate)
83
+
84
+ - traverses rightward along :next
85
+ - when starting or stuck, picks any random word
86
+
87
+ ```rb
88
+ 5.times.map { chain.evaluate length: 10 }
89
+ # => [
90
+ # "by @FlayrahNews https://t.co/LbxzPQ5Zqv back. / together with dung! / American",
91
+ # "thought/ #accidentalhaiku by @news_24_365 https://t.co/kkfz5S3Kut pumpkin / Wes Anderson's Isle",
92
+ # "has been in a lot about / #accidentalhaiku by @UrbanLion_",c
93
+ # "them, my boyfriend used my friends. Or as / #accidentalhaiku",
94
+ # "25 years... / feeling it today. / to write /"
95
+ # ]
96
+ ```
97
+
98
+ 2. [evaluate_favoring_end](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:evaluate_favoring_end)
99
+
100
+ - traverses leftward along :prev
101
+ - when starting or stuck, picks a word that was at the end of one of the original phrases.
102
+ - reverses the result before returning
103
+
104
+ ```rb
105
+ 5.times.map { chain.evaluate_favoring_end length: 10 }
106
+ # => [
107
+ # "revolution / to improve care, / #accidentalhaiku by @Deadspin https://t.co/INsLlMB31G",
108
+ # "to save the songs you thought/ #accidentalhaiku by @Mary_Mulan https://t.co/ixw2EQamHq",
109
+ # "adventure / together with dung! / #accidentalhaiku by @Deadspin https://t.co/INsLlMB31G",
110
+ # "harder / for / creativity? / #accidentalhaiku by @AlbertBrooks https://t.co/DzXbGeYh0Z",
111
+ # "/ Asking for 25 years... / #accidentalhaiku by @StratfordON https://t.co/k81u693AbV"
112
+ # ]
113
+ ```
114
+
115
+ 3. [evaluate_favoring_start](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:evaluate_favoring_start)
116
+
117
+ - traverses rightward along :next
118
+ - when starting or stuck, picks a word that was at the start of one of the original phrases.
119
+
120
+ ```rb
121
+ 5.times.map { chain.evaluate_favoring_start length: 10 }
122
+ # => [
123
+ # "RT if you listened to / to get lost /",
124
+ # "Jets fan who stands for / #accidentalhaiku by @theloniousdev https://t.co/6Rb5F8XySy # ",
125
+ # "The first trailer for / and never come back. # /",
126
+ # "Zooey Deschanel / and never come back. / house in # ",
127
+ # "Oh my friends. Or as / #accidentalhaiku by @timkaine https://t.co/4pgknpmom5 # "
128
+ # ]
129
+ ```
130
+
131
+ Note that it is possible to manually change the lists of start nodes and end nodes using [MarkovBuilder#start_nodes](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:start_nodes) and [MarkovBuilder#end_nodes](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:end_nodes)
132
+
133
+ ## advanced usage: _custom evaluator_
134
+
135
+ The three previously mentioned methods all use [_evaluate](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:_evaluate) under the hood. This method supports any permutation of the following keyword args (all except start_node and probability_bounds are required).
136
+
137
+ - **length**
138
+ number of nodes in the result
139
+ - **direction**
140
+ :next or :prev
141
+ - **start_node**
142
+ the node to use at the beginning
143
+ - **probability_bounds**
144
+ _Array<Int1,Int2>_ where _0 <= Int1 <= Int2 <= 100_
145
+ This is essentially used to "stack the dice", so to speak. Internally, smaller probabilities are checked first. So if A has 50% likelihood and B/C/D/E/F each have 10% likelihood, then B/C/D/E/F can be guaranted by using [0,50] as probability_bounds. This 'stacked' probability is applied any time the program chooses a :next or :prev option.
146
+ - **node_finder**
147
+ A lambda which gets run when the evaluator is starting or stuck. It gets passed random nodes one-by-one. The first one for which the block returns a truthy value is used.
148
+
149
+ Note that [_evaluate](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:_evaluate) returns nodes and so the values must be manually fetched and joined. Here's an example of providing a custom node_finder lambda so that all phrases in the result start with "the":
150
+
151
+ ```rb
152
+ 5.times.map do
153
+ nodes = chain._evaluate(
154
+ direction: :next,
155
+ length: 10,
156
+ node_finder: -> (node) {
157
+ node.value.downcase == "the"
158
+ }
159
+ )
160
+ nodes.map(&:value).join " "
161
+ end
162
+ # => [
163
+ # "the rain / #accidentalhaiku by @theloniousdev https://t.co/6Rb5F8XySy The first trailer",
164
+ # "The first trailer for / #accidentalhaiku by @shiku___ https://t.co/ZutjdsopAo the",
165
+ # "the songs you thought/ #accidentalhaiku by @Mary_Mulan https://t.co/ixw2EQamHq The first",
166
+ # "The first trailer for / #accidentalhaiku by @UrbanLion_ https://t.co/bvM6eeXGj5 The",
167
+ # "the rain / and start / I THOUGHT MY BOYFRIEND"
168
+ # ]
169
+ ```
170
+
171
+ ## advanced usage: _linkage manipulation_
172
+
173
+ There are manipulations available at the [Node](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node) level (accessible through the [MarkovBuilder#nodes](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder:nodes) dict). Keep in mind that there is only a single Node for each unique string. There can be many references to it from other nodes' linkages, but since there is still only a single object, each unique string only has a single set of :next and :previous linkages emanating from it.
174
+
175
+ Although the core linkage data is accessible in [Node#linkages](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:linkages) and [Node#total_num_inputs](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:total_num_inputs), they should not be manipulated directly via these references. Rather, use one of the following methods which are automatically balancing in terms of keeping :next and :previous probabilities mirrored and ensuring that the probabilities sum to 1. That is to say, if I add _node1_ as the :next linkage of _node2_, then _node1_ will have its :prev probabilities balanced and _node2_ will have its :next probabilities balanced.
176
+
177
+ 1. [#add_next_linkage(child_node)](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:add_next_linkage)
178
+ adds a linkage in the :next direction or increases its likelihood
179
+ 2. [#add_prev_linkage(parent_node)](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:add_prev_linkage)
180
+ adds a linkage in the :prev direction or increases its likelihood
181
+ 3. [#remove_next_linkage(child_node)](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:remove_next_linkage)
182
+ removes a linkage in the :next direction or decreases its likelihood
183
+ 4. [#remove_prev_linkage(parent_node)](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:remove_prev_linkage)
184
+ removes a linkage in the :prev direction or decreases its likelihood
185
+ 5. [#add_linkage!(direction, other_node, probability)](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:add_linkage!)
186
+ Force-sets the probability of a linkage. Adjusts the other probabilities so they still sum to 1.
187
+ 6. [#remove_linkage!(direction, other_node)](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:remove_linkage!)
188
+ Completely removes a linkage as an option. Adjusts other probabilities so they still sum to 1.
189
+
190
+ All of these methods can be safely run many times. Note that `remove_next_linkage` and `remove_prev_linkage` do _not_ completely remove the node from the list of options. They just decrement its probability an amount determined by [Node#total_num_inputs](http://rubydoc.info/gems/markov_twitter/MarkovTwitter/MarkovBuilder/Node:total_num_inputs).
191
+
192
+ ## development: code organization
193
+
194
+ The gem boilerplate was scaffolded using a gem I made, [gemmyrb](http://github.com/maxpleaner/gemmyrb).
195
+
196
+ Test scripts are in the [spec/](http://github.com/maxpleaner/markov_twitter/tree/master/spec) folder, although some helper methods are written into the application code at [lib/markov_twitter/test_helper_methods.rb](http://github.com/maxpleaner/markov_twitter/tree/master/lib/markov_twitter/test_helper_methods.rb).
197
+
198
+ The application code is in [lib/](http://github.com/maxpleaner/markov_twitter/tree/lib).
199
+
200
+ Documentation is built with [yard](https://github.com/lsegal/yard) into [doc/](http://github.com/maxpleaner/markov_twitter/tree/master/doc) - it's viewable [on rubydoc](http://rubydoc.info/gems/markov_twitter).
201
+
202
+ ## development: tests
203
+
204
+ To run the tests, install markov_twitter with the development dependencies:
205
+
206
+ ```rb
207
+ gem install markov_twitter --development
208
+ ```
209
+
210
+ Then run `rspec` in the root of the repo.
211
+
212
+ There are 40 test cases at time of writing.
213
+
214
+ By default, Webmock will prevent any real HTTP calls for the twitter-related tests, but this can be disabled (and real Twitter data used) by running the test suite with an environment variable:
215
+
216
+ ```sh
217
+ env DISABLE_WEBMOCK=true rspec
218
+ ```
219
+
220
+ ## development: building docs
221
+
222
+ Docs are built with `yard` from the command line. It has 100% documentation at time of writing. If when building, it shows that something is undocumented, run `yard --list-undoc` to find out where it is.
223
+
224
+ ## development: todos
225
+
226
+ Things which would be interesting to add:
227
+
228
+ - dictionary-based search and replace
229
+ - part-of-speech-based search and replace
230
+
data/bin/console ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pry'
4
+ require 'dotenv'
5
+ require 'markov_twitter'
6
+
7
+ Dotenv.load
8
+
9
+ authenticator = MarkovTwitter::Authenticator.new(
10
+ api_key: ENV.fetch("TWITTER_API_KEY"),
11
+ secret_key: ENV.fetch("TWITTER_SECRET_KEY")
12
+ )
13
+
14
+ tweet_reader = MarkovTwitter::TweetReader.new(
15
+ client: authenticator.client
16
+ )
17
+
18
+ tweets = tweet_reader.get_tweets(username: "@accidental575")
19
+
20
+ chain = MarkovTwitter::MarkovBuilder.new(
21
+ phrases: tweets.map(&:text)
22
+ )
23
+
24
+
25
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'markov_twitter'
4
+ class MarkovTwitter::CLI < Thor
5
+ # Nothing here yet
6
+ end
7
+
8
+ MarkovTwitter::CLI.start ARGV
@@ -0,0 +1,16 @@
1
+ # Wrapper for the twitter gem's client.
2
+ class MarkovTwitter::Authenticator
3
+
4
+ # @return [Twitter::Client]
5
+ attr_reader :client
6
+
7
+ # @param api_key [String] should be stored in ENV var.
8
+ # @param secret_key [String] should be stored in ENV var.
9
+ def initialize(api_key:, secret_key:)
10
+ @client = Twitter::REST::Client.new do |config|
11
+ config.consumer_key = api_key
12
+ config.consumer_secret = secret_key
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,192 @@
1
+ class MarkovTwitter::MarkovBuilder
2
+
3
+ # Represents a single node in a Markov chain.
4
+ class Node
5
+
6
+ # @return [String] a single token, such as a word.
7
+ attr_reader :value
8
+
9
+ # @return [Hash<Symbol, Hash<String, Float>>]
10
+ # the :next and :previous linkages.
11
+ # - Outer hash is keyed by the direction (:next, :prev).
12
+ # - Inner hash represents possible traversals -
13
+ # also keyed by string value, its values are probabilities
14
+ # representing the likelihood of choosing that route.
15
+ attr_accessor :linkages
16
+
17
+ # @return [Hash<Symbol>, Integer]
18
+ # the total number of inputs added in each direction.
19
+ # - also used to re-calculate probabilities.
20
+ attr_accessor :total_num_inputs
21
+
22
+ # @return [Hash<String,Node>]
23
+ # a reference to the attr of the parent MarkovBuilder
24
+ attr_reader :nodes
25
+
26
+ # @param value [String] for example, a word.
27
+ # @param nodes [Hash<String,Node>].
28
+ def initialize(value:, nodes:)
29
+ @value = value
30
+ @linkages = { next: Hash.new(0), prev: Hash.new(0) }
31
+ @total_num_inputs = { next: 0, prev: 0 }
32
+ @nodes = nodes
33
+ end
34
+
35
+ # Adds a single node to the :next linkages and updates probabilities.
36
+ # Also updates the opposite direction,
37
+ # e.g. :prev will be updated if something is added to :next.
38
+ # @param direction [Symbol] either :next or :prev.
39
+ # @param other_node [Node]
40
+ # @param mirror_change [Boolean]
41
+ # whether to update the opposite direction, defaults to true.
42
+ # @return [void]
43
+ def add_and_adjust_probabilities(direction, other_node, mirror_change=true)
44
+ @nodes[other_node.value] ||= other_node
45
+ total_num_inputs[direction] += 1
46
+ unit = get_probability_unit(direction)
47
+ probability_multiplier = (total_num_inputs[direction] - 1) * unit
48
+ linkages[direction].each_key do |node_key|
49
+ linkages[direction][node_key] *= probability_multiplier
50
+ end
51
+ linkages[direction][other_node.value] += unit
52
+ # Add a linkage in the opposite direction to keep :next and :prev in sync
53
+ update_opposite_direction(direction, other_node, __method__) if mirror_change
54
+ end
55
+
56
+ # Determines the weight of a single insertion by looking up the total
57
+ # number of insertions in that direction.
58
+ # @param direction [Symbol] :next or :prev
59
+ # @return [Float] between 0 and 1.
60
+ def get_probability_unit(direction)
61
+ unless total_num_inputs[direction] > 0
62
+ raise ArgumentError, "no inputs were added in <direction>"
63
+ end
64
+ 1.0 / total_num_inputs[direction]
65
+ end
66
+
67
+ # Removes a single node from the :prev linkages and updates probabilities.
68
+ # Safe to run if the other_node is not actually present in the linkages.
69
+ # Mirrors the change in the opposite direction, to keep :prev and :next in sync.
70
+ # @param direction [Symbol] either :next or :prev
71
+ # @param other_node [Node] the node to be removed.
72
+ # @param mirror_change [Boolean]
73
+ # whether to update the opposite direction, defaults to true
74
+ # @return [void]
75
+ def remove_and_adjust_probabilities(direction, other_node, mirror_change=true)
76
+ return unless linkages[direction].has_key? other_node.value
77
+ unit = get_probability_unit(direction)
78
+ if linkages[direction][other_node.value] - unit <= 0
79
+ delete_linkage!(direction, other_node)
80
+ else
81
+ linkages[direction][other_node.value] -= unit
82
+ num_per_direction = total_num_inputs[direction]
83
+ linkages[direction].each_key do |node_key|
84
+ linkages[direction][node_key] *= (
85
+ num_per_direction / (num_per_direction - 1.0)
86
+ )
87
+ end
88
+ total_num_inputs[direction] -= 1
89
+ end
90
+ # Add a linkage in the opposite direction to keep :next and :prev in sync
91
+ update_opposite_direction(direction, other_node, __method__) if mirror_change
92
+ end
93
+
94
+ # Force-removes a linkage, re-adjusting other probabilities
95
+ # but potentially breaking their proportionality.
96
+ # Can be safely run for non-existing nodes.
97
+ # Adjusts the linkages in the opposite direction accordingly.
98
+ # @param direction [Symbol]
99
+ # @param other_node [Node]
100
+ # @param mirror_change [Boolean]
101
+ # whether to update the opposite direction, defaults to true
102
+ # @return [void]
103
+ def delete_linkage!(direction, other_node, mirror_change=true)
104
+ return unless linkages[direction].has_key? other_node.value
105
+ probability = linkages[direction][other_node.value]
106
+ # delete the linkage
107
+ linkages[direction].delete other_node.value
108
+ # distribute the probability evenly among the other options.
109
+ amt_to_add = probability / linkages[direction].keys.length
110
+ linkages[direction].each_key do |key|
111
+ linkages[direction][key] += amt_to_add
112
+ end
113
+ # decrement the total count
114
+ total_num_inputs[direction] -= 1
115
+ # Add a linkage in the opposite direction to keep :next and :prev in sync
116
+ update_opposite_direction(direction, other_node, __method__) if mirror_change
117
+ end
118
+
119
+ # Force-adds a linkage at a specific probability.
120
+ # Readjusts other probabilities but may break their proportionality.
121
+ # Updates the opposite direction to keep :next and :prev in sync
122
+ # @param direction [Symbol]
123
+ # @param other_node [Node]
124
+ # @param probability [Float] between 0 and 1.
125
+ # @param mirror_change [Boolean] whether to update the opposite direction.
126
+ # @return [void]
127
+ def add_linkage!(direction, other_node, probability, mirror_change=true)
128
+ raise ArgumentError, "invalid probability" unless probability.between?(0,1)
129
+ # first remove any existing node there and distribute the probability.
130
+ delete_linkage!(direction, other_node)
131
+ # Re-adjust each probability to account for the added value
132
+ linkages[direction].each_key do |key|
133
+ linkages[direction][key] *= (1 - probability)
134
+ # remove the linkage if it's probability is zero
135
+ if linkages[direction][key].zero?
136
+ delete_linkage!(direction, @nodes[key])
137
+ end
138
+ end
139
+ # Add the new value and set its probability
140
+ binding.pry if other_node.value == "dog"
141
+ linkages[direction][other_node.value] = probability
142
+ # increment the total count
143
+ total_num_inputs[direction] += 1
144
+ # Add a linkage in the opposite direction to keep :next and :prev in sync
145
+ if mirror_change
146
+ update_opposite_direction(direction, other_node, __method__, probability)
147
+ end
148
+ end
149
+
150
+ # Calls given method_name on other_node, passing the opposite direction
151
+ # to the one given as an argument. This keeps :next and :prev in sync.
152
+ # @param direction [Symbol]
153
+ # something should have already been added/removed here
154
+ # @param other_node [Node] the node which was added/removed
155
+ # @param method_name [Symbol] the method to invoke on other_node
156
+ # @return [void]
157
+ def update_opposite_direction(direction, other_node, method_name, *other_args)
158
+ other_direction = (%i{next prev} - [direction])[0]
159
+ other_node.send(method_name, other_direction, self, *other_args, false)
160
+ end
161
+
162
+ # Adds another node to the :next linkages, updating probabilities.
163
+ # @param child_node [Node] to be added.
164
+ # @return [void]
165
+ def add_next_linkage(child_node, mirror_change=true)
166
+ add_and_adjust_probabilities(:next, child_node)
167
+ end
168
+
169
+ # Adds another node to the :prev linkages, updating probabilities.
170
+ # @param parent_node [Node] to be added.
171
+ # @return [void]
172
+ def add_prev_linkage(parent_node, mirror_change=true)
173
+ add_and_adjust_probabilities(:prev, parent_node)
174
+ end
175
+
176
+ # Removes a node from the :next linkages, updating probabilities.
177
+ # @param child_node [Node] to be removed.
178
+ # @return [void]
179
+ def remove_next_linkage(child_node, mirror_change=true)
180
+ remove_and_adjust_probabilities(:next, child_node)
181
+ end
182
+
183
+ # Removes a node from the :prev linkages, updating probabilities.
184
+ # @param parent_node [Node] to be removed.
185
+ # @return [void]
186
+ def remove_prev_linkage(parent_node, mirror_change=true)
187
+ remove_and_adjust_probabilities(:prev, parent_node)
188
+ end
189
+
190
+ end
191
+
192
+ end
@@ -0,0 +1,236 @@
1
+ # Builds a Markov chain from phrases passed as input.
2
+ # A "phrase" is defined here as a tweet.
3
+ class MarkovTwitter::MarkovBuilder
4
+
5
+ # Regex used to split the phrase into tokens.
6
+ # It splits on any number of whitespace\in sequence.
7
+ # Sequences of punctuation characters are treated like any other word.
8
+ SeparatorCharacterRegex = /\s+/
9
+
10
+ # The base dictionary for nodes.
11
+ # There is only a single copy of each node created,
12
+ # although they are referenced in Node#linkages as well.
13
+ # @return [Hash<String, Node>]
14
+ attr_reader :nodes
15
+
16
+ # The nodes that were found at the start of phrases
17
+ # @return [Set<Node>]
18
+ attr_reader :start_nodes
19
+
20
+ # The nodes that were found at the end of phrases
21
+ # @return [Set<Node>]
22
+ attr_reader :end_nodes
23
+
24
+ # lambdas which can be used during evaluation to find the first node,
25
+ # or the next node when "stuck" (meaning there is no :next/:prev node).
26
+ # @return [Lambda<Node>]
27
+ # the lambda should return true for a node that is suitable.
28
+ def node_finders
29
+ @node_finders ||= {
30
+ random: -> (node) { true },
31
+ favor_start: -> (node) { start_nodes.include? node.value },
32
+ favor_end: -> (node) { end_nodes.include? node.value },
33
+ }
34
+ end
35
+
36
+ # Splits a phrase into tokens.
37
+ # @param phrase [String]
38
+ # @return [Array<String>]
39
+ def self.split_phrase(phrase)
40
+ phrase.split(SeparatorCharacterRegex)
41
+ end
42
+
43
+ # @param phrases [Array<String>] e.g. sentences or tweets.
44
+ # processes the phrases to populate @nodes.
45
+ def initialize(phrases: [])
46
+ @nodes = {}
47
+ @start_nodes = Set.new
48
+ @end_nodes = Set.new
49
+ phrases.each &method(:process_phrase)
50
+ end
51
+
52
+ # Splits a phrase into tokens, adds them to @nodes, and creates linkages.
53
+ # @param phrase [String] e.g. a sentence or tweet.
54
+ # @return [void]
55
+ def process_phrase(phrase)
56
+ node_vals = self.class.split_phrase(phrase)
57
+ last_node = nil
58
+ node_vals.length.times do |i|
59
+ nodes = node_vals[i..(i+1)].compact.map do |node_val|
60
+ construct_node(node_val)
61
+ end
62
+ @start_nodes.add(nodes[0].value) if i == 0
63
+ last_node = nodes.last
64
+ add_nodes(*nodes)
65
+ end
66
+ @end_nodes.add last_node.value
67
+ end
68
+
69
+ # Adds a sequence of two tokens to @nodes and creates linkages.
70
+ # if node_val2 is nil, it won't be added and linkages won't be created
71
+ # @param node1 [Node]
72
+ # @param node2 [Node]
73
+ # @return [void]
74
+ def add_nodes(node1, node2=nil)
75
+ unless node1.is_a?(Node)
76
+ raise ArgumentError, "first arg passed to add_nodes is not a Node"
77
+ end
78
+ @nodes[node1.value] ||= node1
79
+ if node2
80
+ @nodes[node2.value] ||= node2
81
+ add_linkages(*@nodes.values_at(*[node1,node2].map(&:value)))
82
+ end
83
+ end
84
+
85
+ # Builds a single node which contains a reference to @nodes.
86
+ # Note that this does do the inverse (it doesn't add the node to @nodes)
87
+ # @param value [String]
88
+ # @return [Node]
89
+ def construct_node(value)
90
+ Node.new(value: value, nodes: @nodes)
91
+ end
92
+
93
+ # Adds bidirectional linkages beween two nodes.
94
+ # the Node class re-calculates the probabilities internally
95
+ # and mirrors the change on :prev.
96
+ # @param node1 [Node] the parent.
97
+ # @param node2 [Node] the child.
98
+ # @return [void]
99
+ def add_linkages(node1, node2)
100
+ node1.add_next_linkage(node2, mirror_change=true)
101
+ end
102
+
103
+ # The default evaluation method to produce a run case.
104
+ # Goes in forward direction with with random nodes as start points.
105
+ # See also #evaluate_favoring_start and #evaluate_favoring_end.
106
+ # See #_evaluate for paramspecs
107
+ # The passed node_node_finder lambda picks a totally random new node.
108
+ # @return [String] the result of #_evaluate joined by whitespace.
109
+ def evaluate(length:, probability_bounds: [0,100], root_node: nil)
110
+ _evaluate(
111
+ length: length,
112
+ probability_bounds: probability_bounds,
113
+ root_node: root_node,
114
+ direction: :next,
115
+ node_finder: node_finders[:random]
116
+ ).map(&:value).join(" ")
117
+ end
118
+
119
+ # See #_evaluate for paramspec.
120
+ # The passed node_node_finder lambda picks a node contained in @start_nodes
121
+ # An error is raised if no nodes match this condition.
122
+ # @return [String] the result of #_evaluate joined by whitespace.
123
+ def evaluate_favoring_start(length:, probability_bounds: [0,100], root_node: nil)
124
+ node_finder = node_finders[:favor_start]
125
+ has_possible_start_node = nodes.values.any? &node_finder
126
+ unless has_possible_start_node
127
+ raise ArgumentError, "@start_nodes is empty; can't evaluate favoring start"
128
+ end
129
+ _evaluate(
130
+ length: length,
131
+ probability_bounds: probability_bounds,
132
+ root_node: root_node,
133
+ direction: :next,
134
+ node_finder: node_finder
135
+ ).map(&:value).join(" ")
136
+ end
137
+
138
+ # See #_evaluate for paramspec.
139
+ # The passed node_node_finder lambda picks a node contained in @end_nodes
140
+ # An error is raised if no nodes match this condition.
141
+ # @return [String] the result of #_evaluate reversed and joined by whitespace.
142
+ def evaluate_favoring_end(length:, probability_bounds: [0,100], root_node: nil)
143
+ node_finder = node_finders[:favor_end]
144
+ has_possible_end_node = nodes.values.any? &node_finder
145
+ unless has_possible_end_node
146
+ raise ArgumentError, "@end_nodes is empty; can't evaluate favoring end"
147
+ end
148
+ _evaluate(
149
+ length: length,
150
+ probability_bounds: probability_bounds,
151
+ root_node: root_node,
152
+ direction: :prev,
153
+ node_finder: node_finder
154
+ ).map(&:value).reverse.join(" ")
155
+ end
156
+
157
+ # An "evaluation" of the markov chain. e.g. a run case.
158
+ # Passes random values through the probability sequences.
159
+ # @param length [Integer] the number of tokens in the result.
160
+ # @param probability_bounds [Array<Integer, Integer>]
161
+ # optional, can limit the probability to a range where
162
+ # 0 <= min <= result <= max <= 100.
163
+ # @param node_finder [Lambda<Node>]
164
+ # during iteration, if the current node has no linkages in <direction>,
165
+ # a new node is selected from the nodes dict. The first randomly-picked
166
+ # node which this lambda returns a truthy value for is selected.
167
+ # @return [Array<Node>] the result tokens in order.
168
+ def _evaluate(
169
+ length:,
170
+ probability_bounds: [0,100],
171
+ root_node: nil,
172
+ direction:,
173
+ node_finder:
174
+ )
175
+ length.times.reduce([]) do |result_nodes|
176
+ root_node ||= get_new_start_point(node_finder)
177
+ result_nodes.push root_node
178
+ root_node = pick_linkage(
179
+ root_node.linkages[direction],
180
+ probability_bounds,
181
+ )
182
+ result_nodes
183
+ end
184
+ end
185
+
186
+ # Gets a random node as a potential start point.
187
+ # @param node_finder [lambda<Node>]
188
+ # any returned node will return a truthy value from this.
189
+ # @return [Node] or nil if one couldn't be found.
190
+ def get_new_start_point(node_finder)
191
+ nodes.values.shuffle.find(&node_finder)
192
+ end
193
+
194
+ # validates the given probability bounds
195
+ # @param bounds [Array<Integer, Integer>]
196
+ # @return [Boolean] indicating whether it is valid
197
+ def check_probability_bounds(bounds)
198
+ bounds1, bounds2 = bounds
199
+ bounds_diff = bounds2 - bounds1
200
+ if (
201
+ (bounds_diff < 0) || (bounds_diff > 100) ||
202
+ (bounds1 < 0) || (bounds2 > 100)
203
+ )
204
+ raise ArgumentError, "wasn't given 0 <= bounds1 <= bounds2 <= 100"
205
+ end
206
+ end
207
+
208
+ # Given "linkages" which includes all possibly node traversals in
209
+ # a predetermined direction, pick one based on their probabilities.
210
+ # @param linkages [Hash<String, Float>] key=token, val=probability
211
+ # @param probability_bounds [Array<Integer,Integer>]
212
+ # Optional, can limit the probability to a range where
213
+ # 0 <= min <= result <= max <= 100.
214
+ # This gets divided by 100 before being compared to the linkage values.
215
+ #
216
+ # @return [Node] or nil if one couldn't be found.
217
+ def pick_linkage(linkages, probability_bounds=[0,100])
218
+ check_probability_bounds(probability_bounds)
219
+ bounds1, bounds2 = probability_bounds
220
+ # pick a random number between the bounds.
221
+ random_num = (rand(bounds2 - bounds1) + bounds1) * 0.01
222
+ # offset is the accumulation of probabilities seen during iteration.
223
+ offset = 0
224
+ # sort to lowest first
225
+ sorted = linkages.sort_by { |name, prob| prob }
226
+ # find the first linkage value that satisfies offset < N(rand) < val.
227
+ new_key = sorted.find do |(key, probability)|
228
+ # increment the offset each time.
229
+ random_num.between?(offset, probability + offset).tap do
230
+ offset += probability
231
+ end
232
+ end
233
+ nodes[new_key&.first]
234
+ end
235
+
236
+ end
@@ -0,0 +1,175 @@
1
+
2
+ # Methods which are included into test case via refinement,
3
+ # so they don't interfere with application code
4
+ # and don't require a namespace.
5
+ module MarkovTwitter::TestHelperMethods
6
+
7
+ # alias
8
+ Authenticator = MarkovTwitter::Authenticator
9
+
10
+ # Builds an authenticator instance with valid credentials.
11
+ # Will raise an error unless the expected ENV vars are defined.
12
+ # @return [Authenticator]
13
+ def build_valid_authenticator
14
+ Authenticator.new(
15
+ api_key: ENV.fetch("TWITTER_API_KEY"),
16
+ secret_key: ENV.fetch("TWITTER_SECRET_KEY")
17
+ )
18
+ end
19
+
20
+ # Builds an authenticator instance with invalid credentials.
21
+ # Should raise errors on subsequent operations.
22
+ # @return [Authenticator]
23
+ def build_invalid_authenticator
24
+ Authenticator.new(api_key: "", secret_key: "")
25
+ end
26
+
27
+ # This is a twitter user I've created that has a fixed set of tweets.
28
+ # It's there to make sure that fetching tweets works correctly.
29
+ # @return [String]
30
+ def get_sample_username
31
+ "max_p_sample"
32
+ end
33
+
34
+ # The expected tweets of the user I manually posted to on Twitter.
35
+ # @return [Array<String>]
36
+ def get_sample_user_tweets
37
+ [
38
+ "don't ever change",
39
+ "A long-term goal of mine is to create a water-based horror game. I've done some work on building this in Unity already.",
40
+ "Many amazing looking animals can be kept in reasonably simple environments, but some require elaborate setups .",
41
+ "I enjoy creating terrariums but it's a lot of work to keep everything balanced so that all the critters survive .",
42
+ "Although I haven't had a cat myself, I have had aquariums, terrariums, and rodents at different points .",
43
+ "i have personally never owned a pet cat, and I'm a bit allergic, but I still enjoy their company .",
44
+ "carnivorous by nature, cats hunt many other wild animals such as gophers and mice. As a result, some people would prefer less outdoor cats .",
45
+ "you have now unsubscribed to cat facts. respond with UNSUBSCRIBE to unsubscribe .",
46
+ "egyption hairless cats are less allergenic than most other cats. they don't have hair and are probably less oily .",
47
+ "the cat in the hat ate and sat. it got fat and couldn't catch a rat ."
48
+ ]
49
+ end
50
+
51
+ # Converts strings into a specific structure that is used internally by
52
+ # the twitter gem. Used for stubbing with Webmock.
53
+ # @param strings [Array<String>]
54
+ # @return [Array<Hash>] where each hash has :id and :text keys.
55
+ def to_tweet_objects(strings)
56
+ strings.map { |string| { id: 0, text: string } }
57
+ end
58
+
59
+ # Sample tweets for which the content does not matter.
60
+ # these are only used to test the pagination of Twitter results.
61
+ # @return [Array<String>]
62
+ def get_stubbed_many_tweets_user_tweets
63
+ 20.times.map &:to_s
64
+ end
65
+
66
+ # This user should raise an error when the twitter gem looks them up.
67
+ # @return [String]
68
+ def get_invalid_username
69
+ "3u9r4j8fjniecn875jdpwqk32mdiy4584vuniwcoekpd932"
70
+ end
71
+
72
+ # A twitter user which has many tweets.
73
+ # Used to test pagination of search results.
74
+ # @return [String]
75
+ def get_many_tweets_username
76
+ "SFist"
77
+ end
78
+
79
+ # Makes twitter's oauth request succeed.
80
+ # Returns without doing anything if DisableWebmock=true in ENV.
81
+ # @return [void]
82
+ def stub_twitter_token_request_with_valid_credentials
83
+ return if ENV["DisableWebmock"] == "true"
84
+ stub_request(
85
+ :post, "https://api.twitter.com/oauth2/token"
86
+ ).to_return(status: 200, body: "", headers: {})
87
+ end
88
+
89
+ # Makes twitter's oauth request fail.
90
+ # Returns without doing anything if DisableWebmock=true in ENV.
91
+ # @return [void]
92
+ def stub_twitter_token_request_with_invalid_credentials
93
+ return if ENV["DisableWebmock"] == "true"
94
+ stub_request(
95
+ :post, "https://api.twitter.com/oauth2/token"
96
+ ).to_return(status: 403, body: "", headers: {})
97
+ end
98
+
99
+ # Makes the twitter user lookup request succeed.
100
+ # Returns without doing anything if DisableWebmock=true in ENV.
101
+ # @return [void]
102
+ def stub_twitter_user_lookup_request_with_valid_username(username)
103
+ return if ENV["DisableWebmock"] == "true"
104
+ stub_request( :get,
105
+ "https://api.twitter.com/1.1/users/show.json?screen_name=#{username}"
106
+ ).to_return(status: 200, body: {id: 0}.to_json, headers: {})
107
+ end
108
+
109
+ # Makes the twitter user lookup request fail.
110
+ # Returns without doing anything if DisableWebmock=true in ENV.
111
+ # @param username [String]
112
+ # @return [void]
113
+ def stub_twitter_user_lookup_request_with_invalid_username(username)
114
+ return if ENV["DisableWebmock"] == "true"
115
+ stub_request( :get,
116
+ "https://api.twitter.com/1.1/users/show.json?screen_name=#{username}"
117
+ ).to_return(status: 404, body: {id: 0}.to_json, headers: {})
118
+ end
119
+
120
+ # Makes the twitter user timeline request succeed.
121
+ # returns without doing anything if DisableWebmock=true in ENV.
122
+ # @param tweets_to_return [Array<Hash>]
123
+ # - the hashes must have keys "id" and "text".
124
+ # @return [void]
125
+ def stub_twitter_user_timeline_request(tweets_to_return)
126
+ return if ENV["DisableWebmock"] == "true"
127
+ stub_request(
128
+ :get,
129
+ "https://api.twitter.com/1.1/statuses/user_timeline.json?user_id=0"
130
+ ).to_return(status: 200, body: tweets_to_return.to_json, headers: {})
131
+ end
132
+
133
+ # Validates that the linkages on a node are as expected.
134
+ # @param node [MarkovBuilder::Node]
135
+ # @param _next [Hash<String,Float>] mapping key to probability.
136
+ # @param prev [Hash<String,Float>] mapping key to probability.
137
+ # @param total_num_inputs [Hash<Symbol,Integer>] keyed by direction.
138
+ def validate_linkages(node, _next: nil, prev: nil, total_num_inputs: nil)
139
+ precision = 0.00001
140
+ if _next
141
+ node.linkages[:next].each do |name, linkage|
142
+ expect(linkage).to be_within(precision).of(_next[name])
143
+ end
144
+ end
145
+ if prev
146
+ node.linkages[:prev].each do |name, linkage|
147
+ expect(linkage).to be_within(precision).of(prev[name])
148
+ end
149
+ end
150
+ if total_num_inputs
151
+ expect(node.total_num_inputs).to eq(total_num_inputs)
152
+ end
153
+ rescue
154
+ binding.pry
155
+ end
156
+
157
+ # A sample phrase used to test manipulations on the markov chain.
158
+ # @return [String]
159
+ def get_sample_phrase_1
160
+ "the cat in the hat"
161
+ end
162
+
163
+ # A sample phrase used to test manipulations on the markov chain.
164
+ # @return [String]
165
+ def get_sample_phrase_2
166
+ "the bat in the flat"
167
+ end
168
+
169
+ # This module can be added with `using MarkovTwitter::TestHelperMethods`.
170
+ # It can also be added globally with `Object.include MarkovTwitter::TestHelperMethods`.
171
+ refine Object do
172
+ include MarkovTwitter::TestHelperMethods
173
+ end
174
+
175
+ end
@@ -0,0 +1,20 @@
1
+ # Fetches the latest tweets.
2
+ class MarkovTwitter::TweetReader
3
+
4
+ # @return [Twitter::REST::Client]
5
+ attr_reader :client
6
+
7
+ # @param client [Twitter::REST::Client]
8
+ def initialize(client:)
9
+ @client = client
10
+ end
11
+
12
+ # @param username [String] must exist om twitter or this will raise an error
13
+ # @return [Array<Hash>]
14
+ # - the hashes will have :text and :id keys
15
+ def get_tweets(username:)
16
+ user = client.user(username)
17
+ client.user_timeline(user)
18
+ end
19
+
20
+ end
@@ -0,0 +1,41 @@
1
+
2
+ ###############################################################################
3
+ # #
4
+ # /\/\ __ _ _ __| | _______ __ #
5
+ # / \ / _` | '__| |/ / _ \ \ / / #
6
+ # / /\/\ \ (_| | | | < (_) \ V / #
7
+ # \/ \/\__,_|_| |_|\_\___/ \_/ #
8
+ # _____ _ _ _ #
9
+ # /__ \__ _(_) |_| |_ ___ _ __ #
10
+ # / /\/\ \ /\ / / | __| __/ _ \ '__ #
11
+ # / / \ V V /| | |_| || __/ | #
12
+ # \/ \_/\_/ |_|\__|\__\___|_| #
13
+ # #
14
+ ###############################################################################
15
+
16
+ # =============================================================================
17
+ # Dependencies
18
+ # =============================================================================
19
+
20
+ # Using a gem to interact with twitter saves a lot of work.
21
+ require 'twitter'
22
+
23
+ # Extensions to Ruby core language.
24
+ require 'active_support/all'
25
+
26
+ # =============================================================================
27
+ # Top level namespace.
28
+ # =============================================================================
29
+
30
+ class MarkovTwitter; end
31
+
32
+ # =============================================================================
33
+ # Individual components.
34
+ # =============================================================================
35
+
36
+ require "markov_twitter/tweet_reader"
37
+ require "markov_twitter/authenticator"
38
+ require "markov_twitter/markov_builder"
39
+ require "markov_twitter/markov_builder/node"
40
+
41
+ require "markov_twitter/test_helper_methods"
data/lib/version.rb ADDED
@@ -0,0 +1,4 @@
1
+ class MarkovTwitter
2
+ # The version of the gem.
3
+ VERSION = '0.0.1'
4
+ end
metadata ADDED
@@ -0,0 +1,167 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: markov_twitter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - max pleaner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
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: twitter
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
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: pry-byebug
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: dotenv
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
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: yard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: ''
126
+ email: maxpleaner@gmail.com
127
+ executables:
128
+ - console
129
+ - markov_twitter
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - README.md
134
+ - bin/console
135
+ - bin/markov_twitter
136
+ - lib/markov_twitter.rb
137
+ - lib/markov_twitter/authenticator.rb
138
+ - lib/markov_twitter/markov_builder.rb
139
+ - lib/markov_twitter/markov_builder/node.rb
140
+ - lib/markov_twitter/test_helper_methods.rb
141
+ - lib/markov_twitter/tweet_reader.rb
142
+ - lib/version.rb
143
+ homepage: http://github.com/maxpleaner/markov_twitter
144
+ licenses:
145
+ - MIT
146
+ metadata: {}
147
+ post_install_message:
148
+ rdoc_options: []
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: '2.3'
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: 2.6.13
161
+ requirements: []
162
+ rubyforge_project:
163
+ rubygems_version: 2.6.13
164
+ signing_key:
165
+ specification_version: 4
166
+ summary: markov chains from twitter posts
167
+ test_files: []