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,25 @@
1
+ require 'analysand/response'
2
+
3
+ module Analysand
4
+ # Public: Wraps responses from /_config.
5
+ #
6
+ # GET/PUT/DELETE /_config does not return a valid JSON object in all cases.
7
+ # This response object therefore does The Simplest Possible Thing and just
8
+ # gives you back the response body as a string.
9
+ class ConfigResponse
10
+ include ResponseHeaders
11
+ include StatusCodePredicates
12
+
13
+ attr_reader :response
14
+ attr_reader :body
15
+
16
+ alias_method :value, :body
17
+
18
+ def initialize(response)
19
+ @response = response
20
+ @body = response.body.chomp
21
+ end
22
+ end
23
+ end
24
+
25
+ # vim:ts=2:sw=2:et:tw=78
@@ -0,0 +1,52 @@
1
+ require 'celluloid/logger'
2
+
3
+ module Analysand
4
+ module ConnectionTesting
5
+ include Celluloid::Logger
6
+
7
+ ##
8
+ # Issues a HEAD request to the given URI. If it responds with a success or
9
+ # redirection code, returns true; otherwise, returns false.
10
+ #
11
+ # If a block is given, yields the request object for customization.
12
+ def test_http_connection(uri)
13
+ begin
14
+ resp = Net::HTTP.start(uri.host, uri.port) do |h|
15
+ req = Net::HTTP::Head.new(uri.request_uri)
16
+ yield req if block_given?
17
+ h.request(req)
18
+ end
19
+
20
+ case resp
21
+ when Net::HTTPSuccess then true
22
+ when Net::HTTPRedirection then true
23
+ else
24
+ error "Expected HEAD #{uri.to_s} to return 200, got #{resp.code} (#{resp.body}) instead"
25
+ false
26
+ end
27
+ rescue => e
28
+ error "#{e.class} (#{e.message}) caught while attempting connection to #{uri.to_s}"
29
+ error e.backtrace.join("\n")
30
+ false
31
+ end
32
+ end
33
+
34
+ ##
35
+ # Periodically checks a URI for success using test_http_connection, and
36
+ # raises an error if test_http_connection does not return success before
37
+ # the timeout is reached.
38
+ def wait_for_http_service(uri, timeout = 30)
39
+ state = 1.upto(timeout) do
40
+ if test_http_connection(Catalog::Settings.solr_uri)
41
+ break :started
42
+ else
43
+ sleep 1
44
+ end
45
+ end
46
+
47
+ unless state == :started
48
+ raise "#{uri.to_s} took longer than #{timeout} seconds to return a success response"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,322 @@
1
+ require 'analysand/errors'
2
+ require 'analysand/http'
3
+ require 'analysand/reading'
4
+ require 'analysand/response'
5
+ require 'analysand/viewing'
6
+ require 'analysand/writing'
7
+
8
+ module Analysand
9
+ ##
10
+ # A wrapper around a CouchDB database in a CouchDB instance.
11
+ #
12
+ # Databases MUST be identified by an absolute URI; instantiating this class
13
+ # with a relative URI will raise an exception.
14
+ #
15
+ #
16
+ # Common tasks
17
+ # ============
18
+ #
19
+ # Creating a database
20
+ # -------------------
21
+ #
22
+ # vdb = Analysand::Database.create!('http://localhost:5984/videos/',
23
+ # credentials)
24
+ #
25
+ # If the database was successfully created, you'll get back a
26
+ # Analysand::Database instance. If database creation failed, a
27
+ # DatabaseError containing a CouchDB response will be raised.
28
+ #
29
+ # You can also instantiate a database and then create it:
30
+ #
31
+ # vdb = Analysand::Database.new('http://localhost:5984/videos')
32
+ # vdb.create(credentials) # => #<Response ...>
33
+ #
34
+ #
35
+ # Dropping a database
36
+ # -------------------
37
+ #
38
+ # Analysand::Database.drop('http://localhost:5984/videos',
39
+ # credentials)
40
+ #
41
+ # # => #<Response code=200 ...>
42
+ # # => #<Response code=401 ...>
43
+ # # => #<Response code=404 ...>
44
+ #
45
+ # You can also instantiate a database and then drop it:
46
+ #
47
+ # db = Analysand::Database.new('http://localhost:5984/videos')
48
+ # db.drop # => #<Response ...>
49
+ #
50
+ # You can also use #drop!, which will raise Analysand::CannotDropDatabase
51
+ # on a non-success response.
52
+ #
53
+ #
54
+ # Opening a database
55
+ # ------------------
56
+ #
57
+ # vdb = Analysand::Database.new('http://localhost:5984/videos/')
58
+ #
59
+ #
60
+ # Closing connections
61
+ # -------------------
62
+ #
63
+ # vdb.close
64
+ #
65
+ # Note that this only closes the connection used for the current thread. If
66
+ # the database object is being used from several threads, there will still
67
+ # be other connections active. To close all connections, you must call
68
+ # #close from all threads that are using the database object.
69
+ #
70
+ # It is safe to call #close without additional synchronization.
71
+ #
72
+ # After close returns, you can re-open a connection by calling #get, #put,
73
+ # etc.
74
+ #
75
+ #
76
+ # Creating a document
77
+ # -------------------
78
+ #
79
+ # doc = { ... }
80
+ # vdb.put(doc_id, doc, credentials) # => #<Response code=201 ...>
81
+ # # => #<Response code=403 ...>
82
+ # # => #<Response code=409 ...>
83
+ #
84
+ # Any object that responds to #to_json with a JSON representation of itself
85
+ # may be used as the document.
86
+ #
87
+ # Updating a document
88
+ # -------------------
89
+ #
90
+ # doc = { '_rev' => rev, ... }
91
+ # vdb.put(doc_id, doc, credentials) # => #<Response code=201 ...>
92
+ # # => #<Response code=401 ...>
93
+ # # => #<Response code=409 ...>
94
+ #
95
+ #
96
+ # You can also use #put!, which will raise Analysand::DocumentNotSaved if the
97
+ # response code is non-success.
98
+ #
99
+ # begin
100
+ # vdb.put!(doc_id, doc, credentials)
101
+ # rescue Analysand::DocumentNotSaved => e
102
+ # puts "Unable to save #{doc_id}, reason: #{e.response.body}"
103
+ # end
104
+ #
105
+ # #put!, if it returns, returns the response.
106
+ #
107
+ #
108
+ # Deleting a document
109
+ # -------------------
110
+ #
111
+ # vdb.delete(doc_id, rev, credentials) # => #<Response code=200 ...>
112
+ # # => #<Response code=401 ...>
113
+ # # => #<Response code=409 ...>
114
+ #
115
+ # You can also use #delete!, which will raise Analysand::DocumentNotDeleted if
116
+ # the response code is non-success.
117
+ #
118
+ #
119
+ # Retrieving a document
120
+ # ---------------------
121
+ #
122
+ # vdb.get(doc_id, credentials) # => #<Response code=200 ...>
123
+ # # => #<Response code=401 ...>
124
+ # # => #<Response code=404 ...>
125
+ #
126
+ # Note: CouchDB treats forward slashes (/) specially. For document IDs, /
127
+ # denotes a separator between document ID and the name of an attachment.
128
+ # This library makes use of that to implement attachment storage and
129
+ # retrieval (see below).
130
+ #
131
+ # If you are using forward slashes in document IDs, you MUST encode them
132
+ # (i.e. replace / with %2F).
133
+ #
134
+ # You can also use #get!, which will raise Analysand::CannotAccessDocument if
135
+ # the response code is non-success.
136
+ #
137
+ #
138
+ # Reading a view
139
+ # --------------
140
+ #
141
+ # vdb.view('video/recent', :key => ['member1'])
142
+ # vdb.view('video/by_artist', :startkey => 'a', :endkey => 'b')
143
+ #
144
+ # Keys are automatically JSON-encoded, as required by CouchDB.
145
+ #
146
+ # If you're running into problems with large key sets generating very long
147
+ # query strings, you can use POST mode (CouchDB 0.9+):
148
+ #
149
+ # vdb.view('video/by_artist', :keys => many_keys, :post => true)
150
+ #
151
+ # If you're reading many records from a view, you may want to stream them
152
+ # in:
153
+ #
154
+ # vdb.view('video/all', :stream => true)
155
+ #
156
+ # View data and metadata may be accessed as follows:
157
+ #
158
+ # resp = vdb.view('video/recent', :limit => 10)
159
+ # resp.total_rows # => 16
160
+ # resp.offset # => 0
161
+ # resp.rows # => an Enumerable
162
+ #
163
+ # See ViewResponse and StreamingViewResponse for more details.
164
+ #
165
+ # You can also use view!, which will raise Analysand::CannotAccessView on a
166
+ # non-success response.
167
+ #
168
+ #
169
+ # Uploading an attachment
170
+ # -----------------------
171
+ #
172
+ # vdb.put_attachment('doc1/attachment', io, {}, credentials)
173
+ # # => #<Response>
174
+ #
175
+ # The second argument MUST be an IO-like object. The third argument MAY
176
+ # contain any of the following options:
177
+ #
178
+ # * :rev: When specified, this will be used as the rev of the document that
179
+ # will own the attachment. When not specified, no rev will be passed in
180
+ # the request. In order to add attachments to existing documents, then,
181
+ # you MUST pass this option.
182
+ # * :content_type: The MIME type of the attachment.
183
+ #
184
+ #
185
+ # Retrieving an attachment
186
+ # ------------------------
187
+ #
188
+ # vdb.get_attachment('doc1/attachment', credentials) do |resp|
189
+ # # resp is a Net::HTTPResponse
190
+ # end
191
+ #
192
+ # or, if you don't need that level of control when reading the response
193
+ # body:
194
+ #
195
+ # vdb.get_attachment('doc1/attachment', credentials)
196
+ # # => Net::HTTPResponse
197
+ #
198
+ # When a block is passed, #get_attachment does not read the response body,
199
+ # leaving that up to the programmer. When a block is _not_ passed,
200
+ # #get_attachment reads the body in full.
201
+ #
202
+ #
203
+ # Pinging a database
204
+ # ------------------
205
+ #
206
+ # Useful for connection testing:
207
+ #
208
+ # vdb.ping # => #<Response code=200 ...>
209
+ #
210
+ #
211
+ # Getting database status
212
+ # -----------------------
213
+ #
214
+ # vdb.status # => { "db_name" => "videos", ... }
215
+ #
216
+ # The returned hash is a parsed form of the JSON received from a GET on the
217
+ # database.
218
+ #
219
+ #
220
+ # Copying a document
221
+ # ------------------
222
+ #
223
+ # vdb.copy('source', 'destination', credentials)
224
+ # # => #<Response code=201 ...>
225
+ # # => #<Response code=401 ...>
226
+ # # => #<Response code=409 ...>
227
+ #
228
+ # To overwrite, you'll need to provide a rev of the destination document:
229
+ #
230
+ # vdb.copy('source', "destination?rev=#{rev}", credentials)
231
+ #
232
+ #
233
+ # Acceptable credentials
234
+ # ======================
235
+ #
236
+ # Every method that interacts with CouchDB has an optional credentials
237
+ # parameter. Two forms of credential are recognized by this class.
238
+ #
239
+ # 1. HTTP Basic authentication: When credentials is a hash of the form
240
+ #
241
+ # { :username => "...", :password => "... }
242
+ #
243
+ # then it will be transformed into an Authorization header for HTTP Basic
244
+ # authentication.
245
+ #
246
+ # 2. Token authentication: When credentials is a string, it is interpreted
247
+ # as a cookie from CouchDB's Session API. The string is used as the
248
+ # value of a Cookie header.
249
+ #
250
+ # There are two ways to retrieve a token:
251
+ #
252
+ # 1. Establishing a session. You can use
253
+ # Analysand::Instance#establish_sesssion for this.
254
+ # 2. CouchDB may also issue updated session cookies as part of a response.
255
+ # You can access such cookies using #session_cookie on the response
256
+ # object.
257
+ #
258
+ # Omitting the credentials argument, or providing a form of credentials not
259
+ # listed here, will result in no credentials being passed in the request.
260
+ #
261
+ #
262
+ # Thread safety
263
+ # =============
264
+ #
265
+ # Database objects may be shared across multiple threads. The HTTP client
266
+ # used by this object (Net::HTTP::Persistent) creates one persistent
267
+ # connection per (uri.host, uri.port, thread) tuple, so connection pooling
268
+ # is also done.
269
+ class Database
270
+ include Errors
271
+ include Http
272
+ include Reading
273
+ include Viewing
274
+ include Writing
275
+
276
+ def initialize(uri)
277
+ init_http_client(uri)
278
+ end
279
+
280
+ def self.create!(uri, credentials = nil)
281
+ new(uri).tap { |db| db.create!(credentials) }
282
+ end
283
+
284
+ def self.drop(uri, credentials = nil)
285
+ new(uri).drop(credentials)
286
+ end
287
+
288
+ def ping(credentials = nil)
289
+ Response.new _get('', credentials)
290
+ end
291
+
292
+ def status(credentials = nil)
293
+ ping(credentials).body
294
+ end
295
+
296
+ def create(credentials = nil)
297
+ Response.new _put('', credentials)
298
+ end
299
+
300
+ def create!(credentials = nil)
301
+ create(credentials).tap do |resp|
302
+ raise ex(DatabaseError, resp) unless resp.success?
303
+ end
304
+ end
305
+
306
+ def drop(credentials = nil)
307
+ Response.new _delete('', credentials)
308
+ end
309
+
310
+ def drop!(credentials = nil)
311
+ drop(credentials).tap do |resp|
312
+ raise ex(CannotDropDatabase, resp) unless resp.success?
313
+ end
314
+ end
315
+
316
+ def json_headers
317
+ { 'Content-Type' => 'application/json' }
318
+ end
319
+ end
320
+ end
321
+
322
+ # vim:ts=2:sw=2:et:tw=78
@@ -0,0 +1,60 @@
1
+ module Analysand
2
+ # Private: Methods to generate exceptions.
3
+ module Errors
4
+ # Instantiates an exception and fills in a response.
5
+ #
6
+ # klass - the exception class
7
+ # response - the response object that caused the error
8
+ def ex(klass, response)
9
+ klass.new("Expected response to have code 2xx, got #{response.code} instead").tap do |ex|
10
+ ex.response = response
11
+ end
12
+ end
13
+
14
+ # Like #ex, but customized for bulk operations. (CouchDB's bulk_docs can
15
+ # return 2xx even on failure; you need to inspect the response body to
16
+ # figure out what happened.)
17
+ #
18
+ # klass - the exception class
19
+ # response - the response object that caused the error
20
+ def bulk_ex(klass, response)
21
+ klass.new("Bulk operation failed (some records reported failure)").tap do |ex|
22
+ ex.response = response
23
+ end
24
+ end
25
+ end
26
+
27
+ class InvalidURIError < StandardError
28
+ end
29
+
30
+ class DatabaseError < StandardError
31
+ attr_accessor :response
32
+ end
33
+
34
+ class ConfigurationNotSaved < DatabaseError
35
+ end
36
+
37
+ class ConfigurationNotDeleted < DatabaseError
38
+ end
39
+
40
+ class DocumentNotSaved < DatabaseError
41
+ end
42
+
43
+ class DocumentNotDeleted < DatabaseError
44
+ end
45
+
46
+ class CannotAccessDocument < DatabaseError
47
+ end
48
+
49
+ class CannotAccessView < DatabaseError
50
+ end
51
+
52
+ class CannotDropDatabase < DatabaseError
53
+ end
54
+
55
+ class BulkOperationFailed < DatabaseError
56
+ end
57
+
58
+ class UnexpectedViewKey < StandardError
59
+ end
60
+ end