folio_client 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e45342fc554ea5de7c33e31a2bc41fd1b550cf1d60a52edd47b18f2c861846e
4
- data.tar.gz: 13b0ddc4db0c2f73c31b2db26c48c9295ffa836e1e5e491b3308382d61021e72
3
+ metadata.gz: 51efd566f32da4e9349cde6c853d91b25db7db35be7f046e5cc8ae445470c25e
4
+ data.tar.gz: b244ae8d8d496baeb51c3e43834bc2f330fe04cbb943ad2e652efdfacbe0a6aa
5
5
  SHA512:
6
- metadata.gz: 5087d3b4a53111f8d442756e4ef4d7d5213c4232650fb46216c48aa877ea12c4bf5a2686ac90cbc575be3c1b7b271079357a00dd25a96f6cd7fc32a2738741f8
7
- data.tar.gz: 1094cccbea7e0d3cfc5f0b602c78b1b1b85aa2374cd1f264dcdf569c3b7bd907153f5f9342ddc5ccc7a3a45e68e5462060f5c4452cbc1c2c5a87fd6f4bd903d8
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.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
- 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
@@ -4,7 +4,7 @@ class FolioClient
4
4
  # Wraps API operations to request new access token if expired
5
5
  class TokenWrapper
6
6
  def self.refresh(config, connection)
7
- yield
7
+ yield.tap { |response| UnexpectedResponse.call(response) unless response.success? }
8
8
  rescue UnauthorizedError
9
9
  config.token = Authenticator.token(config.login_params, connection)
10
10
  yield
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = "0.6.0"
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
@@ -58,21 +59,35 @@ class FolioClient
58
59
  # @param path [String] the path to the Folio API request
59
60
  # @param request [Hash] params to get to the API
60
61
  def get(path, params = {})
61
- response = connection.get(path, params, {"x-okapi-token": config.token})
62
+ response = TokenWrapper.refresh(config, connection) do
63
+ connection.get(path, params, {"x-okapi-token": config.token})
64
+ end
62
65
 
63
66
  UnexpectedResponse.call(response) unless response.success?
64
67
 
68
+ return nil if response.body.blank?
69
+
65
70
  JSON.parse(response.body)
66
71
  end
67
72
 
68
73
  # Send an authenticated post request
74
+ # If the body is JSON, it will be automatically serialized
69
75
  # @param path [String] the path to the Folio API request
70
- # @param request [json] request body to post to the API
71
- def post(path, request = nil)
72
- response = connection.post(path, request, {"x-okapi-token": config.token})
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
79
+ response = TokenWrapper.refresh(config, connection) do
80
+ req_headers = {
81
+ "x-okapi-token": config.token,
82
+ "content-type": content_type
83
+ }
84
+ connection.post(path, req_body, req_headers)
85
+ end
73
86
 
74
87
  UnexpectedResponse.call(response) unless response.success?
75
88
 
89
+ return nil if response.body.blank?
90
+
76
91
  JSON.parse(response.body)
77
92
  end
78
93
 
@@ -85,25 +100,27 @@ class FolioClient
85
100
  end
86
101
 
87
102
  # Public methods available on the FolioClient below
88
- # Wrap methods in `TokenWrapper` to ensure a new token is fetched automatically if expired
89
103
  def fetch_hrid(...)
90
- TokenWrapper.refresh(config, connection) do
91
- inventory = Inventory.new(self)
92
- inventory.fetch_hrid(...)
93
- end
104
+ Inventory
105
+ .new(self)
106
+ .fetch_hrid(...)
94
107
  end
95
108
 
96
109
  def fetch_marc_hash(...)
97
- TokenWrapper.refresh(config, connection) do
98
- source_storage = SourceStorage.new(self)
99
- source_storage.fetch_marc_hash(...)
100
- end
110
+ SourceStorage
111
+ .new(self)
112
+ .fetch_marc_hash(...)
101
113
  end
102
114
 
103
115
  def has_instance_status?(...)
104
- TokenWrapper.refresh(config, connection) do
105
- inventory = Inventory.new(self)
106
- inventory.has_instance_status?(...)
107
- end
116
+ Inventory
117
+ .new(self)
118
+ .has_instance_status?(...)
119
+ end
120
+
121
+ def data_import(...)
122
+ DataImport
123
+ .new(self)
124
+ .import(...)
108
125
  end
109
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.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-02-28 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.4.5
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.