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 +4 -4
- data/CHANGES.md +13 -0
- data/README.md +11 -0
- data/lib/berkeley_library/location/module_info.rb +3 -3
- data/lib/berkeley_library/location/world_cat/config.rb +51 -4
- data/lib/berkeley_library/location/world_cat/libraries_request.rb +27 -17
- data/lib/berkeley_library/location/world_cat/oclc_auth.rb +65 -0
- data/lib/berkeley_library/location/xlsx_writer.rb +11 -12
- metadata +36 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcdd92dafd937a6dabe9cf2104e84311ae590d2db023b80c9c6fb06d0a1f3b40
|
4
|
+
data.tar.gz: fc15382505d0611b660cdca1e5575b780ca00a0a13cb65cf5dec1d3c2a07a501
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 = '
|
6
|
-
AUTHOR_EMAIL = '
|
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 = '
|
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/
|
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
|
-
|
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
|
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
|
-
'
|
31
|
-
'
|
32
|
-
|
33
|
-
|
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
|
-
|
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, '
|
57
|
+
URIs.append(Config.base_uri, 'bibs-holdings')
|
47
58
|
end
|
48
59
|
|
49
|
-
def inst_symbols_from(
|
50
|
-
|
51
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
61
|
-
row_index =
|
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
|
126
|
+
def row_indices_by_oclc_number
|
125
127
|
# Start at 1 to skip header row
|
126
|
-
@
|
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
|
-
|
132
|
-
|
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:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- UC Berkeley Library IT
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
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:
|
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:
|
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:
|
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.
|
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
|