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.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +54 -0
  3. data/.circleci/setup-rubygems.sh +3 -0
  4. data/.github/dependabot.yml +7 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +134 -0
  7. data/.ruby-version +1 -1
  8. data/Gemfile +5 -1
  9. data/Guardfile +75 -0
  10. data/README.md +146 -57
  11. data/Rakefile +3 -1
  12. data/alma.gemspec +21 -12
  13. data/lib/alma.rb +34 -54
  14. data/lib/alma/alma_record.rb +3 -3
  15. data/lib/alma/api_defaults.rb +39 -0
  16. data/lib/alma/availability_response.rb +69 -31
  17. data/lib/alma/bib.rb +54 -29
  18. data/lib/alma/bib_holding.rb +25 -0
  19. data/lib/alma/bib_item.rb +164 -0
  20. data/lib/alma/bib_item_set.rb +93 -0
  21. data/lib/alma/bib_set.rb +5 -10
  22. data/lib/alma/config.rb +10 -4
  23. data/lib/alma/course.rb +47 -0
  24. data/lib/alma/course_set.rb +17 -0
  25. data/lib/alma/electronic.rb +167 -0
  26. data/lib/alma/electronic/README.md +20 -0
  27. data/lib/alma/electronic/batch_utils.rb +224 -0
  28. data/lib/alma/electronic/business.rb +29 -0
  29. data/lib/alma/error.rb +16 -4
  30. data/lib/alma/fine.rb +16 -0
  31. data/lib/alma/fine_set.rb +41 -8
  32. data/lib/alma/item_request_options.rb +23 -0
  33. data/lib/alma/library.rb +29 -0
  34. data/lib/alma/library_set.rb +21 -0
  35. data/lib/alma/loan.rb +31 -2
  36. data/lib/alma/loan_set.rb +62 -4
  37. data/lib/alma/location.rb +29 -0
  38. data/lib/alma/location_set.rb +21 -0
  39. data/lib/alma/renewal_response.rb +25 -14
  40. data/lib/alma/request.rb +167 -0
  41. data/lib/alma/request_options.rb +66 -0
  42. data/lib/alma/request_set.rb +69 -5
  43. data/lib/alma/response.rb +45 -0
  44. data/lib/alma/result_set.rb +27 -35
  45. data/lib/alma/user.rb +142 -86
  46. data/lib/alma/user_request.rb +19 -0
  47. data/lib/alma/user_set.rb +5 -6
  48. data/lib/alma/version.rb +3 -1
  49. data/log/.gitignore +4 -0
  50. metadata +149 -10
  51. data/.travis.yml +0 -5
  52. data/lib/alma/api.rb +0 -33
@@ -0,0 +1,20 @@
1
+ ## Alma::Electronic
2
+
3
+ A wrapper for the Alma::Electronic API.
4
+
5
+ The main entry point is the get methods.
6
+
7
+ ### To get a list of all the collections.
8
+
9
+ ```
10
+ Alma::Electronic.get()
11
+ ```
12
+
13
+ Will also accept these params:
14
+
15
+ | Parameter | Type | Required | Description |
16
+ | --------- | ----------| --------- | ---------------------------------------------------------------------------------------------------------------|
17
+ | q | xs:string | Optional. | Search query. Optional. Searching for words in interface_name, keywords, name or po_line_id (see Brief Search) |
18
+ | limit | xs:int | Optional. | Default: 10Limits the number of results. Optional. Valid values are 0-100. Default value: 10. |
19
+ | offset | xs:int | Optional. | Default: 0Offset of the results returned. Optional. Default value: 0, which methodseans that the first results will be returned. |
20
+
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "alma/electronic"
5
+
6
+ # Contains batch processing utils for Alma Electronic APIs.
7
+ #
8
+ # This class and its methods are used to iterate over Alma Electronic IDs to
9
+ # process and fetch Alma electronic objects via the Alma Electronic APIs. The
10
+ # https calls are logged and can be used to rerun the batch process without
11
+ # making any further http calls or to just rerun parts of the full batch.
12
+ module Alma
13
+ class Electronic::BatchUtils
14
+ attr_reader :notes, :type
15
+
16
+ # @param [Hash] options The options to create a batch instance.
17
+ # @option [true, false] :chain true indicates a new instance of self returned.
18
+ # @option [Array<String>] :ids List of collection ids.
19
+ # @option ["collection", "service", "portfolio"] :type The Alma Electronic object type.
20
+ # @option [String] :tag A string used to tag the batch session.
21
+ # @option [Logger] :logger A logger used t
22
+ def initialize(options = {})
23
+ options ||= options {}
24
+ @chain = options.fetch(:chain, false)
25
+ @ids = options.fetch(:ids, [])
26
+ @type = options.fetch(:type, "collection")
27
+ @tag = options.fetch(:tag, Time.now.to_s)
28
+
29
+ @@logger = options[:logger] || Logger.new("log/electronic_batch_process.log")
30
+ end
31
+
32
+ def get_collection_notes(ids = nil, options = {})
33
+ ids ||= @ids
34
+ get_notes(options.merge(ids: make_collection_ids(ids), type: "collection"))
35
+ end
36
+
37
+ def get_service_notes(ids = nil, options = {})
38
+ ids ||= @ids
39
+ get_notes(options.merge(ids: get_service_ids(ids, options), type: "service"))
40
+ end
41
+
42
+ def get_portfolio_notes(ids = nil, options = {})
43
+ ids ||= @ids
44
+ get_notes(options.merge(ids: get_portfolio_ids(ids, options, type: "portfolio")))
45
+ end
46
+
47
+ def get_notes(options = {})
48
+ options ||= {}
49
+ chain = options.fetch(:chain, @chain)
50
+ ids = options[:ids] || (chain ? build_ids(options) : @ids)
51
+
52
+ type = options.fetch(:type, @type)
53
+ tag = options.fetch(:tag, @tag)
54
+ @notes = ids.inject({}) do |notes, params|
55
+ id = get_id(type, params)
56
+ start = Time.now
57
+
58
+ begin
59
+ item = Alma::Electronic.get(params)
60
+ rescue StandardError => e
61
+ item = { "error" => e.message }
62
+ end
63
+
64
+ data = ["error", "authentication_note", "public_note"].reduce({}) do |acc, field|
65
+ acc[field] = item[field] if item[field].present?
66
+ acc
67
+ end
68
+
69
+ unavailable = item.dig("service_temporarily_unavailable", "value")
70
+ if unavailable == "1" || unavailable == "true"
71
+ data.merge!(item.slice("service_temporarily_unavailable", "service_unavailable_date", "service_unavailable_reason"))
72
+ end
73
+
74
+ if data.present?
75
+ log(params.merge(data).merge(type: type, start: start, tag: tag))
76
+
77
+ notes[id] = data unless data["error"].present?
78
+ end
79
+
80
+ notes
81
+ end
82
+
83
+ self.class.new(options.merge(
84
+ chain: chain,
85
+ ids: ids,
86
+ type: type,
87
+ tag: tag,
88
+ notes: notes,
89
+ logger: @@logger,
90
+ ))
91
+ end
92
+
93
+ def get_service_ids(ids = @ids, options = {})
94
+ tag = options.fetch(:tag, @tag)
95
+ start = Time.now
96
+
97
+ make_collection_ids(ids)
98
+ .map { |id| id.merge(type: "services") }
99
+ .inject([]) do |service_ids, params|
100
+ params.merge!(tag: tag)
101
+
102
+ begin
103
+ item = Alma::Electronic.get(params)
104
+
105
+ if item["errorList"]
106
+ log params.merge(item["errorList"])
107
+ .merge(start: start)
108
+ else
109
+ item["electronic_service"].each { |service|
110
+ service_id = { service_id: service["id"].to_s }
111
+ service_ids << params.slice(:collection_id)
112
+ .merge(service_id)
113
+
114
+ log params.merge(service_id)
115
+ .merge(start: start)
116
+ }
117
+ end
118
+
119
+ rescue StandardError => e
120
+ log params.merge("error" => e.message)
121
+ .merge(start: start)
122
+ end
123
+
124
+ service_ids
125
+ end
126
+ end
127
+
128
+ # Builds the notes object using the logs.
129
+ def build_notes(options = {})
130
+ options ||= {}
131
+ type ||= options.fetch(:type, "collection")
132
+
133
+ get_logged_items(options)
134
+ .select { |item| item.slice("authentication_note", "public_note").values.any?(&:present?) }
135
+ .inject({}) do |nodes, item|
136
+
137
+ id = item["#{type}_id"]
138
+ nodes.merge(id => item.slice("authentication_note", "public_note"))
139
+ end
140
+ end
141
+
142
+ # Builds list of ids from logs based on failed attempts.
143
+ # Useful for rebuilding part of collection.
144
+ def build_failed_ids(options = {})
145
+ successful_ids = build_successful_ids(options)
146
+ get_logged_items(options)
147
+ .select { |item| item.slice("authentication_note", "public_note").values.all?(&:nil?) }
148
+ .map { |item| item["collection_id"] }
149
+ .select { |id| !successful_ids.include? id }
150
+ .uniq
151
+ end
152
+
153
+ # Builds list of ids from logs based on successful attempts.
154
+ # Useful for verifying that failed ids have always failed.
155
+ def build_successful_ids(options = {})
156
+ get_logged_items(options)
157
+ .select { |item| item.slice("authentication_note", "public_note").values.present? }
158
+ .map { |item| item["collection_id"] }
159
+ .uniq
160
+ end
161
+
162
+ # Builds a list of all ids for a specific session.
163
+ # Useful for analytics purpose or rebuilds.
164
+ def build_ids(options = {})
165
+ build_failed_ids(options) + build_successful_ids(options)
166
+ end
167
+
168
+ def print_notes(options = {})
169
+ options ||= {}
170
+ chain = options.fetch(:chain, @chain)
171
+ notes = options[:notes] || chain ? build_notes(options) : @notes
172
+ type = options.fetch(:type, @type)
173
+ tag = options.fetch(:tag, @tag)
174
+
175
+ filename = options.fetch(:filename, "spec/fixtures/#{type}_notes.json")
176
+
177
+ File.open(filename, "w") do |file|
178
+ file.write(JSON.pretty_generate(notes))
179
+ end
180
+
181
+ self.class.new(options.merge(
182
+ chain: chain,
183
+ notes: notes,
184
+ type: type,
185
+ tag: tag,
186
+ logger: @@logger,
187
+ ))
188
+ end
189
+
190
+ private
191
+ def log(params)
192
+ if defined?(LogUtils)
193
+ LogUtils.json_request_logger(@@logger, params)
194
+ else
195
+ @@logger.info(params)
196
+ end
197
+ end
198
+
199
+ def get_id(type, params = {})
200
+ id = "#{type}_id".to_sym
201
+ params[id]
202
+ end
203
+
204
+ def make_collection_ids(ids = @ids)
205
+ ids.map { |id|
206
+ id.class == Hash ? id : { collection_id: id.to_s }
207
+ }
208
+ end
209
+
210
+ # Returns JSON parsed list of logged items
211
+ def get_logged_items(options = {})
212
+ options ||= {}
213
+ type ||= options.fetch(:type, "collection")
214
+ tag ||= options.fetch(:tag, @tag)
215
+ filename = (@@logger.instance_variable_get :@logdev).filename
216
+ File.readlines(filename)
217
+ .map { |log| log.match(/{.*}/).to_s }
218
+ .select(&:present?)
219
+ .map { |json| JSON.parse(json) }
220
+ .select { |item| item["tag"] == tag }
221
+ .select { |item| item["type"] == type }
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "alma/electronic"
5
+
6
+ module Alma
7
+ # Holds some custom business logic for our Alma Electronic API.
8
+ # This class is not intended for public use.
9
+ class Electronic::Business
10
+ # The Service ID is usually the Collection ID grouped by
11
+ # 2 digits with the first number incremented by 1 and the
12
+ # fifth number decremented by 1.
13
+ #
14
+ # @note However, this pattern does not hold for all cases.
15
+ #
16
+ # @param collection_id [String] The electronic collection id.
17
+ def service_id(collection_id)
18
+ collection_id.scan(/.{1,2}/).each_with_index.map { |char, index|
19
+ if index == 0
20
+ "%02d" % (char.to_i + 1)
21
+ elsif index == 4
22
+ "%02d" % (char.to_i - 1)
23
+ else
24
+ char
25
+ end
26
+ }.join
27
+ end
28
+ end
29
+ end
data/lib/alma/error.rb CHANGED
@@ -1,15 +1,27 @@
1
- module Alma::Error
1
+ # frozen_string_literal: true
2
2
 
3
+ module Alma::Error
3
4
  def has_error?
4
5
  !error.empty?
5
6
  end
6
7
 
7
8
  def error_message
8
- (has_error?) ? error['errorList']['error']['errorMessage'] : ''
9
+ (has_error?) ? error["errorList"]["error"]["errorMessage"] : ""
9
10
  end
10
11
 
11
12
  def error
12
- @response.fetch('web_service_result', {})
13
+ @response.fetch("web_service_result", {})
13
14
  end
15
+ end
14
16
 
15
- end
17
+ module Alma
18
+ class StandardError < ::StandardError
19
+ def initialize(message, loggable = {})
20
+ if Alma.configuration.enable_loggable
21
+ message = { error: message }.merge(loggable).to_json
22
+ end
23
+
24
+ super message
25
+ end
26
+ end
27
+ end
data/lib/alma/fine.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class Fine < AlmaRecord
5
+ extend Alma::ApiDefaults
6
+
7
+ def self.where_user(user_id, args = {})
8
+ response = HTTParty.get("#{users_base_path}/#{user_id}/fees", query: args, headers: headers, timeout: timeout)
9
+ if response.code == 200
10
+ Alma::FineSet.new(response)
11
+ else
12
+ raise StandardError, get_body_from(response)
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/alma/fine_set.rb CHANGED
@@ -1,21 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
4
  class FineSet < ResultSet
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
8
+ attr_reader :results, :raw_response
9
+ def_delegators :results, :empty?
10
+
11
+ def initialize(raw_response)
12
+ @raw_response = raw_response
13
+ @response = raw_response.parsed_response
14
+ validate(raw_response)
15
+ @results = @response.fetch(key, [])
16
+ .map { |item| single_record_class.new(item) }
17
+ end
18
+
19
+ def loggable
20
+ { uri: @raw_response&.request&.uri.to_s
21
+ }.select { |k, v| !(v.nil? || v.empty?) }
22
+ end
3
23
 
4
- def top_level_key
5
- 'fees'
24
+ def validate(response)
25
+ if response.code != 200
26
+ message = "Could not find fines."
27
+ log = loggable.merge(response.parsed_response)
28
+ raise ResponseError.new(message, log)
29
+ end
6
30
  end
7
31
 
8
- def response_records_key
9
- 'fee'
32
+ def each(&block)
33
+ @results.each(&block)
34
+ end
35
+
36
+ def success?
37
+ raw_response.response.code.to_s == "200"
38
+ end
39
+
40
+ def key
41
+ "fee"
10
42
  end
11
43
 
12
44
  def sum
13
- @response[top_level_key].fetch('total_sum', 0)
45
+ fetch("total_sum", 0)
14
46
  end
15
47
 
48
+ alias :total_sum :sum
49
+
16
50
  def currency
17
- @response[top_level_key].fetch('currency', nil)
51
+ fetch("currency", nil)
18
52
  end
19
-
20
53
  end
21
- end
54
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class ItemRequestOptions < RequestOptions
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
8
+ def self.get(mms_id, holding_id = nil, item_pid = nil, options = {})
9
+ url = "#{bibs_base_path}/#{mms_id}/holdings/#{holding_id}/items/#{item_pid}/request-options"
10
+ options.select! { |k, _| REQUEST_OPTIONS_PERMITTED_ARGS.include? k }
11
+ response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
12
+ new(response)
13
+ end
14
+
15
+ def validate(response)
16
+ if response.code != 200
17
+ message = "Could not get item request options."
18
+ log = loggable.merge(response.parsed_response)
19
+ raise ResponseError.new(message, log)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class Library < AlmaRecord
5
+ extend Alma::ApiDefaults
6
+
7
+ def self.all(args: {})
8
+ response = HTTParty.get("#{configuration_base_path}/libraries", query: args, headers: headers, timeout: timeout)
9
+ if response.code == 200
10
+ LibrarySet.new(response)
11
+ else
12
+ raise StandardError, get_body_from(response)
13
+ end
14
+ end
15
+
16
+ def self.find(library_code:, args: {})
17
+ response = HTTParty.get("#{configuration_base_path}/libraries/#{library_code}", query: args, headers: headers, timeout: timeout)
18
+ if response.code == 200
19
+ AlmaRecord.new(response)
20
+ else
21
+ raise StandardError, get_body_from(response)
22
+ end
23
+ end
24
+
25
+ def self.get_body_from(response)
26
+ JSON.parse(response.body)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class LibrarySet < ResultSet
5
+ def_delegators :results, :[], :empty?
6
+
7
+ def each(&block)
8
+ results.each(&block)
9
+ end
10
+
11
+ def results
12
+ @results ||= @response.fetch(key, [])
13
+ .map { |item| single_record_class.new(item) }
14
+ end
15
+
16
+ protected
17
+ def key
18
+ "library"
19
+ end
20
+ end
21
+ end