alma 0.2.4 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|