preservation-client 7.4.0 → 8.0.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: 4d817dbb112d807e3b070711015fef2a49f7b750152a2295be3035ae50656ee6
4
- data.tar.gz: d68da61d268e666137004c72ce3418d9a3e46694e82007dc540819b1456baa35
3
+ metadata.gz: c187f939eb6d782795bb422308b7417f024db4c4e9d4d36e0685d7060c4150a0
4
+ data.tar.gz: 9c3f855e31677b447712ba3c35d67e0ab1127b83a469fc17664be82ac0b28f70
5
5
  SHA512:
6
- metadata.gz: 1a10922191104b4055c087b00209e4fe8c03608fa821349e3c50d2f8ec14e403c072d6347efdf038d854f517254511906bcf6ed688d89d073fa3daa072960f03
7
- data.tar.gz: 87a123f701eca1f7e7791c42684aa291e0801a1a9d29c26ea08f683cae8f696e02d2d6535cf2b199e240defd885d298ddbcbae490024728cdebbbf07832c09af
6
+ metadata.gz: 9800102447b72c245667cd69cc90c97878a11a28663cb56012d21678355cc4e4e90dccc6e98a8481cab53ec64507e66a5968ef63d01b083b10959e79424bb54b
7
+ data.tar.gz: ab270e3e62cba84ed0e560b63ab392fd48c07854b35f37abb0e0251d471d78e48df5a7cf3d8a78fb93c06a7450ea1656d99624ac1ff283e799a5f18e410c7dec
data/.circleci/config.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  version: 2.1
2
2
  orbs:
3
- ruby-rails: sul-dlss/ruby-rails@4.8.0
3
+ ruby-rails: sul-dlss/ruby-rails@4.12.0
4
4
  workflows:
5
5
  build:
6
6
  jobs:
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- preservation-client (7.4.0)
4
+ preservation-client (8.0.0)
5
5
  activesupport (>= 4.2)
6
6
  faraday (~> 2.0)
7
+ faraday-retry (~> 2.0)
7
8
  moab-versioning (>= 5.0.0, < 7)
8
9
  zeitwerk (~> 2.1)
9
10
 
@@ -31,7 +32,7 @@ GEM
31
32
  byebug (13.0.0)
32
33
  reline (>= 0.6.0)
33
34
  coderay (1.1.3)
34
- concurrent-ruby (1.3.6)
35
+ concurrent-ruby (1.3.7)
35
36
  connection_pool (3.0.2)
36
37
  crack (1.0.1)
37
38
  bigdecimal
@@ -40,17 +41,19 @@ GEM
40
41
  docile (1.4.1)
41
42
  drb (2.2.3)
42
43
  druid-tools (3.0.0)
43
- faraday (2.14.2)
44
+ faraday (2.14.3)
44
45
  faraday-net_http (>= 2.0, < 3.5)
45
46
  json
46
47
  logger
47
- faraday-net_http (3.4.2)
48
+ faraday-net_http (3.4.4)
48
49
  net-http (~> 0.5)
50
+ faraday-retry (2.4.0)
51
+ faraday (~> 2.0)
49
52
  hashdiff (1.2.1)
50
- i18n (1.14.8)
53
+ i18n (1.15.2)
51
54
  concurrent-ruby (~> 1.0)
52
55
  io-console (0.8.2)
53
- json (2.19.5)
56
+ json (2.20.0)
54
57
  language_server-protocol (3.17.0.5)
55
58
  lint_roller (1.1.0)
56
59
  logger (1.7.0)
@@ -65,11 +68,11 @@ GEM
65
68
  nokogiri-happymapper
66
69
  net-http (0.9.1)
67
70
  uri (>= 0.11.1)
68
- nokogiri (1.19.3-arm64-darwin)
71
+ nokogiri (1.19.4-arm64-darwin)
69
72
  racc (~> 1.4)
70
- nokogiri (1.19.3-x86_64-darwin)
73
+ nokogiri (1.19.4-x86_64-darwin)
71
74
  racc (~> 1.4)
72
- nokogiri (1.19.3-x86_64-linux-gnu)
75
+ nokogiri (1.19.4-x86_64-linux-gnu)
73
76
  racc (~> 1.4)
74
77
  nokogiri-happymapper (0.10.1)
75
78
  nokogiri (~> 1.5)
@@ -106,7 +109,7 @@ GEM
106
109
  diff-lcs (>= 1.2.0, < 2.0)
107
110
  rspec-support (~> 3.13.0)
108
111
  rspec-support (3.13.7)
109
- rubocop (1.86.2)
112
+ rubocop (1.88.0)
110
113
  json (~> 2.3)
111
114
  language_server-protocol (~> 3.17.0.2)
112
115
  lint_roller (~> 1.1.0)
@@ -123,9 +126,10 @@ GEM
123
126
  rubocop-rake (0.7.1)
124
127
  lint_roller (~> 1.1)
125
128
  rubocop (>= 1.72.1)
126
- rubocop-rspec (3.9.0)
129
+ rubocop-rspec (3.10.2)
127
130
  lint_roller (~> 1.1)
128
- rubocop (~> 1.81)
131
+ regexp_parser (>= 2.0)
132
+ rubocop (~> 1.86, >= 1.86.2)
129
133
  ruby-progressbar (1.13.0)
130
134
  securerandom (0.4.1)
131
135
  simplecov (0.22.0)
@@ -144,7 +148,7 @@ GEM
144
148
  addressable (>= 2.8.0)
145
149
  crack (>= 0.3.2)
146
150
  hashdiff (>= 0.4.0, < 2.0.0)
147
- zeitwerk (2.7.5)
151
+ zeitwerk (2.8.2)
148
152
 
149
153
  PLATFORMS
150
154
  arm64-darwin-23
data/README.md CHANGED
@@ -60,6 +60,11 @@ See https://github.com/sul-dlss/preservation_catalog#api for info on obtaining a
60
60
 
61
61
  Note that the preservation service is behind a firewall.
62
62
 
63
+ ### Retries
64
+ HTTP GET requests will be automatically retried for certain errors. The number of retries (`retries_max`) and interval between retries (`retry_interval`) can be specified as part of the configuration of the client.
65
+
66
+ Note that there is special retry behavior (cleaning up files on failure) for `content_to_file`, but it uses the same configuration.
67
+
63
68
  ## API Coverage
64
69
 
65
70
  - druids may be with or without the "druid:" prefix - 'oo000oo0000' or 'druid:oo000oo0000'
@@ -11,6 +11,12 @@ module Preservation
11
11
  class Client
12
12
  # API calls that are about Preserved Objects
13
13
  class Objects < VersionedApiService # rubocop:disable Metrics/ClassLength
14
+ def initialize(connection:, streaming_connection:, retry_max:, retry_interval:, api_version: DEFAULT_API_VERSION)
15
+ super(connection: connection, streaming_connection: streaming_connection, api_version: api_version)
16
+ @retry_max = retry_max
17
+ @retry_interval = retry_interval
18
+ end
19
+
14
20
  # @param [String] druid - with or without prefix: 'druid:bb123cd4567' OR 'bb123cd4567'
15
21
  # @return [Hash] the checksums and filesize for the druid
16
22
  def checksum(druid:)
@@ -69,14 +75,14 @@ module Preservation
69
75
  # @param [String] destination_filepath - absolute or relative path to desired destination file
70
76
  # @param [String] version - the version of the file requested (defaults to nil for latest version)
71
77
  # @param [String, nil] expected_md5 - optional expected md5 checksum for integrity validation
72
- # @param [Integer] max_retries - number of retry attempts after the initial attempt
73
- # @param [Float] delay_seconds - base delay for retry backoff
78
+ # @param [Integer] max - number of retry attempts after the initial attempt
79
+ # @param [Float] interval - base delay in seconds for exponential retry backoff
74
80
  # @raise [Preservation::Client::IntegrityError] if the expected_md5 is provided and does not match the actual md5
75
81
  # @raise [Preservation::Client::NotFoundError] if the specified file is not found
76
82
  # @raise [Preservation::Client::Error] for other errors encountered during download
77
83
  def content_to_file(druid:, filepath:, destination_filepath:, version: nil, expected_md5: nil, # rubocop:disable Metrics/ParameterLists
78
- max_retries: 3, delay_seconds: 0.5)
79
- with_retries(max_retries: max_retries, delay_seconds: delay_seconds) do
84
+ max: nil, interval: nil)
85
+ with_retries(max: max || @retry_max, interval: interval || @retry_interval) do
80
86
  temp_filepath = nil
81
87
 
82
88
  begin
@@ -138,16 +144,15 @@ module Preservation
138
144
  get("objects/#{druid}/file", { category: category, filepath: filepath, version: version }, on_data: on_data)
139
145
  end
140
146
 
141
- def with_retries(max_retries:, delay_seconds:)
147
+ def with_retries(max:, interval:)
142
148
  attempt = 0
143
149
 
144
150
  begin
145
151
  yield
146
152
  rescue StandardError => e
147
- raise if !retryable_error?(e) || attempt >= max_retries
153
+ raise if !retryable_error?(e) || attempt >= max
148
154
 
149
- sleep_seconds = delay_seconds.to_f * (attempt + 1)
150
- sleep(sleep_seconds) unless sleep_seconds.nil?
155
+ sleep(interval.to_f * (Client::RETRY_BACKOFF_FACTOR**attempt))
151
156
  attempt += 1
152
157
  retry
153
158
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Preservation
4
4
  class Client
5
- VERSION = '7.4.0'
5
+ VERSION = '8.0.0'
6
6
  end
7
7
  end
@@ -4,14 +4,15 @@ module Preservation
4
4
  class Client
5
5
  # @abstract API calls to a versioned endpoint
6
6
  class VersionedApiService
7
- def initialize(connection:, api_version: DEFAULT_API_VERSION)
7
+ def initialize(connection:, api_version: DEFAULT_API_VERSION, streaming_connection: nil)
8
8
  @connection = connection
9
9
  @api_version = api_version
10
+ @streaming_connection = streaming_connection
10
11
  end
11
12
 
12
13
  private
13
14
 
14
- attr_reader :connection, :api_version
15
+ attr_reader :connection, :api_version, :streaming_connection
15
16
 
16
17
  # @param path [String] path to be appended to connection url (no leading slash)
17
18
  def get_json(path, object_id)
@@ -42,7 +43,7 @@ module Preservation
42
43
  return http_response(:get, path, params) unless on_data
43
44
 
44
45
  req_url = "#{api_version}/#{path}"
45
- connection.get("#{api_version}/#{path}", params) do |req|
46
+ (streaming_connection || connection).get("#{api_version}/#{path}", params) do |req|
46
47
  req.options.on_data = proc do |chunk, size, env|
47
48
  if env.status >= 300
48
49
  errmsg = "Preservation::Client.#{caller_locations.first.label} " \
@@ -4,6 +4,7 @@ require 'active_support/core_ext/hash/indifferent_access'
4
4
  require 'active_support/core_ext/module/delegation'
5
5
  require 'active_support/core_ext/object/blank'
6
6
  require 'faraday'
7
+ require 'faraday/retry'
7
8
  require 'singleton'
8
9
  require 'zeitwerk'
9
10
 
@@ -53,13 +54,18 @@ module Preservation
53
54
 
54
55
  DEFAULT_API_VERSION = 'v1'
55
56
  DEFAULT_TIMEOUT = 300
57
+ DEFAULT_RETRY_MAX = 3
58
+ DEFAULT_RETRY_INTERVAL = 0.5
59
+ RETRY_BACKOFF_FACTOR = 2
56
60
  TOKEN_HEADER = 'Authorization'
57
61
 
58
62
  include Singleton
59
63
 
60
64
  # @return [Preservation::Client::Objects] an instance of the `Client::Objects` class
61
65
  def objects
62
- @objects ||= Objects.new(connection: connection, api_version: DEFAULT_API_VERSION)
66
+ @objects ||= Objects.new(connection: connection, streaming_connection: streaming_connection,
67
+ retry_max: retry_max, retry_interval: retry_interval,
68
+ api_version: DEFAULT_API_VERSION)
63
69
  end
64
70
 
65
71
  # @return [Preservation::Client::Catalog] an instance of the `Client::Catalog` class
@@ -71,13 +77,19 @@ module Preservation
71
77
  # @param [String] url the endpoint URL
72
78
  # @param [String] token a bearer token for HTTP authentication
73
79
  # @param [Integer] read_timeout the value in seconds of the read timeout
74
- def configure(url:, token:, read_timeout: DEFAULT_TIMEOUT)
80
+ # @param [Integer] retry_max number of retry attempts for GET requests
81
+ # @param [Float] retry_interval base delay in seconds between retries (exponential backoff)
82
+ def configure(url:, token:, read_timeout: DEFAULT_TIMEOUT,
83
+ retry_max: DEFAULT_RETRY_MAX, retry_interval: DEFAULT_RETRY_INTERVAL)
75
84
  instance.url = url
76
85
  instance.token = token
77
86
  instance.read_timeout = read_timeout
87
+ instance.retry_max = retry_max
88
+ instance.retry_interval = retry_interval
78
89
 
79
- # Force connection to be re-established when `.configure` is called
90
+ # Force connections to be re-established when `.configure` is called
80
91
  instance.connection = nil
92
+ instance.streaming_connection = nil
81
93
 
82
94
  self
83
95
  end
@@ -85,7 +97,7 @@ module Preservation
85
97
  delegate :objects, :update, to: :instance
86
98
  end
87
99
 
88
- attr_writer :connection, :read_timeout, :token, :url
100
+ attr_writer :connection, :read_timeout, :retry_interval, :retry_max, :streaming_connection, :token, :url
89
101
 
90
102
  delegate :update, to: :catalog
91
103
 
@@ -103,11 +115,35 @@ module Preservation
103
115
  @read_timeout || raise(Error, 'read timeout has not been configured')
104
116
  end
105
117
 
118
+ def retry_max
119
+ @retry_max || raise(Error, 'retry_max has not been configured')
120
+ end
121
+
122
+ def retry_interval
123
+ @retry_interval || raise(Error, 'retry_interval has not been configured')
124
+ end
125
+
106
126
  def connection
107
- @connection ||= Faraday.new(url, request: { read_timeout: read_timeout }) do |builder|
127
+ @connection ||= build_connection(with_retry: true)
128
+ end
129
+
130
+ def streaming_connection
131
+ @streaming_connection ||= build_connection(with_retry: false)
132
+ end
133
+
134
+ def build_connection(with_retry: true) # rubocop:disable Metrics/AbcSize
135
+ Faraday.new(url, request: { read_timeout: read_timeout }) do |builder|
108
136
  builder.use ErrorFaradayMiddleware
137
+ if with_retry
138
+ builder.request :retry, max: retry_max,
139
+ interval: retry_interval,
140
+ backoff_factor: RETRY_BACKOFF_FACTOR,
141
+ methods: [:get],
142
+ exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS +
143
+ [Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ServerError]
144
+ end
109
145
  builder.use Faraday::Request::UrlEncoded
110
- builder.use Faraday::Response::RaiseError # raise exceptions on 40x, 50x responses
146
+ builder.use Faraday::Response::RaiseError
111
147
  builder.adapter Faraday.default_adapter
112
148
  builder.headers[:user_agent] = user_agent
113
149
  builder.headers[TOKEN_HEADER] = "Bearer #{token}"
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_dependency 'activesupport', '>= 4.2'
33
33
  spec.add_dependency 'faraday', '~> 2.0'
34
+ spec.add_dependency 'faraday-retry', '~> 2.0'
34
35
  spec.add_dependency 'moab-versioning', '>= 5.0.0', '< 7'
35
36
  spec.add_dependency 'zeitwerk', '~> 2.1'
36
37
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: preservation-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.4.0
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Naomi Dushay
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-05-18 00:00:00.000000000 Z
10
+ date: 2026-06-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: faraday-retry
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: moab-versioning
42
56
  requirement: !ruby/object:Gem::Requirement