alma 0.2.8 → 0.3.1

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.
@@ -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
@@ -11,5 +11,16 @@ module Alma::Error
11
11
  def error
12
12
  @response.fetch('web_service_result', {})
13
13
  end
14
+ end
14
15
 
15
- end
16
+ module Alma
17
+ class StandardError < ::StandardError
18
+ def initialize(message, loggable = {})
19
+ if Alma.configuration.enable_loggable
20
+ message = { error: message }.merge(loggable).to_json
21
+ end
22
+
23
+ super message
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ module Alma
2
+ class Fine < AlmaRecord
3
+ extend Alma::ApiDefaults
4
+
5
+ def self.where_user(user_id, args={})
6
+
7
+ response = HTTParty.get("#{users_base_path}/#{user_id}/fees", query: args, headers: headers, timeout: timeout)
8
+ if response.code == 200
9
+ Alma::FineSet.new(response)
10
+ else
11
+ raise StandardError, get_body_from(response)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,43 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
- class FineSet
3
- extend Forwardable
4
- include Enumerable
5
- #include Alma::Error
4
+ class FineSet < ResultSet
5
+ class ResponseError < Alma::StandardError
6
+ end
6
7
 
7
- attr_reader :response
8
- def_delegators :response, :[], :fetch
8
+ attr_reader :results, :raw_response
9
+ def_delegators :results, :empty?
9
10
 
10
- def initialize(response_body_hash)
11
- @response = response_body_hash
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) }
12
17
  end
13
18
 
14
- def key
15
- 'fee'
19
+ def loggable
20
+ { uri: @raw_response&.request&.uri.to_s
21
+ }.select { |k, v| !(v.nil? || v.empty?) }
16
22
  end
17
23
 
18
- def each
19
- @response.fetch(key, []).map{|item| Alma::AlmaRecord.new(item)}
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
20
30
  end
21
- alias list each
22
31
 
23
- def size
24
- each.count
32
+ def each(&block)
33
+ @results.each(&block)
25
34
  end
26
35
 
27
- def sum
28
- fetch('total_sum', 0)
36
+ def success?
37
+ raw_response.response.code.to_s == "200"
29
38
  end
30
- alias :total_sum :sum
31
39
 
32
- def currency
33
- fetch('currency', nil)
40
+ def key
41
+ "fee"
34
42
  end
35
43
 
36
- def total_record_count
37
- fetch('total_record_count', 0)
44
+ def sum
45
+ fetch("total_sum", 0)
38
46
  end
39
- alias :total_records :total_record_count
40
47
 
48
+ alias :total_sum :sum
41
49
 
50
+ def currency
51
+ fetch("currency", nil)
52
+ end
42
53
  end
43
54
  end
@@ -0,0 +1,22 @@
1
+ module Alma
2
+ class ItemRequestOptions < RequestOptions
3
+
4
+ class ResponseError < Alma::StandardError
5
+ end
6
+
7
+ def self.get(mms_id, holding_id=nil, item_pid=nil, options={})
8
+ url = "#{bibs_base_path}/#{mms_id}/holdings/#{holding_id}/items/#{item_pid}/request-options"
9
+ options.select! {|k,_| REQUEST_OPTIONS_PERMITTED_ARGS.include? k }
10
+ response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
11
+ new(response)
12
+ end
13
+
14
+ def validate(response)
15
+ if response.code != 200
16
+ message = "Could not get item request options."
17
+ log = loggable.merge(response.parsed_response)
18
+ raise ResponseError.new(message, log)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,11 +1,16 @@
1
1
  module Alma
2
2
  class Loan < AlmaRecord
3
+ extend Alma::ApiDefaults
3
4
 
4
5
 
5
6
  def renewable?
6
7
  !!renewable
7
8
  end
8
9
 
10
+ def renewable
11
+ response.fetch("renewable", false)
12
+ end
13
+
9
14
  def overdue?
10
15
  loan_status == "Overdue"
11
16
  end
@@ -14,5 +19,18 @@ module Alma
14
19
  Alma::User.renew_loan({user_id: user_id, loan_id: loan_id})
15
20
  end
16
21
 
22
+ def self.where_user(user_id, args={})
23
+ # Always expand renewable unless you really don't want to
24
+ args[:expand] ||= "renewable"
25
+ # Default to upper limit
26
+ args[:limit] ||= 100
27
+ response = HTTParty.get(
28
+ "#{users_base_path}/#{user_id}/loans",
29
+ query: args,
30
+ headers: headers,
31
+ timeout: timeout
32
+ )
33
+ Alma::LoanSet.new(response, args)
34
+ end
17
35
  end
18
36
  end