dpla-analysand 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG +67 -0
  6. data/Gemfile +8 -0
  7. data/LICENSE +22 -0
  8. data/README +48 -0
  9. data/Rakefile +22 -0
  10. data/analysand.gemspec +33 -0
  11. data/bin/analysand +27 -0
  12. data/lib/analysand.rb +3 -0
  13. data/lib/analysand/bulk_response.rb +14 -0
  14. data/lib/analysand/change_watcher.rb +280 -0
  15. data/lib/analysand/config_response.rb +25 -0
  16. data/lib/analysand/connection_testing.rb +52 -0
  17. data/lib/analysand/database.rb +322 -0
  18. data/lib/analysand/errors.rb +60 -0
  19. data/lib/analysand/http.rb +90 -0
  20. data/lib/analysand/instance.rb +255 -0
  21. data/lib/analysand/reading.rb +26 -0
  22. data/lib/analysand/response.rb +35 -0
  23. data/lib/analysand/response_headers.rb +18 -0
  24. data/lib/analysand/session_response.rb +16 -0
  25. data/lib/analysand/status_code_predicates.rb +25 -0
  26. data/lib/analysand/streaming_view_response.rb +90 -0
  27. data/lib/analysand/version.rb +3 -0
  28. data/lib/analysand/view_response.rb +24 -0
  29. data/lib/analysand/view_streaming/builder.rb +142 -0
  30. data/lib/analysand/viewing.rb +95 -0
  31. data/lib/analysand/writing.rb +71 -0
  32. data/script/setup_database.rb +45 -0
  33. data/spec/analysand/a_response.rb +70 -0
  34. data/spec/analysand/change_watcher_spec.rb +102 -0
  35. data/spec/analysand/database_spec.rb +243 -0
  36. data/spec/analysand/database_writing_spec.rb +488 -0
  37. data/spec/analysand/instance_spec.rb +205 -0
  38. data/spec/analysand/response_spec.rb +26 -0
  39. data/spec/analysand/view_response_spec.rb +44 -0
  40. data/spec/analysand/view_streaming/builder_spec.rb +73 -0
  41. data/spec/analysand/view_streaming_spec.rb +122 -0
  42. data/spec/fixtures/vcr_cassettes/get_config.yml +40 -0
  43. data/spec/fixtures/vcr_cassettes/get_many_config.yml +40 -0
  44. data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
  45. data/spec/fixtures/vcr_cassettes/reload_config.yml +114 -0
  46. data/spec/fixtures/vcr_cassettes/unauthorized_put_config.yml +43 -0
  47. data/spec/fixtures/vcr_cassettes/view.yml +40 -0
  48. data/spec/smoke/database_thread_spec.rb +59 -0
  49. data/spec/spec_helper.rb +30 -0
  50. data/spec/support/database_access.rb +40 -0
  51. data/spec/support/example_isolation.rb +86 -0
  52. data/spec/support/test_parameters.rb +39 -0
  53. metadata +283 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43f7060645db528706131e0bb8c6feaa5311cb45
4
+ data.tar.gz: a9d1a3d8e8434f3039d7b0f441a4053000b1bca4
5
+ SHA512:
6
+ metadata.gz: 510cc920abf5f1fa85b82ed86e7d0f8fb4276761a424626122bfa79025c6795b4692481706a57208dde8b5916b0ff3afd4e151a7b496935dfbf84bd384688f29
7
+ data.tar.gz: 1b247083c79d19ec91aa958712f6e7e7d3d80d33546914b3a7776b62ede70745d71ddea5c5f2e1142f5cb3ffe8ade9e7f8e191df6f4e851b3e0d9c2cd02f3a7d
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.sw?
3
+ .rbx
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - rbx-2.3.0
4
+ - 2.0.0
5
+ before_script:
6
+ - "./script/setup_database.rb"
7
+ services:
8
+ - couchdb
@@ -0,0 +1,67 @@
1
+ Issue numbers refer to issues on Analysand's Github tracker:
2
+ https://github.com/yipdw/analysand/issues
3
+
4
+ 3.0.2 (2014-12-01)
5
+ -----------------
6
+
7
+ * Change Celluloid dependency to ~> 0.16.0.
8
+
9
+ 3.0.1 (2014-01-14)
10
+ ------------------
11
+
12
+ * Change Celluloid dependency to ~> 0.15.0.
13
+
14
+ 3.0.0 (2013-07-08)
15
+ ------------------
16
+
17
+ * Change Celluloid dependency to 0.14.
18
+
19
+ 3.0.0.pre2 (2013-04-15)
20
+ -----------------------
21
+
22
+ * Change Celluloid dependency to 0.13.
23
+ Please note: Analysand does not require celluloid/autostart. It's up to you
24
+ to decide whether or not you need that for your application.
25
+
26
+ 3.0.0.pre (2013-02-26)
27
+ ----------------------
28
+
29
+ * Instance#set_config renamed to Instance#put_config
30
+ * Instance#put_admin, Instance#delete_admin for db admin setup
31
+ * JSON encoding/decoding removed from Instance#*_config methods: all values are
32
+ sent to/received from CouchDB verbatim. This means that you'll have to quote all values,
33
+ e.g.
34
+
35
+ instance.set_config("stats/rate", 1200)
36
+
37
+ becomes
38
+
39
+ instance.put_config("stats/rate", '"1200"')
40
+
41
+
42
+ x.y.z (2012-12-31)
43
+ ------------------
44
+
45
+ * Analysand::Writing#bulk_docs! now raises BulkOperationFailed on 401 responses
46
+
47
+ 2.0.0 (2012-11-29)
48
+ ------------------
49
+
50
+ * Instance#establish_session and Instance#renew_session now return a (session,
51
+ Analysand::Response pair)
52
+ * Share HTTP code between Database and Instance
53
+ * Session handling on Instance rewritten: #post_session, #get_session
54
+ * New response methods: #cookies, #session_cookie
55
+
56
+ 1.1.0 (2012-11-03)
57
+ ------------------
58
+
59
+ * View streaming (#3)
60
+ * ChangeWatchers now pass credentials when checking CouchDB status (#4)
61
+ * Some code organization cleanups
62
+ * require "analysand" now loads the Database and Instance classes
63
+
64
+ 1.0.1 (2012-10-01)
65
+ ------------------
66
+
67
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in analysand.gemspec
4
+ gemspec
5
+
6
+ platform :rbx do
7
+ gem 'rubysl'
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 David Yip
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,48 @@
1
+ 1. Analysand
2
+
3
+ Analysand is a CouchDB client library of dubious worth. It was extracted from
4
+ the a-m-v.org catalog application:
5
+ https://code.ninjawedding.org/git/amvorg-underground/catalog.git.
6
+
7
+ Analysand was written for Ruby 1.9. It is known to work on Ruby 1.9.3-p194 and
8
+ Rubinius 2.0.0.
9
+
10
+ 2. Features
11
+
12
+ * GET, PUT, DELETE on databases
13
+ * GET, PUT, DELETE, HEAD, COPY on documents
14
+ * GET, PUT on document attachments
15
+ * GET, POST on views
16
+ * GET, PUT on server configuration
17
+ * GET, PUT, POST on arbitrary service handlers
18
+ * POST /_session
19
+ * POST /_bulk_docs
20
+ * View streaming
21
+ * Celluloid::IO-based change feed watchers
22
+ * Cookie and HTTP Basic authentication for all of the above
23
+ * Database objects can be safely shared across threads
24
+
25
+ 3. Development
26
+
27
+ You'll need a CouchDB >= 1.1.0 instance. I recommend not using a CouchDB
28
+ instance that you're using for anything else; Analysand requires the presence
29
+ of specific admin and non-admin users for its test suite.
30
+
31
+ See spec/support/test_parameters.rb for usernames, passwords, and connection
32
+ information.
33
+
34
+ Naturally, we hang with all the cool kids:
35
+
36
+ * Travis CI: https://travis-ci.org/#!/yipdw/analysand
37
+ * Code Climate: https://codeclimate.com/github/yipdw/analysand
38
+ * Gemnasium: https://gemnasium.com/yipdw/analysand
39
+
40
+ 4. License
41
+
42
+ Copyright 2012 David Yip; made available under the MIT license.
43
+
44
+ 5. Special thanks
45
+
46
+ Fear of Tigers, 3LAU, Ellie Goulding, TeddyLoid, Susumu Hirasawa.
47
+
48
+ # vim:ts=2:sw=2:et:tw=78
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new
7
+
8
+ namespace :git do
9
+ desc 'Strip trailing whitespace from tracked source files'
10
+ task :strip_spaces do
11
+ `git ls-files`.split("\n").each do |file|
12
+ puts file
13
+
14
+ if `file '#{file}'` =~ /text/
15
+ sh "git stripspace < '#{file}' > '#{file}.out'"
16
+ mv "#{file}.out", file
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ task :default => :spec
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/analysand/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["David Yip"]
6
+ gem.email = ["yipdw@member.fsf.org"]
7
+ gem.description = %q{A terrible burden for a couch}
8
+ gem.summary = %q{A CouchDB client of dubious worth}
9
+ gem.homepage = "https://github.com/yipdw/analysand"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "dpla-analysand"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Analysand::VERSION
17
+
18
+ gem.required_ruby_version = '>= 1.9'
19
+
20
+ gem.add_dependency 'celluloid', '~> 0.16.0'
21
+ gem.add_dependency 'celluloid-io'
22
+ gem.add_dependency 'http_parser.rb'
23
+ gem.add_dependency 'json'
24
+ gem.add_dependency 'json-stream'
25
+ gem.add_dependency 'net-http-persistent'
26
+ gem.add_dependency 'rack'
27
+ gem.add_dependency 'yajl-ruby'
28
+
29
+ gem.add_development_dependency 'rake'
30
+ gem.add_development_dependency 'rspec'
31
+ gem.add_development_dependency 'vcr'
32
+ gem.add_development_dependency 'webmock'
33
+ end
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'analysand'
6
+ require 'irb'
7
+ require 'uri'
8
+
9
+ $URI = URI('http://localhost:5984/analysand_test')
10
+
11
+ def make_db(uri = $URI)
12
+ Analysand::Database.new(uri)
13
+ end
14
+
15
+ puts <<-END
16
+ ------------------------------------------------------------------------------
17
+ Type make_db to make an Analysand::Database object. The default URI is
18
+
19
+ #{$URI}
20
+
21
+ To point at different databases, supply a URI object to make_db, e.g.
22
+
23
+ make_db(URI('https://couchdb.example.org:6984/supersekrit'))
24
+ ------------------------------------------------------------------------------
25
+ END
26
+
27
+ IRB.start
@@ -0,0 +1,3 @@
1
+ require 'analysand/database'
2
+ require 'analysand/instance'
3
+ require 'analysand/version'
@@ -0,0 +1,14 @@
1
+ require 'analysand/response'
2
+
3
+ module Analysand
4
+ ##
5
+ # A subclass of Response that adjusts success? to check for individual error
6
+ # records.
7
+ class BulkResponse < Response
8
+ def success?
9
+ super && body.none? { |r| r.has_key?('error') }
10
+ end
11
+ end
12
+ end
13
+
14
+ # vim:ts=2:sw=2:et:tw=78
@@ -0,0 +1,280 @@
1
+ require 'celluloid'
2
+ require 'celluloid/io'
3
+ require 'analysand/connection_testing'
4
+ require 'http/parser'
5
+ require 'net/http'
6
+ require 'rack/utils'
7
+ require 'uri'
8
+ require 'yajl'
9
+
10
+ module Analysand
11
+ ##
12
+ # A Celluloid::IO actor that watches the changes feed of a CouchDB database.
13
+ # When a change is received, it passes the change to a #process method.
14
+ #
15
+ # ChangeWatchers monitor changes using continuous mode and set up a heartbeat
16
+ # to fire approximately every 10 seconds.
17
+ #
18
+ # ChangeWatchers begin watching for changes as soon as they are initialized.
19
+ # To send a shutdown message:
20
+ #
21
+ # a.stop
22
+ #
23
+ # The watcher will terminate on the next heartbeat.
24
+ #
25
+ #
26
+ # Failure modes
27
+ # =============
28
+ #
29
+ # ChangeWatcher deals with the following failures in the following ways:
30
+ #
31
+ # * If Errno::ECONNREFUSED is raised whilst connecting to CouchDB, it will
32
+ # retry the connection in 30 seconds.
33
+ # * If the connection to CouchDB's changes feed is abruptly terminated, it
34
+ # dies.
35
+ # * If an exception is raised during HTTP or JSON parsing, it dies.
36
+ #
37
+ # Situations where the actor dies should be handled by a supervisor.
38
+ #
39
+ #
40
+ # Example usage
41
+ # =============
42
+ #
43
+ # class Accumulator < Analysand::ChangeWatcher
44
+ # attr_accessor :results
45
+ #
46
+ # def initialize(database)
47
+ # super(database)
48
+ #
49
+ # self.results = []
50
+ # end
51
+ #
52
+ # def process(change)
53
+ # results << change
54
+ #
55
+ # # Once a ChangeWatcher has successfully processed a change, it
56
+ # # SHOULD invoke #change_processed.
57
+ # change_processed(change)
58
+ # end
59
+ # end
60
+ #
61
+ # a = Accumulator.new('http://localhost:5984/mydb')
62
+ #
63
+ # # or with supervision:
64
+ # a = Accumulator.supervise('http://localhost:5984/mydb')
65
+ class ChangeWatcher
66
+ include Celluloid::IO
67
+ include Celluloid::Logger
68
+ include ConnectionTesting
69
+ include Rack::Utils
70
+
71
+ # Read at most this many bytes off the socket at a time.
72
+ QUANTUM = 4096
73
+
74
+ ##
75
+ # Checks services. If all services pass muster, enters a read loop.
76
+ #
77
+ # The database parameter may be either a URL-as-string or a
78
+ # Analysand::Database.
79
+ #
80
+ # If overriding the initializer, you MUST call super.
81
+ def initialize(database)
82
+ @db = database
83
+ @waiting = {}
84
+ @http_parser = ::Http::Parser.new(self)
85
+ @json_parser = Yajl::Parser.new
86
+ @json_parser.on_parse_complete = lambda { |doc| process(doc) }
87
+
88
+ async.start
89
+ end
90
+
91
+ # The URI of the changes feed. This URI incorporates any changes
92
+ # made by customize_query.
93
+ def changes_feed_uri
94
+ query = {
95
+ 'feed' => 'continuous',
96
+ 'heartbeat' => '10000'
97
+ }
98
+
99
+ customize_query(query)
100
+
101
+ uri = (@db.respond_to?(:uri) ? @db.uri : URI(@db)).dup
102
+ uri.path += '/_changes'
103
+ uri.query = build_query(query)
104
+ uri
105
+ end
106
+
107
+ # The connection_ok method is called before connecting to the changes feed.
108
+ # By default, it checks that there's an HTTP service listening on the
109
+ # changes feed.
110
+ #
111
+ # If the method returns true, then we connect to the changes feed and begin
112
+ # processing. If it returns false, a warning message is logged and the
113
+ # connection check will be retried in 30 seconds.
114
+ #
115
+ # This method can be overridden if you need to check additional services.
116
+ # When you override the method, make sure that you don't discard the return
117
+ # value of the original definition:
118
+ #
119
+ # # Wrong
120
+ # def connection_ok
121
+ # super
122
+ # ...
123
+ # end
124
+ #
125
+ # # Right
126
+ # def connection_ok
127
+ # ok = super
128
+ #
129
+ # ok && my_other_test
130
+ # end
131
+ def connection_ok
132
+ test_http_connection(changes_feed_uri) do |req|
133
+ customize_request(req)
134
+ end
135
+ end
136
+
137
+ def start
138
+ return if @started
139
+
140
+ @started = true
141
+
142
+ while !connection_ok
143
+ error "Some services used by #{self.class.name} did not check out ok; will retry in 30 seconds"
144
+ sleep 30
145
+ end
146
+
147
+ connect
148
+
149
+ info "#{self.class} entering read loop"
150
+
151
+ @running = true
152
+
153
+ while @running
154
+ @http_parser << @socket.readpartial(QUANTUM)
155
+ end
156
+
157
+ # Once we're done, close things up.
158
+ @started = false
159
+ @socket.close
160
+ end
161
+
162
+ def stop
163
+ @running = false
164
+ end
165
+
166
+ ##
167
+ # Can be used to set query parameters. query is a Hash. The query hash
168
+ # has two default parameters:
169
+ #
170
+ # | Key | Value |
171
+ # | feed | continuous |
172
+ # | heartbeat | 10000 |
173
+ #
174
+ # It is NOT RECOMMENDED that they be changed.
175
+ #
176
+ # By default, this does nothing. Provide behavior in a subclass.
177
+ def customize_query(query)
178
+ end
179
+
180
+ ##
181
+ # Can be used to add headers. req is a Net::HTTP::Get instance.
182
+ #
183
+ # By default, this does nothing. Provide behavior in a subclass.
184
+ def customize_request(req)
185
+ end
186
+
187
+ ##
188
+ # This method should implement your change-processing logic.
189
+ #
190
+ # change is a Hash containing keys id, seq, and changes. See [0] for
191
+ # more information.
192
+ #
193
+ # By default, this does nothing. Provide behavior in a subclass.
194
+ #
195
+ # [0]: http://guide.couchdb.org/draft/notifications.html#continuous
196
+ def process(change)
197
+ end
198
+
199
+ class Waiter < Celluloid::Future
200
+ alias_method :wait, :value
201
+ end
202
+
203
+ ##
204
+ # Returns an object that can be used to block a thread until a document
205
+ # with the given ID has been processed.
206
+ #
207
+ # Intended for testing.
208
+ def waiter_for(id)
209
+ @waiting[id] = true
210
+
211
+ Waiter.new do
212
+ loop do
213
+ break true if !@waiting[id]
214
+ sleep 0.1
215
+ end
216
+ end
217
+ end
218
+
219
+ ##
220
+ # Notify waiters.
221
+ def change_processed(change)
222
+ @waiting.delete(change['id'])
223
+ end
224
+
225
+ ##
226
+ # Http::Parser callback.
227
+ #
228
+ # @private
229
+ def on_headers_complete(parser)
230
+ status = @http_parser.status_code.to_i
231
+
232
+ raise "Request failed: expected status 200, got #{status}" unless status == 200
233
+ end
234
+
235
+ ##
236
+ # Http::Parser callback.
237
+ #
238
+ # @private
239
+ def on_body(chunk)
240
+ @json_parser << chunk
241
+ end
242
+
243
+ ##
244
+ # @private
245
+ def connect
246
+ req = prepare_request
247
+ uri = changes_feed_uri
248
+
249
+ info "#{self.class} connecting to #{req.path}"
250
+
251
+ @socket = TCPSocket.new(uri.host, uri.port)
252
+
253
+ # Make the request.
254
+ data = [
255
+ "GET #{req.path} HTTP/1.1"
256
+ ]
257
+
258
+ req.each_header { |k, v| data << "#{k}: #{v}" }
259
+
260
+ @socket.write(data.join("\r\n"))
261
+ @socket.write("\r\n\r\n")
262
+ end
263
+
264
+ ##
265
+ # @private
266
+ def disconnect
267
+ @socket.close if @socket && !@socket.closed?
268
+ end
269
+
270
+ finalizer :disconnect
271
+
272
+ ##
273
+ # @private
274
+ def prepare_request
275
+ Net::HTTP::Get.new(changes_feed_uri.to_s).tap do |req|
276
+ customize_request(req)
277
+ end
278
+ end
279
+ end
280
+ end