uri_service 0.5.5 → 0.6.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
- SHA1:
3
- metadata.gz: ca89e65074aae6d21cde5a483a23f27d5aede54b
4
- data.tar.gz: 45631b9e4b9bc8807a81ca2b688fd865c4fb6a9b
2
+ SHA256:
3
+ metadata.gz: 2a5e8309e33cf98f187e5f1dc39e5b0c52ea9098146cec6d417c7b4304b7cadc
4
+ data.tar.gz: d05d94ac0bbb449c1492a93fe4ad121dc8399a07843179899794ebdd8293b000
5
5
  SHA512:
6
- metadata.gz: d6de839ed335e958d9bf98289333d6284ca5c4b16a179dfc7fe67a1e205723a3c8764869c89759451ed283ad6e540bb90f53b1bfa83d250faadb0bc2485d19f5
7
- data.tar.gz: 9f75d78623f741c1fab3938d1eb821b224528e1feefea23ae04a763a5f5b7fc29edafabe6ced75aa484d9ba34275e33f9c7edf24428ea6c0f45cbeacfd5fda57
6
+ metadata.gz: 419b95b61f9acd1f729543b49ba530f8f1112a3b5d22aebb1a9ea1308e2fd64057dfbd5a674ab65b2d617d04fc7158ab4367c8dac7e626b66264ad09969f86b6
7
+ data.tar.gz: 6de069fd580769f921b9ca9780a201bc2410f3e7b0f908740b93b37c1935b4b205db662b29c11e8e3d8faf381bfa6d44821f0d6299f21996d06a3a95d54f195c
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A database-backed and Solr-cached lookup/creation service for URIs. Works with or without Rails.
4
4
 
5
+ Note: Version 0.6.x and later are meant for use with Solr 8.
6
+
5
7
  ### Major Concepts:
6
8
 
7
9
  **External Term (UriService::TermType::EXTERNAL)**
@@ -31,7 +33,7 @@ UriService::init({
31
33
  'local_uri_base' => 'http://id.example.com/term/',
32
34
  'temporary_uri_base' => 'com:example:id:temporary:',
33
35
  'solr' => {
34
- 'url' => 'http://localhost:8983/solr/uri_service_test',
36
+ 'url' => 'http://localhost:8983/solr/uri_service',
35
37
  'pool_size' => 5,
36
38
  'pool_timeout' => 5000
37
39
  }
@@ -53,7 +55,7 @@ client = UriService::Client.new({
53
55
  'local_uri_base' => 'http://id.example.com/term/',
54
56
  'temporary_uri_base' => 'com:example:id:temporary:',
55
57
  'solr' => {
56
- 'url' => 'http://localhost:8983/solr/uri_service_test',
58
+ 'url' => 'http://localhost:8983/solr/uri_service',
57
59
  'pool_size' => 5,
58
60
  'pool_timeout' => 5000
59
61
  }
@@ -83,7 +85,7 @@ development:
83
85
  local_uri_base: 'http://id.example.com/term/'
84
86
  temporary_uri_base: 'com:example:id:temporary:'
85
87
  solr:
86
- url: 'http://localhost:8983/solr/uri_service_development'
88
+ url: 'http://localhost:8983/solr/uri_service'
87
89
  pool_size: 5
88
90
  pool_timeout: 5000
89
91
  database:
@@ -96,7 +98,7 @@ test:
96
98
  local_uri_base: 'http://id.example.com/term/'
97
99
  temporary_uri_base: 'com:example:id:temporary:'
98
100
  solr:
99
- url: 'http://localhost:8983/solr/uri_service_test'
101
+ url: 'http://localhost:9983/solr/uri_service'
100
102
  pool_size: 5
101
103
  pool_timeout: 5000
102
104
  database:
@@ -109,7 +111,7 @@ production:
109
111
  local_uri_base: 'http://id.example.com/term/'
110
112
  temporary_uri_base: 'com:example:id:temporary:'
111
113
  solr:
112
- url: 'http://localhost:9983/solr/uri_service_production'
114
+ url: 'http://localhost:8983/solr/uri_service_prod'
113
115
  pool_size: 5
114
116
  pool_timeout: 5000
115
117
  database:
@@ -19,13 +19,15 @@ namespace :uri_service do
19
19
  puts "[Warning] Exception creating rspec rake tasks. This message can be ignored in environments that intentionally do not pull in the RSpec gem (i.e. production)."
20
20
  puts e
21
21
  end
22
-
22
+
23
23
  desc "CI build"
24
24
  task :ci do
25
- Rake::Task["uri_service:ci_with_solr_6_wrapper"].invoke
26
- #Rake::Task["uri_service:ci_with_jetty_wrapper"].invoke
25
+ ENV['APP_ENV'] = 'test'
26
+ Rake::Task["uri_service:ci_prepare"].invoke
27
+ Rake::Task["uri_service:docker:setup_config_files"].invoke
28
+ Rake::Task["uri_service:ci_impl"].invoke
27
29
  end
28
-
30
+
29
31
  desc "Preparation steps for the CI run"
30
32
  task :ci_prepare do
31
33
  # Delete existing test database
@@ -36,65 +38,36 @@ namespace :uri_service do
36
38
  client.create_required_tables
37
39
  FileUtils.mkdir_p('tmp')
38
40
  end
39
-
40
- desc "CI build (using SolrWrapper and Solr 6)"
41
- task ci_with_solr_6_wrapper: :ci_prepare do
42
- solr_version = '6.3.0'
43
- instance_dir = File.join('tmp', "solr-#{solr_version}")
44
- FileUtils.rm_rf(instance_dir)
45
-
46
- puts "Unpacking and starting solr...\n"
47
- SolrWrapper.wrap({
48
- port: 9983,
49
- version: solr_version,
50
- verbose: false,
51
- mirror_url: 'http://lib-solr-mirror.princeton.edu/dist/',
52
- managed: true,
53
- download_path: File.join('tmp', "solr-#{solr_version}.zip"),
54
- instance_dir: instance_dir,
55
- }) do |solr_wrapper_instance|
56
-
57
- # Create collection
58
- solr_wrapper_instance.with_collection(name: 'uri_service_test', dir: File.join('spec/fixtures', 'uri_service_test_cores/uri_service_test-solr6-conf')) do |collection_name|
41
+
42
+ desc 'CI build just running specs'
43
+ task :ci_impl do
44
+ docker_wrapper do
45
+ duration = Benchmark.realtime do
59
46
  Rake::Task["uri_service:rspec"].invoke
60
47
  end
61
-
62
- puts 'Stopping solr...'
48
+ puts "\nCI run finished in #{duration} seconds."
63
49
  end
64
50
  end
65
-
66
- desc "CI build (using JettyWrapper)"
67
- task ci_with_jetty_wrapper: :ci_prepare do
68
-
69
- Jettywrapper.url = "https://github.com/cul/hydra-jetty/archive/solr-only.zip"
70
- Jettywrapper.jetty_dir = File.join('tmp', 'jetty-test')
71
-
72
- unless File.exists?(Jettywrapper.jetty_dir)
73
- puts "\n" + 'No test jetty found. Will download / unzip a copy now.' + "\n"
51
+
52
+ def docker_wrapper(&block)
53
+ unless ENV['APP_ENV'] == 'test'
54
+ raise 'This task should only be run in the test environment (because it clears docker volumes)'
74
55
  end
75
-
76
- Rake::Task["jetty:clean"].invoke # Clear and recreate previous jetty directory
77
-
78
- # Copy solr core fixture to new solr instance
79
- FileUtils.cp_r('spec/fixtures/uri_service_test_cores/uri_service_test', File.join(Jettywrapper.jetty_dir, 'solr'))
80
- # Update solr.xml configuration file so that it recognizes this code
81
- solr_xml_data = File.read(File.join(Jettywrapper.jetty_dir, 'solr/solr.xml'))
82
- solr_xml_data.gsub!('</cores>', ' <core name="uri_service_test" instanceDir="uri_service_test" />' + "\n" + ' </cores>')
83
- File.open(File.join(Jettywrapper.jetty_dir, 'solr/solr.xml'), 'w') { |file| file.write(solr_xml_data) }
84
-
85
- jetty_params = Jettywrapper.load_config.merge({
86
- jetty_home: Jettywrapper.jetty_dir,
87
- solr_home: 'solr',
88
- startup_wait: 75,
89
- jetty_port: 9983,
90
- java_version: '>= 1.8',
91
- java_opts: ["-XX:MaxPermSize=128m", "-Xmx256m"]
92
- })
93
- error = Jettywrapper.wrap(jetty_params) do
94
- Rake::Task["uri_service:rspec"].invoke
56
+
57
+ # Stop docker if it's currently running (so we can delete any old volumes)
58
+ Rake::Task['uri_service:docker:stop'].invoke
59
+ # Rake tasks must be re-enabled if you want to call them again later during the same run
60
+ Rake::Task['uri_service:docker:stop'].reenable
61
+
62
+ ENV['app_env_confirmation'] = ENV['APP_ENV'] # setting this to skip prompt in volume deletion task
63
+ Rake::Task['uri_service:docker:delete_volumes'].invoke
64
+
65
+ Rake::Task['uri_service:docker:start'].invoke
66
+ begin
67
+ block.call
68
+ ensure
69
+ Rake::Task['uri_service:docker:stop'].invoke
95
70
  end
96
- raise "test failures: #{error}" if error
97
-
98
71
  end
99
72
 
100
- end
73
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+ require 'open3'
3
+ require 'net/http'
4
+ require 'rainbow'
5
+
6
+ namespace :uri_service do
7
+ namespace :docker do
8
+ def docker_compose_file_path
9
+ UriService.root.join("docker/docker-compose.#{ENV['APP_ENV']}.yml")
10
+ end
11
+
12
+ def docker_compose_config
13
+ YAML.load_file(docker_compose_file_path)
14
+ end
15
+
16
+ def wait_for_solr_cores_to_load
17
+ expected_port = docker_compose_config['services']['solr']['ports'][0].split(':')[0]
18
+ url_to_check = "http://localhost:#{expected_port}/solr/uri_service/admin/system"
19
+ puts "Waiting for Solr to become available (at #{url_to_check})..."
20
+ Timeout.timeout(20, Timeout::Error, 'Timed out during Solr startup check.') do
21
+ loop do
22
+ begin
23
+ sleep 0.25
24
+ status_code = Net::HTTP.get_response(URI(url_to_check)).code
25
+ if status_code == '200' # Solr is ready to receive requests
26
+ puts 'Solr is available.'
27
+ break
28
+ end
29
+ rescue EOFError, Errno::ECONNRESET => e
30
+ # Try again in response to the above error types
31
+ next
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def running?
38
+ status = `docker compose -f #{UriService.root.join(docker_compose_file_path)} ps`
39
+ status.split("n").count > 1
40
+ end
41
+
42
+ task :setup_config_files do
43
+ docker_compose_template_dir = UriService.root.join('docker/templates')
44
+ docker_compose_dest_dir = UriService.root.join('docker')
45
+ Dir.foreach(docker_compose_template_dir) do |entry|
46
+ next unless entry.end_with?('.yml')
47
+ src_path = File.join(docker_compose_template_dir, entry)
48
+ dst_path = File.join(docker_compose_dest_dir, entry.gsub('.template', ''))
49
+ if File.exist?(dst_path)
50
+ puts Rainbow("File already exists (skipping): #{dst_path}").blue.bright + "\n"
51
+ else
52
+ FileUtils.cp(src_path, dst_path)
53
+ puts Rainbow("Created file at: #{dst_path}").green
54
+ end
55
+ end
56
+ end
57
+
58
+ task :start do
59
+ puts "Starting...\n"
60
+ if running?
61
+ puts "\nAlready running."
62
+ else
63
+ # NOTE: This command rebuilds the container images before each run, to ensure they're
64
+ # always up to date. In most cases, the overhead is minimal if the Dockerfile for an image
65
+ # hasn't changed since the last build.
66
+ `docker compose -f #{docker_compose_file_path} up --build --detach --wait`
67
+ wait_for_solr_cores_to_load
68
+ puts "\nStarted."
69
+ end
70
+ end
71
+
72
+ task :stop do
73
+ puts "Stopping...\n"
74
+ if running?
75
+ puts "\n"
76
+ `docker compose -f #{UriService.root.join(docker_compose_file_path)} down`
77
+ puts "\nStopped"
78
+ else
79
+ puts "Already stopped."
80
+ end
81
+ end
82
+
83
+ task :restart do
84
+ Rake::Task['uri_service:docker:stop'].invoke
85
+ Rake::Task['uri_service:docker:start'].invoke
86
+ end
87
+
88
+ task :status do
89
+ puts running? ? 'Running.' : 'Not running.'
90
+ end
91
+
92
+ task :delete_volumes do
93
+ if running?
94
+ puts 'Error: The volumes are currently in use. Please stop the docker services before deleting the volumes.'
95
+ next
96
+ end
97
+
98
+ puts Rainbow("This will delete ALL Solr data for the selected app "\
99
+ "environment (#{ENV['APP_ENV']}) and cannot be undone. Please confirm that you want to continue "\
100
+ "by typing the name of the selected Rails environment (#{ENV['APP_ENV']}):").red.bright
101
+ print '> '
102
+ response = ENV['app_env_confirmation'] || $stdin.gets.chomp
103
+
104
+ puts ""
105
+
106
+ if response != ENV['APP_ENV']
107
+ puts "Aborting because \"#{ENV['APP_ENV']}\" was not entered."
108
+ next
109
+ end
110
+
111
+ config = docker_compose_config
112
+ volume_prefix = config['name']
113
+ full_volume_names = config['volumes'].keys.map { |short_name| "#{volume_prefix}_#{short_name}" }
114
+
115
+ full_volume_names.map do |full_volume_name|
116
+ if JSON.parse(Open3.capture3("docker volume inspect '#{full_volume_name}'")[0]).length.positive?
117
+ `docker volume rm '#{full_volume_name}'`
118
+ puts Rainbow("Deleted: #{full_volume_name}").green
119
+ else
120
+ puts Rainbow("Skipped: #{full_volume_name} (already deleted)").blue.bright
121
+ end
122
+ end
123
+
124
+ puts 'Done.'
125
+ end
126
+ end
127
+ end
@@ -1,31 +1,33 @@
1
1
  class UriService::Client
2
-
2
+
3
3
  attr_reader :db, :rsolr_pool, :local_uri_base, :temporary_uri_base
4
-
4
+
5
5
  ALPHANUMERIC_UNDERSCORE_KEY_REGEX = /\A[a-z]+[a-z0-9_]*\z/
6
6
  CORE_FIELD_NAMES = ['uri', 'vocabulary_string_key', 'value', 'authority', 'type', 'internal_id']
7
7
  VALID_TYPES = [UriService::TermType::EXTERNAL, UriService::TermType::LOCAL, UriService::TermType::TEMPORARY]
8
-
8
+
9
9
  def initialize(opts)
10
10
  raise UriService::InvalidOptsError, "Must supply opts['local_uri_base'] to initialize method." if opts['local_uri_base'].nil?
11
11
  raise UriService::InvalidOptsError, "Must supply opts['temporary_uri_base'] to initialize method." if opts['temporary_uri_base'].nil?
12
12
  raise UriService::InvalidOptsError, "Must supply opts['database'] to initialize method." if opts['database'].nil?
13
13
  raise UriService::InvalidOptsError, "Must supply opts['solr'] to initialize method." if opts['solr'].nil?
14
-
14
+
15
15
  # Set local_uri_base and temporary_uri_base
16
16
  @local_uri_base = opts['local_uri_base']
17
17
  @temporary_uri_base = opts['temporary_uri_base']
18
-
18
+
19
19
  # Create DB connection pool
20
20
  @db = Sequel.connect(opts['database'])
21
-
21
+
22
22
  # Create Solr connection pool
23
23
  @rsolr_pool = ConnectionPool.new( size: opts['solr']['pool_size'], timeout: (opts['solr']['pool_timeout'].to_f/1000.to_f) ) { RSolr.connect(:url => opts['solr']['url']) }
24
+
25
+ @auto_commit_after_term_creation = opts['solr'].fetch('auto_commit_after_term_creation', true).to_s == 'true'
24
26
  end
25
-
27
+
26
28
  def reindex_all_terms(clear=false, print_progress_to_console=false)
27
29
  self.handle_database_disconnect do
28
-
30
+
29
31
  if print_progress_to_console
30
32
  puts "Getting database term count..."
31
33
  total = @db[UriService::TERMS].count
@@ -33,13 +35,13 @@ class UriService::Client
33
35
  puts "Number of terms to index: #{total.to_s}"
34
36
  puts ""
35
37
  end
36
-
38
+
37
39
  if clear
38
40
  @rsolr_pool.with do |rsolr|
39
41
  rsolr.delete_by_query('*:*');
40
42
  end
41
43
  end
42
-
44
+
43
45
  # Need to use unambiguous order when using paged_each, so we choose to order by DB :id
44
46
  @db[UriService::TERMS].order(:id).paged_each(:rows_per_fetch=>100) do |term_db_row|
45
47
  self.send_term_to_solr(
@@ -51,26 +53,26 @@ class UriService::Client
51
53
  term_db_row[:type],
52
54
  term_db_row[:id],
53
55
  false)
54
-
56
+
55
57
  if print_progress_to_console
56
58
  reindex_counter += 1
57
59
  print "\rIndexed #{reindex_counter.to_s} of #{total.to_s}"
58
60
  end
59
61
  end
60
-
62
+
61
63
  puts "\n" + "Committing solr updates..." if print_progress_to_console
62
64
  self.do_solr_commit
63
65
  puts "Done." if print_progress_to_console
64
66
  end
65
67
  end
66
-
68
+
67
69
  def disconnect!
68
70
  unless @db.nil?
69
71
  db_reference = @db
70
72
  @db = nil
71
73
  db_reference.disconnect
72
74
  end
73
-
75
+
74
76
  unless @rsolr_pool.nil?
75
77
  rsolr_pool_reference = @rsolr_pool
76
78
  @rsolr_pool = nil
@@ -79,10 +81,10 @@ class UriService::Client
79
81
  # but this doesn't hurt.
80
82
  end
81
83
  end
82
-
84
+
83
85
  def connected?
84
86
  return false if @db.nil? || @rsolr_pool.nil?
85
-
87
+
86
88
  begin
87
89
  self.test_connection
88
90
  return true
@@ -90,23 +92,23 @@ class UriService::Client
90
92
  return false
91
93
  end
92
94
  end
93
-
95
+
94
96
  def test_connection
95
97
  @db.test_connection # Raises Sequel::DatabaseConnectionError if connection didn't work
96
98
  @rsolr_pool.with do |rsolr|
97
- rsolr.get('admin/ping') # Raises Errno::ECONNREFUSED if connection didn't work
99
+ rsolr.get('admin/system') # Raises Errno::ECONNREFUSED if connection didn't work
98
100
  end
99
101
  end
100
-
102
+
101
103
  def required_tables_exist?
102
104
  return (UriService.required_tables - @db.tables).length == 0
103
105
  end
104
-
106
+
105
107
  def create_required_tables
106
108
  self.handle_database_disconnect do
107
-
109
+
108
110
  current_tables = @db.tables
109
-
111
+
110
112
  unless current_tables.include?(UriService::VOCABULARIES)
111
113
  @db.create_table UriService::VOCABULARIES do |t|
112
114
  primary_key :id
@@ -117,7 +119,7 @@ class UriService::Client
117
119
  else
118
120
  puts 'Skipped creation of table ' + UriService::VOCABULARIES.to_s + ' because it already exists.'
119
121
  end
120
-
122
+
121
123
  unless current_tables.include?(UriService::TERMS)
122
124
  @db.create_table UriService::TERMS do |t|
123
125
  primary_key :id
@@ -134,14 +136,14 @@ class UriService::Client
134
136
  else
135
137
  puts 'Skipped creation of table ' + UriService::TERMS.to_s + ' because it already exists.'
136
138
  end
137
-
139
+
138
140
  end
139
141
  end
140
-
142
+
141
143
  ##################
142
144
  # Create methods #
143
145
  ##################
144
-
146
+
145
147
  def create_vocabulary(string_key, display_label)
146
148
  self.handle_database_disconnect do
147
149
  if string_key.to_s == 'all'
@@ -151,7 +153,7 @@ class UriService::Client
151
153
  unless string_key =~ ALPHANUMERIC_UNDERSCORE_KEY_REGEX
152
154
  raise UriService::InvalidVocabularyStringKeyError, "Invalid key (can only include lower case letters, numbers or underscores, but cannot start with an underscore): " + string_key
153
155
  end
154
-
156
+
155
157
  @db.transaction do
156
158
  begin
157
159
  @db[UriService::VOCABULARIES].insert(string_key: string_key, display_label: display_label)
@@ -161,27 +163,27 @@ class UriService::Client
161
163
  end
162
164
  end
163
165
  end
164
-
166
+
165
167
  # Creates a new term
166
168
  def create_term(type, opts)
167
169
  raise UriService::InvalidTermTypeError, 'Invalid type: ' + type unless VALID_TYPES.include?(type)
168
-
170
+
169
171
  vocabulary_string_key = opts.delete(:vocabulary_string_key)
170
172
  value = opts.delete(:value)
171
173
  uri = opts.delete(:uri)
172
174
  authority = opts.has_key?(:authority) ? opts.delete(:authority) : ''
173
175
  authority = '' if authority.nil?
174
176
  additional_fields = opts.delete(:additional_fields) || {}
175
-
177
+
176
178
  if type == UriService::TermType::EXTERNAL
177
179
  # URI is required
178
180
  raise UriService::InvalidOptsError, "A uri must be supplied for terms of type #{type}." if uri.nil?
179
-
181
+
180
182
  return create_term_impl(type, vocabulary_string_key, value, uri, authority, additional_fields)
181
183
  else
182
184
  # URI should not be present
183
185
  raise UriService::InvalidOptsError, "A uri cannot supplied for term type: #{type}" unless uri.nil?
184
-
186
+
185
187
  if type == UriService::TermType::TEMPORARY
186
188
  # No two TEMPORARY terms within the same vocabulary can have the same value, so we generate a unique URI from a hash of the (vocabulary_string_key + value) to ensure uniqueness.
187
189
  uri = self.generate_uri_for_temporary_term(vocabulary_string_key, value)
@@ -204,15 +206,15 @@ class UriService::Client
204
206
  # Probabilistically, the error below should never be raised.
205
207
  raise UriService::CouldNotGenerateUriError, "UriService generated a duplicate random UUID (via SecureRandom.uuid) too many times in a row. Probabilistically, this should never happen."
206
208
  end
207
-
209
+
208
210
  end
209
211
  end
210
-
212
+
211
213
  def generate_uri_for_temporary_term(vocabulary_string_key, term_value)
212
214
  uri = URI(@temporary_uri_base + Digest::SHA256.hexdigest(vocabulary_string_key + term_value))
213
215
  return uri.to_s
214
216
  end
215
-
217
+
216
218
  def generate_frozen_term_hash(vocabulary_string_key, value, uri, authority, additional_fields, type, internal_id)
217
219
  hash_to_return = {}
218
220
  hash_to_return['uri'] = uri
@@ -221,20 +223,20 @@ class UriService::Client
221
223
  hash_to_return['authority'] = authority unless authority == ''
222
224
  hash_to_return['vocabulary_string_key'] = vocabulary_string_key
223
225
  hash_to_return['internal_id'] = internal_id
224
-
226
+
225
227
  additional_fields.each do |key, val|
226
228
  hash_to_return[key] = val
227
229
  end
228
-
230
+
229
231
  # Delete nil values
230
232
  hash_to_return.delete_if { |k, v| v.nil? }
231
-
233
+
232
234
  # Freeze hash
233
235
  hash_to_return.freeze # To make this a read-only hash
234
-
236
+
235
237
  return hash_to_return
236
238
  end
237
-
239
+
238
240
  def create_term_solr_doc(vocabulary_string_key, value, uri, authority, additional_fields, type, internal_id)
239
241
  doc = {}
240
242
  doc['uri'] = uri
@@ -243,21 +245,21 @@ class UriService::Client
243
245
  doc['vocabulary_string_key'] = vocabulary_string_key
244
246
  doc['authority'] = authority
245
247
  doc['internal_id'] = internal_id
246
-
248
+
247
249
  doc['additional_fields'] = JSON.generate(additional_fields)
248
-
250
+
249
251
  return doc
250
252
  end
251
-
253
+
252
254
  # Index the DB row term data into solr
253
- def send_term_to_solr(vocabulary_string_key, value, uri, authority, additional_fields, type, internal_id, commit=true)
255
+ def send_term_to_solr(vocabulary_string_key, value, uri, authority, additional_fields, type, internal_id, commit = @auto_commit_after_term_creation)
254
256
  doc = create_term_solr_doc(vocabulary_string_key, value, uri, authority, additional_fields, type, internal_id)
255
257
  @rsolr_pool.with do |rsolr|
256
258
  rsolr.add(doc)
257
259
  rsolr.commit if commit
258
260
  end
259
261
  end
260
-
262
+
261
263
  # Validates additional_fields and verifies that no reserved words are supplied
262
264
  def validate_additional_field_keys(additional_fields)
263
265
  additional_fields.each do |key, value|
@@ -269,41 +271,41 @@ class UriService::Client
269
271
  end
270
272
  end
271
273
  end
272
-
274
+
273
275
  ################
274
276
  # Find methods #
275
277
  ################
276
-
278
+
277
279
  def find_vocabulary(vocabulary_string_key)
278
280
  self.handle_database_disconnect do
279
281
  @db[UriService::VOCABULARIES].where(string_key: vocabulary_string_key).first
280
282
  end
281
283
  end
282
-
284
+
283
285
  # Finds the term with the given uri
284
286
  def find_term_by_uri(uri)
285
287
  results = self.find_terms_where({uri: uri}, 1)
286
288
  return results.length == 1 ? results.first : nil
287
289
  end
288
-
290
+
289
291
  # Finds the term with the given uri
290
292
  def find_term_by_internal_id(id)
291
293
  results = self.find_terms_where({internal_id: id}, 1)
292
294
  return results.length == 1 ? results.first : nil
293
295
  end
294
-
296
+
295
297
  # Finds terms that match the specified conditions
296
298
  def find_terms_where(opts, limit=10)
297
299
  fqs = []
298
-
300
+
299
301
  # Only search on allowed fields
300
302
  unsupported_search_fields = opts.map{|key, val| key.to_s} - CORE_FIELD_NAMES
301
303
  raise UriService::UnsupportedSearchFieldError, "Unsupported search fields: #{unsupported_search_fields.join(', ')}" if unsupported_search_fields.present?
302
-
304
+
303
305
  opts.each do |field_name, val|
304
306
  fqs << (field_name.to_s + ':"' + UriService.solr_escape(val.to_s) + '"')
305
307
  end
306
-
308
+
307
309
  @rsolr_pool.with do |rsolr|
308
310
  response = rsolr.get('select', params: {
309
311
  :q => '*:*',
@@ -323,9 +325,9 @@ class UriService::Client
323
325
  end
324
326
  end
325
327
  end
326
-
328
+
327
329
  def term_solr_doc_to_frozen_term_hash(term_solr_doc)
328
-
330
+
329
331
  uri = term_solr_doc.delete('uri')
330
332
  vocabulary_string_key = term_solr_doc.delete('vocabulary_string_key')
331
333
  value = term_solr_doc.delete('value')
@@ -333,19 +335,19 @@ class UriService::Client
333
335
  type = term_solr_doc.delete('type')
334
336
  additional_fields = JSON.parse(term_solr_doc.delete('additional_fields'))
335
337
  internal_id = term_solr_doc.delete('internal_id')
336
-
338
+
337
339
  return generate_frozen_term_hash(vocabulary_string_key, value, uri, authority, additional_fields, type, internal_id)
338
340
  end
339
-
341
+
340
342
  def find_terms_by_query(vocabulary_string_key, value_query, limit=10, start=0)
341
-
343
+
342
344
  if value_query.blank?
343
345
  return self.list_terms(vocabulary_string_key, limit, start)
344
346
  end
345
-
347
+
346
348
  terms_to_return = []
347
349
  @rsolr_pool.with do |rsolr|
348
-
350
+
349
351
  solr_params = {
350
352
  :q => UriService.solr_escape(value_query),
351
353
  :fq => 'vocabulary_string_key:' + UriService.solr_escape(vocabulary_string_key),
@@ -353,15 +355,15 @@ class UriService::Client
353
355
  :start => start,
354
356
  :sort => 'score desc, value_ssort asc, uri asc' # For consistent sorting
355
357
  }
356
-
358
+
357
359
  if value_query.length < 3
358
360
  # For efficiency, we only do whole term matches for queries < 3 characters
359
361
  solr_params[:qf] = 'value_suggest'
360
362
  solr_params[:pf] = 'value_suggest'
361
363
  end
362
-
364
+
363
365
  response = rsolr.get('suggest', params: solr_params)
364
-
366
+
365
367
  if response['response']['numFound'] > 0
366
368
  response['response']['docs'].each do |doc|
367
369
  terms_to_return << term_solr_doc_to_frozen_term_hash(doc)
@@ -370,11 +372,11 @@ class UriService::Client
370
372
  end
371
373
  return terms_to_return
372
374
  end
373
-
375
+
374
376
  ################
375
377
  # List methods #
376
378
  ################
377
-
379
+
378
380
  # Lists vocabularies alphabetically (by string key) and supports paging through results.
379
381
  def list_vocabularies(limit=10, start=0)
380
382
  self.handle_database_disconnect do
@@ -382,13 +384,13 @@ class UriService::Client
382
384
  return db_rows.map{|row| row.except(:id).stringify_keys!}
383
385
  end
384
386
  end
385
-
387
+
386
388
  # Lists terms alphabetically and supports paging through results.
387
389
  # Useful for browsing through a term list without a query.
388
390
  def list_terms(vocabulary_string_key, limit=10, start=0)
389
391
  terms_to_return = []
390
392
  @rsolr_pool.with do |rsolr|
391
-
393
+
392
394
  solr_params = {
393
395
  :fq => 'vocabulary_string_key:' + UriService.solr_escape(vocabulary_string_key),
394
396
  :q => '*:*',
@@ -396,7 +398,7 @@ class UriService::Client
396
398
  :start => start,
397
399
  :sort => 'value_ssort asc, uri asc' # Include 'uri asc' as part of sort to ensure consistent sorting
398
400
  }
399
-
401
+
400
402
  response = rsolr.get('select', params: solr_params)
401
403
  if response['response']['numFound'] > 0
402
404
  response['response']['docs'].each do |doc|
@@ -406,17 +408,17 @@ class UriService::Client
406
408
  end
407
409
  return terms_to_return
408
410
  end
409
-
411
+
410
412
  ##################
411
413
  # Delete methods #
412
414
  ##################
413
-
415
+
414
416
  def delete_vocabulary(vocabulary_string_key)
415
417
  self.handle_database_disconnect do
416
418
  @db[UriService::VOCABULARIES].where(string_key: vocabulary_string_key).delete
417
419
  end
418
420
  end
419
-
421
+
420
422
  def delete_term(uri, commit=true)
421
423
  self.handle_database_disconnect do
422
424
  @db.transaction do
@@ -428,37 +430,37 @@ class UriService::Client
428
430
  end
429
431
  end
430
432
  end
431
-
433
+
432
434
  ##################
433
435
  # Update methods #
434
436
  ##################
435
-
437
+
436
438
  def update_vocabulary(string_key, new_display_label)
437
439
  self.handle_database_disconnect do
438
440
  dataset = @db[UriService::VOCABULARIES].where(string_key: string_key)
439
441
  raise UriService::NonExistentVocabularyError, "No vocabulary found with string_key: " + string_key if dataset.count == 0
440
-
442
+
441
443
  @db.transaction do
442
444
  dataset.update(display_label: new_display_label)
443
445
  end
444
446
  end
445
447
  end
446
-
448
+
447
449
  # opts format: {:value => 'new value', :authority => 'newauthority', :additional_fields => {'key' => 'value'}}
448
450
  def update_term(uri, opts, merge_additional_fields=true)
449
451
  self.handle_database_disconnect do
450
452
  term_db_row = @db[UriService::TERMS].first(uri: uri)
451
453
  raise UriService::NonExistentUriError, "No term found with uri: " + uri if term_db_row.nil?
452
-
454
+
453
455
  new_value = opts[:value] || term_db_row[:value]
454
456
  new_authority = opts[:authority] || term_db_row[:authority]
455
457
  new_additional_fields = term_db_row[:additional_fields].nil? ? {} : JSON.parse(term_db_row[:additional_fields])
456
-
458
+
457
459
  if term_db_row[:type] == UriService::TermType::TEMPORARY && new_value != term_db_row[:value]
458
460
  # TEMPORARY terms cannot have their values changed, but it is possible to update other fields
459
461
  raise UriService::CannotChangeTemporaryTermValue, "The value of a temporary term cannot be changed. Delete unusued temporary terms or create a new one with a different value."
460
462
  end
461
-
463
+
462
464
  unless opts[:additional_fields].nil?
463
465
  if merge_additional_fields
464
466
  new_additional_fields.merge!(opts[:additional_fields])
@@ -468,16 +470,16 @@ class UriService::Client
468
470
  end
469
471
  end
470
472
  validate_additional_field_keys(new_additional_fields)
471
-
473
+
472
474
  @db.transaction do
473
475
  @db[UriService::TERMS].where(uri: uri).update(value: new_value, value_hash: Digest::SHA256.hexdigest(new_value), authority: new_authority, additional_fields: JSON.generate(new_additional_fields))
474
476
  self.send_term_to_solr(term_db_row[:vocabulary_string_key], new_value, uri, new_authority, new_additional_fields, term_db_row[:type], term_db_row[:id])
475
477
  end
476
-
478
+
477
479
  return generate_frozen_term_hash(term_db_row[:vocabulary_string_key], new_value, uri, new_authority, new_additional_fields, term_db_row[:type], term_db_row[:id])
478
480
  end
479
481
  end
480
-
482
+
481
483
  def handle_database_disconnect
482
484
  tries ||= 3
483
485
  begin
@@ -487,26 +489,26 @@ class UriService::Client
487
489
  retry unless tries == 0
488
490
  end
489
491
  end
490
-
492
+
491
493
  def do_solr_commit
492
494
  @rsolr_pool.with do |rsolr|
493
495
  rsolr.commit
494
496
  end
495
497
  end
496
-
498
+
497
499
  def clear_solr_index
498
500
  @rsolr_pool.with do |rsolr|
499
501
  rsolr.delete_by_query('*:*');
500
502
  rsolr.commit
501
503
  end
502
504
  end
503
-
505
+
504
506
  #########################
505
507
  # BEGIN PRIVATE METHODS #
506
508
  #########################
507
-
509
+
508
510
  private
509
-
511
+
510
512
  # Backing implementation for actual term creation in db/solr.
511
513
  # - Performs some data validations.
512
514
  # - Ensures uniqueness of URIs in database.
@@ -514,11 +516,11 @@ class UriService::Client
514
516
  # create a new TEMPORARY term with an existing value/vocabulary combo,
515
517
  # also adding non-existent supplied additional_fields to the existing temporary term.
516
518
  def create_term_impl(type, vocabulary_string_key, value, uri, authority, additional_fields)
517
-
519
+
518
520
  raise UriService::InvalidTermTypeError, 'Invalid type: ' + type unless VALID_TYPES.include?(type)
519
-
521
+
520
522
  self.handle_database_disconnect do
521
-
523
+
522
524
  if type == UriService::TermType::TEMPORARY
523
525
  # If this is a TEMPORARY term, we need to ensure that the temporary
524
526
  # passed in URI is a hash of the vocabulary + value, just in case this
@@ -529,20 +531,20 @@ class UriService::Client
529
531
  raise UriService::InvalidTemporaryTermUriError, "The supplied URI was not derived from the supplied (vocabulary_string_key+value) pair."
530
532
  end
531
533
  end
532
-
534
+
533
535
  unless uri =~ UriService::VALID_URI_REGEX
534
536
  raise UriService::InvalidUriError, "Invalid URI supplied during term creation: #{uri}"
535
537
  end
536
-
538
+
537
539
  #Ensure that vocabulary with vocabulary_string_key exists
538
540
  if self.find_vocabulary(vocabulary_string_key).nil?
539
541
  raise UriService::NonExistentVocabularyError, "There is no vocabulary with string key: " + vocabulary_string_key.to_s
540
542
  end
541
-
543
+
542
544
  # Stringify and validate keys for additional_fields
543
545
  additional_fields.stringify_keys!
544
546
  validate_additional_field_keys(additional_fields) # This method call raises an error if an invalid additional_field key is supplied
545
-
547
+
546
548
  @db.transaction do
547
549
  value_hash = Digest::SHA256.hexdigest(value)
548
550
 
@@ -559,41 +561,41 @@ class UriService::Client
559
561
  )
560
562
  send_term_to_solr(vocabulary_string_key, value, uri, authority, additional_fields, type, db_id)
561
563
  rescue Sequel::UniqueConstraintViolation
562
-
564
+
563
565
  # If the user is trying to create a new TEMPORARY term and we ran into a Sequel::UniqueConstraintViolation,
564
566
  # that means that the term already exists. We will return that existing term, but also update the term with
565
567
  # any non-existent additional_fields supplied by the user and during this create operation, and a supplied
566
568
  # authority if the term did not already have an authority.
567
569
  if type == UriService::TermType::TEMPORARY
568
570
  temporary_term = self.find_term_by_uri(uri)
569
-
571
+
570
572
  opts = {}
571
573
  non_existent_additional_fields = additional_fields.keys - temporary_term.keys
572
-
574
+
573
575
  if non_existent_additional_fields.length > 0
574
576
  additional_fields_to_merge_in = additional_fields.select{|k, v| non_existent_additional_fields.include?(k)}
575
577
  opts[:additional_fields] = additional_fields_to_merge_in
576
578
  end
577
-
579
+
578
580
  if temporary_term['authority'].nil? && authority.length > 0 && authority != temporary_term['authority']
579
581
  opts[:authority] = authority
580
582
  end
581
-
583
+
582
584
  if opts.size > 0
583
585
  temporary_term = UriService.client.update_term(temporary_term['uri'], opts, true)
584
586
  end
585
-
587
+
586
588
  return temporary_term
587
589
  end
588
-
590
+
589
591
  raise UriService::ExistingUriError, "A term already exists with uri: " + uri + " (conflict found via uri_hash check)"
590
592
  end
591
-
593
+
592
594
  return generate_frozen_term_hash(vocabulary_string_key, value, uri, authority, additional_fields, type, db_id)
593
595
  end
594
596
  end
595
597
  end
596
-
598
+
597
599
  end
598
600
 
599
601
  class UriService::Error < StandardError;end
@@ -610,4 +612,4 @@ class UriService::ExistingVocabularyStringKeyError < UriService::Error;end
610
612
  class UriService::NonExistentUriError < UriService::Error;end
611
613
  class UriService::NonExistentVocabularyError < UriService::Error;end
612
614
  class UriService::UnsupportedObjectTypeError < UriService::Error;end
613
- class UriService::UnsupportedSearchFieldError < UriService::Error;end
615
+ class UriService::UnsupportedSearchFieldError < UriService::Error;end
@@ -1,9 +1,9 @@
1
1
  module UriService
2
-
3
- VERSION = '0.5.5'
4
-
2
+
3
+ VERSION = '0.6.0'
4
+
5
5
  def self.version
6
6
  VERSION
7
7
  end
8
-
9
- end
8
+
9
+ end
data/lib/uri_service.rb CHANGED
@@ -6,36 +6,35 @@ require 'uri'
6
6
  require 'yaml'
7
7
 
8
8
  module UriService
9
-
10
9
  # Constants
11
10
  VOCABULARY = :vocabulary
12
11
  VOCABULARIES = :vocabularies
13
12
  TERM = :term
14
13
  TERMS = :terms
15
14
  VALID_URI_REGEX = /\A#{URI::regexp}\z/
16
-
15
+
17
16
  # Initialize the main instance of UriService::Client
18
17
  # opts format: { 'local_uri_base' => 'http://id.example.com/term/', temporary_uri_base: 'temporary:', 'solr' => {...solr config...}, 'database' => {...database config...} }
19
18
  def self.init(opts)
20
19
  if @client && @client.connected?
21
20
  @client.disconnect!
22
21
  end
23
-
22
+
24
23
  @client = UriService::Client.new(opts)
25
24
  end
26
25
 
27
26
  def self.client
28
27
  return @client
29
28
  end
30
-
29
+
31
30
  def self.version
32
31
  return UriService::VERSION
33
32
  end
34
-
33
+
35
34
  def self.required_tables
36
35
  return [UriService::VOCABULARIES, UriService::TERMS]
37
36
  end
38
-
37
+
39
38
  # Wrapper around escape method for different versions of RSolr
40
39
  def self.solr_escape(str)
41
40
  if RSolr.respond_to?(:solr_escape)
@@ -44,11 +43,14 @@ module UriService
44
43
  return RSolr.escape(str) # Fall back to older method
45
44
  end
46
45
  end
47
-
46
+
47
+ def self.root
48
+ Pathname.new(File.dirname(__dir__))
49
+ end
48
50
  end
49
51
 
50
52
  require "uri_service/version"
51
53
  require "uri_service/term_type"
52
54
  require "uri_service/client"
53
55
 
54
- require 'uri_service/railtie' if defined?(Rails)
56
+ require 'uri_service/railtie' if defined?(Rails)
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uri_service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.5
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric O'Hanlon
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2015-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rsolr
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,21 +25,21 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: sequel
28
+ name: connection_pool
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.26.0
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.26.0
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: connection_pool
42
+ name: rdf
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: activesupport
56
+ name: rsolr
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,89 +67,89 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rdf
70
+ name: sequel
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: 4.26.0
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: 4.26.0
83
83
  - !ruby/object:Gem::Dependency
84
- name: rake
84
+ name: jettywrapper
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '10.1'
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '10.1'
96
+ version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: rspec
98
+ name: rainbow
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '3.1'
103
+ version: '3.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '3.1'
110
+ version: '3.0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: jettywrapper
112
+ name: mysql2
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
117
+ version: 0.3.18
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: '0'
124
+ version: 0.3.18
125
125
  - !ruby/object:Gem::Dependency
126
- name: sqlite3
126
+ name: rake
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '10.1'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '10.1'
139
139
  - !ruby/object:Gem::Dependency
140
- name: mysql2
140
+ name: rspec
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ">="
143
+ - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 0.3.18
145
+ version: '3.1'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - ">="
150
+ - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 0.3.18
152
+ version: '3.1'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: solr_wrapper
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: sqlite3
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "<"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.6'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "<"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.6'
167
181
  description: A service for registering local URIs and performing both local and remote
168
182
  URI lookups.
169
183
  email: elo2112@columbia.edu
@@ -175,6 +189,7 @@ files:
175
189
  - lib/tasks/uri_service.rake
176
190
  - lib/tasks/uri_service/ci.rake
177
191
  - lib/tasks/uri_service/db.rake
192
+ - lib/tasks/uri_service/docker.rake
178
193
  - lib/tasks/uri_service/solr.rake
179
194
  - lib/uri_service.rb
180
195
  - lib/uri_service/client.rb
@@ -185,7 +200,7 @@ homepage: https://github.com/cul/uri_service
185
200
  licenses:
186
201
  - MIT
187
202
  metadata: {}
188
- post_install_message:
203
+ post_install_message:
189
204
  rdoc_options: []
190
205
  require_paths:
191
206
  - lib
@@ -200,9 +215,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
200
215
  - !ruby/object:Gem::Version
201
216
  version: '0'
202
217
  requirements: []
203
- rubyforge_project:
204
- rubygems_version: 2.6.14
205
- signing_key:
218
+ rubygems_version: 3.0.3.1
219
+ signing_key:
206
220
  specification_version: 4
207
221
  summary: A service for registering local URIs and performing both local and remote
208
222
  URI lookups.