berkeley_library-location 2.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af0453d4f60ad7276b64c48937b36cc76d250eb2a41ff23914f30d7c35edbf85
4
- data.tar.gz: 2cb9fb72635d239f6b6726f4278b555913b9f209dddb43dbeb03072b495de3d1
3
+ metadata.gz: dcdd92dafd937a6dabe9cf2104e84311ae590d2db023b80c9c6fb06d0a1f3b40
4
+ data.tar.gz: fc15382505d0611b660cdca1e5575b780ca00a0a13cb65cf5dec1d3c2a07a501
5
5
  SHA512:
6
- metadata.gz: 5a9dc82778e93207bc4f23e328b1699f4cb58abed4b7deda70cb8ec713b4c47a95d36be19fdab962b591df78c15182af841d111ef941a6d8ab2436b6587109fe
7
- data.tar.gz: f6f78720f85d04530e656940d5dcaa1056cfd086de7ca280253d4a3d6aeca22a98236ad6ba2dc6666c4b3b6e765bd1e0cf3842dc69104e7b30eaa50685a69e9e
6
+ metadata.gz: e7f912cc954ceaf098cd7711cffc6016c0cfa945d7b63eb46e1a6c546d321962df4e76abb8e21ee8d57153590cacc7996f4d6ad4a90ae2f40fe91b46467b2e98
7
+ data.tar.gz: b8096c7cdc373ac6d79f0b966c2cfce33c959cc562c8b0d002ee1265ba6c0fb38eda81fb46ef8fff9accf6e033cef5b1b7e952bc96926b06eb3198235452d540
data/CHANGES.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 4.0.0 (2025-01-08)
2
+
3
+ - Update from Worldcat API v1 to API v2
4
+ - Changed some of the tests from mocks to use VCR
5
+
6
+ # 3.0.0 (2023-10-10)
7
+
8
+ - Support duplicate OCLC numbers
9
+
10
+ # 2.0.0 (2023-06-06)
11
+
12
+ - Rename from "holdings" to "location"
13
+
1
14
  # 2.0.0 (2023-06-06)
2
15
 
3
16
  - Rename from "holdings" to "location"
data/README.md CHANGED
@@ -4,3 +4,14 @@
4
4
  [![Gem Version](https://img.shields.io/gem/v/berkeley_library-location.svg)](https://github.com/BerkeleyLibrary/location/releases)
5
5
 
6
6
  Miscellaneous location-related utilities for the UC Berkeley Library.
7
+
8
+ Updated to Worldcat API Version 2:
9
+ https://developer.api.oclc.org/wcv2#/Member%20General%20Holdings/find-bib-holdings
10
+
11
+
12
+ NOTE: For new and updated tests we've implemented the use of VCR to handle API mocks. Therefore in order to run tests, you'll need to have your API Key and Secret set as environment variables:
13
+ LIT_WORLDCAT_API_KEY
14
+ LIT_WORLDCAT_API_SECRET
15
+
16
+ Once the cassettes are created you can force a re-recording of cassettes by adding the following ENV:
17
+ RE_RECORD_VCR="true"
@@ -2,12 +2,12 @@ module BerkeleyLibrary
2
2
  module Location
3
3
  module ModuleInfo
4
4
  NAME = 'berkeley_library-location'.freeze
5
- AUTHOR = 'David Moles'.freeze
6
- AUTHOR_EMAIL = 'dmoles@berkeley.edu'.freeze
5
+ AUTHOR = 'UC Berkeley Library IT'.freeze
6
+ AUTHOR_EMAIL = 'libdevelopers@lists.berkeley.edu'.freeze
7
7
  SUMMARY = 'Locaton-related utilities for the UC Berkeley Library'.freeze
8
8
  DESCRIPTION = 'A collection of location-related utilities for the UC Berkeley Library'.freeze
9
9
  LICENSE = 'MIT'.freeze
10
- VERSION = '2.0.0'.freeze
10
+ VERSION = '4.0.0'.freeze
11
11
  HOMEPAGE = 'https://github.com/BerkeleyLibrary/location'.freeze
12
12
  end
13
13
  end
@@ -6,22 +6,30 @@ module BerkeleyLibrary
6
6
  module Config
7
7
  include BerkeleyLibrary::Util::URIs
8
8
 
9
- # The environment variable from which to read the WorldCat API key.
9
+ # The environment variable from which to read the WorldCat API key and secret.
10
10
  ENV_WORLDCAT_API_KEY = 'LIT_WORLDCAT_API_KEY'.freeze
11
+ ENV_WORLDCAT_API_SECRET = 'LIT_WORLDCAT_API_SECRET'.freeze
11
12
 
12
13
  # The environment variable from which to read the WorldCat base URL.
13
14
  ENV_WORLDCAT_BASE_URL = 'LIT_WORLDCAT_BASE_URL'.freeze
14
15
 
16
+ # The environment variable from which to read the OCLC Token URL.
17
+ ENV_OCLC_TOKEN_URL = 'LIT_OCLC_TOKEN_URL'.freeze
18
+
15
19
  # The default WorldCat base URL, if ENV_WORLDCAT_BASE_URL is not set.
16
- DEFAULT_WORLDCAT_BASE_URL = 'https://www.worldcat.org/webservices/'.freeze
20
+ # DEFAULT_WORLDCAT_BASE_URL = 'https://www.worldcat.org/webservices/'.freeze
21
+ DEFAULT_WORLDCAT_BASE_URL = 'https://americas.discovery.api.oclc.org/worldcat/search/v2/'.freeze
22
+
23
+ # The default OCLC Token URL, if ENV_OCLC_TOKEN_URL is not set.
24
+ DEFAULT_OCLC_TOKEN_URL = 'https://oauth.oclc.org/token'.freeze
17
25
 
18
26
  class << self
19
27
  include Config
20
28
  end
21
29
 
22
- # Sets the WorldCat API key.
30
+ # Sets the WorldCat API key and secret
23
31
  # @param value [String] the API key.
24
- attr_writer :api_key
32
+ attr_writer :api_key, :api_secret
25
33
 
26
34
  # Gets the WorldCat API key.
27
35
  # @return [String, nil] the WorldCat API key, or `nil` if not set.
@@ -29,14 +37,27 @@ module BerkeleyLibrary
29
37
  @api_key ||= default_worldcat_api_key
30
38
  end
31
39
 
40
+ # Sets the WorldCat API secret.
41
+ def api_secret
42
+ @api_secret ||= default_worldcat_api_secret
43
+ end
44
+
32
45
  def base_uri
33
46
  @base_uri ||= default_worldcat_base_uri
34
47
  end
35
48
 
49
+ def token_uri
50
+ @token_uri ||= default_oclc_token_uri
51
+ end
52
+
36
53
  def base_uri=(value)
37
54
  @base_uri = uri_or_nil(value)
38
55
  end
39
56
 
57
+ def token_uri=(value)
58
+ @token_uri = uri_or_nil(value)
59
+ end
60
+
40
61
  private
41
62
 
42
63
  def reset!
@@ -47,14 +68,26 @@ module BerkeleyLibrary
47
68
  ENV[ENV_WORLDCAT_API_KEY] || rails_worldcat_api_key
48
69
  end
49
70
 
71
+ def default_worldcat_api_secret
72
+ ENV[ENV_WORLDCAT_API_SECRET] || rails_worldcat_api_secret
73
+ end
74
+
50
75
  def default_worldcat_base_uri
51
76
  uri_or_nil(default_worldcat_base_url)
52
77
  end
53
78
 
79
+ def default_oclc_token_uri
80
+ uri_or_nil(default_oclc_token_url)
81
+ end
82
+
54
83
  def default_worldcat_base_url
55
84
  ENV[ENV_WORLDCAT_BASE_URL] || rails_worldcat_base_url || DEFAULT_WORLDCAT_BASE_URL
56
85
  end
57
86
 
87
+ def default_oclc_token_url
88
+ ENV[ENV_OCLC_TOKEN_URL] || rails_oclc_token_url || DEFAULT_OCLC_TOKEN_URL
89
+ end
90
+
58
91
  def rails_worldcat_base_url
59
92
  return unless (rails_config = self.rails_config)
60
93
  return unless rails_config.respond_to?(:worldcat_base_url)
@@ -62,6 +95,13 @@ module BerkeleyLibrary
62
95
  rails_config.worldcat_base_url
63
96
  end
64
97
 
98
+ def rails_oclc_token_url
99
+ return unless (rails_config = self.rails_config)
100
+ return unless rails_config.respond_to?(:oclc_token_url)
101
+
102
+ rails_config.oclc_token_url
103
+ end
104
+
65
105
  def rails_worldcat_api_key
66
106
  return unless (rails_config = self.rails_config)
67
107
  return unless rails_config.respond_to?(:worldcat_api_key)
@@ -69,6 +109,13 @@ module BerkeleyLibrary
69
109
  rails_config.worldcat_api_key
70
110
  end
71
111
 
112
+ def rails_worldcat_api_secret
113
+ return unless (rails_config = self.rails_config)
114
+ return unless rails_config.respond_to?(:worldcat_api_secret)
115
+
116
+ rails_config.worldcat_api_secret
117
+ end
118
+
72
119
  def rails_config
73
120
  return unless defined?(Rails)
74
121
  return unless (app = Rails.application)
@@ -1,55 +1,65 @@
1
1
  require 'nokogiri'
2
+ require 'json'
3
+ require 'jsonpath'
2
4
  require 'berkeley_library/util'
3
5
  require 'berkeley_library/location/oclc_number'
4
6
  require 'berkeley_library/location/world_cat/symbols'
7
+ require 'berkeley_library/location/world_cat/oclc_auth'
5
8
 
6
9
  module BerkeleyLibrary
7
10
  module Location
8
11
  module WorldCat
9
- # @see https://developer.api.oclc.org/wcv1#/Holdings
12
+ # @see https://developer.api.oclc.org/wcv2#/Member%20General%20Holdings/find-bib-holdings
10
13
  class LibrariesRequest
11
14
  include BerkeleyLibrary::Util
12
15
 
13
- XPATH_INST_ID_VALS = '/holdings/holding/institutionIdentifier/value'.freeze
16
+ JPATH_INST_ID_VALS = '$.briefRecords[*].institutionHolding.briefHoldings[*].oclcSymbol'.freeze
14
17
 
15
18
  attr_reader :oclc_number, :symbols
16
19
 
17
20
  def initialize(oclc_number, symbols: Symbols::ALL)
21
+ @oclc_token = OCLCAuth.instance
18
22
  @oclc_number = OCLCNumber.ensure_oclc_number!(oclc_number)
19
23
  @symbols = Symbols.ensure_valid!(symbols)
20
24
  end
21
25
 
22
26
  def uri
23
- @uri ||= URIs.append(libraries_base_uri, URIs.path_escape(oclc_number))
27
+ @uri ||= URIs.append(libraries_base_uri)
24
28
  end
25
29
 
26
- # TODO: Check that this works w/more than 10 results
27
- # See https://developer.api.oclc.org/wcv1#/Holdings
28
30
  def params
29
31
  @params ||= {
30
- 'oclcsymbol' => symbols.join(','),
31
- 'servicelevel' => 'full',
32
- 'frbrGrouping' => 'off',
33
- 'wskey' => Config.api_key
32
+ 'oclcNumber' => oclc_number,
33
+ 'heldBySymbol' => symbols.join(',')
34
+ }
35
+ end
36
+
37
+ def headers
38
+ @headers ||= {
39
+ 'Authorization' => "Bearer #{oclc_token.access_token}"
34
40
  }
35
41
  end
36
42
 
37
43
  def execute
38
- response_body = URIs.get(uri, params:, log: false)
39
- inst_symbols = inst_symbols_from(response_body)
40
- inst_symbols.select { |sym| symbols.include?(sym) } # just in case
44
+ response_body = URIs.get(uri, params:, headers:, log: false)
45
+ inst_symbols_from(response_body)
41
46
  end
42
47
 
43
48
  private
44
49
 
50
+ # OCLC changed to a token-based authentication system
51
+ # separating auth from request
52
+ def oclc_token
53
+ @oclc_token ||= OCLCAuth.new
54
+ end
55
+
45
56
  def libraries_base_uri
46
- URIs.append(Config.base_uri, 'catalog', 'content', 'libraries')
57
+ URIs.append(Config.base_uri, 'bibs-holdings')
47
58
  end
48
59
 
49
- def inst_symbols_from(xml)
50
- xml_doc = Nokogiri::XML(xml)
51
- id_vals = xml_doc.xpath(XPATH_INST_ID_VALS)
52
- id_vals.filter_map { |value| value.text.strip }
60
+ def inst_symbols_from(json)
61
+ path = JsonPath.new(JPATH_INST_ID_VALS)
62
+ path.on(json)
53
63
  end
54
64
  end
55
65
  end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ require 'singleton'
3
+
4
+ module BerkeleyLibrary
5
+ module Location
6
+ module WorldCat
7
+ class OCLCAuth
8
+ include Singleton
9
+
10
+ attr_accessor :token
11
+
12
+ def initialize
13
+ # rubocop:disable Lint/DisjunctiveAssignmentInConstructor
14
+ @token ||= fetch_token
15
+ # rubocop:enable Lint/DisjunctiveAssignmentInConstructor:
16
+ end
17
+
18
+ def fetch_token
19
+ url = oclc_token_url
20
+
21
+ http = Net::HTTP.new(url.host, url.port)
22
+ http.use_ssl = url.scheme == 'https'
23
+
24
+ request = Net::HTTP::Post.new(url.request_uri)
25
+ request.basic_auth(Config.api_key, Config.api_secret)
26
+ request['Accept'] = 'application/json'
27
+ response = http.request(request)
28
+
29
+ JSON.parse(response.body, symbolize_names: true)
30
+ end
31
+
32
+ # def token
33
+ # @token = get_token if token_expired?
34
+ # @token
35
+ # end
36
+
37
+ def oclc_token_url
38
+ URI.parse("#{Config.token_uri}?#{URI.encode_www_form(token_params)}")
39
+ end
40
+
41
+ # Before every request check if the token is expired (OCLC tokens expire after 20 minutes)
42
+ def access_token
43
+ @token = token if token_expired?
44
+ @token[:access_token]
45
+ end
46
+
47
+ private
48
+
49
+ def token_params
50
+ {
51
+ grant_type: 'client_credentials',
52
+ scope: 'wcapi:view_institution_holdings'
53
+ }
54
+ end
55
+
56
+ def token_expired?
57
+ return true if @token.nil? || @token[:expires_at].nil?
58
+
59
+ Time.parse(@token[:expires_at]) <= Time.now
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
@@ -30,9 +30,11 @@ module BerkeleyLibrary
30
30
  end
31
31
 
32
32
  def <<(result)
33
- r_index = row_index_for(result.oclc_number)
34
- write_wc_cols(r_index, result) if rlf || uc
35
- write_ht_cols(r_index, result) if hathi_trust
33
+ r_indices = row_indices_for(result.oclc_number)
34
+ r_indices.each do |idx|
35
+ write_wc_cols(idx, result) if rlf || uc
36
+ write_ht_cols(idx, result) if hathi_trust
37
+ end
36
38
  end
37
39
 
38
40
  private
@@ -57,8 +59,8 @@ module BerkeleyLibrary
57
59
  ht_col_index if hathi_trust
58
60
  end
59
61
 
60
- def row_index_for(oclc_number)
61
- row_index = row_index_by_oclc_number[oclc_number]
62
+ def row_indices_for(oclc_number)
63
+ row_index = row_indices_by_oclc_number[oclc_number]
62
64
  return row_index if row_index
63
65
 
64
66
  raise ArgumentError, "Unknown OCLC number: #{oclc_number}"
@@ -121,18 +123,15 @@ module BerkeleyLibrary
121
123
  @ht_err_col_index ||= ss.ensure_column!(COL_HATHI_TRUST_ERROR)
122
124
  end
123
125
 
124
- def row_index_by_oclc_number
126
+ def row_indices_by_oclc_number
125
127
  # Start at 1 to skip header row
126
- @row_index_by_oclc_number ||= (1...ss.row_count).each_with_object({}) do |r_index, r_indices|
128
+ @row_indices_by_oclc_number ||= (1...ss.row_count).each_with_object({}) do |r_index, r_indices|
127
129
  oclc_number_raw = ss.value_at(r_index, oclc_col_index)
128
130
  next unless oclc_number_raw
129
131
 
130
132
  oclc_number = oclc_number_raw.to_s
131
- if r_indices.key?(oclc_number)
132
- logger.warn("Skipping duplicate OCLC number #{oclc_number} in row #{r_index}")
133
- else
134
- r_indices[oclc_number] = r_index
135
- end
133
+ r_indices[oclc_number] ||= []
134
+ r_indices[oclc_number] << r_index
136
135
  end
137
136
  end
138
137
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: berkeley_library-location
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - David Moles
7
+ - UC Berkeley Library IT
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-07 00:00:00.000000000 Z
11
+ date: 2025-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: berkeley_library-logging
@@ -44,6 +44,20 @@ dependencies:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: 0.1.9
47
+ - !ruby/object:Gem::Dependency
48
+ name: jsonpath
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.5.8
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.5.8
47
61
  - !ruby/object:Gem::Dependency
48
62
  name: marcel
49
63
  requirement: !ruby/object:Gem::Requirement
@@ -218,14 +232,14 @@ dependencies:
218
232
  requirements:
219
233
  - - "~>"
220
234
  - !ruby/object:Gem::Version
221
- version: 0.17.0
235
+ version: 1.7.1
222
236
  type: :development
223
237
  prerelease: false
224
238
  version_requirements: !ruby/object:Gem::Requirement
225
239
  requirements:
226
240
  - - "~>"
227
241
  - !ruby/object:Gem::Version
228
- version: 0.17.0
242
+ version: 1.7.1
229
243
  - !ruby/object:Gem::Dependency
230
244
  name: simplecov
231
245
  requirement: !ruby/object:Gem::Requirement
@@ -240,6 +254,20 @@ dependencies:
240
254
  - - "~>"
241
255
  - !ruby/object:Gem::Version
242
256
  version: '0.21'
257
+ - !ruby/object:Gem::Dependency
258
+ name: vcr
259
+ requirement: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - "~>"
262
+ - !ruby/object:Gem::Version
263
+ version: '6.1'
264
+ type: :development
265
+ prerelease: false
266
+ version_requirements: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - "~>"
269
+ - !ruby/object:Gem::Version
270
+ version: '6.1'
243
271
  - !ruby/object:Gem::Dependency
244
272
  name: webmock
245
273
  requirement: !ruby/object:Gem::Requirement
@@ -255,7 +283,7 @@ dependencies:
255
283
  - !ruby/object:Gem::Version
256
284
  version: '3.12'
257
285
  description: A collection of location-related utilities for the UC Berkeley Library
258
- email: dmoles@berkeley.edu
286
+ email: libdevelopers@lists.berkeley.edu
259
287
  executables: []
260
288
  extensions: []
261
289
  extra_rdoc_files: []
@@ -276,6 +304,7 @@ files:
276
304
  - lib/berkeley_library/location/world_cat.rb
277
305
  - lib/berkeley_library/location/world_cat/config.rb
278
306
  - lib/berkeley_library/location/world_cat/libraries_request.rb
307
+ - lib/berkeley_library/location/world_cat/oclc_auth.rb
279
308
  - lib/berkeley_library/location/world_cat/symbols.rb
280
309
  - lib/berkeley_library/location/xlsx_reader.rb
281
310
  - lib/berkeley_library/location/xlsx_writer.rb
@@ -306,7 +335,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
306
335
  - !ruby/object:Gem::Version
307
336
  version: '0'
308
337
  requirements: []
309
- rubygems_version: 3.3.25
338
+ rubygems_version: 3.4.19
310
339
  signing_key:
311
340
  specification_version: 4
312
341
  summary: Locaton-related utilities for the UC Berkeley Library