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