berkeley_library-tind 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +18 -0
- data/.gitignore +388 -0
- data/.idea/inspectionProfiles/Project_Default.xml +20 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/tind.iml +138 -0
- data/.idea/vcs.xml +6 -0
- data/.rubocop.yml +334 -0
- data/.ruby-version +1 -0
- data/.simplecov +8 -0
- data/.yardopts +1 -0
- data/CHANGES.md +58 -0
- data/Dockerfile +57 -0
- data/Gemfile +3 -0
- data/Jenkinsfile +18 -0
- data/LICENSE.md +21 -0
- data/README.md +73 -0
- data/Rakefile +20 -0
- data/berkeley_library-tind.gemspec +50 -0
- data/bin/tind-export +14 -0
- data/docker-compose.yml +15 -0
- data/lib/berkeley_library/tind.rb +3 -0
- data/lib/berkeley_library/tind/api.rb +1 -0
- data/lib/berkeley_library/tind/api/api.rb +132 -0
- data/lib/berkeley_library/tind/api/api_exception.rb +131 -0
- data/lib/berkeley_library/tind/api/collection.rb +82 -0
- data/lib/berkeley_library/tind/api/date_range.rb +67 -0
- data/lib/berkeley_library/tind/api/format.rb +32 -0
- data/lib/berkeley_library/tind/api/search.rb +100 -0
- data/lib/berkeley_library/tind/config.rb +103 -0
- data/lib/berkeley_library/tind/export.rb +1 -0
- data/lib/berkeley_library/tind/export/column.rb +54 -0
- data/lib/berkeley_library/tind/export/column_group.rb +144 -0
- data/lib/berkeley_library/tind/export/column_group_list.rb +131 -0
- data/lib/berkeley_library/tind/export/column_width_calculator.rb +76 -0
- data/lib/berkeley_library/tind/export/config.rb +154 -0
- data/lib/berkeley_library/tind/export/csv_exporter.rb +29 -0
- data/lib/berkeley_library/tind/export/export.rb +47 -0
- data/lib/berkeley_library/tind/export/export_command.rb +168 -0
- data/lib/berkeley_library/tind/export/export_exception.rb +8 -0
- data/lib/berkeley_library/tind/export/export_format.rb +67 -0
- data/lib/berkeley_library/tind/export/exporter.rb +105 -0
- data/lib/berkeley_library/tind/export/filter.rb +52 -0
- data/lib/berkeley_library/tind/export/no_results_error.rb +7 -0
- data/lib/berkeley_library/tind/export/ods_exporter.rb +138 -0
- data/lib/berkeley_library/tind/export/row.rb +24 -0
- data/lib/berkeley_library/tind/export/row_metrics.rb +18 -0
- data/lib/berkeley_library/tind/export/table.rb +175 -0
- data/lib/berkeley_library/tind/export/table_metrics.rb +116 -0
- data/lib/berkeley_library/tind/marc.rb +1 -0
- data/lib/berkeley_library/tind/marc/xml_reader.rb +144 -0
- data/lib/berkeley_library/tind/module_info.rb +14 -0
- data/lib/berkeley_library/util/arrays.rb +178 -0
- data/lib/berkeley_library/util/logging.rb +1 -0
- data/lib/berkeley_library/util/ods/spreadsheet.rb +170 -0
- data/lib/berkeley_library/util/ods/xml/content_doc.rb +26 -0
- data/lib/berkeley_library/util/ods/xml/document_node.rb +57 -0
- data/lib/berkeley_library/util/ods/xml/element_node.rb +106 -0
- data/lib/berkeley_library/util/ods/xml/loext/table_protection.rb +26 -0
- data/lib/berkeley_library/util/ods/xml/manifest/file_entry.rb +42 -0
- data/lib/berkeley_library/util/ods/xml/manifest/manifest.rb +73 -0
- data/lib/berkeley_library/util/ods/xml/manifest_doc.rb +26 -0
- data/lib/berkeley_library/util/ods/xml/namespace.rb +46 -0
- data/lib/berkeley_library/util/ods/xml/office/automatic_styles.rb +181 -0
- data/lib/berkeley_library/util/ods/xml/office/body.rb +17 -0
- data/lib/berkeley_library/util/ods/xml/office/document_content.rb +98 -0
- data/lib/berkeley_library/util/ods/xml/office/document_styles.rb +39 -0
- data/lib/berkeley_library/util/ods/xml/office/font_face_decls.rb +30 -0
- data/lib/berkeley_library/util/ods/xml/office/scripts.rb +17 -0
- data/lib/berkeley_library/util/ods/xml/office/spreadsheet.rb +37 -0
- data/lib/berkeley_library/util/ods/xml/office/styles.rb +39 -0
- data/lib/berkeley_library/util/ods/xml/style/cell_style.rb +58 -0
- data/lib/berkeley_library/util/ods/xml/style/column_style.rb +36 -0
- data/lib/berkeley_library/util/ods/xml/style/default_style.rb +31 -0
- data/lib/berkeley_library/util/ods/xml/style/family.rb +85 -0
- data/lib/berkeley_library/util/ods/xml/style/font_face.rb +46 -0
- data/lib/berkeley_library/util/ods/xml/style/paragraph_properties.rb +30 -0
- data/lib/berkeley_library/util/ods/xml/style/row_style.rb +37 -0
- data/lib/berkeley_library/util/ods/xml/style/style.rb +44 -0
- data/lib/berkeley_library/util/ods/xml/style/table_cell_properties.rb +40 -0
- data/lib/berkeley_library/util/ods/xml/style/table_column_properties.rb +30 -0
- data/lib/berkeley_library/util/ods/xml/style/table_properties.rb +25 -0
- data/lib/berkeley_library/util/ods/xml/style/table_row_properties.rb +28 -0
- data/lib/berkeley_library/util/ods/xml/style/table_style.rb +27 -0
- data/lib/berkeley_library/util/ods/xml/style/text_properties.rb +52 -0
- data/lib/berkeley_library/util/ods/xml/styles_doc.rb +26 -0
- data/lib/berkeley_library/util/ods/xml/table/named_expressions.rb +17 -0
- data/lib/berkeley_library/util/ods/xml/table/repeatable.rb +38 -0
- data/lib/berkeley_library/util/ods/xml/table/table.rb +193 -0
- data/lib/berkeley_library/util/ods/xml/table/table_cell.rb +46 -0
- data/lib/berkeley_library/util/ods/xml/table/table_column.rb +43 -0
- data/lib/berkeley_library/util/ods/xml/table/table_row.rb +136 -0
- data/lib/berkeley_library/util/ods/xml/text/p.rb +118 -0
- data/lib/berkeley_library/util/paths.rb +111 -0
- data/lib/berkeley_library/util/stringios.rb +30 -0
- data/lib/berkeley_library/util/strings.rb +42 -0
- data/lib/berkeley_library/util/sys_exits.rb +15 -0
- data/lib/berkeley_library/util/times.rb +22 -0
- data/lib/berkeley_library/util/uris.rb +44 -0
- data/lib/berkeley_library/util/uris/appender.rb +162 -0
- data/lib/berkeley_library/util/uris/requester.rb +62 -0
- data/lib/berkeley_library/util/uris/validator.rb +32 -0
- data/rakelib/bundle.rake +8 -0
- data/rakelib/coverage.rake +11 -0
- data/rakelib/gem.rake +54 -0
- data/rakelib/rubocop.rake +18 -0
- data/rakelib/spec.rake +2 -0
- data/spec/.rubocop.yml +40 -0
- data/spec/berkeley_library/tind/api/api_exception_spec.rb +91 -0
- data/spec/berkeley_library/tind/api/api_spec.rb +143 -0
- data/spec/berkeley_library/tind/api/collection_spec.rb +74 -0
- data/spec/berkeley_library/tind/api/date_range_spec.rb +110 -0
- data/spec/berkeley_library/tind/api/format_spec.rb +54 -0
- data/spec/berkeley_library/tind/api/search_spec.rb +364 -0
- data/spec/berkeley_library/tind/config_spec.rb +86 -0
- data/spec/berkeley_library/tind/export/column_group_spec.rb +29 -0
- data/spec/berkeley_library/tind/export/column_spec.rb +43 -0
- data/spec/berkeley_library/tind/export/config_spec.rb +206 -0
- data/spec/berkeley_library/tind/export/export_command_spec.rb +169 -0
- data/spec/berkeley_library/tind/export/export_format_spec.rb +59 -0
- data/spec/berkeley_library/tind/export/export_matcher.rb +112 -0
- data/spec/berkeley_library/tind/export/export_spec.rb +150 -0
- data/spec/berkeley_library/tind/export/exporter_spec.rb +125 -0
- data/spec/berkeley_library/tind/export/row_spec.rb +118 -0
- data/spec/berkeley_library/tind/export/table_spec.rb +322 -0
- data/spec/berkeley_library/tind/marc/xml_reader_spec.rb +93 -0
- data/spec/berkeley_library/util/arrays_spec.rb +340 -0
- data/spec/berkeley_library/util/ods/spreadsheet_spec.rb +124 -0
- data/spec/berkeley_library/util/ods/xml/content_doc_spec.rb +121 -0
- data/spec/berkeley_library/util/ods/xml/manifest/file_entry_spec.rb +27 -0
- data/spec/berkeley_library/util/ods/xml/manifest/manifest_spec.rb +33 -0
- data/spec/berkeley_library/util/ods/xml/office/document_content_spec.rb +60 -0
- data/spec/berkeley_library/util/ods/xml/style/automatic_styles_spec.rb +37 -0
- data/spec/berkeley_library/util/ods/xml/style/family_spec.rb +57 -0
- data/spec/berkeley_library/util/ods/xml/table/table_row_spec.rb +179 -0
- data/spec/berkeley_library/util/ods/xml/table/table_spec.rb +218 -0
- data/spec/berkeley_library/util/paths_spec.rb +90 -0
- data/spec/berkeley_library/util/stringios_spec.rb +34 -0
- data/spec/berkeley_library/util/strings_spec.rb +27 -0
- data/spec/berkeley_library/util/times_spec.rb +39 -0
- data/spec/berkeley_library/util/uris_spec.rb +118 -0
- data/spec/data/collection-names.txt +438 -0
- data/spec/data/collections.json +4827 -0
- data/spec/data/disjoint-records.xml +187 -0
- data/spec/data/record-184453.xml +58 -0
- data/spec/data/record-184458.xml +63 -0
- data/spec/data/record-187888.xml +78 -0
- data/spec/data/records-api-search-cjk-p1.xml +6381 -0
- data/spec/data/records-api-search-cjk-p2.xml +5 -0
- data/spec/data/records-api-search-p1.xml +4506 -0
- data/spec/data/records-api-search-p2.xml +4509 -0
- data/spec/data/records-api-search-p3.xml +4506 -0
- data/spec/data/records-api-search-p4.xml +4509 -0
- data/spec/data/records-api-search-p5.xml +4506 -0
- data/spec/data/records-api-search-p6.xml +2436 -0
- data/spec/data/records-api-search-p7.xml +5 -0
- data/spec/data/records-api-search.xml +234 -0
- data/spec/data/records-manual-search.xml +547 -0
- data/spec/spec_helper.rb +30 -0
- data/test/profile/table_from_records_profile.rb +46 -0
- 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
|