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,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,3 @@
1
+ module Analysand
2
+ VERSION = "3.0.2"
3
+ 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