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.
@@ -7,7 +7,7 @@ module Alma
7
7
  attr_accessor :availability
8
8
 
9
9
  def initialize(response)
10
- @availability = parse_bibs_data(response.list)
10
+ @availability = parse_bibs_data(response.each)
11
11
  end
12
12
 
13
13
  # Data structure for holdings information of bib records.
@@ -1,5 +1,6 @@
1
1
  module Alma
2
2
  class Bib
3
+ extend Alma::ApiDefaults
3
4
  extend Forwardable
4
5
 
5
6
  def self.find(ids, args)
@@ -9,8 +10,9 @@ module Alma
9
10
  def self.get_bibs(ids, args={})
10
11
  response = HTTParty.get(
11
12
  self.bibs_base_path,
12
- query: {mms_id: ids_from_array(ids)},
13
- headers: headers
13
+ query: {mms_id: ids_from_array(ids) }.merge(args),
14
+ headers: headers,
15
+ timeout: timeout
14
16
  )
15
17
 
16
18
  if response.code == 200
@@ -48,33 +50,13 @@ module Alma
48
50
 
49
51
  private
50
52
 
51
- def self.bibs_base_path
52
- "#{self.region}/almaws/v1/bibs"
53
- end
54
-
55
53
  def bibs_base_path
56
54
  self.class.bibs_base_path
57
55
  end
58
56
 
59
- def self.headers
60
- { "Authorization": "apikey #{self.apikey}",
61
- "Accept": "application/json",
62
- "Content-Type": "application/json" }
63
- end
64
-
65
-
66
57
  def headers
67
58
  self.class.headers
68
59
  end
69
-
70
-
71
- def self.apikey
72
- Alma.configuration.apikey
73
- end
74
-
75
- def self.region
76
- Alma.configuration.region
77
- end
78
60
 
79
61
  def self.get_body_from(response)
80
62
  JSON.parse(response.body)
@@ -1,6 +1,7 @@
1
1
  require 'alma/bib_item_set'
2
2
  module Alma
3
3
  class BibItem
4
+ extend Alma::ApiDefaults
4
5
  extend Forwardable
5
6
 
6
7
  attr_reader :item
@@ -15,8 +16,8 @@ module Alma
15
16
  holding_id = options.delete(:holding_id) || "ALL"
16
17
  options.select! {|k,_| PERMITTED_ARGS.include? k }
17
18
  url = "#{bibs_base_path}/#{mms_id}/holdings/#{holding_id}/items"
18
- response = HTTParty.get(url, headers: headers, query: options)
19
- BibItemSet.new(response)
19
+ response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
20
+ BibItemSet.new(response, options.merge({mms_id: mms_id, holding_id: holding_id}))
20
21
  end
21
22
 
22
23
  def initialize(item)
@@ -51,7 +52,6 @@ module Alma
51
52
  in_temp_location? ? temp_location_name : holding_location_name
52
53
  end
53
54
 
54
-
55
55
  def holding_library
56
56
  item_data.dig("library", "value")
57
57
  end
@@ -93,13 +93,11 @@ module Alma
93
93
  end
94
94
 
95
95
  def call_number
96
- if has_temp_call_number?
97
- holding_data.fetch("temp_call_number")
98
- elsif has_alt_call_number?
99
- alt_call_number
100
- else
101
- holding_data.fetch("call_number","")
102
- end
96
+ if has_temp_call_number?
97
+ holding_data.fetch("temp_call_number")
98
+ else
99
+ holding_data.fetch("call_number","")
100
+ end
103
101
  end
104
102
 
105
103
  def has_alt_call_number?
@@ -149,26 +147,5 @@ module Alma
149
147
  def public_note
150
148
  item_data.fetch("public_note", "")
151
149
  end
152
-
153
- private
154
-
155
- def self.region
156
- Alma.configuration.region
157
- end
158
-
159
- def self.bibs_base_path
160
- "#{region}/almaws/v1/bibs"
161
- end
162
-
163
- def self.headers
164
- { "Authorization": "apikey #{apikey}",
165
- "Accept": "application/json",
166
- "Content-Type": "application/json" }
167
- end
168
-
169
- def self.apikey
170
- Alma.configuration.apikey
171
- end
172
150
  end
173
-
174
151
  end
@@ -1,23 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
- class BibItemSet
3
- extend Forwardable
4
4
 
5
- # Let BibItemSet respond to the Enumerable duck type
6
- # by delegating responsibility for #each to items
7
- include Enumerable
8
- attr_accessor :items
9
- def_delegators :items, :each
5
+ class BibItemSet < ResultSet
10
6
 
11
- def_delegators :items,:[], :[]=, :empty?, :size
7
+ class ResponseError < ::Alma::StandardError
8
+ end
12
9
 
10
+ attr_accessor :items
13
11
  attr_reader :raw_response, :total_record_count
12
+
13
+ def_delegators :items, :[], :[]=, :empty?, :size, :each
14
14
  def_delegators :raw_response, :response, :request
15
15
 
16
- def initialize(response)
16
+ def initialize(response, options={})
17
17
  @raw_response = response
18
- parsed = JSON.parse(response.body)
18
+ parsed = response.parsed_response
19
19
  @total_record_count = parsed["total_record_count"]
20
- @items = parsed.fetch("item",[]).map {|item| BibItem.new(item)}
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,
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
21
39
  end
22
40
 
23
41
  def grouped_by_library
@@ -29,5 +47,36 @@ module Alma
29
47
  clone.items = reject(&:missing_or_lost?)
30
48
  clone
31
49
  end
50
+
51
+ def all
52
+ Enumerator.new do |yielder|
53
+ offset = 0
54
+ loop do
55
+ r = (offset == 0) ? self : single_record_class.find(@mms_id, options=@options.merge({limit: 100, offset: offset}))
56
+ unless r.empty?
57
+ r.map { |item| yielder << item }
58
+ offset += 100
59
+ else
60
+ raise StopIteration
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def each(&block)
67
+ @items.each(&block)
68
+ end
69
+
70
+ def success?
71
+ raw_response.response.code.to_s == "200"
72
+ end
73
+
74
+ def key
75
+ "item"
76
+ end
77
+
78
+ def single_record_class
79
+ Alma::BibItem
80
+ end
32
81
  end
33
82
  end
@@ -1,31 +1,17 @@
1
- module Alma
2
- class BibSet
3
-
4
- extend Forwardable
5
- include Enumerable
6
- #include Alma::Error
7
-
8
- attr_reader :response
9
- def_delegators :list, :each, :size
10
- def_delegators :response, :[], :fetch
11
-
12
- def initialize(response_body_hash)
13
- @response = response_body_hash
14
- end
1
+ # frozen_string_literal: true
15
2
 
16
- def list
17
- @list ||= response.fetch(key, []).map do |record|
18
- Alma::Bib.new(record)
19
- end
3
+ module Alma
4
+ class BibSet < ResultSet
5
+ def key
6
+ "bib"
20
7
  end
21
8
 
22
- def key
23
- 'bib'
9
+ def single_record_class
10
+ Alma::Bib
24
11
  end
25
12
 
26
13
  def total_record_count
27
14
  size
28
15
  end
29
-
30
16
  end
31
17
  end
@@ -9,12 +9,16 @@ module Alma
9
9
  end
10
10
 
11
11
  class Configuration
12
- attr_accessor :apikey, :region
12
+ attr_accessor :apikey, :region, :enable_loggable
13
+ attr_accessor :timeout, :http_retries, :logger
13
14
 
14
15
  def initialize
15
16
  @apikey = "TEST_API_KEY"
16
17
  @region = 'https://api-na.hosted.exlibrisgroup.com'
18
+ @enable_loggable = false
19
+ @timeout = 5
20
+ @http_retries = 3
21
+ @logger = Logger.new(STDOUT)
17
22
  end
18
-
19
23
  end
20
- end
24
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+ require "httparty"
3
+ require "active_support"
4
+ require "active_support/core_ext"
5
+ require "alma/config"
6
+
7
+ module Alma
8
+ # Alma::Electronic APIs wrapper.
9
+ class Electronic
10
+
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