alma 0.2.4 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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