collectionspace-client 0.14.1 → 0.15.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.
data/examples/search.rb CHANGED
@@ -1,34 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
4
- require 'awesome_print'
5
- require 'collectionspace/client'
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ require "awesome_print"
5
+ require "collectionspace/client"
6
6
 
7
7
  config = CollectionSpace::Configuration.new(
8
- base_uri: 'https://core.dev.collectionspace.org/cspace-services',
9
- username: 'admin@core.collectionspace.org',
10
- password: 'Administrator'
8
+ base_uri: "https://core.dev.collectionspace.org/cspace-services",
9
+ username: "admin@core.collectionspace.org",
10
+ password: "Administrator"
11
11
  )
12
12
 
13
13
  client = CollectionSpace::Client.new(config)
14
14
 
15
15
  search_args = {
16
- path: 'groups',
17
- namespace: 'groups_common',
18
- field: 'title',
16
+ path: "groups",
17
+ namespace: "groups_common",
18
+ field: "title",
19
19
  expression: "ILIKE '%D%'"
20
20
  }
21
21
 
22
- puts 'Search: %D'
22
+ puts "Search: %D"
23
23
  response = client.search(
24
24
  CollectionSpace::Search.new.from_hash(search_args),
25
- { sortBy: CollectionSpace::Search::DEFAULT_SORT }
25
+ {sortBy: CollectionSpace::Search::DEFAULT_SORT}
26
26
  )
27
27
  if response.result.success?
28
- response.parsed['abstract_common_list']['list_item'].map do |i|
29
- puts i['uri']
28
+ response.parsed["abstract_common_list"]["list_item"].map do |i|
29
+ puts i["uri"]
30
30
  end
31
31
  end
32
32
 
33
- puts 'Object and authority term searches have been moved to spec.'
34
- puts 'See helpers_spec.rb examples describing find'
33
+ puts "Object and authority term searches have been moved to spec."
34
+ puts "See helpers_spec.rb examples describing find"
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- puts 'Update password has been moved to Rake task: `cli:update_password[args...]`'
3
+ puts "Update password has been moved to Rake task: `cli:update_password[args...]`"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CollectionSpace
4
+ # CollectionSpace batch
5
+ class Batch
6
+ def self.all
7
+ [
8
+ {
9
+ name: "Update Current Location",
10
+ notes: "Recompute the current location of Object records, based on the " \
11
+ "related Location/Movement/Inventory records. Runs on a single record " \
12
+ "or all records.",
13
+ doctype: %w[CollectionObject],
14
+ supports_single_doc: "true",
15
+ supports_doc_list: "false",
16
+ supports_group: "false",
17
+ supports_no_context: "true",
18
+ creates_new_focus: "false",
19
+ classname:
20
+ "org.collectionspace.services.batch.nuxeo.UpdateObjectLocationBatchJob"
21
+ },
22
+ {
23
+ name: "Update Inventory Status",
24
+ notes: "Set the inventory status of selected Object records. Runs on a " \
25
+ "record list only.",
26
+ doctype: %w[CollectionObject],
27
+ supports_single_doc: "false",
28
+ supports_doc_list: "true",
29
+ supports_group: "false",
30
+ supports_no_context: "false",
31
+ creates_new_focus: "false",
32
+ classname:
33
+ "org.collectionspace.services.batch.nuxeo.UpdateInventoryStatusBatchJob"
34
+ },
35
+ {
36
+ name: "Merge Authority Items",
37
+ notes: "Merge an authority item into a target, and update all " \
38
+ "referencing records. Runs on a single record only.",
39
+ doctype: %w[],
40
+ supports_single_doc: "true",
41
+ supports_doc_list: "false",
42
+ supports_group: "false",
43
+ supports_no_context: "false",
44
+ creates_new_focus: "false",
45
+ classname:
46
+ "org.collectionspace.services.batch.nuxeo.MergeAuthorityItemsBatchJob"
47
+ }
48
+ ]
49
+ end
50
+
51
+ def self.find(key, value)
52
+ all.find { |batch| batch[key] == value }
53
+ end
54
+ end
55
+ end
@@ -8,28 +8,39 @@ module CollectionSpace
8
8
 
9
9
  def initialize(config = Configuration.new)
10
10
  unless config.is_a? CollectionSpace::Configuration
11
- raise CollectionSpace::ArgumentError, 'Invalid configuration object'
11
+ raise CollectionSpace::ArgumentError, "Invalid configuration object"
12
12
  end
13
13
 
14
14
  @config = config
15
15
  end
16
16
 
17
17
  def get(path, options = {})
18
- request 'GET', path, options
18
+ request "GET", path, options
19
19
  end
20
20
 
21
21
  def post(path, payload, options = {})
22
22
  check_payload(payload)
23
- request 'POST', path, { body: payload }.merge(options)
23
+ request "POST", path, {body: payload}.merge(options)
24
+ end
25
+
26
+ def post_file(file, options = {})
27
+ file = File.expand_path(file)
28
+ raise ArgumentError, "cannot find file #{file}" unless File.exist? file
29
+
30
+ request "POST", "blobs", {
31
+ body: {
32
+ file: File.open(file)
33
+ }
34
+ }.merge(options)
24
35
  end
25
36
 
26
37
  def put(path, payload, options = {})
27
38
  check_payload(payload)
28
- request 'PUT', path, { body: payload }.merge(options)
39
+ request "PUT", path, {body: payload}.merge(options)
29
40
  end
30
41
 
31
42
  def delete(path)
32
- request 'DELETE', path
43
+ request "DELETE", path
33
44
  end
34
45
 
35
46
  private
@@ -10,10 +10,11 @@ module CollectionSpace
10
10
  page_size: 25,
11
11
  include_deleted: false,
12
12
  throttle: 0,
13
+ verbose: false,
13
14
  verify_ssl: true
14
15
  }.freeze
15
16
 
16
- attr_accessor :base_uri, :username, :password, :page_size, :include_deleted, :throttle, :verify_ssl
17
+ attr_accessor :base_uri, :username, :password, :page_size, :include_deleted, :throttle, :verbose, :verify_ssl
17
18
 
18
19
  def initialize(settings = {})
19
20
  settings = DEFAULTS.merge(settings)
@@ -4,17 +4,35 @@ module CollectionSpace
4
4
  # Helper methods for client requests
5
5
  module Helpers
6
6
  # add / update batch job
7
- def add_batch_job(name, template, data = {}, params = { pgSz: 100 })
8
- payload = Template.process(template, data)
9
- response = get('batch', { query: params })
10
- create_or_update(response, 'batch', 'name', name, payload)
7
+ def add_batch_job(name, template, data = {}, params = {pgSz: 100})
8
+ payload = Template.process(template, data)
9
+ response = get("batch", {query: params})
10
+ create_or_update(response, "batch", "name", name, payload)
11
+ end
12
+
13
+ # add / update batches and data updates
14
+ def add_batch(data = {}, params = {pgSz: 100})
15
+ payload = Template.process("batch", data)
16
+ response = get("batch", {query: params})
17
+ create_or_update(response, "batch", "name", data[:name], payload)
11
18
  end
12
19
 
13
20
  # add / update reports
14
- def add_report(data = {}, params = { pgSz: 100 })
15
- payload = Template.process('report', data)
16
- response = get('reports', { query: params })
17
- create_or_update(response, 'reports', 'name', data[:name], payload)
21
+ def add_report(data = {}, params = {pgSz: 100})
22
+ payload = Template.process("report", data)
23
+ response = get("reports", {query: params})
24
+ create_or_update(response, "reports", "name", data[:name], payload)
25
+ end
26
+
27
+ # returns Array of authority doctypes for use in setting up batches
28
+ def authority_doctypes
29
+ response = get("/servicegroups/authority")
30
+ unless response.result.success?
31
+ raise CollectionSpace::RequestError, response.result.body
32
+ end
33
+
34
+ result = response.result.parsed_response
35
+ result.dig("document", "servicegroups_common", "hasDocTypes", "hasDocType")
18
36
  end
19
37
 
20
38
  # get ALL records at path by paging through record set
@@ -24,10 +42,10 @@ module CollectionSpace
24
42
  return [] unless iterations.positive?
25
43
 
26
44
  Enumerator::Lazy.new(0...iterations) do |yielder, i|
27
- response = request('GET', path, options.merge(query: { pgNum: i }))
45
+ response = request("GET", path, options.merge(query: {pgNum: i}))
28
46
  raise CollectionSpace::RequestError, response.result.body unless response.result.success?
29
47
 
30
- items_in_page = response.parsed[list_type].fetch('itemsInPage', 0).to_i
48
+ items_in_page = response.parsed[list_type].fetch("itemsInPage", 0).to_i
31
49
  list_items = items_in_page.positive? ? response.parsed[list_type][list_item] : []
32
50
  list_items = [list_items] if items_in_page == 1
33
51
 
@@ -37,26 +55,25 @@ module CollectionSpace
37
55
 
38
56
  def count(path)
39
57
  list_type, = get_list_types(path)
40
- response = request('GET', path, query: { pgNum: 0, pgSz: 1 })
58
+ response = request("GET", path, query: {pgNum: 0, pgSz: 1})
41
59
  raise CollectionSpace::RequestError, response.result.body unless response.result.success?
42
60
 
43
- response.parsed[list_type]['totalItems'].to_i
61
+ response.parsed[list_type]["totalItems"].to_i
44
62
  end
45
63
 
46
64
  # get the tenant domain from a system required top level authority (person)
47
65
  def domain
48
- path = 'personauthorities'
49
- response = request('GET', path, query: { pgNum: 0, pgSz: 1 })
66
+ path = "personauthorities"
67
+ response = request("GET", path, query: {pgNum: 0, pgSz: 1})
50
68
  raise CollectionSpace::RequestError, response.result.body unless response.result.success?
51
69
 
52
- refname = response.parsed.dig(*get_list_types(path), 'refName')
70
+ refname = response.parsed.dig(*get_list_types(path), "refName")
53
71
  CollectionSpace::RefName.parse(refname)[:domain]
54
72
  end
55
73
 
56
74
  # find procedure or object by type and id
57
75
  # find authority/vocab term by type, subtype, and refname
58
- # rubocop:disable Metrics/ParameterLists
59
- def find(type:, value:, subtype: nil, field: nil, schema: 'common', sort: nil, operator: '=')
76
+ def find(type:, value:, subtype: nil, field: nil, schema: "common", sort: nil, operator: "=")
60
77
  service = CollectionSpace::Service.get(type: type, subtype: subtype)
61
78
  field ||= service[:term] # this will be set if it is an authority or vocabulary, otherwise nil
62
79
  field ||= service[:identifier]
@@ -64,7 +81,7 @@ module CollectionSpace
64
81
  path: service[:path],
65
82
  namespace: "#{service[:ns_prefix]}_#{schema}",
66
83
  field: field,
67
- expression: "#{operator} '#{value.gsub(/'/, '\\\\\'')}'"
84
+ expression: "#{operator} '#{value.gsub(/'/, "\\\\'")}'"
68
85
  )
69
86
  search(search_args, sortBy: CollectionSpace::Search::DEFAULT_SORT)
70
87
  end
@@ -75,74 +92,118 @@ module CollectionSpace
75
92
  # @param rel_type [String<'affects', 'hasBroader'>, nil] to be searched as `prd` value
76
93
  def find_relation(subject_csid:, object_csid:, rel_type: nil)
77
94
  if rel_type
78
- get('relations', query: { 'sbj' => subject_csid, 'obj' => object_csid, 'prd' => rel_type })
95
+ get("relations", query: {"sbj" => subject_csid, "obj" => object_csid, "prd" => rel_type})
79
96
  else
80
97
  warn(
81
98
  "No rel_type specified, so multiple types of relations between #{subject_csid} and #{object_csid} may be returned",
82
99
  uplevel: 1
83
100
  )
84
- get('relations', query: { 'sbj' => subject_csid, 'obj' => object_csid })
101
+ get("relations", query: {"sbj" => subject_csid, "obj" => object_csid})
85
102
  end
86
103
  end
87
104
 
88
105
  def get_list_types(path)
89
106
  {
90
- 'accounts' => %w[accounts_common_list account_list_item],
91
- 'relations' => %w[relations_common_list relation_list_item]
107
+ "accounts" => %w[accounts_common_list account_list_item],
108
+ "relations" => %w[relations_common_list relation_list_item]
92
109
  }.fetch(path, %w[abstract_common_list list_item])
93
110
  end
94
111
 
95
112
  def reindex_full_text(doctype, csids = [])
96
113
  if csids.any?
97
114
  run_job(
98
- 'Reindex Full Text', :reindex_full_text, :reindex_by_csids, { doctype: doctype, csids: csids }
115
+ "Reindex Full Text", :reindex_full_text, :reindex_by_csids, {doctype: doctype, csids: csids}
99
116
  )
100
117
  else
101
118
  run_job(
102
- 'Reindex Full Text', :reindex_full_text, :reindex_by_doctype, { doctype: doctype }
119
+ "Reindex Full Text", :reindex_full_text, :reindex_by_doctype, {doctype: doctype}
103
120
  )
104
121
  end
105
122
  end
106
123
 
107
- def reset_media_blob(id, url)
108
- raise CollectionSpace::ArgumentError, "Not a valid url #{url}" unless URI.parse(url).instance_of? URI::HTTPS
124
+ # @param id [String] media record's identificationNumber value
125
+ # @param url [String] blobUri value
126
+ # @param verbose [Boolean] whether to put brief report of outcome to STDOUT
127
+ # @param ensure_safe_url [Boolean] set to false if using FILE URIs or
128
+ # other non-HTTPS URIs
129
+ # @param delete_existing_blob [Boolean] set to false if you have already
130
+ # manually deleted blobs
131
+ def reset_media_blob(id:, url:, verbose: false,
132
+ ensure_safe_url: true,
133
+ delete_existing_blob: true)
134
+ if ensure_safe_url
135
+ unless URI.parse(url).instance_of? URI::HTTPS
136
+ raise CollectionSpace::ArgumentError, "Not a valid url #{url}"
137
+ end
138
+ end
109
139
 
110
- response = find(type: 'media', value: id, field: 'identificationNumber')
111
- raise CollectionSpace::RequestError, response.result.body unless response.result.success?
140
+ response = find(type: "media", value: id, field: "identificationNumber")
141
+ unless response.result.success?
142
+ if verbose
143
+ puts "#{id}\tfailure\tAPI request error: #{response.result.body}"
144
+ else
145
+ raise CollectionSpace::RequestError, response.result.body
146
+ end
147
+ end
112
148
 
113
149
  found = response.parsed
114
- total = found['abstract_common_list']['totalItems'].to_i
115
- raise CollectionSpace::NotFoundError, "Media #{id} not found" if total.zero?
116
- raise CollectionSpace::DuplicateIdFound, "Found multiple media records for #{id}" unless total == 1
150
+ total = found["abstract_common_list"]["totalItems"].to_i
151
+
152
+ if total.zero?
153
+ msg = "Media #{id} not found"
154
+ if verbose
155
+ puts "#{id}\tfailure\t#{msg}"
156
+ else
157
+ raise CollectionSpace::NotFoundError, msg
158
+ end
159
+ elsif total > 1
160
+ msg = "Found multiple media records for #{id}"
161
+ if verbose
162
+ puts "#{id}\tfailure\t#{msg}"
163
+ else
164
+ raise CollectionSpace::DuplicateIdFound, msg
165
+ end
166
+ end
117
167
 
118
- media_uri = found['abstract_common_list']['list_item']['uri']
119
- blob_csid = found['abstract_common_list']['list_item']['blobCsid']
168
+ media_uri = found["abstract_common_list"]["list_item"]["uri"]
120
169
 
121
- delete("/blobs/#{blob_csid}") if blob_csid
170
+ if delete_existing_blob
171
+ blob_csid = found["abstract_common_list"]["list_item"]["blobCsid"]
172
+ delete("/blobs/#{blob_csid}") if blob_csid
173
+ end
122
174
 
123
- payload = Template.process(:reset_media_blob, { id: id })
124
- put(media_uri, payload, query: { 'blobUri' => url })
175
+ payload = Template.process(:reset_media_blob, {id: id})
176
+ response = put(media_uri, payload, query: {"blobUri" => url})
177
+ if verbose
178
+ if response.result.success?
179
+ puts "#{id}\tsuccess\t"
180
+ else
181
+ puts "#{id}\tfailure\t#{response.parsed}"
182
+ end
183
+ else
184
+ response
185
+ end
125
186
  end
126
187
 
127
188
  def run_job(name, template, invoke_template, data = {})
128
189
  payload = Template.process(invoke_template, data)
129
- job = add_batch_job(name, template)
130
- path = job.parsed['document']['collectionspace_core']['uri']
190
+ job = add_batch_job(name, template)
191
+ path = job.parsed["document"]["collectionspace_core"]["uri"]
131
192
  post(path, payload)
132
193
  end
133
194
 
134
195
  def search(query, params = {})
135
196
  options = prepare_query(query, params)
136
- request 'GET', query.path, options
197
+ request "GET", query.path, options
137
198
  end
138
199
 
139
200
  def keyword_search(type:, value:, subtype: nil, sort: nil)
140
201
  service = CollectionSpace::Service.get(type: type, subtype: subtype)
141
- options = prepare_keyword_query(value, { sortBy: CollectionSpace::Search::DEFAULT_SORT })
142
- request 'GET', service[:path], options
202
+ options = prepare_keyword_query(value, {sortBy: CollectionSpace::Search::DEFAULT_SORT})
203
+ request "GET", service[:path], options
143
204
  end
144
205
 
145
- def service(type:, subtype: '')
206
+ def service(type:, subtype: "")
146
207
  CollectionSpace::Service.get(type: type, subtype: subtype)
147
208
  end
148
209
 
@@ -151,18 +212,18 @@ module CollectionSpace
151
212
  def create_or_update(response, path, property, value, payload)
152
213
  list_type, item_type = get_list_types(path)
153
214
  item = response.find(list_type, item_type, property, value)
154
- path = item ? "#{path}/#{item['csid']}" : path
215
+ path = item ? "#{path}/#{item["csid"]}" : path
155
216
  item ? put(path, payload) : post(path, payload)
156
217
  end
157
218
 
158
219
  def prepare_query(query, params = {})
159
220
  query_string = "#{query.namespace}:#{query.field} #{query.expression}"
160
- { query: { as: query_string }.merge(params) }
221
+ {query: {as: query_string}.merge(params)}
161
222
  end
162
223
 
163
224
  def prepare_keyword_query(query, sort = {})
164
- query_string = query.downcase.gsub(' ', '+')
165
- { query: { kw: query_string }.merge(sort) }
225
+ query_string = query.downcase.tr(" ", "+")
226
+ {query: {kw: query_string}.merge(sort)}
166
227
  end
167
228
  end
168
229
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'strscan'
3
+ require "strscan"
4
4
 
5
5
  module CollectionSpace
6
6
  # CollectionSpace RefName
7
7
  #
8
8
  # There are four patterns we need to handle:
9
- #
9
+ #
10
10
  # - urn:cspace:domain:type:name(subtype)'label' : Top level authority/vocabulary
11
11
  # - urn:cspace:domain:type:name(subtype):item:name(identifier)'label' : Authority/vocabulary term
12
12
  # - urn:cspace:domain:type:id(identifier)'label' : Collectionobject
@@ -15,38 +15,38 @@ module CollectionSpace
15
15
  attr_reader :domain, :type, :subtype, :identifier, :label
16
16
 
17
17
  def initialize(refname)
18
- @refname = refname
19
- @domain = nil
20
- @type = nil
21
- @subtype = nil
18
+ @refname = refname
19
+ @domain = nil
20
+ @type = nil
21
+ @subtype = nil
22
22
  @identifier = nil
23
- @label = nil
23
+ @label = nil
24
24
  parse
25
25
  end
26
26
 
27
27
  def parse
28
28
  scanner = StringScanner.new(@refname)
29
- scanner.skip('urn:cspace:')
30
- @domain = to_next_colon(scanner)
29
+ scanner.skip("urn:cspace:")
30
+ @domain = to_next_colon(scanner)
31
31
  @type = to_next_colon(scanner)
32
32
 
33
33
  case next_segment(scanner)
34
- when 'name'
34
+ when "name"
35
35
  set_subtype(scanner)
36
- when 'id'
36
+ when "id"
37
37
  set_identifier(scanner)
38
38
  end
39
39
 
40
40
  self
41
41
  end
42
-
42
+
43
43
  # Convenience class method, so new instance of RefName does not have to be instantiated in order to parse
44
44
  #
45
45
  # As of v0.13.1, return_class is added and defaults to nil for backward compatibility
46
46
  # Eventually this default will be deprecated, and a parsed RefName object will be returned as the default.
47
47
  # Any new code written using this method should set the return_class parameter to :refname_obj
48
48
  def self.parse(refname, return_class = nil)
49
- return_class == :refname_obj ? new(refname) : new(refname).to_h
49
+ (return_class == :refname_obj) ? new(refname) : new(refname).to_h
50
50
  end
51
51
 
52
52
  # Returns a parsed RefName object as a hash.
@@ -67,12 +67,12 @@ module CollectionSpace
67
67
  def next_segment(scanner)
68
68
  segment = scanner.check_until(/\(/)
69
69
  return nil unless segment
70
-
71
- segment.delete_suffix('(')
70
+
71
+ segment.delete_suffix("(")
72
72
  end
73
73
 
74
74
  def set_identifier(scanner)
75
- scanner.skip('id(')
75
+ scanner.skip("id(")
76
76
  @identifier = to_end_paren(scanner)
77
77
  return if scanner.eos?
78
78
 
@@ -85,30 +85,30 @@ module CollectionSpace
85
85
  end
86
86
 
87
87
  def set_subtype(scanner)
88
- scanner.skip('name(')
88
+ scanner.skip("name(")
89
89
  @subtype = to_end_paren(scanner)
90
90
 
91
91
  case next_segment(scanner)
92
92
  when nil
93
93
  set_label(scanner)
94
- when ':item:name'
94
+ when ":item:name"
95
95
  set_term_identifier(scanner)
96
96
  end
97
97
  end
98
98
 
99
99
  def set_term_identifier(scanner)
100
- scanner.skip(':item:name(')
100
+ scanner.skip(":item:name(")
101
101
  @identifier = to_end_paren(scanner)
102
102
  scanner.skip("'")
103
103
  set_label(scanner)
104
104
  end
105
-
105
+
106
106
  def to_end_paren(scanner)
107
- scanner.scan_until(/\)/).delete_suffix(')')
107
+ scanner.scan_until(/\)/).delete_suffix(")")
108
108
  end
109
109
 
110
110
  def to_next_colon(scanner)
111
- scanner.scan_until(/:/).delete_suffix(':')
111
+ scanner.scan_until(/:/).delete_suffix(":")
112
112
  end
113
113
  end
114
114
  end