alma 0.2.4 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +54 -0
- data/.circleci/setup-rubygems.sh +3 -0
- data/.github/dependabot.yml +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +134 -0
- data/.ruby-version +1 -1
- data/Gemfile +5 -1
- data/Guardfile +75 -0
- data/README.md +146 -57
- data/Rakefile +3 -1
- data/alma.gemspec +21 -12
- data/lib/alma.rb +34 -54
- data/lib/alma/alma_record.rb +3 -3
- data/lib/alma/api_defaults.rb +39 -0
- data/lib/alma/availability_response.rb +69 -31
- data/lib/alma/bib.rb +54 -29
- data/lib/alma/bib_holding.rb +25 -0
- data/lib/alma/bib_item.rb +164 -0
- data/lib/alma/bib_item_set.rb +93 -0
- data/lib/alma/bib_set.rb +5 -10
- data/lib/alma/config.rb +10 -4
- data/lib/alma/course.rb +47 -0
- data/lib/alma/course_set.rb +17 -0
- data/lib/alma/electronic.rb +167 -0
- data/lib/alma/electronic/README.md +20 -0
- data/lib/alma/electronic/batch_utils.rb +224 -0
- data/lib/alma/electronic/business.rb +29 -0
- data/lib/alma/error.rb +16 -4
- data/lib/alma/fine.rb +16 -0
- data/lib/alma/fine_set.rb +41 -8
- data/lib/alma/item_request_options.rb +23 -0
- data/lib/alma/library.rb +29 -0
- data/lib/alma/library_set.rb +21 -0
- data/lib/alma/loan.rb +31 -2
- data/lib/alma/loan_set.rb +62 -4
- data/lib/alma/location.rb +29 -0
- data/lib/alma/location_set.rb +21 -0
- data/lib/alma/renewal_response.rb +25 -14
- data/lib/alma/request.rb +167 -0
- data/lib/alma/request_options.rb +66 -0
- data/lib/alma/request_set.rb +69 -5
- data/lib/alma/response.rb +45 -0
- data/lib/alma/result_set.rb +27 -35
- data/lib/alma/user.rb +142 -86
- data/lib/alma/user_request.rb +19 -0
- data/lib/alma/user_set.rb +5 -6
- data/lib/alma/version.rb +3 -1
- data/log/.gitignore +4 -0
- metadata +149 -10
- data/.travis.yml +0 -5
- data/lib/alma/api.rb +0 -33
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "alma/bib_item_set"
|
4
|
+
module Alma
|
5
|
+
class BibItem
|
6
|
+
extend Alma::ApiDefaults
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :item
|
10
|
+
def_delegators :item, :[], :has_key?, :keys, :to_json
|
11
|
+
|
12
|
+
PERMITTED_ARGS = [
|
13
|
+
:limit, :offset, :expand, :user_id, :current_library,
|
14
|
+
:current_location, :q, :order_by, :direction
|
15
|
+
]
|
16
|
+
|
17
|
+
def self.find(mms_id, options = {})
|
18
|
+
holding_id = options.delete(:holding_id) || "ALL"
|
19
|
+
options.select! { |k, _| PERMITTED_ARGS.include? k }
|
20
|
+
url = "#{bibs_base_path}/#{mms_id}/holdings/#{holding_id}/items"
|
21
|
+
response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
|
22
|
+
BibItemSet.new(response, options.merge({ mms_id: mms_id, holding_id: holding_id }))
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.find_by_barcode(barcode)
|
26
|
+
response = HTTParty.get(items_base_path, headers: headers, query: { item_barcode: barcode }, timeout: timeout, follow_redirects: true)
|
27
|
+
new(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.scan(mms_id:, holding_id:, item_pid:, options: {})
|
31
|
+
url = "#{bibs_base_path}/#{mms_id}/holdings/#{holding_id}/items/#{item_pid}"
|
32
|
+
response = HTTParty.post(url, headers: headers, query: options)
|
33
|
+
new(response)
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(item)
|
37
|
+
@item = item
|
38
|
+
end
|
39
|
+
|
40
|
+
def holding_data
|
41
|
+
@item.fetch("holding_data", {})
|
42
|
+
end
|
43
|
+
|
44
|
+
def item_data
|
45
|
+
@item.fetch("item_data", {})
|
46
|
+
end
|
47
|
+
|
48
|
+
def in_temp_location?
|
49
|
+
holding_data.fetch("in_temp_location", false)
|
50
|
+
end
|
51
|
+
|
52
|
+
def library
|
53
|
+
in_temp_location? ? temp_library : holding_library
|
54
|
+
end
|
55
|
+
|
56
|
+
def library_name
|
57
|
+
in_temp_location? ? temp_library_name : holding_library_name
|
58
|
+
end
|
59
|
+
|
60
|
+
def location
|
61
|
+
in_temp_location? ? temp_location : holding_location
|
62
|
+
end
|
63
|
+
|
64
|
+
def location_name
|
65
|
+
in_temp_location? ? temp_location_name : holding_location_name
|
66
|
+
end
|
67
|
+
|
68
|
+
def holding_library
|
69
|
+
item_data.dig("library", "value")
|
70
|
+
end
|
71
|
+
|
72
|
+
def holding_library_name
|
73
|
+
item_data.dig("library", "desc")
|
74
|
+
end
|
75
|
+
|
76
|
+
def holding_location
|
77
|
+
item_data.dig("location", "value")
|
78
|
+
end
|
79
|
+
|
80
|
+
def holding_location_name
|
81
|
+
item_data.dig("location", "desc")
|
82
|
+
end
|
83
|
+
|
84
|
+
def temp_library
|
85
|
+
holding_data.dig("temp_library", "value")
|
86
|
+
end
|
87
|
+
|
88
|
+
def temp_library_name
|
89
|
+
holding_data.dig("temp_library", "desc")
|
90
|
+
end
|
91
|
+
|
92
|
+
def temp_location
|
93
|
+
holding_data.dig("temp_location", "value")
|
94
|
+
end
|
95
|
+
|
96
|
+
def temp_location_name
|
97
|
+
holding_data.dig("temp_location", "desc")
|
98
|
+
end
|
99
|
+
|
100
|
+
def temp_call_number
|
101
|
+
holding_data.fetch("temp_call_number", "")
|
102
|
+
end
|
103
|
+
|
104
|
+
def has_temp_call_number?
|
105
|
+
!temp_call_number.empty?
|
106
|
+
end
|
107
|
+
|
108
|
+
def call_number
|
109
|
+
if has_temp_call_number?
|
110
|
+
holding_data.fetch("temp_call_number")
|
111
|
+
else
|
112
|
+
holding_data.fetch("call_number", "")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def has_alt_call_number?
|
117
|
+
!alt_call_number.empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
def alt_call_number
|
121
|
+
item_data.fetch("alternative_call_number", "")
|
122
|
+
end
|
123
|
+
|
124
|
+
def has_process_type?
|
125
|
+
!process_type.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
def process_type
|
129
|
+
item_data.dig("process_type", "value") || ""
|
130
|
+
end
|
131
|
+
|
132
|
+
def missing_or_lost?
|
133
|
+
!!process_type.match(/MISSING|LOST_LOAN/)
|
134
|
+
end
|
135
|
+
|
136
|
+
def base_status
|
137
|
+
item_data.dig("base_status", "value") || ""
|
138
|
+
end
|
139
|
+
|
140
|
+
def in_place?
|
141
|
+
base_status == "1"
|
142
|
+
end
|
143
|
+
|
144
|
+
def circulation_policy
|
145
|
+
item_data.dig("policy", "desc") || ""
|
146
|
+
end
|
147
|
+
|
148
|
+
def non_circulating?
|
149
|
+
circulation_policy.include?("Non-circulating")
|
150
|
+
end
|
151
|
+
|
152
|
+
def description
|
153
|
+
item_data.fetch("description", "")
|
154
|
+
end
|
155
|
+
|
156
|
+
def physical_material_type
|
157
|
+
item_data.fetch("physical_material_type", "")
|
158
|
+
end
|
159
|
+
|
160
|
+
def public_note
|
161
|
+
item_data.fetch("public_note", "")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Alma
|
4
|
+
class BibItemSet < ResultSet
|
5
|
+
ITEMS_PER_PAGE = 100
|
6
|
+
|
7
|
+
class ResponseError < ::Alma::StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :items
|
11
|
+
attr_reader :raw_response, :total_record_count
|
12
|
+
|
13
|
+
def_delegators :items, :[], :[]=, :empty?, :size, :each
|
14
|
+
def_delegators :raw_response, :response, :request
|
15
|
+
|
16
|
+
def initialize(response, options = {})
|
17
|
+
@raw_response = response
|
18
|
+
parsed = response.parsed_response
|
19
|
+
@total_record_count = parsed["total_record_count"]
|
20
|
+
@options = options
|
21
|
+
@mms_id = @options.delete(:mms_id)
|
22
|
+
|
23
|
+
validate(response)
|
24
|
+
@items = parsed.fetch(key, []).map { |item| single_record_class.new(item) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def loggable
|
28
|
+
{ total_record_count: @total_record_count.to_s,
|
29
|
+
mms_id: @mms_id,
|
30
|
+
uri: @raw_response&.request&.uri.to_s
|
31
|
+
}.select { |k, v| !(v.nil? || v.empty?) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate(response)
|
35
|
+
if response.code != 200
|
36
|
+
log = loggable.merge(response.parsed_response)
|
37
|
+
raise ResponseError.new("Could not get bib items.", log)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def grouped_by_library
|
42
|
+
group_by(&:library)
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter_missing_and_lost
|
46
|
+
clone = dup
|
47
|
+
clone.items = reject(&:missing_or_lost?)
|
48
|
+
clone
|
49
|
+
end
|
50
|
+
|
51
|
+
def all
|
52
|
+
@last_page_index ||= false
|
53
|
+
Enumerator.new do |yielder|
|
54
|
+
offset = 0
|
55
|
+
while (!@last_page_index || @last_page_index >= offset / items_per_page) do
|
56
|
+
r = (offset == 0) ? self : single_record_class.find(@mms_id, options = @options.merge({ limit: items_per_page, offset: offset }))
|
57
|
+
unless r.empty?
|
58
|
+
r.map { |item| yielder << item }
|
59
|
+
@last_page_index = (offset / items_per_page)
|
60
|
+
else
|
61
|
+
@last_page_index = @last_page_index ? @last_page_index - 1 : -1
|
62
|
+
end
|
63
|
+
|
64
|
+
if r.size == items_per_page
|
65
|
+
@last_page_index += 1
|
66
|
+
end
|
67
|
+
|
68
|
+
offset += items_per_page
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def each(&block)
|
74
|
+
@items.each(&block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def success?
|
78
|
+
raw_response.response.code.to_s == "200"
|
79
|
+
end
|
80
|
+
|
81
|
+
def key
|
82
|
+
"item"
|
83
|
+
end
|
84
|
+
|
85
|
+
def single_record_class
|
86
|
+
Alma::BibItem
|
87
|
+
end
|
88
|
+
|
89
|
+
def items_per_page
|
90
|
+
ITEMS_PER_PAGE
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/alma/bib_set.rb
CHANGED
@@ -1,22 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Alma
|
2
4
|
class BibSet < ResultSet
|
3
|
-
|
4
|
-
|
5
|
-
'bibs'
|
6
|
-
end
|
7
|
-
|
8
|
-
def response_records_key
|
9
|
-
'bib'
|
5
|
+
def key
|
6
|
+
"bib"
|
10
7
|
end
|
11
8
|
|
12
9
|
def single_record_class
|
13
10
|
Alma::Bib
|
14
11
|
end
|
15
12
|
|
16
|
-
# Doesn't seem to actually return a total record count as documented.
|
17
13
|
def total_record_count
|
18
|
-
|
14
|
+
size
|
19
15
|
end
|
20
|
-
|
21
16
|
end
|
22
17
|
end
|
data/lib/alma/config.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Alma
|
2
4
|
class << self
|
3
5
|
attr_accessor :configuration
|
@@ -9,12 +11,16 @@ module Alma
|
|
9
11
|
end
|
10
12
|
|
11
13
|
class Configuration
|
12
|
-
attr_accessor :apikey, :region
|
14
|
+
attr_accessor :apikey, :region, :enable_loggable
|
15
|
+
attr_accessor :timeout, :http_retries, :logger
|
13
16
|
|
14
17
|
def initialize
|
15
18
|
@apikey = "TEST_API_KEY"
|
16
|
-
@region =
|
19
|
+
@region = "https://api-na.hosted.exlibrisgroup.com"
|
20
|
+
@enable_loggable = false
|
21
|
+
@timeout = 5
|
22
|
+
@http_retries = 3
|
23
|
+
@logger = Logger.new(STDOUT)
|
17
24
|
end
|
18
|
-
|
19
25
|
end
|
20
|
-
end
|
26
|
+
end
|
data/lib/alma/course.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Alma
|
4
|
+
class Course
|
5
|
+
extend Alma::ApiDefaults
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def self.all_courses(args: {})
|
9
|
+
response = HTTParty.get("#{courses_base_path}/courses",
|
10
|
+
query: args,
|
11
|
+
headers: headers,
|
12
|
+
timeout: timeout)
|
13
|
+
if response.code == 200
|
14
|
+
Alma::CourseSet.new(get_body_from(response))
|
15
|
+
else
|
16
|
+
raise StandardError, get_body_from(response)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :response
|
21
|
+
|
22
|
+
# The Course object can respond directly to Hash like access of attributes
|
23
|
+
def_delegators :response, :[], :[]=, :has_key?, :keys, :to_json
|
24
|
+
|
25
|
+
def initialize(response_body)
|
26
|
+
@response = response_body
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def self.get_body_from(response)
|
32
|
+
JSON.parse(response.body)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.courses_base_path
|
36
|
+
"https://api-na.hosted.exlibrisgroup.com/almaws/v1"
|
37
|
+
end
|
38
|
+
|
39
|
+
def courses_base_path
|
40
|
+
self.class.courses_base_path
|
41
|
+
end
|
42
|
+
|
43
|
+
def headers
|
44
|
+
self.class.headers
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "httparty"
|
4
|
+
require "active_support"
|
5
|
+
require "active_support/core_ext"
|
6
|
+
require "alma/config"
|
7
|
+
|
8
|
+
module Alma
|
9
|
+
# Alma::Electronic APIs wrapper.
|
10
|
+
class Electronic
|
11
|
+
class ElectronicError < ArgumentError
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get(params = {})
|
15
|
+
retries_count = 0
|
16
|
+
response = nil
|
17
|
+
while retries_count < http_retries do
|
18
|
+
begin
|
19
|
+
response = get_api(params)
|
20
|
+
break;
|
21
|
+
|
22
|
+
rescue Net::ReadTimeout
|
23
|
+
retries_count += 1
|
24
|
+
log.error("Retrying http after timeout with : #{params}")
|
25
|
+
no_more_retries_left = retries_count == http_retries
|
26
|
+
|
27
|
+
raise Net::ReadTimeout.new("Failed due to net timeout after #{http_retries}: #{params}") if no_more_retries_left
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
return response
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.get_totals
|
35
|
+
@totals ||= get(limit: "0").data["total_record_count"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.log
|
39
|
+
Alma.configuration.logger
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.get_ids
|
43
|
+
total = get_totals()
|
44
|
+
limit = 100
|
45
|
+
offset = 0
|
46
|
+
log.info("Retrieving #{total} collection ids.")
|
47
|
+
groups = Array.new(total / limit + 1, limit)
|
48
|
+
@ids ||= groups.map { |limit|
|
49
|
+
prev_offset = offset
|
50
|
+
offset += limit
|
51
|
+
{ offset: prev_offset, limit: limit }
|
52
|
+
}
|
53
|
+
.map { |params| Thread.new { self.get(params) } }
|
54
|
+
.map(&:value).map(&:data)
|
55
|
+
.map { |data| data["electronic_collection"].map { |coll| coll["id"] } }
|
56
|
+
.flatten.uniq
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.http_retries
|
60
|
+
Alma.configuration.http_retries
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
class ElectronicAPI
|
65
|
+
include ::HTTParty
|
66
|
+
include ::Enumerable
|
67
|
+
extend ::Forwardable
|
68
|
+
|
69
|
+
REQUIRED_PARAMS = []
|
70
|
+
RESOURCE = "/almaws/v1/electronic"
|
71
|
+
|
72
|
+
attr_reader :params, :data
|
73
|
+
def_delegators :@data, :each, :each_pair, :fetch, :values, :keys, :dig,
|
74
|
+
:slice, :except, :to_h, :to_hash, :[], :with_indifferent_access
|
75
|
+
|
76
|
+
def initialize(params = {})
|
77
|
+
@params = params
|
78
|
+
headers = self.class::headers
|
79
|
+
log.info(url: url, query: params)
|
80
|
+
response = self.class::get(url, headers: headers, query: params, timeout: timeout)
|
81
|
+
@data = JSON.parse(response.body) rescue {}
|
82
|
+
end
|
83
|
+
|
84
|
+
def url
|
85
|
+
"#{Alma.configuration.region}#{resource}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def timeout
|
89
|
+
Alma.configuration.timeout
|
90
|
+
end
|
91
|
+
|
92
|
+
def log
|
93
|
+
Alma::Electronic.log
|
94
|
+
end
|
95
|
+
|
96
|
+
def resource
|
97
|
+
@params.inject(self.class::RESOURCE) { |path, param|
|
98
|
+
key = param.first
|
99
|
+
value = param.last
|
100
|
+
|
101
|
+
if key && value
|
102
|
+
path.gsub(/:#{key}/, value.to_s)
|
103
|
+
else
|
104
|
+
path
|
105
|
+
end
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.can_process?(params = {})
|
110
|
+
type = self.to_s.split("::").last.parameterize
|
111
|
+
self::REQUIRED_PARAMS.all? { |param| params.include? param } &&
|
112
|
+
params[:type].blank? || params[:type] == type
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
def self.headers
|
117
|
+
{ "Authorization": "apikey #{apikey}",
|
118
|
+
"Accept": "application/json",
|
119
|
+
"Content-Type": "application/json" }
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.apikey
|
123
|
+
Alma.configuration.apikey
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class Portfolio < ElectronicAPI
|
128
|
+
REQUIRED_PARAMS = [ :collection_id, :service_id, :portfolio_id ]
|
129
|
+
RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id/e-services/:service_id/portfolios/:portfolio_id"
|
130
|
+
end
|
131
|
+
|
132
|
+
class Service < ElectronicAPI
|
133
|
+
REQUIRED_PARAMS = [ :collection_id, :service_id ]
|
134
|
+
RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id/e-services/:service_id"
|
135
|
+
end
|
136
|
+
|
137
|
+
class Services < ElectronicAPI
|
138
|
+
REQUIRED_PARAMS = [ :collection_id, :type ]
|
139
|
+
RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id/e-services"
|
140
|
+
end
|
141
|
+
|
142
|
+
class Collection < ElectronicAPI
|
143
|
+
REQUIRED_PARAMS = [ :collection_id ]
|
144
|
+
RESOURCE = "/almaws/v1/electronic/e-collections/:collection_id"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Catch all Electronic API.
|
148
|
+
# By default returns all collections
|
149
|
+
class Collections < ElectronicAPI
|
150
|
+
REQUIRED_PARAMS = []
|
151
|
+
RESOURCE = "/almaws/v1/electronic/e-collections"
|
152
|
+
|
153
|
+
def self.can_process?(params = {})
|
154
|
+
true
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Order matters because parameters can repeat.
|
159
|
+
REGISTERED_APIs = [Portfolio, Service, Services, Collection, Collections]
|
160
|
+
|
161
|
+
def self.get_api(params)
|
162
|
+
REGISTERED_APIs
|
163
|
+
.find { |m| m.can_process? params }
|
164
|
+
.new(params)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|