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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CHANGELOG +67 -0
- data/Gemfile +8 -0
- data/LICENSE +22 -0
- data/README +48 -0
- data/Rakefile +22 -0
- data/analysand.gemspec +33 -0
- data/bin/analysand +27 -0
- data/lib/analysand.rb +3 -0
- data/lib/analysand/bulk_response.rb +14 -0
- data/lib/analysand/change_watcher.rb +280 -0
- data/lib/analysand/config_response.rb +25 -0
- data/lib/analysand/connection_testing.rb +52 -0
- data/lib/analysand/database.rb +322 -0
- data/lib/analysand/errors.rb +60 -0
- data/lib/analysand/http.rb +90 -0
- data/lib/analysand/instance.rb +255 -0
- data/lib/analysand/reading.rb +26 -0
- data/lib/analysand/response.rb +35 -0
- data/lib/analysand/response_headers.rb +18 -0
- data/lib/analysand/session_response.rb +16 -0
- data/lib/analysand/status_code_predicates.rb +25 -0
- data/lib/analysand/streaming_view_response.rb +90 -0
- data/lib/analysand/version.rb +3 -0
- data/lib/analysand/view_response.rb +24 -0
- data/lib/analysand/view_streaming/builder.rb +142 -0
- data/lib/analysand/viewing.rb +95 -0
- data/lib/analysand/writing.rb +71 -0
- data/script/setup_database.rb +45 -0
- data/spec/analysand/a_response.rb +70 -0
- data/spec/analysand/change_watcher_spec.rb +102 -0
- data/spec/analysand/database_spec.rb +243 -0
- data/spec/analysand/database_writing_spec.rb +488 -0
- data/spec/analysand/instance_spec.rb +205 -0
- data/spec/analysand/response_spec.rb +26 -0
- data/spec/analysand/view_response_spec.rb +44 -0
- data/spec/analysand/view_streaming/builder_spec.rb +73 -0
- data/spec/analysand/view_streaming_spec.rb +122 -0
- data/spec/fixtures/vcr_cassettes/get_config.yml +40 -0
- data/spec/fixtures/vcr_cassettes/get_many_config.yml +40 -0
- data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
- data/spec/fixtures/vcr_cassettes/reload_config.yml +114 -0
- data/spec/fixtures/vcr_cassettes/unauthorized_put_config.yml +43 -0
- data/spec/fixtures/vcr_cassettes/view.yml +40 -0
- data/spec/smoke/database_thread_spec.rb +59 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/database_access.rb +40 -0
- data/spec/support/example_isolation.rb +86 -0
- data/spec/support/test_parameters.rb +39 -0
- 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
|