dpla-analysand 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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,90 @@
|
|
1
|
+
require 'analysand/response_headers'
|
2
|
+
require 'analysand/status_code_predicates'
|
3
|
+
require 'analysand/view_streaming/builder'
|
4
|
+
require 'fiber'
|
5
|
+
|
6
|
+
module Analysand
|
7
|
+
# Public: Controls streaming of view data.
|
8
|
+
#
|
9
|
+
# This class is meant to be used by Analysand::Database#view. It exports the
|
10
|
+
# same interface as ViewResponse.
|
11
|
+
#
|
12
|
+
# Examples:
|
13
|
+
#
|
14
|
+
# resp = db.view('view/something', :stream => true)
|
15
|
+
#
|
16
|
+
# resp.total_rows # => 1000000
|
17
|
+
# resp.offset # => 0
|
18
|
+
# resp.rows.take(100) # => first 100 rows
|
19
|
+
class StreamingViewResponse
|
20
|
+
include Enumerable
|
21
|
+
include ResponseHeaders
|
22
|
+
include StatusCodePredicates
|
23
|
+
|
24
|
+
# Internal: The HTTP response.
|
25
|
+
#
|
26
|
+
# This is set by Analysand::Database#stream_view. The #etag and #code
|
27
|
+
# methods use this for header information.
|
28
|
+
attr_accessor :response
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@reader = Fiber.new { yield self; "" }
|
32
|
+
@generator = ViewStreaming::Builder.new
|
33
|
+
|
34
|
+
# Analysand::Database#stream_view issues the request. When the response
|
35
|
+
# arrives, it yields control back here. Subsequent resumes read the
|
36
|
+
# body.
|
37
|
+
#
|
38
|
+
# We do this to provide the response headers as soon as possible.
|
39
|
+
@reader.resume
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Yields documents in the view stream.
|
43
|
+
#
|
44
|
+
# Note that #docs and #rows advance the same stream, so expect to miss half
|
45
|
+
# your rows if you do something like
|
46
|
+
#
|
47
|
+
# resp.docs.zip(resp.rows)
|
48
|
+
#
|
49
|
+
# If this is a problem for you, let me know and we can work out a solution.
|
50
|
+
def docs
|
51
|
+
to_enum(:get_docs)
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_docs
|
55
|
+
each { |r| yield r['doc'] if r.has_key?('doc') }
|
56
|
+
end
|
57
|
+
|
58
|
+
def total_rows
|
59
|
+
read until @generator.total_rows
|
60
|
+
|
61
|
+
@generator.total_rows
|
62
|
+
end
|
63
|
+
|
64
|
+
def offset
|
65
|
+
read until @generator.offset
|
66
|
+
|
67
|
+
@generator.offset
|
68
|
+
end
|
69
|
+
|
70
|
+
def read
|
71
|
+
@generator << @reader.resume
|
72
|
+
end
|
73
|
+
|
74
|
+
def each
|
75
|
+
return to_enum unless block_given?
|
76
|
+
|
77
|
+
while @reader.alive?
|
78
|
+
read while @reader.alive? && @generator.staged_rows.empty?
|
79
|
+
|
80
|
+
until @generator.staged_rows.empty?
|
81
|
+
yield @generator.staged_rows.shift
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def rows
|
87
|
+
self
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'analysand/response'
|
2
|
+
|
3
|
+
module Analysand
|
4
|
+
##
|
5
|
+
# A subclass of Response with additional view-specific accessors: total_rows,
|
6
|
+
# offset, and rows.
|
7
|
+
class ViewResponse < Response
|
8
|
+
def total_rows
|
9
|
+
body['total_rows']
|
10
|
+
end
|
11
|
+
|
12
|
+
def offset
|
13
|
+
body['offset']
|
14
|
+
end
|
15
|
+
|
16
|
+
def rows
|
17
|
+
body['rows']
|
18
|
+
end
|
19
|
+
|
20
|
+
def docs
|
21
|
+
rows.map { |r| r['doc'] }.compact
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'analysand/errors'
|
2
|
+
require 'json/stream'
|
3
|
+
|
4
|
+
module Analysand
|
5
|
+
module ViewStreaming
|
6
|
+
# Private: A wrapper around JSON::Stream::Parser that extracts data from a
|
7
|
+
# CouchDB view document.
|
8
|
+
class Builder
|
9
|
+
attr_reader :offset
|
10
|
+
attr_reader :total_rows
|
11
|
+
|
12
|
+
# Rows constructed by the JSON parser that are ready for
|
13
|
+
# StreamingViewResponse#each.
|
14
|
+
attr_reader :staged_rows
|
15
|
+
|
16
|
+
# JSON::Stream::Parser callback methods.
|
17
|
+
CALLBACK_METHODS = %w(
|
18
|
+
start_object end_object start_array end_array key value
|
19
|
+
)
|
20
|
+
|
21
|
+
# If we find a key in the toplevel view object that isn't one of these,
|
22
|
+
# we raise UnexpectedViewKey.
|
23
|
+
KNOWN_KEYS = %w(
|
24
|
+
total_rows offset rows
|
25
|
+
)
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@in_rows = false
|
29
|
+
@parser = JSON::Stream::Parser.new
|
30
|
+
@stack = []
|
31
|
+
@staged_rows = []
|
32
|
+
|
33
|
+
CALLBACK_METHODS.each { |name| @parser.send(name, &method(name)) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def <<(data)
|
37
|
+
@parser << data
|
38
|
+
end
|
39
|
+
|
40
|
+
def start_object
|
41
|
+
# We don't need to do anything for the toplevel view object, so just
|
42
|
+
# focus on the objects in rows.
|
43
|
+
if @in_rows
|
44
|
+
@stack.push ObjectNode.new
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def end_object
|
49
|
+
if @in_rows
|
50
|
+
# If the stack's empty and we've come to the end of an object, assume
|
51
|
+
# we've exited the rows key. Trailing keys are handled by
|
52
|
+
# check_toplevel_key_validity.
|
53
|
+
if @stack.empty?
|
54
|
+
@in_rows = false
|
55
|
+
else
|
56
|
+
obj = @stack.pop.to_object
|
57
|
+
|
58
|
+
# If obj was the only thing on the stack and we're processing rows,
|
59
|
+
# then we've completed an object and need to stage it for the
|
60
|
+
# object stream.
|
61
|
+
if @stack.empty?
|
62
|
+
staged_rows << obj
|
63
|
+
else
|
64
|
+
@stack.last << obj
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def start_array
|
71
|
+
# If we're not in the rows array but "rows" was the last key we've
|
72
|
+
# seen, then we're entering the rows array. Otherwise, we're building
|
73
|
+
# an array in a row.
|
74
|
+
if !@in_rows && @stack.pop == 'rows'
|
75
|
+
@in_rows = true
|
76
|
+
elsif @in_rows
|
77
|
+
@stack.push []
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def end_array
|
82
|
+
if @in_rows
|
83
|
+
obj = @stack.pop
|
84
|
+
|
85
|
+
# If there's nothing on the row stack, it means that we've hit the
|
86
|
+
# end of the rows array.
|
87
|
+
if @stack.empty?
|
88
|
+
@in_rows = false
|
89
|
+
else
|
90
|
+
@stack.last << obj
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def key(k)
|
96
|
+
if !@in_rows
|
97
|
+
check_toplevel_key_validity(k)
|
98
|
+
@stack.push k
|
99
|
+
else
|
100
|
+
@stack.last << k
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def value(v)
|
105
|
+
if !@in_rows
|
106
|
+
case @stack.pop
|
107
|
+
when 'total_rows'; @total_rows = v
|
108
|
+
when 'offset'; @offset = v
|
109
|
+
end
|
110
|
+
else
|
111
|
+
@stack.last << v
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def check_toplevel_key_validity(k)
|
116
|
+
if !KNOWN_KEYS.include?(k)
|
117
|
+
raise UnexpectedViewKey, "Unexpected key #{k} in top-level view object"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Simplifies key/value pair construction.
|
122
|
+
class ObjectNode
|
123
|
+
def initialize
|
124
|
+
@obj = {}
|
125
|
+
end
|
126
|
+
|
127
|
+
def to_object
|
128
|
+
@obj
|
129
|
+
end
|
130
|
+
|
131
|
+
def <<(term)
|
132
|
+
if !@key
|
133
|
+
@key = term
|
134
|
+
elsif @key
|
135
|
+
@obj[@key] = term
|
136
|
+
@key = nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'analysand/errors'
|
2
|
+
require 'analysand/streaming_view_response'
|
3
|
+
require 'analysand/view_response'
|
4
|
+
require 'fiber'
|
5
|
+
|
6
|
+
module Analysand
|
7
|
+
module Viewing
|
8
|
+
JSON_VALUE_PARAMETERS = %w(key keys startkey endkey).map(&:to_sym)
|
9
|
+
|
10
|
+
def all_docs(parameters = {}, credentials = nil)
|
11
|
+
view('_all_docs', parameters, credentials)
|
12
|
+
end
|
13
|
+
|
14
|
+
def all_docs!(parameters = {}, credentials = nil)
|
15
|
+
view!('_all_docs', parameters, credentials)
|
16
|
+
end
|
17
|
+
|
18
|
+
def view(view_name, parameters = {}, credentials = nil)
|
19
|
+
stream = parameters.delete(:stream)
|
20
|
+
view_path = expand_view_path(view_name)
|
21
|
+
|
22
|
+
if stream
|
23
|
+
stream_view(view_path, parameters, credentials)
|
24
|
+
else
|
25
|
+
return_view(view_path, parameters, credentials)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def view!(view_name, parameters = {}, credentials = nil)
|
30
|
+
view(view_name, parameters, credentials).tap do |resp|
|
31
|
+
raise ex(CannotAccessView, resp) unless resp.success?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stream_view(view_path, parameters, credentials)
|
36
|
+
StreamingViewResponse.new do |sresp|
|
37
|
+
do_view_query(view_path, parameters, credentials) do |resp|
|
38
|
+
sresp.response = resp
|
39
|
+
Fiber.yield
|
40
|
+
resp.read_body { |data| Fiber.yield(data) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def return_view(view_path, parameters, credentials)
|
46
|
+
resp = do_view_query(view_path, parameters, credentials)
|
47
|
+
|
48
|
+
ViewResponse.new resp
|
49
|
+
end
|
50
|
+
|
51
|
+
def do_view_query(view_path, parameters, credentials, &block)
|
52
|
+
use_post = parameters.delete(:post)
|
53
|
+
|
54
|
+
if use_post
|
55
|
+
post_view(view_path, parameters, credentials, block)
|
56
|
+
else
|
57
|
+
get_view(view_path, parameters, credentials, block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_view(view_path, parameters, credentials, block)
|
62
|
+
encode_parameters(parameters)
|
63
|
+
_get(view_path, credentials, parameters, {}, nil, block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def post_view(view_path, parameters, credentials, block)
|
67
|
+
body = {
|
68
|
+
'keys' => parameters.delete(:keys)
|
69
|
+
}.reject { |_, v| v.nil? }
|
70
|
+
|
71
|
+
encode_parameters(parameters)
|
72
|
+
|
73
|
+
_post(view_path, credentials, parameters, json_headers, body.to_json, block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def encode_parameters(parameters)
|
77
|
+
JSON_VALUE_PARAMETERS.each do |p|
|
78
|
+
if parameters.has_key?(p)
|
79
|
+
parameters[p] = parameters[p].to_json
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def expand_view_path(view_name)
|
85
|
+
if view_name.start_with?('_design') || !view_name.include?('/')
|
86
|
+
view_name
|
87
|
+
else
|
88
|
+
design_doc, view_name = view_name.split('/', 2)
|
89
|
+
"_design/#{design_doc}/_view/#{view_name}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# vim:ts=2:sw=2:et:tw=78
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'analysand/bulk_response'
|
2
|
+
require 'analysand/errors'
|
3
|
+
require 'analysand/response'
|
4
|
+
|
5
|
+
module Analysand
|
6
|
+
module Writing
|
7
|
+
def put(doc_id, doc, credentials = nil, options = {})
|
8
|
+
query = options
|
9
|
+
|
10
|
+
Response.new _put(doc_id, credentials, options, json_headers, doc.to_json)
|
11
|
+
end
|
12
|
+
|
13
|
+
def put!(doc_id, doc, credentials = nil, options = {})
|
14
|
+
put(doc_id, doc, credentials, options).tap do |resp|
|
15
|
+
raise ex(DocumentNotSaved, resp) unless resp.success?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def ensure_full_commit(credentials = nil, options = {})
|
20
|
+
Response.new _post('_ensure_full_commit', credentials, options, json_headers, {}.to_json)
|
21
|
+
end
|
22
|
+
|
23
|
+
def bulk_docs(docs, credentials = nil, options = {})
|
24
|
+
body = { 'docs' => docs }
|
25
|
+
body['all_or_nothing'] = true if options[:all_or_nothing]
|
26
|
+
|
27
|
+
BulkResponse.new _post('_bulk_docs', credentials, {}, json_headers, body.to_json)
|
28
|
+
end
|
29
|
+
|
30
|
+
def bulk_docs!(docs, credentials = nil, options = {})
|
31
|
+
bulk_docs(docs, credentials, options).tap do |resp|
|
32
|
+
raise bulk_ex(BulkOperationFailed, resp) unless resp.success?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def copy(source, destination, credentials = nil)
|
37
|
+
headers = { 'Destination' => destination }
|
38
|
+
|
39
|
+
Response.new _copy(source, credentials, {}, headers, nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
def put_attachment(loc, io, credentials = nil, options = {})
|
43
|
+
query = {}
|
44
|
+
headers = {}
|
45
|
+
|
46
|
+
if options[:rev]
|
47
|
+
query['rev'] = options[:rev]
|
48
|
+
end
|
49
|
+
|
50
|
+
if options[:content_type]
|
51
|
+
headers['Content-Type'] = options[:content_type]
|
52
|
+
end
|
53
|
+
|
54
|
+
Response.new _put(loc, credentials, query, headers, io.read)
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(doc_id, rev, credentials = nil)
|
58
|
+
headers = { 'If-Match' => rev }
|
59
|
+
|
60
|
+
Response.new _delete(doc_id, credentials, {}, headers, nil)
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete!(doc_id, rev, credentials = nil)
|
64
|
+
delete(doc_id, rev, credentials).tap do |resp|
|
65
|
+
raise ex(DocumentNotDeleted, resp) unless resp.success?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# vim:ts=2:sw=2:et:tw=78
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path('../../spec/support/test_parameters', __FILE__)
|
4
|
+
require 'digest'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
include TestParameters
|
8
|
+
|
9
|
+
admin_command = ['curl',
|
10
|
+
'-X PUT',
|
11
|
+
%Q{--data-binary '"#{admin_password}"'},
|
12
|
+
"#{instance_uri}/_config/admins/#{admin_username}"
|
13
|
+
].join(' ')
|
14
|
+
|
15
|
+
salt = "ff518dabd59b04b527de7c55179059a46ac54976"
|
16
|
+
|
17
|
+
member_doc = {
|
18
|
+
"name" => member1_username,
|
19
|
+
"salt" => salt,
|
20
|
+
"password_sha" => Digest::SHA1.hexdigest("#{member1_password}#{salt}"),
|
21
|
+
"type" => "user",
|
22
|
+
"roles" => []
|
23
|
+
}
|
24
|
+
|
25
|
+
member_command = ['curl',
|
26
|
+
'-X PUT',
|
27
|
+
"--data-binary '#{member_doc.to_json}'",
|
28
|
+
"-u #{admin_username}:#{admin_password}",
|
29
|
+
"#{instance_uri}/_users/org.couchdb.user:#{member1_username}"
|
30
|
+
].join(' ')
|
31
|
+
|
32
|
+
db_command = ['curl',
|
33
|
+
'-X PUT',
|
34
|
+
"-u #{admin_username}:#{admin_password}",
|
35
|
+
database_uri.to_s
|
36
|
+
].join(' ')
|
37
|
+
|
38
|
+
[ admin_command,
|
39
|
+
member_command,
|
40
|
+
db_command
|
41
|
+
].each do |cmd|
|
42
|
+
puts cmd
|
43
|
+
system cmd
|
44
|
+
puts
|
45
|
+
end
|