folio_client 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop/custom.yml +20 -0
- data/.rubocop_todo.yml +1 -24
- data/Gemfile.lock +21 -4
- data/README.md +10 -0
- data/Rakefile +2 -4
- data/folio_client.gemspec +2 -0
- data/lib/folio_client/data_import.rb +58 -0
- data/lib/folio_client/job_status.rb +93 -0
- data/lib/folio_client/version.rb +1 -1
- data/lib/folio_client.rb +30 -10
- metadata +33 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51efd566f32da4e9349cde6c853d91b25db7db35be7f046e5cc8ae445470c25e
|
4
|
+
data.tar.gz: b244ae8d8d496baeb51c3e43834bc2f330fe04cbb943ad2e652efdfacbe0a6aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 16a20ff6ec80f84d828f773ff8c3b1eeeda67dd0472674134aa975a822b37d0d9f86c65ef3ccb03d26c800ffa3a001386d973a77c03ab8d1ef9aeda7dfd312f4
|
7
|
+
data.tar.gz: e34c0b895a3bfea9174a6836c0a5af28afd1de88743430ba3b2d75494ebe42675e8e1b7a9015823e1e065ba28ed6dbbea864111cf5dd05fc3f5f7b8daf3938b7
|
data/.rspec
CHANGED
data/.rubocop/custom.yml
CHANGED
@@ -9,6 +9,8 @@ AllCops:
|
|
9
9
|
# Per team developer playbook
|
10
10
|
RSpec/MultipleMemoizedHelpers:
|
11
11
|
Enabled: false
|
12
|
+
RSpec/MultipleExpectations:
|
13
|
+
Max: 3
|
12
14
|
|
13
15
|
RSpec/BeEq: # new in 2.9.0
|
14
16
|
Enabled: true
|
@@ -40,3 +42,21 @@ RSpec/Rails/HaveHttpStatus: # new in 2.12
|
|
40
42
|
Enabled: true
|
41
43
|
RSpec/Rails/InferredSpecType: # new in 2.14
|
42
44
|
Enabled: true
|
45
|
+
Capybara/MatchStyle: # new in 2.17
|
46
|
+
Enabled: true
|
47
|
+
Capybara/NegationMatcher: # new in 2.14
|
48
|
+
Enabled: true
|
49
|
+
Capybara/SpecificActions: # new in 2.14
|
50
|
+
Enabled: true
|
51
|
+
Capybara/SpecificFinders: # new in 2.13
|
52
|
+
Enabled: true
|
53
|
+
Capybara/SpecificMatcher: # new in 2.12
|
54
|
+
Enabled: true
|
55
|
+
RSpec/DuplicatedMetadata: # new in 2.16
|
56
|
+
Enabled: true
|
57
|
+
RSpec/PendingWithoutReason: # new in 2.16
|
58
|
+
Enabled: true
|
59
|
+
RSpec/FactoryBot/FactoryNameStyle: # new in 2.16
|
60
|
+
Enabled: true
|
61
|
+
RSpec/Rails/MinitestAssertions: # new in 2.17
|
62
|
+
Enabled: true
|
data/.rubocop_todo.yml
CHANGED
@@ -1,30 +1,7 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2023-
|
3
|
+
# on 2023-03-03 17:57:09 UTC using RuboCop version 1.44.1.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
|
-
|
9
|
-
# Offense count: 20
|
10
|
-
# This cop supports safe autocorrection (--autocorrect).
|
11
|
-
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
|
12
|
-
# SupportedStyles: space, no_space, compact
|
13
|
-
# SupportedStylesForEmptyBraces: space, no_space
|
14
|
-
Layout/SpaceInsideHashLiteralBraces:
|
15
|
-
Exclude:
|
16
|
-
- 'lib/folio_client.rb'
|
17
|
-
- 'spec/folio_client/authenticator_spec.rb'
|
18
|
-
- 'spec/folio_client_spec.rb'
|
19
|
-
|
20
|
-
# Offense count: 1
|
21
|
-
# This cop supports safe autocorrection (--autocorrect).
|
22
|
-
# Configuration parameters: EnforcedStyle.
|
23
|
-
# SupportedStyles: be, be_nil
|
24
|
-
RSpec/BeNil:
|
25
|
-
Exclude:
|
26
|
-
- 'spec/folio_client_spec.rb'
|
27
|
-
|
28
|
-
# Offense count: 1
|
29
|
-
RSpec/MultipleExpectations:
|
30
|
-
Max: 3
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
folio_client (0.
|
4
|
+
folio_client (0.7.0)
|
5
5
|
activesupport (>= 4.2, < 8)
|
6
|
+
dry-monads
|
6
7
|
faraday
|
8
|
+
marc
|
7
9
|
zeitwerk
|
8
10
|
|
9
11
|
GEM
|
@@ -23,6 +25,13 @@ GEM
|
|
23
25
|
rexml
|
24
26
|
diff-lcs (1.5.0)
|
25
27
|
docile (1.4.0)
|
28
|
+
dry-core (1.0.0)
|
29
|
+
concurrent-ruby (~> 1.0)
|
30
|
+
zeitwerk (~> 2.6)
|
31
|
+
dry-monads (1.6.0)
|
32
|
+
concurrent-ruby (~> 1.0)
|
33
|
+
dry-core (~> 1.0, < 2)
|
34
|
+
zeitwerk (~> 2.6)
|
26
35
|
faraday (2.7.4)
|
27
36
|
faraday-net_http (>= 2.0, < 3.1)
|
28
37
|
ruby2_keywords (>= 0.0.4)
|
@@ -32,7 +41,11 @@ GEM
|
|
32
41
|
concurrent-ruby (~> 1.0)
|
33
42
|
json (2.6.3)
|
34
43
|
language_server-protocol (3.17.0.3)
|
35
|
-
|
44
|
+
marc (1.2.0)
|
45
|
+
rexml
|
46
|
+
scrub_rb (>= 1.0.1, < 2)
|
47
|
+
unf
|
48
|
+
minitest (5.18.0)
|
36
49
|
parallel (1.22.1)
|
37
50
|
parser (3.2.1.0)
|
38
51
|
ast (~> 2.4.1)
|
@@ -64,7 +77,7 @@ GEM
|
|
64
77
|
rubocop-ast (>= 1.24.1, < 2.0)
|
65
78
|
ruby-progressbar (~> 1.7)
|
66
79
|
unicode-display_width (>= 2.4.0, < 3.0)
|
67
|
-
rubocop-ast (1.
|
80
|
+
rubocop-ast (1.27.0)
|
68
81
|
parser (>= 3.2.1.0)
|
69
82
|
rubocop-capybara (2.17.1)
|
70
83
|
rubocop (~> 1.41)
|
@@ -74,8 +87,9 @@ GEM
|
|
74
87
|
rubocop-rspec (2.18.1)
|
75
88
|
rubocop (~> 1.33)
|
76
89
|
rubocop-capybara (~> 2.17)
|
77
|
-
ruby-progressbar (1.
|
90
|
+
ruby-progressbar (1.13.0)
|
78
91
|
ruby2_keywords (0.0.5)
|
92
|
+
scrub_rb (1.0.1)
|
79
93
|
simplecov (0.22.0)
|
80
94
|
docile (~> 1.1)
|
81
95
|
simplecov-html (~> 0.11)
|
@@ -88,6 +102,9 @@ GEM
|
|
88
102
|
rubocop-performance (= 1.15.2)
|
89
103
|
tzinfo (2.0.6)
|
90
104
|
concurrent-ruby (~> 1.0)
|
105
|
+
unf (0.1.4)
|
106
|
+
unf_ext
|
107
|
+
unf_ext (0.0.8.2)
|
91
108
|
unicode-display_width (2.4.2)
|
92
109
|
webmock (3.18.1)
|
93
110
|
addressable (>= 2.8.0)
|
data/README.md
CHANGED
@@ -70,6 +70,16 @@ client.fetch_marc_hash(instance_hrid: "a7927874")
|
|
70
70
|
=> {"fields"=>
|
71
71
|
[{"003"=>"FOLIO"}....]
|
72
72
|
}
|
73
|
+
|
74
|
+
# Import a MARC record
|
75
|
+
data_importer = client.data_import(marc: my_marc, job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
|
76
|
+
# If called too quickly, might get Failure(:not_found)
|
77
|
+
data_importer.status
|
78
|
+
=> Failure(:pending)
|
79
|
+
data_importer.wait_until_complete
|
80
|
+
=> Success()
|
81
|
+
data_importer.instance_hrid
|
82
|
+
=> Success("in00000000010")
|
73
83
|
```
|
74
84
|
|
75
85
|
## Development
|
data/Rakefile
CHANGED
@@ -2,11 +2,9 @@
|
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rspec/core/rake_task"
|
5
|
-
|
6
|
-
RSpec::Core::RakeTask.new(:spec)
|
7
|
-
|
8
5
|
require "rubocop/rake_task"
|
9
6
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
8
|
RuboCop::RakeTask.new
|
11
9
|
|
12
|
-
task default: %i[spec
|
10
|
+
task default: %i[rubocop spec]
|
data/folio_client.gemspec
CHANGED
@@ -34,6 +34,8 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_dependency "activesupport", ">= 4.2", "< 8"
|
35
35
|
spec.add_dependency "faraday"
|
36
36
|
spec.add_dependency "zeitwerk"
|
37
|
+
spec.add_dependency "marc"
|
38
|
+
spec.add_dependency "dry-monads"
|
37
39
|
|
38
40
|
spec.add_development_dependency "rake", "~> 13.0"
|
39
41
|
spec.add_development_dependency "rspec", "~> 3.0"
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "marc"
|
5
|
+
require "stringio"
|
6
|
+
|
7
|
+
class FolioClient
|
8
|
+
# Imports MARC records into FOLIO
|
9
|
+
class DataImport
|
10
|
+
# @param client [FolioClient] the configured client
|
11
|
+
def initialize(client)
|
12
|
+
@client = client
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param record [MARC::Record] record to be imported
|
16
|
+
# @param job_profile_id [String] job profile id to use for import
|
17
|
+
# @param job_profile_name [String] job profile name to use for import
|
18
|
+
def import(marc:, job_profile_id:, job_profile_name:)
|
19
|
+
response_hash = client.post("/data-import/uploadDefinitions", {fileDefinitions: [{name: marc_filename}]})
|
20
|
+
upload_definition_id = response_hash.dig("fileDefinitions", 0, "uploadDefinitionId")
|
21
|
+
job_execution_id = response_hash.dig("fileDefinitions", 0, "jobExecutionId")
|
22
|
+
file_definition_id = response_hash.dig("fileDefinitions", 0, "id")
|
23
|
+
|
24
|
+
upload_file_response_hash = client.post("/data-import/uploadDefinitions/#{upload_definition_id}/files/#{file_definition_id}", marc_binary(marc), content_type: "application/octet-stream")
|
25
|
+
|
26
|
+
client.post(
|
27
|
+
"/data-import/uploadDefinitions/#{upload_definition_id}/processFiles",
|
28
|
+
{
|
29
|
+
uploadDefinition: upload_file_response_hash,
|
30
|
+
jobProfileInfo: {
|
31
|
+
id: job_profile_id,
|
32
|
+
name: job_profile_name,
|
33
|
+
dataType: "MARC"
|
34
|
+
}
|
35
|
+
}
|
36
|
+
)
|
37
|
+
|
38
|
+
JobStatus.new(client, job_execution_id: job_execution_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :client, :marc, :job_profile_id, :job_profile_name
|
44
|
+
|
45
|
+
def marc_filename
|
46
|
+
@marc_filename ||= "#{DateTime.now.iso8601}.marc"
|
47
|
+
end
|
48
|
+
|
49
|
+
def marc_binary(marc)
|
50
|
+
StringIO.open do |io|
|
51
|
+
MARC::Writer.new(io) do |writer|
|
52
|
+
writer.write(marc)
|
53
|
+
end
|
54
|
+
io.string
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
require "dry/monads"
|
5
|
+
|
6
|
+
class FolioClient
|
7
|
+
# Wraps operations waiting for results from jobs
|
8
|
+
class JobStatus
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
attr_reader :job_execution_id
|
12
|
+
|
13
|
+
# @param client [FolioClient] the configured client
|
14
|
+
# @param job_execution_id [String] ID of the job to be checked on
|
15
|
+
def initialize(client, job_execution_id:)
|
16
|
+
@client = client
|
17
|
+
@job_execution_id = job_execution_id
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Dry::Monads::Result] Success if job is complete,
|
21
|
+
# Failure(:pending) if job is still running,
|
22
|
+
# Failure(:error) if job has errors
|
23
|
+
# Failure(:not_found) if job is not found
|
24
|
+
def status
|
25
|
+
response_hash = client.get("/metadata-provider/jobSummary/#{job_execution_id}")
|
26
|
+
|
27
|
+
return Failure(:error) if response_hash["totalErrors"].positive?
|
28
|
+
return Failure(:pending) if response_hash.dig("sourceRecordSummary", "totalCreatedEntities").zero? && response_hash.dig("sourceRecordSummary", "totalUpdatedEntities").zero?
|
29
|
+
|
30
|
+
Success()
|
31
|
+
rescue ResourceNotFound
|
32
|
+
# Checking the status immediately after starting the import may result in a 404.
|
33
|
+
Failure(:not_found)
|
34
|
+
end
|
35
|
+
|
36
|
+
def wait_until_complete(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
|
37
|
+
wait_with_timeout(wait_secs: wait_secs, timeout_secs: timeout_secs) { status }
|
38
|
+
end
|
39
|
+
|
40
|
+
def instance_hrid
|
41
|
+
current_status = status
|
42
|
+
return current_status unless current_status.success?
|
43
|
+
|
44
|
+
@instance_hrid ||= wait_with_timeout do
|
45
|
+
response = client
|
46
|
+
.get("/metadata-provider/journalRecords/#{job_execution_id}")
|
47
|
+
.fetch("journalRecords", [])
|
48
|
+
.find { |journal_record| journal_record["entityType"] == "INSTANCE" }
|
49
|
+
&.fetch("entityHrId", nil)
|
50
|
+
|
51
|
+
response.nil? ? Failure() : Success(response)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_reader :client
|
58
|
+
|
59
|
+
def default_wait_secs
|
60
|
+
1
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_timeout_secs
|
64
|
+
5 * 60
|
65
|
+
end
|
66
|
+
|
67
|
+
def wait_with_timeout(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
|
68
|
+
Timeout.timeout(timeout_secs) do
|
69
|
+
loop.with_index do |_, i|
|
70
|
+
result = yield
|
71
|
+
|
72
|
+
# If a 404, wait a bit longer before raising an error.
|
73
|
+
check_not_found(result, i)
|
74
|
+
return result if done_waiting?(result)
|
75
|
+
|
76
|
+
sleep(wait_secs)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
rescue Timeout::Error
|
80
|
+
Failure(:timeout)
|
81
|
+
end
|
82
|
+
|
83
|
+
def done_waiting?(result)
|
84
|
+
result.success? || (result.failure? && result.failure == :error)
|
85
|
+
end
|
86
|
+
|
87
|
+
def check_not_found(result, index)
|
88
|
+
return unless result.failure? && result.failure == :not_found && index > 2
|
89
|
+
|
90
|
+
raise ResourceNotFound, "Job not found"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/folio_client/version.rb
CHANGED
data/lib/folio_client.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
|
+
require "active_support/core_ext/object/blank"
|
4
5
|
require "faraday"
|
5
6
|
require "singleton"
|
6
7
|
require "ostruct"
|
@@ -49,7 +50,7 @@ class FolioClient
|
|
49
50
|
end
|
50
51
|
|
51
52
|
delegate :config, :connection, :get, :post, to: :instance
|
52
|
-
delegate :fetch_hrid, :fetch_marc_hash, :has_instance_status?, to: :instance
|
53
|
+
delegate :fetch_hrid, :fetch_marc_hash, :has_instance_status?, :data_import, to: :instance
|
53
54
|
end
|
54
55
|
|
55
56
|
attr_accessor :config
|
@@ -64,19 +65,29 @@ class FolioClient
|
|
64
65
|
|
65
66
|
UnexpectedResponse.call(response) unless response.success?
|
66
67
|
|
68
|
+
return nil if response.body.blank?
|
69
|
+
|
67
70
|
JSON.parse(response.body)
|
68
71
|
end
|
69
72
|
|
70
73
|
# Send an authenticated post request
|
74
|
+
# If the body is JSON, it will be automatically serialized
|
71
75
|
# @param path [String] the path to the Folio API request
|
72
|
-
# @param
|
73
|
-
def post(path,
|
76
|
+
# @param body [Object] body to post to the API as JSON
|
77
|
+
def post(path, body = nil, content_type: "application/json")
|
78
|
+
req_body = (content_type == "application/json") ? body&.to_json : body
|
74
79
|
response = TokenWrapper.refresh(config, connection) do
|
75
|
-
|
80
|
+
req_headers = {
|
81
|
+
"x-okapi-token": config.token,
|
82
|
+
"content-type": content_type
|
83
|
+
}
|
84
|
+
connection.post(path, req_body, req_headers)
|
76
85
|
end
|
77
86
|
|
78
87
|
UnexpectedResponse.call(response) unless response.success?
|
79
88
|
|
89
|
+
return nil if response.body.blank?
|
90
|
+
|
80
91
|
JSON.parse(response.body)
|
81
92
|
end
|
82
93
|
|
@@ -90,17 +101,26 @@ class FolioClient
|
|
90
101
|
|
91
102
|
# Public methods available on the FolioClient below
|
92
103
|
def fetch_hrid(...)
|
93
|
-
|
94
|
-
|
104
|
+
Inventory
|
105
|
+
.new(self)
|
106
|
+
.fetch_hrid(...)
|
95
107
|
end
|
96
108
|
|
97
109
|
def fetch_marc_hash(...)
|
98
|
-
|
99
|
-
|
110
|
+
SourceStorage
|
111
|
+
.new(self)
|
112
|
+
.fetch_marc_hash(...)
|
100
113
|
end
|
101
114
|
|
102
115
|
def has_instance_status?(...)
|
103
|
-
|
104
|
-
|
116
|
+
Inventory
|
117
|
+
.new(self)
|
118
|
+
.has_instance_status?(...)
|
119
|
+
end
|
120
|
+
|
121
|
+
def data_import(...)
|
122
|
+
DataImport
|
123
|
+
.new(self)
|
124
|
+
.import(...)
|
105
125
|
end
|
106
126
|
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.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Mangiafico
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -58,6 +58,34 @@ dependencies:
|
|
58
58
|
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: marc
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: dry-monads
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
61
89
|
- !ruby/object:Gem::Dependency
|
62
90
|
name: rake
|
63
91
|
requirement: !ruby/object:Gem::Requirement
|
@@ -162,7 +190,9 @@ files:
|
|
162
190
|
- folio_client.gemspec
|
163
191
|
- lib/folio_client.rb
|
164
192
|
- lib/folio_client/authenticator.rb
|
193
|
+
- lib/folio_client/data_import.rb
|
165
194
|
- lib/folio_client/inventory.rb
|
195
|
+
- lib/folio_client/job_status.rb
|
166
196
|
- lib/folio_client/source_storage.rb
|
167
197
|
- lib/folio_client/token_wrapper.rb
|
168
198
|
- lib/folio_client/unexpected_response.rb
|
@@ -189,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
219
|
- !ruby/object:Gem::Version
|
190
220
|
version: '0'
|
191
221
|
requirements: []
|
192
|
-
rubygems_version: 3.3.
|
222
|
+
rubygems_version: 3.3.3
|
193
223
|
signing_key:
|
194
224
|
specification_version: 4
|
195
225
|
summary: Interface for interacting with the Folio ILS API.
|