berkeley_library-tind 0.4.0

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 (162) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build.yml +18 -0
  3. data/.gitignore +388 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +20 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/tind.iml +138 -0
  8. data/.idea/vcs.xml +6 -0
  9. data/.rubocop.yml +334 -0
  10. data/.ruby-version +1 -0
  11. data/.simplecov +8 -0
  12. data/.yardopts +1 -0
  13. data/CHANGES.md +58 -0
  14. data/Dockerfile +57 -0
  15. data/Gemfile +3 -0
  16. data/Jenkinsfile +18 -0
  17. data/LICENSE.md +21 -0
  18. data/README.md +73 -0
  19. data/Rakefile +20 -0
  20. data/berkeley_library-tind.gemspec +50 -0
  21. data/bin/tind-export +14 -0
  22. data/docker-compose.yml +15 -0
  23. data/lib/berkeley_library/tind.rb +3 -0
  24. data/lib/berkeley_library/tind/api.rb +1 -0
  25. data/lib/berkeley_library/tind/api/api.rb +132 -0
  26. data/lib/berkeley_library/tind/api/api_exception.rb +131 -0
  27. data/lib/berkeley_library/tind/api/collection.rb +82 -0
  28. data/lib/berkeley_library/tind/api/date_range.rb +67 -0
  29. data/lib/berkeley_library/tind/api/format.rb +32 -0
  30. data/lib/berkeley_library/tind/api/search.rb +100 -0
  31. data/lib/berkeley_library/tind/config.rb +103 -0
  32. data/lib/berkeley_library/tind/export.rb +1 -0
  33. data/lib/berkeley_library/tind/export/column.rb +54 -0
  34. data/lib/berkeley_library/tind/export/column_group.rb +144 -0
  35. data/lib/berkeley_library/tind/export/column_group_list.rb +131 -0
  36. data/lib/berkeley_library/tind/export/column_width_calculator.rb +76 -0
  37. data/lib/berkeley_library/tind/export/config.rb +154 -0
  38. data/lib/berkeley_library/tind/export/csv_exporter.rb +29 -0
  39. data/lib/berkeley_library/tind/export/export.rb +47 -0
  40. data/lib/berkeley_library/tind/export/export_command.rb +168 -0
  41. data/lib/berkeley_library/tind/export/export_exception.rb +8 -0
  42. data/lib/berkeley_library/tind/export/export_format.rb +67 -0
  43. data/lib/berkeley_library/tind/export/exporter.rb +105 -0
  44. data/lib/berkeley_library/tind/export/filter.rb +52 -0
  45. data/lib/berkeley_library/tind/export/no_results_error.rb +7 -0
  46. data/lib/berkeley_library/tind/export/ods_exporter.rb +138 -0
  47. data/lib/berkeley_library/tind/export/row.rb +24 -0
  48. data/lib/berkeley_library/tind/export/row_metrics.rb +18 -0
  49. data/lib/berkeley_library/tind/export/table.rb +175 -0
  50. data/lib/berkeley_library/tind/export/table_metrics.rb +116 -0
  51. data/lib/berkeley_library/tind/marc.rb +1 -0
  52. data/lib/berkeley_library/tind/marc/xml_reader.rb +144 -0
  53. data/lib/berkeley_library/tind/module_info.rb +14 -0
  54. data/lib/berkeley_library/util/arrays.rb +178 -0
  55. data/lib/berkeley_library/util/logging.rb +1 -0
  56. data/lib/berkeley_library/util/ods/spreadsheet.rb +170 -0
  57. data/lib/berkeley_library/util/ods/xml/content_doc.rb +26 -0
  58. data/lib/berkeley_library/util/ods/xml/document_node.rb +57 -0
  59. data/lib/berkeley_library/util/ods/xml/element_node.rb +106 -0
  60. data/lib/berkeley_library/util/ods/xml/loext/table_protection.rb +26 -0
  61. data/lib/berkeley_library/util/ods/xml/manifest/file_entry.rb +42 -0
  62. data/lib/berkeley_library/util/ods/xml/manifest/manifest.rb +73 -0
  63. data/lib/berkeley_library/util/ods/xml/manifest_doc.rb +26 -0
  64. data/lib/berkeley_library/util/ods/xml/namespace.rb +46 -0
  65. data/lib/berkeley_library/util/ods/xml/office/automatic_styles.rb +181 -0
  66. data/lib/berkeley_library/util/ods/xml/office/body.rb +17 -0
  67. data/lib/berkeley_library/util/ods/xml/office/document_content.rb +98 -0
  68. data/lib/berkeley_library/util/ods/xml/office/document_styles.rb +39 -0
  69. data/lib/berkeley_library/util/ods/xml/office/font_face_decls.rb +30 -0
  70. data/lib/berkeley_library/util/ods/xml/office/scripts.rb +17 -0
  71. data/lib/berkeley_library/util/ods/xml/office/spreadsheet.rb +37 -0
  72. data/lib/berkeley_library/util/ods/xml/office/styles.rb +39 -0
  73. data/lib/berkeley_library/util/ods/xml/style/cell_style.rb +58 -0
  74. data/lib/berkeley_library/util/ods/xml/style/column_style.rb +36 -0
  75. data/lib/berkeley_library/util/ods/xml/style/default_style.rb +31 -0
  76. data/lib/berkeley_library/util/ods/xml/style/family.rb +85 -0
  77. data/lib/berkeley_library/util/ods/xml/style/font_face.rb +46 -0
  78. data/lib/berkeley_library/util/ods/xml/style/paragraph_properties.rb +30 -0
  79. data/lib/berkeley_library/util/ods/xml/style/row_style.rb +37 -0
  80. data/lib/berkeley_library/util/ods/xml/style/style.rb +44 -0
  81. data/lib/berkeley_library/util/ods/xml/style/table_cell_properties.rb +40 -0
  82. data/lib/berkeley_library/util/ods/xml/style/table_column_properties.rb +30 -0
  83. data/lib/berkeley_library/util/ods/xml/style/table_properties.rb +25 -0
  84. data/lib/berkeley_library/util/ods/xml/style/table_row_properties.rb +28 -0
  85. data/lib/berkeley_library/util/ods/xml/style/table_style.rb +27 -0
  86. data/lib/berkeley_library/util/ods/xml/style/text_properties.rb +52 -0
  87. data/lib/berkeley_library/util/ods/xml/styles_doc.rb +26 -0
  88. data/lib/berkeley_library/util/ods/xml/table/named_expressions.rb +17 -0
  89. data/lib/berkeley_library/util/ods/xml/table/repeatable.rb +38 -0
  90. data/lib/berkeley_library/util/ods/xml/table/table.rb +193 -0
  91. data/lib/berkeley_library/util/ods/xml/table/table_cell.rb +46 -0
  92. data/lib/berkeley_library/util/ods/xml/table/table_column.rb +43 -0
  93. data/lib/berkeley_library/util/ods/xml/table/table_row.rb +136 -0
  94. data/lib/berkeley_library/util/ods/xml/text/p.rb +118 -0
  95. data/lib/berkeley_library/util/paths.rb +111 -0
  96. data/lib/berkeley_library/util/stringios.rb +30 -0
  97. data/lib/berkeley_library/util/strings.rb +42 -0
  98. data/lib/berkeley_library/util/sys_exits.rb +15 -0
  99. data/lib/berkeley_library/util/times.rb +22 -0
  100. data/lib/berkeley_library/util/uris.rb +44 -0
  101. data/lib/berkeley_library/util/uris/appender.rb +162 -0
  102. data/lib/berkeley_library/util/uris/requester.rb +62 -0
  103. data/lib/berkeley_library/util/uris/validator.rb +32 -0
  104. data/rakelib/bundle.rake +8 -0
  105. data/rakelib/coverage.rake +11 -0
  106. data/rakelib/gem.rake +54 -0
  107. data/rakelib/rubocop.rake +18 -0
  108. data/rakelib/spec.rake +2 -0
  109. data/spec/.rubocop.yml +40 -0
  110. data/spec/berkeley_library/tind/api/api_exception_spec.rb +91 -0
  111. data/spec/berkeley_library/tind/api/api_spec.rb +143 -0
  112. data/spec/berkeley_library/tind/api/collection_spec.rb +74 -0
  113. data/spec/berkeley_library/tind/api/date_range_spec.rb +110 -0
  114. data/spec/berkeley_library/tind/api/format_spec.rb +54 -0
  115. data/spec/berkeley_library/tind/api/search_spec.rb +364 -0
  116. data/spec/berkeley_library/tind/config_spec.rb +86 -0
  117. data/spec/berkeley_library/tind/export/column_group_spec.rb +29 -0
  118. data/spec/berkeley_library/tind/export/column_spec.rb +43 -0
  119. data/spec/berkeley_library/tind/export/config_spec.rb +206 -0
  120. data/spec/berkeley_library/tind/export/export_command_spec.rb +169 -0
  121. data/spec/berkeley_library/tind/export/export_format_spec.rb +59 -0
  122. data/spec/berkeley_library/tind/export/export_matcher.rb +112 -0
  123. data/spec/berkeley_library/tind/export/export_spec.rb +150 -0
  124. data/spec/berkeley_library/tind/export/exporter_spec.rb +125 -0
  125. data/spec/berkeley_library/tind/export/row_spec.rb +118 -0
  126. data/spec/berkeley_library/tind/export/table_spec.rb +322 -0
  127. data/spec/berkeley_library/tind/marc/xml_reader_spec.rb +93 -0
  128. data/spec/berkeley_library/util/arrays_spec.rb +340 -0
  129. data/spec/berkeley_library/util/ods/spreadsheet_spec.rb +124 -0
  130. data/spec/berkeley_library/util/ods/xml/content_doc_spec.rb +121 -0
  131. data/spec/berkeley_library/util/ods/xml/manifest/file_entry_spec.rb +27 -0
  132. data/spec/berkeley_library/util/ods/xml/manifest/manifest_spec.rb +33 -0
  133. data/spec/berkeley_library/util/ods/xml/office/document_content_spec.rb +60 -0
  134. data/spec/berkeley_library/util/ods/xml/style/automatic_styles_spec.rb +37 -0
  135. data/spec/berkeley_library/util/ods/xml/style/family_spec.rb +57 -0
  136. data/spec/berkeley_library/util/ods/xml/table/table_row_spec.rb +179 -0
  137. data/spec/berkeley_library/util/ods/xml/table/table_spec.rb +218 -0
  138. data/spec/berkeley_library/util/paths_spec.rb +90 -0
  139. data/spec/berkeley_library/util/stringios_spec.rb +34 -0
  140. data/spec/berkeley_library/util/strings_spec.rb +27 -0
  141. data/spec/berkeley_library/util/times_spec.rb +39 -0
  142. data/spec/berkeley_library/util/uris_spec.rb +118 -0
  143. data/spec/data/collection-names.txt +438 -0
  144. data/spec/data/collections.json +4827 -0
  145. data/spec/data/disjoint-records.xml +187 -0
  146. data/spec/data/record-184453.xml +58 -0
  147. data/spec/data/record-184458.xml +63 -0
  148. data/spec/data/record-187888.xml +78 -0
  149. data/spec/data/records-api-search-cjk-p1.xml +6381 -0
  150. data/spec/data/records-api-search-cjk-p2.xml +5 -0
  151. data/spec/data/records-api-search-p1.xml +4506 -0
  152. data/spec/data/records-api-search-p2.xml +4509 -0
  153. data/spec/data/records-api-search-p3.xml +4506 -0
  154. data/spec/data/records-api-search-p4.xml +4509 -0
  155. data/spec/data/records-api-search-p5.xml +4506 -0
  156. data/spec/data/records-api-search-p6.xml +2436 -0
  157. data/spec/data/records-api-search-p7.xml +5 -0
  158. data/spec/data/records-api-search.xml +234 -0
  159. data/spec/data/records-manual-search.xml +547 -0
  160. data/spec/spec_helper.rb +30 -0
  161. data/test/profile/table_from_records_profile.rb +46 -0
  162. metadata +585 -0
@@ -0,0 +1,131 @@
1
+ require 'net/http/status'
2
+
3
+ module BerkeleyLibrary
4
+ module TIND
5
+ module API
6
+ # Wrapper for network-related exceptions.
7
+ class APIException < StandardError
8
+ # @return [String, nil] the request URI, if any
9
+ attr_reader :url
10
+
11
+ # @return [Hash, nil] the API query parameters, if any
12
+ attr_reader :params
13
+
14
+ # @return [Integer, nil] the numeric HTTP status code, if any
15
+ attr_reader :status_code
16
+
17
+ # @return [String, nil] the HTTP status message, if any
18
+ attr_reader :status_message
19
+
20
+ # @return [RestClient::Response, nil] the response, if any
21
+ attr_reader :response
22
+
23
+ # Initializes a new APIException.
24
+ #
25
+ # @option opts [String] :msg the exception message (if not present, a default message will be constructed)
26
+ # @option opts [String] :url the request URL, if any
27
+ # @option opts [Hash] :params the query or form parameters, if any
28
+ # @option opts [Integer] :status_code the numeric HTTP status code, if any
29
+ # @option opts [String] :status_message a human-readable string representation of the HTTP status
30
+ # (if not present, a default will be constructed)
31
+ # @option opts [RestClient::Response] :response the HTTP response, if any
32
+ def initialize(msg, **opts)
33
+ super(msg)
34
+
35
+ @url = opts[:url].to_s if opts.key?(:url)
36
+ @params = opts[:params]
37
+ @status_code, default_status_message = format_status(opts[:status_code])
38
+ @status_message = opts[:status_message] || default_status_message
39
+ @response = opts[:response]
40
+ end
41
+
42
+ def body
43
+ return @body if instance_variable_defined?(:@body)
44
+
45
+ @body = response && response.body
46
+ end
47
+
48
+ private
49
+
50
+ def format_status(status_code)
51
+ return unless (numeric_status = Integer(status_code, exception: false))
52
+
53
+ status_name = Net::HTTP::STATUS_CODES[numeric_status]
54
+ default_status_message = [numeric_status, status_name].compact.join(' ')
55
+
56
+ [numeric_status, default_status_message]
57
+ end
58
+
59
+ class << self
60
+ # @param ex [Exception] the exception to wrap
61
+ # @option opts [String] :msg the exception message (if not present, a default message will be constructed)
62
+ # @option opts [String] :url the request URL, if any
63
+ # @option opts [Hash] :params the query or form parameters, if any
64
+ # @option opts [String] :msg_context context information to prepend to the default message
65
+ def wrap(ex, **opts)
66
+ raise ArgumentError, "Can't wrap a nil error" unless ex
67
+
68
+ msg = opts[:msg] || message_from(ex, opts[:url], opts[:params], opts[:detail])
69
+ options = format_options(ex, opts[:url], opts[:params])
70
+ APIException.new(msg, **options)
71
+ end
72
+
73
+ private
74
+
75
+ def format_options(ex, url, params)
76
+ {}.tap do |opts|
77
+ opts[:url] = url if url
78
+ opts[:params] = params if params
79
+ next unless %i[http_code message response].all? { |f| ex.respond_to?(f) }
80
+
81
+ opts[:status_code] = ex.http_code
82
+ opts[:status_message] = ex.message
83
+ opts[:response] = ex.response
84
+ end
85
+ end
86
+
87
+ def message_from(ex, url, params, detail)
88
+ ''.tap do |msg|
89
+ msg << "#{detail}: " if detail
90
+ msg << (url ? "#{API.format_request(url, params)} returned #{ex}" : ex.to_s)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # Exception raised when the API key is nil or blank.
97
+ #
98
+ # NOTE: TIND incorrectly returns 403 Forbidden in this case, but we don't even bother
99
+ # to ask, we just simulate a 401.
100
+ class APIKeyNotSet < APIException
101
+ # @param endpoint_uri [URI] the endpoint URI
102
+ # @param params [Hash, nil] the query parameters
103
+ def initialize(endpoint_uri, params)
104
+ request_str = API.format_request(endpoint_uri, params)
105
+ super("#{request_str} failed; API key not set", status_code: 401)
106
+ end
107
+ end
108
+
109
+ # Exception raised when the TIND base URI is nil or blank.
110
+ class BaseURINotSet < APIException
111
+ # @param endpoint [String, Symbol] the endpoint
112
+ # @param params [Hash, nil] the query parameters
113
+ def initialize(endpoint, params)
114
+ msg = BaseURINotSet.format_message(endpoint, params)
115
+ super(msg, status_code: 404)
116
+ end
117
+
118
+ class << self
119
+ def format_message(endpoint, params)
120
+ "request to endpoint #{endpoint.inspect}".tap do |msg|
121
+ if (query_string = API.format_query(params))
122
+ msg << " with query #{query_string}"
123
+ end
124
+ msg << ' failed; base URI not set'
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,82 @@
1
+ require 'json'
2
+ require 'berkeley_library/util/logging'
3
+ require 'berkeley_library/tind/api/api_exception'
4
+
5
+ module BerkeleyLibrary
6
+ module TIND
7
+ module API
8
+ class Collection
9
+ attr_reader :name, :nb_rec, :children, :translations
10
+ alias size nb_rec
11
+
12
+ def initialize(name, nb_rec, children, translations)
13
+ @name = name
14
+ @nb_rec = nb_rec
15
+ @children = children
16
+ @translations = translations
17
+ end
18
+
19
+ def name_en
20
+ return unless (names = translations['name'])
21
+
22
+ names['en']
23
+ end
24
+
25
+ def each_descendant(include_self: false, &block)
26
+ yield self if include_self
27
+
28
+ children.each { |c| c.each_descendant(include_self: include_self, &block) }
29
+ end
30
+
31
+ class << self
32
+ include BerkeleyLibrary::Logging
33
+
34
+ ENDPOINT = 'collections'.freeze
35
+
36
+ def all
37
+ json = API.get(ENDPOINT, depth: 100)
38
+ all_from_json(json)
39
+ rescue API::APIException => e
40
+ logger.error(e)
41
+ []
42
+ end
43
+
44
+ def each_collection(&block)
45
+ return to_enum(:each_collection) unless block_given?
46
+
47
+ all.each { |c| c.each_descendant(include_self: true, &block) }
48
+ end
49
+
50
+ # Returns an array of collection tree roots, which can be traversed
51
+ # with {Collection#each_descendant}.
52
+ #
53
+ # @return [Array<Collection>] an array of top-level collections
54
+ def all_from_json(json)
55
+ ensure_hash(json).map do |name, attrs|
56
+ translations = attrs['translations']
57
+ Collection.new(
58
+ name,
59
+ attrs['nb_rec'],
60
+ all_from_json(attrs['children']),
61
+ translations
62
+ )
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def ensure_hash(json)
69
+ return {} unless json
70
+ return json if hash_like?(json)
71
+
72
+ JSON.parse(json)
73
+ end
74
+
75
+ def hash_like?(h)
76
+ h.respond_to?(:each_key) && h.respond_to?(:each_value)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,67 @@
1
+ require 'berkeley_library/util/times'
2
+ require 'berkeley_library/tind/config'
3
+
4
+ module BerkeleyLibrary
5
+ module TIND
6
+ module API
7
+ class DateRange
8
+ FORMAT = '%Y-%m-%d %H:%M:%S'.freeze
9
+
10
+ attr_reader :from_time, :until_time
11
+
12
+ def initialize(from_time:, until_time:, mtime: false)
13
+ @from_time, @until_time = DateRange.ensure_valid_range(from_time, until_time)
14
+ @mtime = mtime
15
+ end
16
+
17
+ def mtime?
18
+ @mtime
19
+ end
20
+
21
+ def to_params
22
+ { d1: format_param(from_time), d2: format_param(until_time) }.tap do |params|
23
+ params[:dt] = 'm' if mtime?
24
+ end
25
+ end
26
+
27
+ alias eql ==
28
+
29
+ def ==(other)
30
+ return false unless other.class == self.class
31
+
32
+ [from_time, until_time] == [other.from_time, other.until_time]
33
+ end
34
+
35
+ class << self
36
+ def from_range(range)
37
+ DateRange.new(from_time: range.first, until_time: range.last)
38
+ end
39
+
40
+ def ensure_date_range(date_range)
41
+ return unless date_range
42
+ return date_range if date_range.is_a?(DateRange)
43
+ return DateRange.from_range(date_range) if date_range.respond_to?(:first) && date_range.respond_to?(:last)
44
+
45
+ raise ArgumentError, "Can't convert #{date_range.inspect} to #{DateRange}"
46
+ end
47
+
48
+ def ensure_valid_range(from_time, until_time)
49
+ ftime, utime = [from_time, until_time].map { |t| BerkeleyLibrary::Util::Times.ensure_utc(t) }
50
+ return [ftime, utime] if ftime <= utime
51
+
52
+ raise ArgumentError, "Not a valid range: #{from_time.inspect}..#{until_time.inspect}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def format_param(t)
59
+ tz = BerkeleyLibrary::TIND::Config.timezone
60
+ t_utc = BerkeleyLibrary::Util::Times.ensure_utc(t) # just to be sure
61
+ t_local = tz.utc_to_local(t_utc)
62
+ t_local.strftime(FORMAT)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ require 'typesafe_enum'
2
+
3
+ module BerkeleyLibrary
4
+ module TIND
5
+ module API
6
+ class Format < TypesafeEnum::Base
7
+ %i[ID XML FILES JSON].each { |fmt| new(fmt) }
8
+
9
+ def to_s
10
+ # noinspection RubyYardReturnMatch
11
+ value
12
+ end
13
+
14
+ def to_str
15
+ value
16
+ end
17
+
18
+ class << self
19
+ def ensure_format(format)
20
+ return unless format
21
+ return format if format.is_a?(Format)
22
+
23
+ fmt = Format.find_by_value(format.to_s.downcase)
24
+ return fmt if fmt
25
+
26
+ raise ArgumentError, "Unknown #{Format}: #{format.inspect}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,100 @@
1
+ require 'berkeley_library/util/logging'
2
+ require 'berkeley_library/tind/marc/xml_reader'
3
+ require 'berkeley_library/tind/api/date_range'
4
+ require 'berkeley_library/tind/api/format'
5
+
6
+ module BerkeleyLibrary
7
+ module TIND
8
+ module API
9
+ class Search
10
+ include BerkeleyLibrary::Logging
11
+
12
+ attr_reader :collection, :pattern, :index, :date_range, :format
13
+
14
+ def initialize(collection: nil, pattern: nil, index: nil, date_range: nil, format: Format::XML)
15
+ raise ArgumentError, 'Search requires a collection' unless collection
16
+
17
+ @collection = collection
18
+ @pattern = pattern
19
+ @index = index
20
+ @date_range = DateRange.ensure_date_range(date_range)
21
+ @format = Format.ensure_format(format)
22
+ end
23
+
24
+ # rubocop: disable Metrics/AbcSize
25
+ def params
26
+ @params ||= {}.tap do |params|
27
+ params[:c] = collection if collection
28
+ params[:p] = pattern if pattern
29
+ params[:f] = index if index
30
+ params.merge!(date_range.to_params) if date_range
31
+ params[:format] = self.format.to_s if self.format
32
+ end
33
+ end
34
+ # rubocop: enable Metrics/AbcSize
35
+
36
+ # Performs this search and returns the results as array.
37
+ # @return [Array<MARC::Record>] the results
38
+ def results
39
+ each_result.to_a
40
+ end
41
+
42
+ # Iterates over the records returned by this search.
43
+ # @overload each_result(freeze: false, &block)
44
+ # Yields each record to the provided block.
45
+ # @param freeze [Boolean] whether to freeze each record before yielding.
46
+ # @yieldparam marc_record [MARC::Record] each record
47
+ # @return [self]
48
+ # @overload each_result(freeze: false)
49
+ # Returns an enumerator of the records.
50
+ # @param freeze [Boolean] whether to freeze each record before yielding.
51
+ # @return [Enumerable<MARC::Record>] the records
52
+ def each_result(freeze: false, &block)
53
+ return to_enum(:each_result, freeze: freeze) unless block_given?
54
+
55
+ perform_search(freeze: freeze, &block)
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ def perform_search(search_id: nil, freeze: false, &block)
62
+ logger.info("perform_search(search_id: #{search_id.inspect})")
63
+ params = search_id ? self.params.merge(search_id: search_id) : self.params
64
+ next_search_id = perform_single_search(params, freeze, &block)
65
+ perform_search(search_id: next_search_id, freeze: freeze, &block) if next_search_id
66
+ end
67
+
68
+ def perform_single_search(params, freeze, &block)
69
+ API.get(:search, params) do |body|
70
+ process_body(body, freeze, &block)
71
+ ensure
72
+ body.close
73
+ end
74
+ rescue APIException => e
75
+ return nil if empty_result?(e)
76
+
77
+ raise
78
+ end
79
+
80
+ def process_body(body, freeze, &block)
81
+ xml_reader = BerkeleyLibrary::TIND::MARC::XMLReader.read(body, freeze: freeze)
82
+ xml_reader.each(&block)
83
+ logger.debug("yielded #{xml_reader.records_yielded} of #{xml_reader.total} records")
84
+ logger.debug("next search ID: #{xml_reader.search_id}")
85
+ xml_reader.search_id if xml_reader.records_yielded > 0
86
+ end
87
+
88
+ def empty_result?(api_ex)
89
+ return false unless api_ex.status_code == 500
90
+ return false unless (body = api_ex.body)
91
+ return false unless (result = JSON.parse(body, symbolize_names: true))
92
+
93
+ result[:success] == false && result[:error].include?('0 values')
94
+ rescue JSON::ParserError
95
+ false
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,103 @@
1
+ require 'berkeley_library/util/uris'
2
+ require 'berkeley_library/tind/module_info'
3
+
4
+ require 'tzinfo'
5
+
6
+ module BerkeleyLibrary
7
+ module TIND
8
+ module Config
9
+
10
+ # The environment variable from which to read the TIND API key.
11
+ ENV_TIND_API_KEY = 'LIT_TIND_API_KEY'.freeze
12
+
13
+ # The root URL for the TIND installation
14
+ ENV_TIND_BASE_URL = 'LIT_TIND_BASE_URL'.freeze
15
+
16
+ DEFAULT_TZID = 'America/Los_Angeles'.freeze
17
+ DEFAULT_USER_AGENT = "#{ModuleInfo::NAME} #{ModuleInfo::VERSION} (#{ModuleInfo::HOMEPAGE})".freeze
18
+
19
+ class << self
20
+ include BerkeleyLibrary::Util::URIs
21
+
22
+ # Sets the TIND API key.
23
+ # @param value [String] the API key.
24
+ attr_writer :api_key
25
+
26
+ # Gets the TIND API key.
27
+ # @return [String, nil] the TIND API key, or `nil` if not set.
28
+ def api_key
29
+ @api_key ||= default_tind_api_key
30
+ end
31
+
32
+ def base_uri
33
+ @base_uri ||= default_tind_base_uri
34
+ end
35
+
36
+ def base_uri=(value)
37
+ @base_uri = uri_or_nil(value)
38
+ end
39
+
40
+ def timezone
41
+ @timezone ||= default_timezone
42
+ end
43
+
44
+ def timezone=(value)
45
+ raise ArgumentError, "Not a #{TZInfo::Timezone}" unless value.respond_to?(:utc_to_local)
46
+
47
+ @timezone = value
48
+ end
49
+
50
+ def user_agent
51
+ @user_agent || DEFAULT_USER_AGENT
52
+ end
53
+
54
+ def user_agent=(value)
55
+ raise ArgumentError, 'TIND firewall rules require a user agent' if blank?(value)
56
+
57
+ @user_agent = value
58
+ end
59
+
60
+ def blank?(v)
61
+ v.nil? || v.to_s.strip.empty?
62
+ end
63
+
64
+ private
65
+
66
+ def default_timezone
67
+ TZInfo::Timezone.get(Config::DEFAULT_TZID)
68
+ end
69
+
70
+ def default_tind_api_key
71
+ ENV[Config::ENV_TIND_API_KEY] || rails_tind_api_key
72
+ end
73
+
74
+ def default_tind_base_uri
75
+ return unless (base_url = ENV[Config::ENV_TIND_BASE_URL] || rails_tind_base_uri)
76
+
77
+ uri_or_nil(base_url)
78
+ end
79
+
80
+ def rails_tind_base_uri
81
+ return unless (rails_config = self.rails_config)
82
+ return unless rails_config.respond_to?(:tind_base_uri)
83
+
84
+ rails_config.tind_base_uri
85
+ end
86
+
87
+ def rails_tind_api_key
88
+ return unless (rails_config = self.rails_config)
89
+ return unless rails_config.respond_to?(:tind_api_key)
90
+
91
+ rails_config.tind_api_key
92
+ end
93
+
94
+ def rails_config
95
+ return unless defined?(Rails)
96
+ return unless (app = Rails.application)
97
+
98
+ app.config
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end