folio_client 0.11.0 → 0.13.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/.autoupdate/postupdate +0 -0
- data/.rubocop/custom.yml +17 -3
- data/.rubocop.yml +5 -1
- data/Gemfile.lock +48 -30
- data/README.md +20 -4
- data/api_test.rb +38 -0
- data/lib/folio_client/data_import.rb +10 -7
- data/lib/folio_client/job_status.rb +15 -13
- data/lib/folio_client/source_storage.rb +33 -0
- data/lib/folio_client/version.rb +1 -1
- data/lib/folio_client.rb +56 -13
- metadata +7 -7
- data/lib/folio_client/token_wrapper.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3b597d27e912d256a895af940e312d24e85acf5c234f48788c6716a4e8947de
|
|
4
|
+
data.tar.gz: 9700fb07ff086e5b6c833897abd921adaa8efd19e173c8948484ad5d554614a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8256c90e8c80390e5536d4c988140d30074481d6d8b958a3e8321b81e6dc0f2f7634f3e5253abe7c9b60b22fbebb5246fe6f9052c93b658d621a108a1598ff9
|
|
7
|
+
data.tar.gz: 99e52e340c66cc0233e577e73840880c2a0f9b2eea758b42418784124a524d1c7d1fc73ede20dd8bdd77aa0d69f44843eebd0f82e08067306b687b0082dc326b
|
data/.autoupdate/postupdate
CHANGED
|
File without changes
|
data/.rubocop/custom.yml
CHANGED
|
@@ -34,9 +34,9 @@ RSpec/SubjectDeclaration: # new in 2.5
|
|
|
34
34
|
Enabled: true
|
|
35
35
|
RSpec/VerifiedDoubleReference: # new in 2.10.0
|
|
36
36
|
Enabled: true
|
|
37
|
-
|
|
37
|
+
FactoryBot/ConsistentParenthesesStyle: # new in 2.14
|
|
38
38
|
Enabled: true
|
|
39
|
-
|
|
39
|
+
FactoryBot/SyntaxMethods: # new in 2.7
|
|
40
40
|
Enabled: true
|
|
41
41
|
RSpec/Rails/AvoidSetupHook: # new in 2.4
|
|
42
42
|
Enabled: true
|
|
@@ -58,7 +58,7 @@ RSpec/DuplicatedMetadata: # new in 2.16
|
|
|
58
58
|
Enabled: true
|
|
59
59
|
RSpec/PendingWithoutReason: # new in 2.16
|
|
60
60
|
Enabled: true
|
|
61
|
-
|
|
61
|
+
FactoryBot/FactoryNameStyle: # new in 2.16
|
|
62
62
|
Enabled: true
|
|
63
63
|
RSpec/Rails/MinitestAssertions: # new in 2.17
|
|
64
64
|
Enabled: true
|
|
@@ -68,3 +68,17 @@ RSpec/SkipBlockInsideExample: # new in 2.19
|
|
|
68
68
|
Enabled: true
|
|
69
69
|
RSpec/Rails/TravelAround: # new in 2.19
|
|
70
70
|
Enabled: true
|
|
71
|
+
FactoryBot/AssociationStyle: # new in 2.23
|
|
72
|
+
Enabled: true
|
|
73
|
+
FactoryBot/FactoryAssociationWithStrategy: # new in 2.23
|
|
74
|
+
Enabled: true
|
|
75
|
+
FactoryBot/RedundantFactoryOption: # new in 2.23
|
|
76
|
+
Enabled: true
|
|
77
|
+
RSpec/BeEmpty: # new in 2.20
|
|
78
|
+
Enabled: true
|
|
79
|
+
RSpec/ContainExactly: # new in 2.19
|
|
80
|
+
Enabled: true
|
|
81
|
+
RSpec/IndexedLet: # new in 2.20
|
|
82
|
+
Enabled: true
|
|
83
|
+
RSpec/MatchArray: # new in 2.19
|
|
84
|
+
Enabled: true
|
data/.rubocop.yml
CHANGED
|
@@ -3,12 +3,16 @@ inherit_mode:
|
|
|
3
3
|
- Exclude
|
|
4
4
|
|
|
5
5
|
require:
|
|
6
|
+
- standard
|
|
7
|
+
- standard-custom
|
|
8
|
+
- standard-performance
|
|
6
9
|
- rubocop-performance
|
|
7
10
|
- rubocop-rspec
|
|
8
|
-
- standard
|
|
9
11
|
|
|
10
12
|
inherit_gem:
|
|
11
13
|
standard: config/base.yml
|
|
14
|
+
standard-performance: config/base.yml
|
|
15
|
+
standard-custom: config/base.yml
|
|
12
16
|
|
|
13
17
|
inherit_from:
|
|
14
18
|
- .rubocop/custom.yml
|
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.13.0)
|
|
5
5
|
activesupport (>= 4.2, < 8)
|
|
6
6
|
dry-monads
|
|
7
7
|
faraday
|
|
@@ -11,82 +11,92 @@ PATH
|
|
|
11
11
|
GEM
|
|
12
12
|
remote: https://rubygems.org/
|
|
13
13
|
specs:
|
|
14
|
-
activesupport (7.0.
|
|
14
|
+
activesupport (7.0.8)
|
|
15
15
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
16
16
|
i18n (>= 1.6, < 2)
|
|
17
17
|
minitest (>= 5.1)
|
|
18
18
|
tzinfo (~> 2.0)
|
|
19
|
-
addressable (2.8.
|
|
19
|
+
addressable (2.8.5)
|
|
20
20
|
public_suffix (>= 2.0.2, < 6.0)
|
|
21
21
|
ast (2.4.2)
|
|
22
|
+
base64 (0.1.1)
|
|
22
23
|
byebug (11.1.3)
|
|
23
24
|
concurrent-ruby (1.2.2)
|
|
24
25
|
crack (0.4.5)
|
|
25
26
|
rexml
|
|
26
27
|
diff-lcs (1.5.0)
|
|
27
28
|
docile (1.4.0)
|
|
28
|
-
dry-core (1.0.
|
|
29
|
+
dry-core (1.0.1)
|
|
29
30
|
concurrent-ruby (~> 1.0)
|
|
30
31
|
zeitwerk (~> 2.6)
|
|
31
32
|
dry-monads (1.6.0)
|
|
32
33
|
concurrent-ruby (~> 1.0)
|
|
33
34
|
dry-core (~> 1.0, < 2)
|
|
34
35
|
zeitwerk (~> 2.6)
|
|
35
|
-
faraday (2.7.
|
|
36
|
+
faraday (2.7.11)
|
|
37
|
+
base64
|
|
36
38
|
faraday-net_http (>= 2.0, < 3.1)
|
|
37
39
|
ruby2_keywords (>= 0.0.4)
|
|
38
40
|
faraday-net_http (3.0.2)
|
|
39
41
|
hashdiff (1.0.1)
|
|
40
|
-
i18n (1.
|
|
42
|
+
i18n (1.14.1)
|
|
41
43
|
concurrent-ruby (~> 1.0)
|
|
42
44
|
json (2.6.3)
|
|
43
45
|
language_server-protocol (3.17.0.3)
|
|
46
|
+
lint_roller (1.1.0)
|
|
44
47
|
marc (1.2.0)
|
|
45
48
|
rexml
|
|
46
49
|
scrub_rb (>= 1.0.1, < 2)
|
|
47
50
|
unf
|
|
48
|
-
minitest (5.
|
|
49
|
-
parallel (1.
|
|
50
|
-
parser (3.2.
|
|
51
|
+
minitest (5.20.0)
|
|
52
|
+
parallel (1.23.0)
|
|
53
|
+
parser (3.2.2.3)
|
|
51
54
|
ast (~> 2.4.1)
|
|
52
|
-
|
|
55
|
+
racc
|
|
56
|
+
public_suffix (5.0.3)
|
|
57
|
+
racc (1.7.1)
|
|
53
58
|
rainbow (3.1.1)
|
|
54
59
|
rake (13.0.6)
|
|
55
|
-
regexp_parser (2.
|
|
56
|
-
rexml (3.2.
|
|
60
|
+
regexp_parser (2.8.1)
|
|
61
|
+
rexml (3.2.6)
|
|
57
62
|
rspec (3.12.0)
|
|
58
63
|
rspec-core (~> 3.12.0)
|
|
59
64
|
rspec-expectations (~> 3.12.0)
|
|
60
65
|
rspec-mocks (~> 3.12.0)
|
|
61
|
-
rspec-core (3.12.
|
|
66
|
+
rspec-core (3.12.2)
|
|
62
67
|
rspec-support (~> 3.12.0)
|
|
63
|
-
rspec-expectations (3.12.
|
|
68
|
+
rspec-expectations (3.12.3)
|
|
64
69
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
65
70
|
rspec-support (~> 3.12.0)
|
|
66
|
-
rspec-mocks (3.12.
|
|
71
|
+
rspec-mocks (3.12.6)
|
|
67
72
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
68
73
|
rspec-support (~> 3.12.0)
|
|
69
|
-
rspec-support (3.12.
|
|
70
|
-
rubocop (1.
|
|
74
|
+
rspec-support (3.12.1)
|
|
75
|
+
rubocop (1.56.4)
|
|
76
|
+
base64 (~> 0.1.1)
|
|
71
77
|
json (~> 2.3)
|
|
78
|
+
language_server-protocol (>= 3.17.0)
|
|
72
79
|
parallel (~> 1.10)
|
|
73
|
-
parser (>= 3.2.
|
|
80
|
+
parser (>= 3.2.2.3)
|
|
74
81
|
rainbow (>= 2.2.2, < 4.0)
|
|
75
82
|
regexp_parser (>= 1.8, < 3.0)
|
|
76
83
|
rexml (>= 3.2.5, < 4.0)
|
|
77
|
-
rubocop-ast (>= 1.
|
|
84
|
+
rubocop-ast (>= 1.28.1, < 2.0)
|
|
78
85
|
ruby-progressbar (~> 1.7)
|
|
79
86
|
unicode-display_width (>= 2.4.0, < 3.0)
|
|
80
|
-
rubocop-ast (1.
|
|
87
|
+
rubocop-ast (1.29.0)
|
|
81
88
|
parser (>= 3.2.1.0)
|
|
82
|
-
rubocop-capybara (2.
|
|
89
|
+
rubocop-capybara (2.19.0)
|
|
83
90
|
rubocop (~> 1.41)
|
|
84
|
-
rubocop-
|
|
91
|
+
rubocop-factory_bot (2.24.0)
|
|
92
|
+
rubocop (~> 1.33)
|
|
93
|
+
rubocop-performance (1.19.1)
|
|
85
94
|
rubocop (>= 1.7.0, < 2.0)
|
|
86
95
|
rubocop-ast (>= 0.4.0)
|
|
87
|
-
rubocop-rspec (2.
|
|
96
|
+
rubocop-rspec (2.24.1)
|
|
88
97
|
rubocop (~> 1.33)
|
|
89
98
|
rubocop-capybara (~> 2.17)
|
|
99
|
+
rubocop-factory_bot (~> 2.22)
|
|
90
100
|
ruby-progressbar (1.13.0)
|
|
91
101
|
ruby2_keywords (0.0.5)
|
|
92
102
|
scrub_rb (1.0.1)
|
|
@@ -96,21 +106,29 @@ GEM
|
|
|
96
106
|
simplecov_json_formatter (~> 0.1)
|
|
97
107
|
simplecov-html (0.12.3)
|
|
98
108
|
simplecov_json_formatter (0.1.4)
|
|
99
|
-
standard (1.
|
|
109
|
+
standard (1.31.1)
|
|
100
110
|
language_server-protocol (~> 3.17.0.2)
|
|
101
|
-
|
|
102
|
-
rubocop
|
|
111
|
+
lint_roller (~> 1.0)
|
|
112
|
+
rubocop (~> 1.56.2)
|
|
113
|
+
standard-custom (~> 1.0.0)
|
|
114
|
+
standard-performance (~> 1.2)
|
|
115
|
+
standard-custom (1.0.2)
|
|
116
|
+
lint_roller (~> 1.0)
|
|
117
|
+
rubocop (~> 1.50)
|
|
118
|
+
standard-performance (1.2.0)
|
|
119
|
+
lint_roller (~> 1.1)
|
|
120
|
+
rubocop-performance (~> 1.19.0)
|
|
103
121
|
tzinfo (2.0.6)
|
|
104
122
|
concurrent-ruby (~> 1.0)
|
|
105
123
|
unf (0.1.4)
|
|
106
124
|
unf_ext
|
|
107
125
|
unf_ext (0.0.8.2)
|
|
108
|
-
unicode-display_width (2.
|
|
109
|
-
webmock (3.
|
|
126
|
+
unicode-display_width (2.5.0)
|
|
127
|
+
webmock (3.19.1)
|
|
110
128
|
addressable (>= 2.8.0)
|
|
111
129
|
crack (>= 0.3.2)
|
|
112
130
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
113
|
-
zeitwerk (2.6.
|
|
131
|
+
zeitwerk (2.6.12)
|
|
114
132
|
|
|
115
133
|
PLATFORMS
|
|
116
134
|
x86_64-darwin-19
|
|
@@ -130,4 +148,4 @@ DEPENDENCIES
|
|
|
130
148
|
webmock
|
|
131
149
|
|
|
132
150
|
BUNDLED WITH
|
|
133
|
-
2.4.
|
|
151
|
+
2.4.13
|
data/README.md
CHANGED
|
@@ -71,15 +71,15 @@ client.fetch_marc_hash(instance_hrid: "a7927874")
|
|
|
71
71
|
[{"003"=>"FOLIO"}....]
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
# Import
|
|
75
|
-
data_importer = client.data_import(
|
|
74
|
+
# Import MARC records into FOLIO
|
|
75
|
+
data_importer = client.data_import(records: [marc_record1, marc_record2], job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
|
|
76
76
|
# If called too quickly, might get Failure(:not_found)
|
|
77
77
|
data_importer.status
|
|
78
78
|
=> Failure(:pending)
|
|
79
79
|
data_importer.wait_until_complete
|
|
80
80
|
=> Success()
|
|
81
|
-
data_importer.
|
|
82
|
-
=> Success("in00000000010")
|
|
81
|
+
data_importer.instance_hrids
|
|
82
|
+
=> Success(["in00000000010", "in00000000011"])
|
|
83
83
|
|
|
84
84
|
# Get list of organizations (filtered with an optional query)
|
|
85
85
|
# see https://s3.amazonaws.com/foliodocs/api/mod-organizations/p/organizations.html#organizations_organizations_get
|
|
@@ -131,6 +131,22 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
|
131
131
|
|
|
132
132
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
133
133
|
|
|
134
|
+
## Integration Testing
|
|
135
|
+
|
|
136
|
+
To test that the gem works against the Folio APIs, run `api_test.rb` via:
|
|
137
|
+
|
|
138
|
+
```shell
|
|
139
|
+
# NOTE: This is bash syntax, YMMV
|
|
140
|
+
$ export OKAPI_PASSWORD=$(vault kv get --field=content puppet/application/folio/stage/app_sdr_password)
|
|
141
|
+
$ export OKAPI_TENANT=sul
|
|
142
|
+
$ export OKAPI_USER=app_sdr
|
|
143
|
+
$ export OKAPI_URL=https://okapi-stage.stanford.edu
|
|
144
|
+
# NOTE: The args below are a list of MARC files
|
|
145
|
+
$ bundle exec ruby ./api_test.rb /path/to/marc/files/test.mrc /another/marc/file/at/foobar.marc
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Inspect the output and make sure there are no errors.
|
|
149
|
+
|
|
134
150
|
## Contributing
|
|
135
151
|
|
|
136
152
|
Bug reports and pull requests are welcome on GitHub at https://github.com/sul-dlss/folio_client.
|
data/api_test.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "folio_client"
|
|
5
|
+
|
|
6
|
+
marc_files = *ARGV
|
|
7
|
+
|
|
8
|
+
client =
|
|
9
|
+
FolioClient.configure(
|
|
10
|
+
url: ENV["OKAPI_URL"],
|
|
11
|
+
login_params: {
|
|
12
|
+
username: ENV["OKAPI_USER"],
|
|
13
|
+
password: ENV["OKAPI_PASSWORD"]
|
|
14
|
+
},
|
|
15
|
+
okapi_headers: {
|
|
16
|
+
"X-Okapi-Tenant": ENV["OKAPI_TENANT"],
|
|
17
|
+
"User-Agent": "folio_client gem (testing)"
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
pp(client.fetch_marc_hash(instance_hrid: "a666"))
|
|
22
|
+
|
|
23
|
+
puts client.fetch_marc_xml(instance_hrid: "a666")
|
|
24
|
+
puts client.fetch_marc_xml(barcode: "20503330279")
|
|
25
|
+
|
|
26
|
+
records = marc_files.flat_map do |marc_file_path|
|
|
27
|
+
MARC::Reader.new(marc_file_path).to_a
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
data_importer =
|
|
31
|
+
client.data_import(
|
|
32
|
+
records: records,
|
|
33
|
+
job_profile_id: "e34d7b92-9b83-11eb-a8b3-0242ac130003",
|
|
34
|
+
job_profile_name: "Default - Create instance and SRS MARC Bib"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
puts data_importer.wait_until_complete
|
|
38
|
+
puts data_importer.instance_hrids
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
|
-
require "marc"
|
|
5
4
|
require "stringio"
|
|
6
5
|
|
|
7
6
|
class FolioClient
|
|
@@ -14,17 +13,21 @@ class FolioClient
|
|
|
14
13
|
@client = client
|
|
15
14
|
end
|
|
16
15
|
|
|
17
|
-
# @param
|
|
16
|
+
# @param records [Array<MARC::Record>] records to be imported
|
|
18
17
|
# @param job_profile_id [String] job profile id to use for import
|
|
19
18
|
# @param job_profile_name [String] job profile name to use for import
|
|
20
19
|
# @return [JobStatus] a job status instance to get information about the data import job
|
|
21
|
-
def import(
|
|
20
|
+
def import(records:, job_profile_id:, job_profile_name:)
|
|
22
21
|
response_hash = client.post("/data-import/uploadDefinitions", {fileDefinitions: [{name: marc_filename}]})
|
|
23
22
|
upload_definition_id = response_hash.dig("fileDefinitions", 0, "uploadDefinitionId")
|
|
24
23
|
job_execution_id = response_hash.dig("fileDefinitions", 0, "jobExecutionId")
|
|
25
24
|
file_definition_id = response_hash.dig("fileDefinitions", 0, "id")
|
|
26
25
|
|
|
27
|
-
upload_file_response_hash = client.post(
|
|
26
|
+
upload_file_response_hash = client.post(
|
|
27
|
+
"/data-import/uploadDefinitions/#{upload_definition_id}/files/#{file_definition_id}",
|
|
28
|
+
marc_binary(records),
|
|
29
|
+
content_type: "application/octet-stream"
|
|
30
|
+
)
|
|
28
31
|
|
|
29
32
|
client.post(
|
|
30
33
|
"/data-import/uploadDefinitions/#{upload_definition_id}/processFiles",
|
|
@@ -51,16 +54,16 @@ class FolioClient
|
|
|
51
54
|
|
|
52
55
|
private
|
|
53
56
|
|
|
54
|
-
attr_reader :client, :
|
|
57
|
+
attr_reader :client, :job_profile_id, :job_profile_name
|
|
55
58
|
|
|
56
59
|
def marc_filename
|
|
57
60
|
@marc_filename ||= "#{DateTime.now.iso8601}.marc"
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
def marc_binary(
|
|
63
|
+
def marc_binary(records)
|
|
61
64
|
StringIO.open do |io|
|
|
62
65
|
MARC::Writer.new(io) do |writer|
|
|
63
|
-
writer.write(
|
|
66
|
+
records.each { |record| writer.write(record) }
|
|
64
67
|
end
|
|
65
68
|
io.string
|
|
66
69
|
end
|
|
@@ -17,15 +17,17 @@ class FolioClient
|
|
|
17
17
|
@job_execution_id = job_execution_id
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# @
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
20
|
+
# @todo An "ERROR" approach means one or more records failed, but it does
|
|
21
|
+
# not mean they all fail. We will likely need a more nuanced way to
|
|
22
|
+
# handle this eventually.
|
|
23
|
+
#
|
|
24
|
+
# @return [Dry::Monads::Result] Success() if job is complete,
|
|
25
|
+
# Failure(:pending) if job is still running,
|
|
26
|
+
# Failure(:not_found) if job is not found
|
|
24
27
|
def status
|
|
25
|
-
response_hash = client.get("/
|
|
28
|
+
response_hash = client.get("/change-manager/jobExecutions/#{job_execution_id}")
|
|
26
29
|
|
|
27
|
-
return Failure(:
|
|
28
|
-
return Failure(:pending) if response_hash.dig("sourceRecordSummary", "totalCreatedEntities").zero? && response_hash.dig("sourceRecordSummary", "totalUpdatedEntities").zero?
|
|
30
|
+
return Failure(:pending) if !["COMMITTED", "ERROR"].include?(response_hash["status"])
|
|
29
31
|
|
|
30
32
|
Success()
|
|
31
33
|
rescue ResourceNotFound
|
|
@@ -37,18 +39,18 @@ class FolioClient
|
|
|
37
39
|
wait_with_timeout(wait_secs: wait_secs, timeout_secs: timeout_secs) { status }
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
def
|
|
42
|
+
def instance_hrids
|
|
41
43
|
current_status = status
|
|
42
44
|
return current_status unless current_status.success?
|
|
43
45
|
|
|
44
|
-
@
|
|
46
|
+
@instance_hrids ||= wait_with_timeout do
|
|
45
47
|
response = client
|
|
46
48
|
.get("/metadata-provider/journalRecords/#{job_execution_id}")
|
|
47
49
|
.fetch("journalRecords", [])
|
|
48
|
-
.
|
|
49
|
-
|
|
50
|
+
.select { |journal_record| journal_record["entityType"] == "INSTANCE" && journal_record["actionStatus"] == "COMPLETED" }
|
|
51
|
+
.filter_map { |instance_record| instance_record["entityHrId"] }
|
|
50
52
|
|
|
51
|
-
response.
|
|
53
|
+
response.empty? ? Failure() : Success(response)
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
|
|
@@ -61,7 +63,7 @@ class FolioClient
|
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def default_timeout_secs
|
|
64
|
-
|
|
66
|
+
10 * 60
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def wait_with_timeout(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
class FolioClient
|
|
4
4
|
# Lookup records in Folio Source Storage
|
|
5
5
|
class SourceStorage
|
|
6
|
+
FIELDS_TO_REMOVE = %w[001 003].freeze
|
|
7
|
+
|
|
6
8
|
attr_accessor :client
|
|
7
9
|
|
|
8
10
|
# @param client [FolioClient] the configured client
|
|
@@ -24,5 +26,36 @@ class FolioClient
|
|
|
24
26
|
|
|
25
27
|
response_hash["sourceRecords"].first["parsedRecord"]["content"]
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
# get marc bib data as MARCXML from folio given an instance HRID
|
|
31
|
+
# @param instance_hrid [String] the instance HRID to use for MARC lookup
|
|
32
|
+
# @param barcode [String] the barcode to use for MARC lookup
|
|
33
|
+
# @return [String] MARCXML string
|
|
34
|
+
# @raise [ResourceNotFound]
|
|
35
|
+
# @raise [MultipleResourcesFound]
|
|
36
|
+
def fetch_marc_xml(instance_hrid: nil, barcode: nil)
|
|
37
|
+
raise ArgumentError, "Either a barcode or a Folio instance HRID must be provided" if barcode.nil? && instance_hrid.nil?
|
|
38
|
+
|
|
39
|
+
instance_hrid ||= client.fetch_hrid(barcode: barcode)
|
|
40
|
+
|
|
41
|
+
raise ResourceNotFound, "Catalog record not found. HRID: #{instance_hrid} | Barcode: #{barcode}" if instance_hrid.blank?
|
|
42
|
+
|
|
43
|
+
marc_record = MARC::Record.new_from_hash(
|
|
44
|
+
fetch_marc_hash(instance_hrid: instance_hrid)
|
|
45
|
+
)
|
|
46
|
+
updated_marc = MARC::Record.new
|
|
47
|
+
updated_marc.leader = marc_record.leader
|
|
48
|
+
marc_record.fields.each do |field|
|
|
49
|
+
# explicitly remove all listed tags from the record
|
|
50
|
+
next if FIELDS_TO_REMOVE.include?(field.tag)
|
|
51
|
+
|
|
52
|
+
updated_marc.fields << field
|
|
53
|
+
end
|
|
54
|
+
# explicitly inject the instance_hrid into the 001 field
|
|
55
|
+
updated_marc.fields << MARC::ControlField.new("001", instance_hrid)
|
|
56
|
+
# explicitly inject FOLIO into the 003 field
|
|
57
|
+
updated_marc.fields << MARC::ControlField.new("003", "FOLIO")
|
|
58
|
+
updated_marc.to_xml.to_s
|
|
59
|
+
end
|
|
27
60
|
end
|
|
28
61
|
end
|
data/lib/folio_client/version.rb
CHANGED
data/lib/folio_client.rb
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
|
4
4
|
require "active_support/core_ext/object/blank"
|
|
5
5
|
require "faraday"
|
|
6
|
-
require "
|
|
6
|
+
require "marc"
|
|
7
7
|
require "ostruct"
|
|
8
|
+
require "singleton"
|
|
8
9
|
require "zeitwerk"
|
|
9
10
|
|
|
10
11
|
# Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
|
|
@@ -47,26 +48,35 @@ class FolioClient
|
|
|
47
48
|
# @param url [String] the folio API URL
|
|
48
49
|
# @param login_params [Hash] the folio client login params (username:, password:)
|
|
49
50
|
# @param okapi_headers [Hash] the okapi specific headers to add (X-Okapi-Tenant:, User-Agent:)
|
|
51
|
+
# @return [FolioClient] the configured Singleton class
|
|
50
52
|
def configure(url:, login_params:, okapi_headers:, timeout: default_timeout)
|
|
51
53
|
instance.config = OpenStruct.new(
|
|
54
|
+
# For the initial token, use a dummy value to avoid hitting any APIs
|
|
55
|
+
# during configuration, allowing `with_token_refresh_when_unauthorized` to handle
|
|
56
|
+
# auto-magic token refreshing. Why not immediately get a valid token? Our apps
|
|
57
|
+
# commonly invoke client `.configure` methods in the initializer in all
|
|
58
|
+
# application environments, even those that are never expected to
|
|
59
|
+
# connect to production APIs, such as local development machines.
|
|
60
|
+
#
|
|
61
|
+
# NOTE: `nil` and blank string cannot be used as dummy values here as
|
|
62
|
+
# they lead to a malformed request to be sent, which triggers an
|
|
63
|
+
# exception not rescued by `with_token_refresh_when_unauthorized`
|
|
64
|
+
token: "a temporary dummy token to avoid hitting the API before it is needed",
|
|
52
65
|
url: url,
|
|
53
66
|
login_params: login_params,
|
|
54
67
|
okapi_headers: okapi_headers,
|
|
55
68
|
timeout: timeout
|
|
56
69
|
)
|
|
57
70
|
|
|
58
|
-
# NOTE: The token cannot be set above, since `#connection` relies on
|
|
59
|
-
# `instance.config` parameters having already been set.
|
|
60
|
-
instance.config.token = Authenticator.token(login_params, connection)
|
|
61
|
-
|
|
62
71
|
self
|
|
63
72
|
end
|
|
64
73
|
|
|
65
|
-
delegate :config, :connection, :data_import, :default_timeout,
|
|
66
|
-
:fetch_external_id, :fetch_hrid, :fetch_instance_info,
|
|
67
|
-
:
|
|
68
|
-
:
|
|
69
|
-
|
|
74
|
+
delegate :config, :connection, :data_import, :default_timeout,
|
|
75
|
+
:edit_marc_json, :fetch_external_id, :fetch_hrid, :fetch_instance_info,
|
|
76
|
+
:fetch_marc_hash, :fetch_marc_xml, :get, :has_instance_status?,
|
|
77
|
+
:http_get_headers, :http_post_and_put_headers, :interface_details,
|
|
78
|
+
:job_profiles, :organization_interfaces, :organizations, :post, :put, to:
|
|
79
|
+
:instance end
|
|
70
80
|
|
|
71
81
|
attr_accessor :config
|
|
72
82
|
|
|
@@ -74,7 +84,7 @@ class FolioClient
|
|
|
74
84
|
# @param path [String] the path to the Folio API request
|
|
75
85
|
# @param params [Hash] params to get to the API
|
|
76
86
|
def get(path, params = {})
|
|
77
|
-
response =
|
|
87
|
+
response = with_token_refresh_when_unauthorized do
|
|
78
88
|
connection.get(path, params, {"x-okapi-token": config.token})
|
|
79
89
|
end
|
|
80
90
|
|
|
@@ -91,7 +101,7 @@ class FolioClient
|
|
|
91
101
|
# @param body [Object] body to post to the API as JSON
|
|
92
102
|
def post(path, body = nil, content_type: "application/json")
|
|
93
103
|
req_body = (content_type == "application/json") ? body&.to_json : body
|
|
94
|
-
response =
|
|
104
|
+
response = with_token_refresh_when_unauthorized do
|
|
95
105
|
req_headers = {
|
|
96
106
|
"x-okapi-token": config.token,
|
|
97
107
|
"content-type": content_type
|
|
@@ -112,7 +122,7 @@ class FolioClient
|
|
|
112
122
|
# @param body [Object] body to put to the API as JSON
|
|
113
123
|
def put(path, body = nil, content_type: "application/json")
|
|
114
124
|
req_body = (content_type == "application/json") ? body&.to_json : body
|
|
115
|
-
response =
|
|
125
|
+
response = with_token_refresh_when_unauthorized do
|
|
116
126
|
req_headers = {
|
|
117
127
|
"x-okapi-token": config.token,
|
|
118
128
|
"content-type": content_type
|
|
@@ -166,6 +176,13 @@ class FolioClient
|
|
|
166
176
|
.fetch_marc_hash(...)
|
|
167
177
|
end
|
|
168
178
|
|
|
179
|
+
# @see SourceStorage#fetch_marc_xml
|
|
180
|
+
def fetch_marc_xml(...)
|
|
181
|
+
SourceStorage
|
|
182
|
+
.new(self)
|
|
183
|
+
.fetch_marc_xml(...)
|
|
184
|
+
end
|
|
185
|
+
|
|
169
186
|
# @see Inventory#has_instance_status?
|
|
170
187
|
def has_instance_status?(...)
|
|
171
188
|
Inventory
|
|
@@ -218,4 +235,30 @@ class FolioClient
|
|
|
218
235
|
def default_timeout
|
|
219
236
|
120
|
|
220
237
|
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
# Wraps API operations to request new access token if expired.
|
|
242
|
+
# @yieldreturn response [Faraday::Response] the response to inspect
|
|
243
|
+
#
|
|
244
|
+
# @note You likely want to make sure you're wrapping a _single_ HTTP request in this
|
|
245
|
+
# method, because 1) all calls in the block will be retried from the top if there's
|
|
246
|
+
# an authN failure detected, and 2) only the response returned by the block will be
|
|
247
|
+
# inspected for authN failure.
|
|
248
|
+
# Related: consider that the client instance and its token will live across many
|
|
249
|
+
# invocations of the FolioClient methods once the client is configured by a consuming application,
|
|
250
|
+
# since this class is a Singleton. Thus, a token may expire between any two calls (i.e. it
|
|
251
|
+
# isn't necessary for a set of operations to collectively take longer than the token lifetime for
|
|
252
|
+
# expiry to fall in the middle of that related set of HTTP calls).
|
|
253
|
+
def with_token_refresh_when_unauthorized
|
|
254
|
+
response = yield
|
|
255
|
+
|
|
256
|
+
# if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
|
|
257
|
+
if response.status == 401
|
|
258
|
+
config.token = Authenticator.token(config.login_params, connection)
|
|
259
|
+
response = yield
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
response
|
|
263
|
+
end
|
|
221
264
|
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.13.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-
|
|
11
|
+
date: 2023-10-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -187,6 +187,7 @@ files:
|
|
|
187
187
|
- LICENSE
|
|
188
188
|
- README.md
|
|
189
189
|
- Rakefile
|
|
190
|
+
- api_test.rb
|
|
190
191
|
- folio_client.gemspec
|
|
191
192
|
- lib/folio_client.rb
|
|
192
193
|
- lib/folio_client/authenticator.rb
|
|
@@ -196,7 +197,6 @@ files:
|
|
|
196
197
|
- lib/folio_client/organizations.rb
|
|
197
198
|
- lib/folio_client/records_editor.rb
|
|
198
199
|
- lib/folio_client/source_storage.rb
|
|
199
|
-
- lib/folio_client/token_wrapper.rb
|
|
200
200
|
- lib/folio_client/unexpected_response.rb
|
|
201
201
|
- lib/folio_client/version.rb
|
|
202
202
|
homepage: https://github.com/sul-dlss/folio_client
|
|
@@ -206,7 +206,7 @@ metadata:
|
|
|
206
206
|
source_code_uri: https://github.com/sul-dlss/folio_client
|
|
207
207
|
changelog_uri: https://github.com/sul-dlss/folio_client/releases
|
|
208
208
|
rubygems_mfa_required: 'true'
|
|
209
|
-
post_install_message:
|
|
209
|
+
post_install_message:
|
|
210
210
|
rdoc_options: []
|
|
211
211
|
require_paths:
|
|
212
212
|
- lib
|
|
@@ -221,8 +221,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
221
221
|
- !ruby/object:Gem::Version
|
|
222
222
|
version: '0'
|
|
223
223
|
requirements: []
|
|
224
|
-
rubygems_version: 3.
|
|
225
|
-
signing_key:
|
|
224
|
+
rubygems_version: 3.4.19
|
|
225
|
+
signing_key:
|
|
226
226
|
specification_version: 4
|
|
227
227
|
summary: Interface for interacting with the Folio ILS API.
|
|
228
228
|
test_files: []
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class FolioClient
|
|
4
|
-
# Wraps API operations to request new access token if expired
|
|
5
|
-
class TokenWrapper
|
|
6
|
-
def self.refresh(config, connection)
|
|
7
|
-
yield.tap { |response| UnexpectedResponse.call(response) unless response.success? }
|
|
8
|
-
rescue UnauthorizedError
|
|
9
|
-
config.token = Authenticator.token(config.login_params, connection)
|
|
10
|
-
yield
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|