folio_client 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|