dpla-analysand 3.0.2

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.
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