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