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