folio_client 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db6b3bd02eac883e93ca7a24725f1c3d4f1ef48b34a3aa6e9326070e1903f600
4
- data.tar.gz: e2592286592f8fe064b326373dc3f8ca3123a7fa775fedcb23f00da97dc58638
3
+ metadata.gz: 51efd566f32da4e9349cde6c853d91b25db7db35be7f046e5cc8ae445470c25e
4
+ data.tar.gz: b244ae8d8d496baeb51c3e43834bc2f330fe04cbb943ad2e652efdfacbe0a6aa
5
5
  SHA512:
6
- metadata.gz: 7ee03f5f2ae933bab57d6a3c6b2be09d7933b98f51ea66c07ffb09e821c4a5c9a08564125c01cd0858a4347465f8e588314b25bc26296851e61e593f8e1c42bd
7
- data.tar.gz: '0170689c2a3d9bd2547b976cd43b7258cc3dd6c51a5a8caba9ba62774d3425e62f624024c155d3b728a7dea8df30bce23ee7f111f628c5c1649c8327891e1cc9'
6
+ metadata.gz: 16a20ff6ec80f84d828f773ff8c3b1eeeda67dd0472674134aa975a822b37d0d9f86c65ef3ccb03d26c800ffa3a001386d973a77c03ab8d1ef9aeda7dfd312f4
7
+ data.tar.gz: e34c0b895a3bfea9174a6836c0a5af28afd1de88743430ba3b2d75494ebe42675e8e1b7a9015823e1e065ba28ed6dbbea864111cf5dd05fc3f5f7b8daf3938b7
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
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-02-10 00:58:06 UTC using RuboCop version 1.44.1.
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.6.1)
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
- minitest (5.17.0)
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.26.0)
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.11.0)
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 rubocop]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
5
5
  end
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 request [json] request body to post to the API
73
- def post(path, request = nil)
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
- connection.post(path, request, {"x-okapi-token": config.token})
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
- inventory = Inventory.new(self)
94
- inventory.fetch_hrid(...)
104
+ Inventory
105
+ .new(self)
106
+ .fetch_hrid(...)
95
107
  end
96
108
 
97
109
  def fetch_marc_hash(...)
98
- source_storage = SourceStorage.new(self)
99
- source_storage.fetch_marc_hash(...)
110
+ SourceStorage
111
+ .new(self)
112
+ .fetch_marc_hash(...)
100
113
  end
101
114
 
102
115
  def has_instance_status?(...)
103
- inventory = Inventory.new(self)
104
- inventory.has_instance_status?(...)
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.6.1
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-02 00:00:00.000000000 Z
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.7
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.