onc_certification_g10_test_kit 2.0.0.rc1
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 +7 -0
- data/LICENSE +201 -0
- data/lib/inferno/exceptions.rb +31 -0
- data/lib/inferno/ext/bloomer.rb +24 -0
- data/lib/inferno/repositiories/validators.rb +17 -0
- data/lib/inferno/repositiories/value_sets.rb +26 -0
- data/lib/inferno/terminology/bcp47.rb +95 -0
- data/lib/inferno/terminology/bcp_13.rb +26 -0
- data/lib/inferno/terminology/codesystem.rb +49 -0
- data/lib/inferno/terminology/expected_manifest.yml +1123 -0
- data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
- data/lib/inferno/terminology/loader.rb +298 -0
- data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
- data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
- data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
- data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
- data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
- data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
- data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
- data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
- data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
- data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
- data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
- data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
- data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
- data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
- data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
- data/lib/inferno/terminology/tasks.rb +11 -0
- data/lib/inferno/terminology/terminology_configuration.rb +52 -0
- data/lib/inferno/terminology/terminology_validation.rb +42 -0
- data/lib/inferno/terminology/validator.rb +64 -0
- data/lib/inferno/terminology/value_set.rb +462 -0
- data/lib/inferno/terminology.rb +16 -0
- data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
- data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
- data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
- data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
- data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
- data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
- data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
- data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
- data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
- data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
- data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
- data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
- data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
- data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
- data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
- data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
- data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
- data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
- data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
- data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
- data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
- data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
- data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
- data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
- data/lib/onc_certification_g10_test_kit/version.rb +3 -0
- data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
- data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
- data/lib/onc_certification_g10_test_kit.rb +223 -0
- metadata +310 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'rubygems/package'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
require 'zlib'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require_relative '../exceptions'
|
|
7
|
+
|
|
8
|
+
module Inferno
|
|
9
|
+
module Terminology
|
|
10
|
+
module FHIRPackageManager
|
|
11
|
+
class << self
|
|
12
|
+
REGISTRY_SERVER_URL = 'https://packages.fhir.org'.freeze
|
|
13
|
+
# Get the FHIR Package from the registry.
|
|
14
|
+
#
|
|
15
|
+
# e.g. get_package('hl7.fhir.us.core#3.1.1')
|
|
16
|
+
#
|
|
17
|
+
# @param [String] package The FHIR Package
|
|
18
|
+
def get_package(package, destination, desired_types = [])
|
|
19
|
+
package_url = package
|
|
20
|
+
.split('#')
|
|
21
|
+
.prepend(REGISTRY_SERVER_URL)
|
|
22
|
+
.join('/')
|
|
23
|
+
|
|
24
|
+
tar_file_name = "tmp/#{package.split('#').join('-')}.tgz"
|
|
25
|
+
|
|
26
|
+
File.open(tar_file_name, 'w') do |output_file|
|
|
27
|
+
output_file.binmode
|
|
28
|
+
block = proc do |response|
|
|
29
|
+
response.read_body do |chunk|
|
|
30
|
+
output_file.write chunk
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
RestClient::Request.execute(method: :get, url: package_url, block_response: block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
tar = Gem::Package::TarReader.new(Zlib::GzipReader.open("tmp/#{package.split('#').join('-')}.tgz"))
|
|
37
|
+
|
|
38
|
+
path = File.join destination.split('/')
|
|
39
|
+
FileUtils.mkdir_p(path)
|
|
40
|
+
|
|
41
|
+
tar.each do |entry|
|
|
42
|
+
next if entry.directory?
|
|
43
|
+
|
|
44
|
+
next unless entry.full_name.start_with? 'package/'
|
|
45
|
+
|
|
46
|
+
file_name = entry.full_name.split('/').last
|
|
47
|
+
next if desired_types.present? && !file_name.start_with?(*desired_types)
|
|
48
|
+
|
|
49
|
+
resource = JSON.parse(entry.read) if file_name.end_with? '.json'
|
|
50
|
+
next unless resource&.[]('url')
|
|
51
|
+
|
|
52
|
+
encoded_name = "#{encode_name(resource['url'])}.json"
|
|
53
|
+
encoded_file_name = File.join(path, encoded_name)
|
|
54
|
+
if File.exist?(encoded_file_name) && !resource['url'] == JSON.parse(File.read(encoded_file_name))['url']
|
|
55
|
+
raise FileExistsException, "#{encoded_name} already exists for #{resource['url']}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
File.write(encoded_file_name, resource.to_json)
|
|
59
|
+
end
|
|
60
|
+
File.delete(tar_file_name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def encode_name(name)
|
|
64
|
+
Zlib.crc32(name)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/all'
|
|
5
|
+
require 'bloomer'
|
|
6
|
+
require 'bloomer/msgpackable'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'pry'
|
|
9
|
+
|
|
10
|
+
require_relative '../exceptions'
|
|
11
|
+
require_relative '../ext/bloomer'
|
|
12
|
+
require_relative '../repositiories/validators'
|
|
13
|
+
require_relative '../repositiories/value_sets'
|
|
14
|
+
require_relative 'fhir_package_manager'
|
|
15
|
+
require_relative 'value_set'
|
|
16
|
+
require_relative 'validator'
|
|
17
|
+
|
|
18
|
+
module Inferno
|
|
19
|
+
module Terminology
|
|
20
|
+
class Loader
|
|
21
|
+
SKIP_SYS = [
|
|
22
|
+
'http://hl7.org/fhir/ValueSet/message-events', # has 0 codes
|
|
23
|
+
'http://hl7.org/fhir/ValueSet/care-team-category', # has 0 codes
|
|
24
|
+
'http://hl7.org/fhir/ValueSet/action-participant-role', # has 0 codes
|
|
25
|
+
'http://hl7.org/fhir/ValueSet/example-filter', # has fake property acme-plasma
|
|
26
|
+
'http://hl7.org/fhir/ValueSet/all-distance-units', # UCUM filter "canonical"
|
|
27
|
+
'http://hl7.org/fhir/ValueSet/all-time-units', # UCUM filter "canonical"
|
|
28
|
+
'http://hl7.org/fhir/ValueSet/example-intensional', # Unhandled filter parent =
|
|
29
|
+
'http://hl7.org/fhir/ValueSet/use-context', # ValueSet contains an unknown ValueSet
|
|
30
|
+
'http://hl7.org/fhir/ValueSet/media-modality', # ValueSet contains an unknown ValueSet
|
|
31
|
+
'http://hl7.org/fhir/ValueSet/example-hierarchical' # Example valueset with fake codes
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
@value_sets_repo = Inferno::Repositories::ValueSets.new
|
|
35
|
+
@validators_repo = Inferno::Repositories::Validators.new
|
|
36
|
+
|
|
37
|
+
@missing_validators = nil
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
attr_reader :validators_repo, :value_sets_repo
|
|
41
|
+
|
|
42
|
+
def load_value_sets_from_directory(directory, include_subdirectories = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
43
|
+
directory += '/**/' if include_subdirectories
|
|
44
|
+
value_set_files = Dir["#{directory}/*.json"]
|
|
45
|
+
value_set_files.each do |vs_file|
|
|
46
|
+
next unless JSON.parse(File.read(vs_file))['resourceType'] == 'ValueSet'
|
|
47
|
+
|
|
48
|
+
add_value_set_from_file(vs_file)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Creates the valueset validators, based on the passed in parameters and
|
|
53
|
+
# the value_sets_repo
|
|
54
|
+
#
|
|
55
|
+
# @param type [Symbol] the type of validators to create, either :bloom or
|
|
56
|
+
# :csv
|
|
57
|
+
# @param [String] minimum_binding_strength the lowest binding strength for
|
|
58
|
+
# which we should build validators
|
|
59
|
+
# @param [Boolean] include_umls a flag to determine if we should build
|
|
60
|
+
# validators that require UMLS
|
|
61
|
+
# @param [Boolean] delete_existing a flag to determine whether any
|
|
62
|
+
# existing validators of `type` should be deleted before the creation
|
|
63
|
+
# tasks run. Default to `true`. If `false`, the existing validators will
|
|
64
|
+
# be read in and combined with the validators created in this step.
|
|
65
|
+
def create_validators(
|
|
66
|
+
type: :bloom,
|
|
67
|
+
minimum_binding_strength: 'example',
|
|
68
|
+
include_umls: true,
|
|
69
|
+
delete_existing: true
|
|
70
|
+
)
|
|
71
|
+
strengths = ['example', 'preferred', 'extensible', 'required'].drop_while do |s|
|
|
72
|
+
s != minimum_binding_strength
|
|
73
|
+
end
|
|
74
|
+
umls_code_systems = Set.new(ValueSet::SAB.keys)
|
|
75
|
+
root_dir = "resources/terminology/validators/#{type}"
|
|
76
|
+
|
|
77
|
+
FileUtils.rm_r(root_dir, force: true) if delete_existing
|
|
78
|
+
FileUtils.mkdir_p(root_dir)
|
|
79
|
+
|
|
80
|
+
vs_validators = get_value_sets(strengths).map do |vs_url, vs|
|
|
81
|
+
next if SKIP_SYS.include? vs_url
|
|
82
|
+
next if !include_umls && !umls_code_systems.disjoint?(Set.new(vs.included_code_systems))
|
|
83
|
+
|
|
84
|
+
Inferno.logger.debug "Processing #{vs_url}"
|
|
85
|
+
filename = "#{root_dir}/#{(URI(vs.url).host + URI(vs.url).path).gsub(%r{[./]}, '_')}"
|
|
86
|
+
begin
|
|
87
|
+
# Save the validator to file, and get the "new" count of number of codes
|
|
88
|
+
new_count = save_to_file(vs.value_set, filename, type)
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
url: vs_url,
|
|
92
|
+
file: name_by_type(File.basename(filename), type),
|
|
93
|
+
count: new_count,
|
|
94
|
+
type: type.to_s,
|
|
95
|
+
code_systems: vs.included_code_systems
|
|
96
|
+
}
|
|
97
|
+
rescue UnknownCodeSystemException,
|
|
98
|
+
FilterOperationException,
|
|
99
|
+
UnknownValueSetException,
|
|
100
|
+
URI::InvalidURIError => e
|
|
101
|
+
Inferno.logger.warn "#{e.message} for ValueSet: #{vs_url}"
|
|
102
|
+
next
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
vs_validators.compact!
|
|
106
|
+
|
|
107
|
+
code_systems = vs_validators.flat_map { |validator| validator[:code_systems] }.uniq
|
|
108
|
+
vs = ValueSet.new(@db)
|
|
109
|
+
|
|
110
|
+
cs_validators = code_systems.map do |cs_name|
|
|
111
|
+
next if SKIP_SYS.include? cs_name
|
|
112
|
+
next if !include_umls && umls_code_systems.include?(cs_name)
|
|
113
|
+
|
|
114
|
+
Inferno.logger.debug "Processing #{cs_name}"
|
|
115
|
+
begin
|
|
116
|
+
cs = vs.code_system_set(cs_name)
|
|
117
|
+
filename = "#{root_dir}/#{bloom_file_name(cs_name)}"
|
|
118
|
+
new_count = save_to_file(cs, filename, type)
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
url: cs_name,
|
|
122
|
+
file: name_by_type(File.basename(filename), type),
|
|
123
|
+
count: new_count,
|
|
124
|
+
type: type.to_s,
|
|
125
|
+
code_systems: cs_name
|
|
126
|
+
}
|
|
127
|
+
rescue UnknownCodeSystemException,
|
|
128
|
+
FilterOperationException,
|
|
129
|
+
UnknownValueSetException,
|
|
130
|
+
URI::InvalidURIError => e
|
|
131
|
+
Inferno.logger.warn "#{e.message} for CodeSystem #{cs_name}"
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
validators = (vs_validators + cs_validators).compact
|
|
136
|
+
|
|
137
|
+
# Write manifest for loading later
|
|
138
|
+
File.write("#{root_dir}/manifest.yml", validators.to_yaml)
|
|
139
|
+
|
|
140
|
+
create_code_system_metadata(cs_validators.map { |validator| validator[:url] }, root_dir)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def create_code_system_metadata(system_urls, root_dir)
|
|
144
|
+
vs = ValueSet.new(@db)
|
|
145
|
+
metadata_path = "#{root_dir}/metadata.yml"
|
|
146
|
+
metadata =
|
|
147
|
+
if File.file? metadata_path
|
|
148
|
+
YAML.load_file(metadata_path)
|
|
149
|
+
else
|
|
150
|
+
{}
|
|
151
|
+
end
|
|
152
|
+
system_urls.each do |url|
|
|
153
|
+
abbreviation = vs.umls_abbreviation(url)
|
|
154
|
+
next if abbreviation.nil?
|
|
155
|
+
|
|
156
|
+
versions = @db.execute("SELECT SVER FROM mrsab WHERE RSAB='#{abbreviation}' AND SABIN='Y'").flatten
|
|
157
|
+
restriction_level = @db.execute(
|
|
158
|
+
"SELECT SRL FROM mrsab WHERE RSAB='#{abbreviation}' AND SABIN='Y'"
|
|
159
|
+
).flatten.first
|
|
160
|
+
system_metadata = metadata[url] || vs.code_system_metadata(url).dup || {}
|
|
161
|
+
system_metadata[:versions] ||= []
|
|
162
|
+
system_metadata[:versions].concat(versions).uniq!
|
|
163
|
+
system_metadata[:restriction_level] = restriction_level
|
|
164
|
+
|
|
165
|
+
metadata[url] = system_metadata
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
File.write(metadata_path, metadata.to_yaml)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# NOTE: resources/value_sets.yml controls which value sets get loaded.
|
|
172
|
+
# It is currently manually generated from the US Core metadata.
|
|
173
|
+
def get_value_sets(strengths)
|
|
174
|
+
expected_vs_urls =
|
|
175
|
+
YAML.load_file(File.join('resources', 'value_sets.yml'))
|
|
176
|
+
.select { |vs| strengths.include? vs[:strength] }
|
|
177
|
+
.map! { |vs| vs[:system] }
|
|
178
|
+
.compact
|
|
179
|
+
.uniq
|
|
180
|
+
|
|
181
|
+
available_value_sets = value_sets_repo.select_by_url(expected_vs_urls)
|
|
182
|
+
|
|
183
|
+
# Throw an error message for each missing valueset
|
|
184
|
+
# But don't halt the rake task
|
|
185
|
+
(expected_vs_urls - available_value_sets.keys).each do |missing_vs_url|
|
|
186
|
+
Inferno.logger.error "Inferno doesn't know about valueset #{missing_vs_url}"
|
|
187
|
+
end
|
|
188
|
+
available_value_sets
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Chooses which filetype to save the validator as, based on the type variable passed in
|
|
192
|
+
def save_to_file(codeset, filename, type)
|
|
193
|
+
case type
|
|
194
|
+
when :bloom
|
|
195
|
+
save_bloom_to_file(codeset, name_by_type(filename, type))
|
|
196
|
+
when :csv
|
|
197
|
+
save_csv_to_file(codeset, name_by_type(filename, type))
|
|
198
|
+
else
|
|
199
|
+
raise 'Unknown Validator Type!'
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def name_by_type(filename, type)
|
|
204
|
+
case type
|
|
205
|
+
when :bloom
|
|
206
|
+
"#{filename}.msgpack"
|
|
207
|
+
when :csv
|
|
208
|
+
"#{filename}.csv"
|
|
209
|
+
else
|
|
210
|
+
raise 'Unknown Validator Type!'
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Saves the valueset bloomfilter to a msgpack file
|
|
215
|
+
#
|
|
216
|
+
# @param [String] filename the name of the file
|
|
217
|
+
def save_bloom_to_file(codings, filename)
|
|
218
|
+
# If the file already exists, load it in
|
|
219
|
+
bloom_filter =
|
|
220
|
+
if File.file? filename
|
|
221
|
+
Bloomer::Scalable.from_msgpack(File.read(filename))
|
|
222
|
+
else
|
|
223
|
+
Bloomer::Scalable.create_with_sufficient_size(codings.length)
|
|
224
|
+
end
|
|
225
|
+
codings.each do |coding|
|
|
226
|
+
bloom_filter.add_without_duplication("#{coding[:system]}|#{coding[:code]}")
|
|
227
|
+
end
|
|
228
|
+
bloom_file = File.new(filename, 'wb')
|
|
229
|
+
bloom_file.write(bloom_filter.to_msgpack) unless bloom_filter.nil?
|
|
230
|
+
|
|
231
|
+
bloom_filter.count
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Saves the valueset to a csv
|
|
235
|
+
# @param [String] filename the name of the file
|
|
236
|
+
def save_csv_to_file(codings, filename)
|
|
237
|
+
# If the file already exists, add it to the Set
|
|
238
|
+
csv_set = Set.new
|
|
239
|
+
if File.file? filename
|
|
240
|
+
CSV.read(filename).each do |code_array|
|
|
241
|
+
csv_set.add({ code: code_array[1], system: code_array[0] })
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
codings.merge csv_set
|
|
245
|
+
|
|
246
|
+
CSV.open(filename, 'wb') do |csv|
|
|
247
|
+
codings.each do |coding|
|
|
248
|
+
csv << [coding[:system], coding[:code]]
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
codings.length
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def register_umls_db(database)
|
|
255
|
+
FileUtils.mkdir_p File.dirname(database)
|
|
256
|
+
@db = SQLite3::Database.new database
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def add_value_set_from_file(vs_file)
|
|
260
|
+
vs = ValueSet.new(@db)
|
|
261
|
+
vs.read_value_set(vs_file)
|
|
262
|
+
value_sets_repo.insert(vs)
|
|
263
|
+
vs
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def load_validators(directory = 'resources/terminology/validators/bloom')
|
|
267
|
+
manifest_file = "#{directory}/manifest.yml"
|
|
268
|
+
return unless File.file? manifest_file
|
|
269
|
+
|
|
270
|
+
validator_metadata = YAML.load_file(manifest_file)
|
|
271
|
+
validator_metadata.each do |metadata|
|
|
272
|
+
metadata[:bloom_filter] =
|
|
273
|
+
Bloomer::Scalable.from_msgpack(File.read("#{directory}/#{metadata[:file]}"))
|
|
274
|
+
validators_repo.insert(Validator.new(metadata))
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def bloom_file_name(codesystem)
|
|
279
|
+
system = codesystem.tr('|', '_')
|
|
280
|
+
uri = URI(system)
|
|
281
|
+
return (uri.host + uri.path).gsub(%r{[./]}, '_') if uri.host && uri.port
|
|
282
|
+
|
|
283
|
+
system.gsub(/\W/, '_')
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def missing_validators
|
|
287
|
+
return @missing_validators if @missing_validators
|
|
288
|
+
|
|
289
|
+
required_value_sets =
|
|
290
|
+
value_sets_repo
|
|
291
|
+
.select_by_binding_strength(['required', 'extensible', 'preferred'])
|
|
292
|
+
.map(&:value_set_url)
|
|
293
|
+
@missing_validators = required_value_sets.compact - validators_repo.all_urls
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module Inferno
|
|
4
|
+
module Terminology
|
|
5
|
+
module Tasks
|
|
6
|
+
class CheckBuiltTerminology
|
|
7
|
+
MIME_TYPE_SYSTEMS = [
|
|
8
|
+
'http://hl7.org/fhir/ValueSet/mimetypes',
|
|
9
|
+
'urn:ietf:bcp:13'
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
if mismatched_value_sets.blank?
|
|
14
|
+
Inferno.logger.info 'Terminology built successfully.'
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if only_mime_types_mismatch?
|
|
19
|
+
Inferno.logger.info <<~MIME
|
|
20
|
+
Terminology built successfully.
|
|
21
|
+
|
|
22
|
+
Mime-type based terminology did not match, but this can be a
|
|
23
|
+
result of using a newer version of the `mime-types-data` gem and
|
|
24
|
+
does not necessarily reflect a problem with the terminology build.
|
|
25
|
+
The expected mime-types codes were generated with version
|
|
26
|
+
`mime-types-data` version `3.2021.0901`.
|
|
27
|
+
MIME
|
|
28
|
+
else
|
|
29
|
+
Inferno.logger.info 'Terminology build results different than expected.'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
mismatched_value_sets.each do |value_set|
|
|
33
|
+
Inferno.logger.info mismatched_value_set_message(value_set)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def expected_manifest
|
|
38
|
+
YAML.load_file(File.join(__dir__, '..', 'expected_manifest.yml'))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def new_manifest_path
|
|
42
|
+
@new_manifest_path ||=
|
|
43
|
+
File.join(Dir.pwd, 'resources', 'terminology', 'validators', 'bloom', 'manifest.yml')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def new_manifest
|
|
47
|
+
return [] unless File.exist? new_manifest_path
|
|
48
|
+
|
|
49
|
+
YAML.load_file(new_manifest_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def mismatched_value_sets
|
|
53
|
+
@mismatched_value_sets ||=
|
|
54
|
+
expected_manifest.reject do |expected_value_set|
|
|
55
|
+
url = expected_value_set[:url]
|
|
56
|
+
new_value_set(url) == expected_value_set
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def new_value_set(url)
|
|
61
|
+
new_manifest.find { |value_set| value_set[:url] == url }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def only_mime_types_mismatch?
|
|
65
|
+
mismatched_value_sets.all? { |value_set| MIME_TYPE_SYSTEMS.include? value_set[:url] }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def mismatched_value_set_message(expected_value_set)
|
|
69
|
+
url = expected_value_set[:url]
|
|
70
|
+
actual_value_set = new_value_set(url)
|
|
71
|
+
|
|
72
|
+
"#{url}: Expected codes: #{expected_value_set[:count]} Actual codes: #{actual_value_set&.dig(:count) || 0}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module Terminology
|
|
3
|
+
module Tasks
|
|
4
|
+
class CleanupPrecursors
|
|
5
|
+
include TempDir
|
|
6
|
+
|
|
7
|
+
attr_reader :version
|
|
8
|
+
|
|
9
|
+
def initialize(version:)
|
|
10
|
+
@version = version
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
Inferno.logger.info "removing terminology precursor files in #{versioned_temp_dir}"
|
|
15
|
+
FileUtils.remove_dir(umls_dir, true)
|
|
16
|
+
FileUtils.remove_dir(umls_subset_dir, true)
|
|
17
|
+
FileUtils.rm(umls_zip_path, force: true)
|
|
18
|
+
FileUtils.rm(pipe_files, force: true)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module Terminology
|
|
3
|
+
module Tasks
|
|
4
|
+
class CountCodesInValueSet
|
|
5
|
+
attr_reader :value_set_url
|
|
6
|
+
|
|
7
|
+
def initialize(vs:) # rubocop:disable Naming/MethodParameterName
|
|
8
|
+
@value_set_url = vs
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
Loader.register_umls_db File.join(TEMP_DIR, 'umls.db')
|
|
13
|
+
Loader.load_value_sets_from_directory(PACKAGE_DIR, true)
|
|
14
|
+
vs = Repositories::ValueSets.new.find(value_set_url)
|
|
15
|
+
Inferno.logger.info vs&.valueset&.count
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative 'temp_dir'
|
|
2
|
+
|
|
3
|
+
module Inferno
|
|
4
|
+
module Terminology
|
|
5
|
+
module Tasks
|
|
6
|
+
class CreateValueSetValidators
|
|
7
|
+
include TempDir
|
|
8
|
+
|
|
9
|
+
attr_reader :minimum_binding_strength, :version, :delete_existing, :type
|
|
10
|
+
|
|
11
|
+
def initialize(minimum_binding_strength:, version:, delete_existing:, type:)
|
|
12
|
+
@minimum_binding_strength = minimum_binding_strength
|
|
13
|
+
@version = version
|
|
14
|
+
@delete_existing = delete_existing != 'false'
|
|
15
|
+
@type = type.to_sym
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
Loader.register_umls_db db_for_version
|
|
20
|
+
Loader.load_value_sets_from_directory(Inferno::Terminology::PACKAGE_DIR, true)
|
|
21
|
+
Loader.create_validators(
|
|
22
|
+
type: type,
|
|
23
|
+
minimum_binding_strength: minimum_binding_strength,
|
|
24
|
+
delete_existing: delete_existing
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def db_for_version
|
|
29
|
+
File.join(versioned_temp_dir, 'umls.db')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module Terminology
|
|
3
|
+
module Tasks
|
|
4
|
+
class DownloadFHIRTerminology
|
|
5
|
+
def run
|
|
6
|
+
FileUtils.mkdir_p PACKAGE_DIR
|
|
7
|
+
|
|
8
|
+
download_fhir_r4
|
|
9
|
+
download_fhir_expansions
|
|
10
|
+
download_us_core
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def download_fhir_r4
|
|
14
|
+
FHIRPackageManager.get_package('hl7.fhir.r4.core#4.0.1', PACKAGE_DIR, ['ValueSet', 'CodeSystem'])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def download_us_core
|
|
18
|
+
FHIRPackageManager.get_package('hl7.fhir.us.core#3.1.1', PACKAGE_DIR, ['ValueSet', 'CodeSystem'])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def download_fhir_expansions
|
|
22
|
+
FHIRPackageManager.get_package('hl7.fhir.r4.expansions#4.0.1', PACKAGE_DIR, ['ValueSet', 'CodeSystem'])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'rest-client'
|
|
3
|
+
require_relative 'temp_dir'
|
|
4
|
+
|
|
5
|
+
module Inferno
|
|
6
|
+
module Terminology
|
|
7
|
+
module Tasks
|
|
8
|
+
class DownloadUMLS
|
|
9
|
+
include TempDir
|
|
10
|
+
|
|
11
|
+
UMLS_FILE_URLS = {
|
|
12
|
+
'2019' => 'https://download.nlm.nih.gov/umls/kss/2019AB/umls-2019AB-full.zip',
|
|
13
|
+
'2020' => 'https://download.nlm.nih.gov/umls/kss/2020AB/umls-2020AB-full.zip',
|
|
14
|
+
'2021' => 'https://download.nlm.nih.gov/umls/kss/2021AA/umls-2021AA-full.zip'
|
|
15
|
+
}.freeze
|
|
16
|
+
TICKET_GRANTING_TICKET_URL = 'https://utslogin.nlm.nih.gov/cas/v1/api-key'.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :version, :api_key
|
|
19
|
+
|
|
20
|
+
def initialize(version:, apikey:)
|
|
21
|
+
@version = version
|
|
22
|
+
@api_key = apikey
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
# Adapted from https://documentation.uts.nlm.nih.gov/automating-downloads.html
|
|
27
|
+
|
|
28
|
+
FileUtils.mkdir_p(versioned_temp_dir)
|
|
29
|
+
|
|
30
|
+
target_file = UMLS_FILE_URLS[version]
|
|
31
|
+
|
|
32
|
+
Inferno.logger.info 'Getting Ticket Granting Ticket'
|
|
33
|
+
ticket_granting_ticket_html = RestClient::Request.execute(
|
|
34
|
+
method: :post,
|
|
35
|
+
url: TICKET_GRANTING_TICKET_URL,
|
|
36
|
+
payload: {
|
|
37
|
+
apikey: api_key
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
ticket_granting_ticket =
|
|
41
|
+
Nokogiri::HTML(ticket_granting_ticket_html.body).at_css('form').attributes['action'].value
|
|
42
|
+
|
|
43
|
+
Inferno.logger.info 'Getting ticket'
|
|
44
|
+
ticket = RestClient::Request.execute(
|
|
45
|
+
method: :post,
|
|
46
|
+
url: ticket_granting_ticket,
|
|
47
|
+
payload: {
|
|
48
|
+
service: target_file
|
|
49
|
+
}
|
|
50
|
+
).body
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
Inferno.logger.info 'Downloading'
|
|
54
|
+
RestClient::Request.execute(
|
|
55
|
+
method: :get,
|
|
56
|
+
url: "#{target_file}?ticket=#{ticket}",
|
|
57
|
+
max_redirects: 0
|
|
58
|
+
)
|
|
59
|
+
rescue RestClient::ExceptionWithResponse => e
|
|
60
|
+
ticket = RestClient::Request.execute(
|
|
61
|
+
method: :post,
|
|
62
|
+
url: ticket_granting_ticket,
|
|
63
|
+
payload: {
|
|
64
|
+
service: e.response.headers[:location]
|
|
65
|
+
}
|
|
66
|
+
).body
|
|
67
|
+
target_location = umls_zip_path
|
|
68
|
+
follow_redirect(e.response.headers[:location], target_location, ticket, e.response.headers[:set_cookie])
|
|
69
|
+
end
|
|
70
|
+
Inferno.logger.info 'Finished Downloading!'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def follow_redirect(location, file_location, ticket, cookie = nil)
|
|
74
|
+
return unless location
|
|
75
|
+
|
|
76
|
+
size = 0
|
|
77
|
+
percent = 0
|
|
78
|
+
current_percent = 0
|
|
79
|
+
File.open(file_location, 'w') do |f|
|
|
80
|
+
f.binmode
|
|
81
|
+
block = proc do |response|
|
|
82
|
+
Inferno.logger.info response.header['content-type']
|
|
83
|
+
if response.header['content-type'] == 'application/zip'
|
|
84
|
+
total = response.header['content-length'].to_i
|
|
85
|
+
response.read_body do |chunk|
|
|
86
|
+
f.write chunk
|
|
87
|
+
size += chunk.size
|
|
88
|
+
percent = ((size * 100) / total).round unless total.zero?
|
|
89
|
+
if current_percent != percent
|
|
90
|
+
current_percent = percent
|
|
91
|
+
Inferno.logger.info "#{percent}% complete"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
follow_redirect(response.header['location'], file_location, ticket, response.header['set-cookie'])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
RestClient::Request.execute(
|
|
99
|
+
method: :get,
|
|
100
|
+
url: "#{location}?ticket=#{ticket}",
|
|
101
|
+
headers: { cookie: cookie },
|
|
102
|
+
block_response: block
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|