folio_api_client 0.4.3 → 0.5.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: c9101df88d1bcae72de9607a5646631b5b4a3ecb2f281f4e81064ea47b1ee9b3
4
- data.tar.gz: b9e838309973de0b1b66069bf2aa7c6a5c96f32f8a6b8d167a8cb9cc07d87626
3
+ metadata.gz: 4f56685abde3488956608b9a829b88079a22ed2dac7608ed792b1c6eafc2e662
4
+ data.tar.gz: 1fafd404369f0a3061c079e7922da6caea6735cb8b4a2e943512af815aaf3621
5
5
  SHA512:
6
- metadata.gz: 5f087226a449dc37b26445d40f0d99fe752c6f9203123bc0d6aef87f050a5257c0c6b371ee7441905406454933ecb9d37df1c539a21a82f068d6ded468902663
7
- data.tar.gz: e8d41ca37c67901d6db368509e72f1360bcd509e71a179aa6443defe544d145bcef7a2a56e0f1a40c16dae7ad1d914c0a3e1f36d9d6197470d08479883307e8b
6
+ metadata.gz: '09241496ff463b9c8f01a9b2a801698908d426cc30080ca72a81a2b09480a0ba8b116a7b1f2f6cad1ad13f53ae00872b202f3faff30d306ab18620ceaee40266'
7
+ data.tar.gz: 2a33854f99b12cbf278573a8e4ec3eed28474bb9b14a4ca9af8089905718952d2b804daf810006c8ecea6c9cb7d4620fec3ec5d9b7d00a9502bf5f1a87efda29
data/README.md CHANGED
@@ -54,6 +54,9 @@ client.find_instance_record(instance_record_id: 'some-instance-record-id')
54
54
  client.find_instance_record(instance_record_hrid: 'some-instance-record-hrid')
55
55
  client.find_source_record(instance_record_id: 'some-instance-record-id')
56
56
  client.find_source_record(instance_record_hrid: 'some-instance-record-hrid')
57
+ client.find_source_marc_records(modified_since: '2025-01-01T00:00:00Z') { |marc_record_hash| }
58
+ client.find_source_marc_records(with_965_value: '965abc') { |marc_record_hash| }
59
+ client.find_source_marc_records(modified_since: '2025-01-01T00:00:00Z', with_965_value: '965abc') { |marc_record_hash| }
57
60
 
58
61
  # Convert a FOLIO MARC source record to a marc gem MARC::Record object:
59
62
  source_record = client.find_source_record(instance_record_id: 'some-instance-record-id')
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioApiClient
4
- module Finders
4
+ module Finders # rubocop:disable Metrics/ModuleLength
5
5
  def find_item_record(barcode:)
6
6
  item_search_results = self.get('/item-storage/items', { query: "barcode==#{barcode}", limit: 2 })['items']
7
7
  return nil if item_search_results.empty?
@@ -90,6 +90,52 @@ class FolioApiClient
90
90
  source_record_search_results['sourceRecords'].first
91
91
  end
92
92
 
93
+ # Retrieve and yield marc records, filtered by the given modified_since and with_965_value parameters.
94
+ # NOTE: This method skips staff-suppressed FOLIO records.
95
+ def find_source_marc_records(modified_since: nil, with_965_value: nil, &block) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
96
+ # FOLIO does not allow an offset value higher than 9999, but this method needs to be able to retrieve
97
+ # result sets that have more than 9999 results, so we break big queries up into a bunch of smaller
98
+ # queries that split up the results based on the first character of the instance UUIDs. The first character
99
+ # of a UUID is a hex character (0-9 or a-f), which means that we will perform 16 different searches
100
+ # (i.e. "all of the results that have instance ids that start with a", then "all of the results that have
101
+ # instance ids that start with b", and so on).
102
+
103
+ # Since we're splitting up the query into a bunch of different sub-queries, we need to do a
104
+ # non-prefix-filtered query first just to get the total number of results.
105
+ total_query = response = self.get(
106
+ 'search/instances',
107
+ marc_records_query(modified_since: modified_since, with_965_value: with_965_value).merge({ limit: 0 })
108
+ )
109
+ total_records = total_query['totalRecords']
110
+
111
+ with_uuid_prefixes do |uuid_prefix|
112
+ query = marc_records_query(modified_since: modified_since, with_965_value: with_965_value,
113
+ uuid_prefix: uuid_prefix)
114
+ loop do
115
+ response = self.get('search/instances', query)
116
+ process_marc_for_instance(response['instances'], total_records, &block) if block
117
+ break if (query[:offset] + query[:limit]) >= response['totalRecords']
118
+
119
+ query[:offset] += query[:limit]
120
+ end
121
+ end
122
+ end
123
+
124
+ # UUIDs can start with
125
+ def with_uuid_prefixes(&block)
126
+ (('0'..'9').to_a + ('a'..'f').to_a).each(&block)
127
+ end
128
+
129
+ def process_marc_for_instance(instances, total_records, &block)
130
+ instances.each do |instance|
131
+ source_record = find_source_record(instance_record_id: instance['id'])
132
+ next if source_record.nil? # Occasionally, we find an instance record without a source record. Skip these.
133
+
134
+ marc_content = source_record.dig('parsedRecord', 'content')
135
+ yield(marc_content, total_records) if marc_content && block
136
+ end
137
+ end
138
+
93
139
  def source_record_query(instance_record_id: nil, instance_record_hrid: nil)
94
140
  return { instanceId: instance_record_id } if instance_record_id
95
141
  return { instanceHrid: instance_record_hrid } if instance_record_hrid
@@ -112,5 +158,34 @@ class FolioApiClient
112
158
  raise FolioApiClient::Exceptions::MissingQueryFieldError,
113
159
  'Missing query field. Must supply a code.'
114
160
  end
161
+
162
+ def marc_records_query(modified_since: nil, with_965_value: nil, uuid_prefix: nil) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity
163
+ params = { limit: 100, offset: 0 }
164
+
165
+ if modified_since.nil? && with_965_value.nil?
166
+ raise FolioApiClient::Exceptions::MissingQueryFieldError,
167
+ 'Missing query field. Must supply either modified_since or with_965_value.'
168
+ end
169
+
170
+ if modified_since && !modified_since.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/)
171
+ raise ArgumentError,
172
+ %(Invalid format for modified_since argument. Must be a date string like "2025-10-03T16:49:00Z".)
173
+ end
174
+
175
+ query_parts = []
176
+ query_parts << "metadata.updatedDate>=\"#{modified_since}\"" if modified_since
177
+ query_parts << %(identifiers.value="#{with_965_value}") if with_965_value
178
+
179
+ # Only include non-staff-suppressed records because staff-suppressed records represent deleted records in FOLIO.
180
+ # Reminder: staff-suppressed records are NOT the same as discovery-suppressed records. Discovery-suppressed
181
+ # records are ones that aren't displayed to the public, and we DO want to include discovery-suppressed records
182
+ # in the results returned by this query.
183
+ query_parts << 'staffSuppress==false'
184
+
185
+ query_parts << %(id="#{uuid_prefix}*") if uuid_prefix
186
+
187
+ params[:query] = query_parts.join(' and ')
188
+ params
189
+ end
115
190
  end
116
191
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioApiClient
4
- VERSION = '0.4.3'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -55,6 +55,12 @@ class FolioApiClient
55
55
  # one time in responde to a 401 or 403.
56
56
  refresh_auth_token!
57
57
 
58
+ # Re-run block
59
+ yield
60
+ rescue Faraday::ConnectionFailed
61
+ # If we make too many rapid requests in a row, FOLIO sometimes disconnects.
62
+ # If this happens, we'll sleep for a little while and retry the request.
63
+ sleep 5
58
64
  # Re-run block
59
65
  yield
60
66
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: folio_api_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric O'Hanlon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-25 00:00:00.000000000 Z
11
+ date: 2025-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday