folio_client 0.7.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +11 -11
- data/README.md +13 -2
- data/lib/folio_client/holdings.rb +26 -0
- data/lib/folio_client/inventory.rb +34 -4
- data/lib/folio_client/job_status.rb +1 -1
- data/lib/folio_client/records_editor.rb +41 -0
- data/lib/folio_client/source_storage.rb +1 -0
- data/lib/folio_client/unexpected_response.rb +1 -1
- data/lib/folio_client/version.rb +1 -1
- data/lib/folio_client.rb +50 -3
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f400cbad25cc1f7a7a4af985a510c597af5dea7b18cf636d5e6cb8196b4ef059
|
4
|
+
data.tar.gz: 26d9485bebd8780fafe498b15827575703316c229ed8c35e361aee3246ee70fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 472ca9f6c928aa3353b8f928cce63030329e62a7c26655700aaaef6c835e777f58d0771c8b58d8bfd50ee59a7c2c1724063246884ab529556b7cd92ee2065dfd
|
7
|
+
data.tar.gz: 2add95c0f3492db0c80f1770c40576d7bbac019a31674a9b0124a47706b065e1b02e67735519dddc2cf4f832216d453ddb3126906a4c89f4d4eac1f59c70fd6d
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
folio_client (0.
|
4
|
+
folio_client (0.9.0)
|
5
5
|
activesupport (>= 4.2, < 8)
|
6
6
|
dry-monads
|
7
7
|
faraday
|
@@ -11,7 +11,7 @@ PATH
|
|
11
11
|
GEM
|
12
12
|
remote: https://rubygems.org/
|
13
13
|
specs:
|
14
|
-
activesupport (7.0.4.
|
14
|
+
activesupport (7.0.4.3)
|
15
15
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
16
16
|
i18n (>= 1.6, < 2)
|
17
17
|
minitest (>= 5.1)
|
@@ -47,7 +47,7 @@ GEM
|
|
47
47
|
unf
|
48
48
|
minitest (5.18.0)
|
49
49
|
parallel (1.22.1)
|
50
|
-
parser (3.2.1.
|
50
|
+
parser (3.2.1.1)
|
51
51
|
ast (~> 2.4.1)
|
52
52
|
public_suffix (5.0.1)
|
53
53
|
rainbow (3.1.1)
|
@@ -63,28 +63,28 @@ GEM
|
|
63
63
|
rspec-expectations (3.12.2)
|
64
64
|
diff-lcs (>= 1.2.0, < 2.0)
|
65
65
|
rspec-support (~> 3.12.0)
|
66
|
-
rspec-mocks (3.12.
|
66
|
+
rspec-mocks (3.12.4)
|
67
67
|
diff-lcs (>= 1.2.0, < 2.0)
|
68
68
|
rspec-support (~> 3.12.0)
|
69
69
|
rspec-support (3.12.0)
|
70
|
-
rubocop (1.
|
70
|
+
rubocop (1.48.1)
|
71
71
|
json (~> 2.3)
|
72
72
|
parallel (~> 1.10)
|
73
73
|
parser (>= 3.2.0.0)
|
74
74
|
rainbow (>= 2.2.2, < 4.0)
|
75
75
|
regexp_parser (>= 1.8, < 3.0)
|
76
76
|
rexml (>= 3.2.5, < 4.0)
|
77
|
-
rubocop-ast (>= 1.
|
77
|
+
rubocop-ast (>= 1.26.0, < 2.0)
|
78
78
|
ruby-progressbar (~> 1.7)
|
79
79
|
unicode-display_width (>= 2.4.0, < 3.0)
|
80
80
|
rubocop-ast (1.27.0)
|
81
81
|
parser (>= 3.2.1.0)
|
82
82
|
rubocop-capybara (2.17.1)
|
83
83
|
rubocop (~> 1.41)
|
84
|
-
rubocop-performance (1.
|
84
|
+
rubocop-performance (1.16.0)
|
85
85
|
rubocop (>= 1.7.0, < 2.0)
|
86
86
|
rubocop-ast (>= 0.4.0)
|
87
|
-
rubocop-rspec (2.
|
87
|
+
rubocop-rspec (2.19.0)
|
88
88
|
rubocop (~> 1.33)
|
89
89
|
rubocop-capybara (~> 2.17)
|
90
90
|
ruby-progressbar (1.13.0)
|
@@ -96,10 +96,10 @@ GEM
|
|
96
96
|
simplecov_json_formatter (~> 0.1)
|
97
97
|
simplecov-html (0.12.3)
|
98
98
|
simplecov_json_formatter (0.1.4)
|
99
|
-
standard (1.
|
99
|
+
standard (1.25.2)
|
100
100
|
language_server-protocol (~> 3.17.0.2)
|
101
|
-
rubocop (= 1.
|
102
|
-
rubocop-performance (= 1.
|
101
|
+
rubocop (= 1.48.1)
|
102
|
+
rubocop-performance (= 1.16.0)
|
103
103
|
tzinfo (2.0.6)
|
104
104
|
concurrent-ruby (~> 1.0)
|
105
105
|
unf (0.1.4)
|
data/README.md
CHANGED
@@ -65,13 +65,13 @@ client.fetch_hrid(barcode: "12345")
|
|
65
65
|
=> "a7927874"
|
66
66
|
|
67
67
|
# Request a MARC record given an instance hrid
|
68
|
-
# returns a hash if found; raises FolioClient::
|
68
|
+
# returns a hash if found; raises FolioClient::ResourceNotFound if instance_hrid not found
|
69
69
|
client.fetch_marc_hash(instance_hrid: "a7927874")
|
70
70
|
=> {"fields"=>
|
71
71
|
[{"003"=>"FOLIO"}....]
|
72
72
|
}
|
73
73
|
|
74
|
-
# Import a MARC record
|
74
|
+
# Import a MARC record into FOLIO
|
75
75
|
data_importer = client.data_import(marc: my_marc, job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
|
76
76
|
# If called too quickly, might get Failure(:not_found)
|
77
77
|
data_importer.status
|
@@ -80,6 +80,17 @@ data_importer.wait_until_complete
|
|
80
80
|
=> Success()
|
81
81
|
data_importer.instance_hrid
|
82
82
|
=> Success("in00000000010")
|
83
|
+
|
84
|
+
# Create a Holdings record
|
85
|
+
holdings_client = client.holdings(instance_id: "99a6d818-d523-42f3-9844-81cf3187dbad")
|
86
|
+
holdings_client.create(permanent_location_id: "1b14e21c-8d47-45c7-bc49-456a0086422b", holdings_type_id: "996f93e2-5b5e-4cf2-9168-33ced1f95eed")
|
87
|
+
=> {
|
88
|
+
"id" => "581f6289-001f-49d1-bab7-035f4d878cbd",
|
89
|
+
"_version" => 1,
|
90
|
+
"hrid" => "ho00000000065",
|
91
|
+
"holdingsTypeId" => "996f93e2-5b5e-4cf2-9168-33ced1f95eed"
|
92
|
+
...
|
93
|
+
}
|
83
94
|
```
|
84
95
|
|
85
96
|
## Development
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class FolioClient
|
4
|
+
# Manage holdings records in the Folio inventory
|
5
|
+
class Holdings
|
6
|
+
attr_accessor :client, :instance_id
|
7
|
+
|
8
|
+
# @param client [FolioClient] the configured client
|
9
|
+
# @param instance_id [String] the UUID of the instance to which the holdings record belongs
|
10
|
+
def initialize(client, instance_id:)
|
11
|
+
@client = client
|
12
|
+
@instance_id = instance_id
|
13
|
+
end
|
14
|
+
|
15
|
+
# create a holdings record for the instance
|
16
|
+
# @param permanent_location_id [String] the UUID of the permanent location
|
17
|
+
# @param holdings_type_id [String] the UUID of the holdings type
|
18
|
+
def create(holdings_type_id:, permanent_location_id:)
|
19
|
+
client.post("/holdings-storage/holdings", {
|
20
|
+
instanceId: instance_id,
|
21
|
+
permanentLocationId: permanent_location_id,
|
22
|
+
holdingsTypeId: holdings_type_id
|
23
|
+
})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -10,8 +10,9 @@ class FolioClient
|
|
10
10
|
@client = client
|
11
11
|
end
|
12
12
|
|
13
|
-
#
|
14
|
-
# @
|
13
|
+
# get instance HRID from barcode
|
14
|
+
# @param barcode [String] barcode
|
15
|
+
# @return [String,nil] instance HRID if present, otherwise nil.
|
15
16
|
def fetch_hrid(barcode:)
|
16
17
|
# find the instance UUID for this barcode
|
17
18
|
instance = client.get("/search/instances", {query: "items.barcode==#{barcode}"})
|
@@ -24,9 +25,38 @@ class FolioClient
|
|
24
25
|
result.dig("hrid")
|
25
26
|
end
|
26
27
|
|
27
|
-
#
|
28
|
+
# get instance external ID from HRID
|
29
|
+
# @param hrid [String] instance HRID
|
30
|
+
# @return [String,nil] instance external ID if present, otherwise nil.
|
31
|
+
# @raise [ResourceNotFound, MultipleResourcesFound] if search does not return exactly 1 result
|
32
|
+
def fetch_external_id(hrid:)
|
33
|
+
instance_response = client.get("/search/instances", {query: "hrid==#{hrid}"})
|
34
|
+
record_count = instance_response["totalRecords"]
|
35
|
+
raise ResourceNotFound, "No matching instance found for #{hrid}" if instance_response["totalRecords"] == 0
|
36
|
+
raise MultipleResourcesFound, "Expected 1 record for #{hrid}, but found #{record_count}" if record_count > 1
|
37
|
+
|
38
|
+
instance_response.dig("instances", 0, "id")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Retrieve basic information about a instance record. Example usage: get the external ID and _version for update using
|
42
|
+
# optimistic locking when the HRID is available: `fetch_instance_info(hrid: 'a1234').slice('id', '_version')`
|
43
|
+
# (or vice versa if the external ID is available).
|
44
|
+
# @param external_id [String] an external ID for the desired instance record
|
45
|
+
# @param hrid [String] an instance HRID for the desired instance record
|
46
|
+
# @return [Hash] information about the record.
|
47
|
+
# @raise [ArgumentError] if the caller does not provide exactly one of external_id or hrid
|
48
|
+
def fetch_instance_info(external_id: nil, hrid: nil)
|
49
|
+
raise ArgumentError, "must pass exactly one of external_id or HRID" unless external_id.present? || hrid.present?
|
50
|
+
raise ArgumentError, "must pass exactly one of external_id or HRID" if external_id.present? && hrid.present?
|
51
|
+
|
52
|
+
external_id ||= fetch_external_id(hrid: hrid)
|
53
|
+
client.get("/inventory/instances/#{external_id}")
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param hrid [String] instance HRID
|
28
57
|
# @param status_id [String] uuid for an instance status code
|
29
|
-
# @
|
58
|
+
# @return true if instance status matches the uuid param, false otherwise
|
59
|
+
# @raise [ResourceNotFound] if search by instance HRID returns 0 results
|
30
60
|
def has_instance_status?(hrid:, status_id:)
|
31
61
|
# get the instance record and its statusId
|
32
62
|
instance = client.get("/inventory/instances", {query: "hrid==#{hrid}"})
|
@@ -87,7 +87,7 @@ class FolioClient
|
|
87
87
|
def check_not_found(result, index)
|
88
88
|
return unless result.failure? && result.failure == :not_found && index > 2
|
89
89
|
|
90
|
-
raise ResourceNotFound, "Job not found"
|
90
|
+
raise ResourceNotFound, "Job #{job_execution_id} not found after #{index} retries"
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class FolioClient
|
4
|
+
class RecordsEditor
|
5
|
+
attr_accessor :client
|
6
|
+
|
7
|
+
# @param client [FolioClient] the configured client
|
8
|
+
def initialize(client)
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
# Given an HRID, retrieves the associated MARC JSON, yields it to the caller as a hash,
|
13
|
+
# and attempts to re-save it, using optimistic locking to prevent accidental overwrite,
|
14
|
+
# in case another user or process has updated the record in the time between retrieval and
|
15
|
+
# attempted save.
|
16
|
+
# @param hrid [String] the HRID of the MARC record to be edited and saved
|
17
|
+
# @yieldparam record_json [Hash] a hash representation of the MARC JSON for the
|
18
|
+
# HRID; the updated hash will be saved when control is returned from the block.
|
19
|
+
# @note in limited manual testing, optimistic locking behaved like so when two edit attempts collided:
|
20
|
+
# * One updating client would eventually raise a timeout. This updating client would actually write successfully, and version the record.
|
21
|
+
# * The other updating client would raise a StandardError, with a message like 'duplicate key value violates unique constraint \"idx_records_matched_id_gen\"'.
|
22
|
+
# This client would fail to write.
|
23
|
+
# * As opposed to the expected behavior of the "winner" getting a 200 ok response, and the "loser" getting a 409 conflict response.
|
24
|
+
# @todo If this is a problem in practice, see if it's possible to have Folio respond in a more standard way; or, workaround with error handling.
|
25
|
+
def edit_marc_json(hrid:)
|
26
|
+
instance_info = client.fetch_instance_info(hrid: hrid)
|
27
|
+
|
28
|
+
version = instance_info["_version"]
|
29
|
+
external_id = instance_info["id"]
|
30
|
+
|
31
|
+
record_json = client.get("/records-editor/records", {externalId: external_id})
|
32
|
+
|
33
|
+
parsed_record_id = record_json["parsedRecordId"]
|
34
|
+
record_json["relatedRecordVersion"] = version # setting this field on the JSON we send back is what will allow optimistic locking to catch stale updates
|
35
|
+
|
36
|
+
yield record_json
|
37
|
+
|
38
|
+
client.put("/records-editor/records/#{parsed_record_id}", record_json)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -10,6 +10,7 @@ class FolioClient
|
|
10
10
|
@client = client
|
11
11
|
end
|
12
12
|
|
13
|
+
# get marc bib data from folio given an instance HRID
|
13
14
|
# @param instance_hrid [String] the key to use for MARC lookup
|
14
15
|
# @return [Hash] hash representation of the MARC. should be usable by MARC::Record.new_from_hash (from ruby-marc gem)
|
15
16
|
# @raises NotFound, MultipleRecordsForIdentifier
|
@@ -13,7 +13,7 @@ class FolioClient
|
|
13
13
|
when 404
|
14
14
|
raise ResourceNotFound, "Endpoint not found or resource does not exist: #{response.body}"
|
15
15
|
when 422
|
16
|
-
raise
|
16
|
+
raise ValidationError, "There was a validation problem with the request: #{response.body} "
|
17
17
|
when 500
|
18
18
|
raise ServiceUnavailable, "The remote server returned an internal server error."
|
19
19
|
else
|
data/lib/folio_client/version.rb
CHANGED
data/lib/folio_client.rb
CHANGED
@@ -17,7 +17,7 @@ class FolioClient
|
|
17
17
|
# Base class for all FolioClient errors
|
18
18
|
class Error < StandardError; end
|
19
19
|
|
20
|
-
# Error raised by the Folio Auth API returns a
|
20
|
+
# Error raised by the Folio Auth API returns a 401 Unauthorized
|
21
21
|
class UnauthorizedError < Error; end
|
22
22
|
|
23
23
|
# Error raised when the Folio API returns a 404 NotFound, or returns 0 results when one was expected
|
@@ -32,6 +32,9 @@ class FolioClient
|
|
32
32
|
# Error raised when the Folio API returns a 500
|
33
33
|
class ServiceUnavailable < Error; end
|
34
34
|
|
35
|
+
# Error raised when the Folio API returns a 422 Unprocessable Entity
|
36
|
+
class ValidationError < Error; end
|
37
|
+
|
35
38
|
DEFAULT_HEADERS = {
|
36
39
|
accept: "application/json, text/plain",
|
37
40
|
content_type: "application/json"
|
@@ -49,8 +52,8 @@ class FolioClient
|
|
49
52
|
self
|
50
53
|
end
|
51
54
|
|
52
|
-
delegate :config, :connection, :get, :post, to: :instance
|
53
|
-
delegate :fetch_hrid, :fetch_marc_hash, :has_instance_status?, :data_import, to: :instance
|
55
|
+
delegate :config, :connection, :get, :post, :put, to: :instance
|
56
|
+
delegate :fetch_hrid, :fetch_external_id, :fetch_instance_info, :fetch_marc_hash, :has_instance_status?, :data_import, :holdings, :edit_marc_json, to: :instance
|
54
57
|
end
|
55
58
|
|
56
59
|
attr_accessor :config
|
@@ -91,6 +94,27 @@ class FolioClient
|
|
91
94
|
JSON.parse(response.body)
|
92
95
|
end
|
93
96
|
|
97
|
+
# Send an authenticated put request
|
98
|
+
# If the body is JSON, it will be automatically serialized
|
99
|
+
# @param path [String] the path to the Folio API request
|
100
|
+
# @param body [Object] body to put to the API as JSON
|
101
|
+
def put(path, body = nil, content_type: "application/json")
|
102
|
+
req_body = (content_type == "application/json") ? body&.to_json : body
|
103
|
+
response = TokenWrapper.refresh(config, connection) do
|
104
|
+
req_headers = {
|
105
|
+
"x-okapi-token": config.token,
|
106
|
+
"content-type": content_type
|
107
|
+
}
|
108
|
+
connection.put(path, req_body, req_headers)
|
109
|
+
end
|
110
|
+
|
111
|
+
UnexpectedResponse.call(response) unless response.success?
|
112
|
+
|
113
|
+
return nil if response.body.blank?
|
114
|
+
|
115
|
+
JSON.parse(response.body)
|
116
|
+
end
|
117
|
+
|
94
118
|
# the base connection to the Folio API
|
95
119
|
def connection
|
96
120
|
@connection ||= Faraday.new(
|
@@ -106,6 +130,18 @@ class FolioClient
|
|
106
130
|
.fetch_hrid(...)
|
107
131
|
end
|
108
132
|
|
133
|
+
def fetch_external_id(...)
|
134
|
+
Inventory
|
135
|
+
.new(self)
|
136
|
+
.fetch_external_id(...)
|
137
|
+
end
|
138
|
+
|
139
|
+
def fetch_instance_info(...)
|
140
|
+
Inventory
|
141
|
+
.new(self)
|
142
|
+
.fetch_instance_info(...)
|
143
|
+
end
|
144
|
+
|
109
145
|
def fetch_marc_hash(...)
|
110
146
|
SourceStorage
|
111
147
|
.new(self)
|
@@ -123,4 +159,15 @@ class FolioClient
|
|
123
159
|
.new(self)
|
124
160
|
.import(...)
|
125
161
|
end
|
162
|
+
|
163
|
+
def holdings(...)
|
164
|
+
Holdings
|
165
|
+
.new(self, ...)
|
166
|
+
end
|
167
|
+
|
168
|
+
def edit_marc_json(...)
|
169
|
+
RecordsEditor
|
170
|
+
.new(self)
|
171
|
+
.edit_marc_json(...)
|
172
|
+
end
|
126
173
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: folio_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Mangiafico
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -191,8 +191,10 @@ files:
|
|
191
191
|
- lib/folio_client.rb
|
192
192
|
- lib/folio_client/authenticator.rb
|
193
193
|
- lib/folio_client/data_import.rb
|
194
|
+
- lib/folio_client/holdings.rb
|
194
195
|
- lib/folio_client/inventory.rb
|
195
196
|
- lib/folio_client/job_status.rb
|
197
|
+
- lib/folio_client/records_editor.rb
|
196
198
|
- lib/folio_client/source_storage.rb
|
197
199
|
- lib/folio_client/token_wrapper.rb
|
198
200
|
- lib/folio_client/unexpected_response.rb
|
@@ -204,7 +206,7 @@ metadata:
|
|
204
206
|
source_code_uri: https://github.com/sul-dlss/folio_client
|
205
207
|
changelog_uri: https://github.com/sul-dlss/folio_client/releases
|
206
208
|
rubygems_mfa_required: 'true'
|
207
|
-
post_install_message:
|
209
|
+
post_install_message:
|
208
210
|
rdoc_options: []
|
209
211
|
require_paths:
|
210
212
|
- lib
|
@@ -219,8 +221,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
219
221
|
- !ruby/object:Gem::Version
|
220
222
|
version: '0'
|
221
223
|
requirements: []
|
222
|
-
rubygems_version: 3.3.
|
223
|
-
signing_key:
|
224
|
+
rubygems_version: 3.3.7
|
225
|
+
signing_key:
|
224
226
|
specification_version: 4
|
225
227
|
summary: Interface for interacting with the Folio ILS API.
|
226
228
|
test_files: []
|