alma 0.2.8 → 0.3.1

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