berkeley_library-location 3.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: 27ec12d4fe5561d87ad2b96cdcedb68d935aa2b36d0df9ed28dd25570af071b4
4
- data.tar.gz: 1e0fefbfafda0938e0dd8cb714a5a0d0da4ea96e8476a922f7ff55aba91bd550
3
+ metadata.gz: dcdd92dafd937a6dabe9cf2104e84311ae590d2db023b80c9c6fb06d0a1f3b40
4
+ data.tar.gz: fc15382505d0611b660cdca1e5575b780ca00a0a13cb65cf5dec1d3c2a07a501
5
5
  SHA512:
6
- metadata.gz: 8cdfc0b0f370d282d4a86e791a15bfc5de856e821d360840a058b6bfd6385816fcb032a48dd9dd5a182a04767e2013990295ddb79841c41902f270e28b5208cd
7
- data.tar.gz: 18c2c7c1d25beed576a4bcef69863e9cb1b8beed29e33c5e1180c38cba0e374189bf65811c5e93daf47cb7fd74f4fcb60716fbb34c121a7762b799d2943aa289
6
+ metadata.gz: e7f912cc954ceaf098cd7711cffc6016c0cfa945d7b63eb46e1a6c546d321962df4e76abb8e21ee8d57153590cacc7996f4d6ad4a90ae2f40fe91b46467b2e98
7
+ data.tar.gz: b8096c7cdc373ac6d79f0b966c2cfce33c959cc562c8b0d002ee1265ba6c0fb38eda81fb46ef8fff9accf6e033cef5b1b7e952bc96926b06eb3198235452d540
data/CHANGES.md CHANGED
@@ -1,3 +1,8 @@
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
+
1
6
  # 3.0.0 (2023-10-10)
2
7
 
3
8
  - Support duplicate OCLC numbers
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"
@@ -7,7 +7,7 @@ module BerkeleyLibrary
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 = '3.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
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: 3.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - UC Berkeley Library IT
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-19 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
@@ -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
@@ -291,7 +320,7 @@ metadata:
291
320
  source_code_uri: https://github.com/BerkeleyLibrary/location
292
321
  changelog_uri: https://github.com/BerkeleyLibrary/location/CHANGELOG.md
293
322
  rubygems_mfa_required: 'true'
294
- post_install_message:
323
+ post_install_message:
295
324
  rdoc_options: []
296
325
  require_paths:
297
326
  - lib
@@ -306,8 +335,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
306
335
  - !ruby/object:Gem::Version
307
336
  version: '0'
308
337
  requirements: []
309
- rubygems_version: 3.4.10
310
- signing_key:
338
+ rubygems_version: 3.4.19
339
+ signing_key:
311
340
  specification_version: 4
312
341
  summary: Locaton-related utilities for the UC Berkeley Library
313
342
  test_files: []