qpid 0.1.2

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.
@@ -0,0 +1,20 @@
1
+ === 0.1.2 2010-09-09
2
+
3
+ * 1 bug fix:
4
+ * Changed updatestatus so that it recognizes a NEW patient from QPID, and returns a nil time.
5
+
6
+ === 0.1.1 2010-07-19
7
+
8
+ * 1 minor enhancement:
9
+ * Small changes for Ruby 1.9 compatibility (but we're not there yet)
10
+
11
+ === 0.1.0 2010-07-19
12
+
13
+ * 1 major enhancement:
14
+ * Library implemented
15
+ * Tests implemented
16
+
17
+ === 0.0.1 2009-10-21
18
+
19
+ * 1 major enhancement:
20
+ * Initial release
@@ -0,0 +1,16 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ lib/qpid.rb
6
+ script/console
7
+ test/test_helper.rb
8
+ test/test_qpid.rb
9
+ test/content/3000888_report_pat_4313382_1.xml
10
+ test/content/3000888_report_rad_63580395.xml
11
+ test/content/3000888_search_type.xml
12
+ test/content/3000888_search_type_fromdate.xml
13
+ test/content/3000888_search_type_rad.xml
14
+ test/mock_http_client.rb
15
+ test/qpid.yml
16
+ test/qpid_test_data.yml
@@ -0,0 +1,61 @@
1
+ = qpid
2
+
3
+ * http://github.com/talkasab/qpid
4
+
5
+ == DESCRIPTION:
6
+
7
+ Interface to QPID, the Queriable Patient Inference Dossier system.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Determine when the QPID system last updated a patient's information
12
+ * Request a reload/reindexing of the patient's information, and find out when complete
13
+ * Get a summary of search results
14
+ * Get individual reports
15
+
16
+ == SYNOPSIS:
17
+
18
+ qpid = Qpid.new(YAML.load_file('qpid.yml'))
19
+ qpid.timeout_interval = 50
20
+ qpid.query_interval = 4.5
21
+
22
+ mrn = "654321"
23
+ # Reload the QPID index for the data
24
+ qpid.reload(mrn)
25
+ # Do a search for items of type "Endoscopy" with title "Colonoscopy" done after Jan 1, 2008
26
+ results = qpid.search(mrn, Qpid.type(:NDO),
27
+ Qpid.title("Colonoscopy"), Qpid.fromdate(Date.new(2008,1,1)))
28
+
29
+ == REQUIREMENTS:
30
+
31
+ * httpclient (to go talk to the QPID server)
32
+ * happymapper (to map XML to objects)
33
+
34
+ == INSTALL:
35
+
36
+ * sudo gem install qpid
37
+
38
+ == LICENSE:
39
+
40
+ (The MIT License)
41
+
42
+ Copyright (c) 2009 Tarik Alkasab, MD, PhD
43
+
44
+ Permission is hereby granted, free of charge, to any person obtaining
45
+ a copy of this software and associated documentation files (the
46
+ 'Software'), to deal in the Software without restriction, including
47
+ without limitation the rights to use, copy, modify, merge, publish,
48
+ distribute, sublicense, and/or sell copies of the Software, and to
49
+ permit persons to whom the Software is furnished to do so, subject to
50
+ the following conditions:
51
+
52
+ The above copyright notice and this permission notice shall be
53
+ included in all copies or substantial portions of the Software.
54
+
55
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
56
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
57
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
58
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
59
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
60
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
61
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/qpid'
6
+
7
+ Hoe.plugin :newgem
8
+
9
+ # Generate all the Rake tasks
10
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
11
+ $hoe = Hoe.spec 'qpid' do
12
+ self.developer 'Tarik K. Alkasab, MD, PhD', 'talkasab@partners.org'
13
+ self.rubyforge_name = self.name # TODO this is default value
14
+ self.extra_deps = [['happymapper','>= 0.3.0'], ['httpclient', '>= 2.1.5.2']]
15
+ end
16
+
17
+ require 'newgem/tasks'
18
+ Dir['tasks/**/*.rake'].each { |t| load t }
19
+
20
+ # TODO - want other tests/tasks run by default? Add them to the list
21
+ # remove_task :default
22
+ # task :default => [:spec, :features]
@@ -0,0 +1,186 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'httpclient'
5
+ require 'cgi'
6
+ require 'happymapper'
7
+
8
+ class Qpid
9
+ VERSION = '0.1.2'
10
+
11
+ class SearchItem
12
+ include HappyMapper
13
+ tag 'searchitem'
14
+ element :type, String
15
+ element :mid, String, :tag => "MID"
16
+ element :date, Date, :tag => "Scheduledate"
17
+ element :site, String
18
+ element :status, String
19
+ element :desc, String
20
+ element :context, String
21
+ end
22
+
23
+ DEF_PORT = 80
24
+ DEF_TIMEOUT_INTERVAL = 300
25
+ DEF_QUERY_INTERVAL = 9.5
26
+ DEF_ERROR_TOLERANCE = 2
27
+
28
+ class NoPatientError < RuntimeError
29
+ def initialize(mrn)
30
+ super("QPID cannot find patient with MRN #{mrn}")
31
+ end
32
+ end
33
+
34
+ class BadContentError < RuntimeError
35
+ def initialize(task, resp, err=nil)
36
+ super("While #{task}, got unexpected data: #{ resp ? resp : 'nil' }" + (err ? " (Underlying error: #{err})" : ''))
37
+ end
38
+ end
39
+
40
+ class TimeoutError < RuntimeError
41
+ def initialize(task, err=nil)
42
+ super("While #{task}, timed out waiting for a resopnse" + (err ? " (Underlying error: #{err})" : ''))
43
+ end
44
+ end
45
+
46
+ attr_accessor :host, :path, :user, :port, :timeout_interval, :query_interval, :error_tolerance
47
+ attr_reader :client
48
+
49
+ def initialize(params = {})
50
+ missing_params = []
51
+ @user = params[:user] || params['user'] || missing_params << 'user'
52
+ @host = params[:host] || params['host'] || missing_params << 'host'
53
+ @path = params[:path] || params['path'] || missing_params << 'path'
54
+ if missing_params.length > 0
55
+ raise ArgumentError, "Did not specify #{missing_params.join(', ')} to initialize QPID object"
56
+ end
57
+ @port = params[:port] || DEF_PORT
58
+ @timeout_interval = params[:timeout_interval] || params['timeout_interval'] || DEF_TIMEOUT_INTERVAL
59
+ @query_interval = params[:query_interval] || params['query_interval'] || DEF_QUERY_INTERVAL
60
+ @error_tolerance = params[:error_tolerance] || params['error_tolerance'] || DEF_ERROR_TOLERANCE
61
+ @client = params[:client] || params['client'] || HTTPClient.new
62
+ if (params[:http_user] && params[:http_password])
63
+ client.set_auth(nil, params[:http_user], params[:http_password])
64
+ elsif (params['http_user'] && params['http_password'])
65
+ client.set_auth(nil, params['http_user'], params['http_password'])
66
+ end
67
+ end
68
+
69
+ def function_uri(function, mrn, params = {})
70
+ query = query({:function => function, :MRN => mrn}.merge(params))
71
+ URI::HTTP.build(:host => host, :port => port, :path => path, :query => query )
72
+ end
73
+
74
+ def query(params = {})
75
+ params[:USER] = user
76
+ params[:xml] ||= 1
77
+ params.map { |p| "#{CGI.escape(p[0].to_s)}=#{CGI.escape(p[1].to_s)}" }.join('&')
78
+ end
79
+
80
+ # Get the time a patient's record was last updated in QPID
81
+ def updatestatus(mrn)
82
+ case xml = client.get_content(function_uri(:updatestatus, mrn))
83
+ when /<message>NO PATIENT<\/message>/
84
+ raise Qpid::NoPatientError.new(mrn)
85
+ when /<message>NEW<\/message>/
86
+ nil
87
+ when /<lastupdate>(.+)<\/lastupdate>/
88
+ begin
89
+ DateTime.parse($1)
90
+ rescue Exception => e
91
+ raise Qpid::BadContentError.new("parsing time in update status for MRN #{mrn}", xml, e.inspect)
92
+ end
93
+ else
94
+ raise Qpid::BadContentError.new("getting update status for MRN #{mrn}", xml)
95
+ end
96
+ end
97
+
98
+ # Causes QPID to re-update the patient's record. Returns true when done, or false if
99
+ # the attempt times out.
100
+ def reload(mrn)
101
+ start_time = Time.now
102
+ error_count = error_tolerance
103
+ task_description = "reloading patient #{mrn}"
104
+ while true
105
+ case resp = client.get_content(function_uri(:reload, mrn))
106
+ when /<message>OK<\/message>/
107
+ return true
108
+ when /<message>(NEW|UPDATING|STALE)<\/message>/
109
+ return false if Time.now - start_time > timeout_interval
110
+ sleep query_interval
111
+ when /<message>NO PATIENT<\/message>/
112
+ raise Qpid::NoPatientError.new(mrn)
113
+ else
114
+ error_count = error_count - 1
115
+ if error_count > 0
116
+ sleep query_interval/2
117
+ else
118
+ raise Qpid::BadContentError.new(task_description, resp)
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ # Does a search in the patient's medical record (as of the last time they were updated)
125
+ # and returns an array of Qpid::SearchItem objects which match the search
126
+ def search(mrn, *search_terms)
127
+ search_string = search_terms.join(' and ')
128
+ task_description = "searching MRN #{mrn} with [#{search_string}]"
129
+ case resp = client.get_content(function_uri(:search, mrn, :search => search_string))
130
+ when /<message>NO PATIENT<\/message>/
131
+ raise Qpid::NoPatientError.new(mrn)
132
+ when /<searchlist>/
133
+ begin
134
+ SearchItem.parse(resp)
135
+ rescue Exception => e
136
+ raise Qpid::BadContentError.new(task_description, resp, e.inspect)
137
+ end
138
+ else
139
+ raise Qpid::BadContentError.new(task_description, resp)
140
+ end
141
+ end
142
+
143
+ @@report_re = /<pre>(.+)<\/pre>/m
144
+
145
+ # Actually get the report for a given patient (specified by MRN) referred to by the
146
+ # given Qpid::SearchItem object.
147
+ def report(mrn, search_item)
148
+ case data = client.get_content(function_uri(:report, mrn, :type => search_item.type, :MID => search_item.mid))
149
+ when /<message>NO PATIENT<\/message>/
150
+ raise Qpid::NoPatientError.new(mrn)
151
+ when /<table id=\"report\">/
152
+ match = @@report_re.match(data)
153
+ match[1]
154
+ else
155
+ raise Qpid::BadContentError.new("getting report for MRN #{mrn}, item [#{search_item.inspect}]", data)
156
+ end
157
+ end
158
+
159
+ def self.type(type)
160
+ "type:#{type.to_s.upcase}"
161
+ end
162
+
163
+ def self.todate(date)
164
+ "todate:" + date.strftime("\"%m/%d/%Y\"")
165
+ end
166
+
167
+ def self.fromdate(date)
168
+ "fromdate:" + date.strftime("\"%m/%d/%Y\"")
169
+ end
170
+
171
+ def self.last(num_items)
172
+ "last:#{num_items}"
173
+ end
174
+
175
+ def self.daysago(num_days)
176
+ "daysago:#{num_days}"
177
+ end
178
+
179
+ def self.title(string)
180
+ "title:#{string}"
181
+ end
182
+
183
+ def self.ondate(date)
184
+ todate(date) + " AND " + fromdate(date)
185
+ end
186
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/qpid.rb'}"
9
+ if File.exist?(File.join(File.dirname(__FILE__), '..', 'qpid.yml'))
10
+ puts "qpid = Qpid.new(YAML.load_file('qpid.yml'))"
11
+ end
12
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,4 @@
1
+ require 'stringio'
2
+ require 'test/unit'
3
+ require 'rubygems'
4
+ require File.dirname(__FILE__) + '/../lib/qpid'
@@ -0,0 +1,102 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
+ require File.join(File.dirname(__FILE__), 'mock_http_client.rb')
3
+
4
+ class TestQpid < Test::Unit::TestCase
5
+
6
+ def setup
7
+ m=MockHttpClient.new(File.join(File.dirname(__FILE__), '/qpid_test_data.yml'),
8
+ Logger.new(File.join(File.dirname(__FILE__), 'test.log')))
9
+ @qpid = Qpid.new(YAML.load_file('test/qpid.yml').merge(:client => m))
10
+ end
11
+
12
+ def test_updatestatus_known
13
+ updatetime = @qpid.updatestatus("3000888")
14
+ assert_not_nil updatetime, "Should get a non-nil object from updatestatus()"
15
+ assert_kind_of DateTime, updatetime, "Should get a DateTime object from updatestatus()"
16
+ assert updatetime == @qpid.client.updatetimes["3000888"], "Time from updatestatus() should be same as set in mock client"
17
+ end
18
+
19
+ def test_updatestatus_unknown
20
+ assert_raise(Qpid::NoPatientError) { @qpid.updatestatus("3000999") }
21
+ end
22
+
23
+ def test_updatestatus_now_nil
24
+ assert_raise(Qpid::BadContentError) {
25
+ @qpid.client.next_response_nil = 1
26
+ @qpid.updatestatus("3000888")
27
+ }
28
+ end
29
+
30
+ def test_reload
31
+ @qpid.timeout_interval = 5.0
32
+ expected_delay = @qpid.client.delays["3000888"] + 1.0
33
+ start = Time.now
34
+ assert @qpid.reload("3000888"), "Should get true from reloading evenutally"
35
+ delay = Time.now - start
36
+ assert delay < expected_delay, "Took #{delay} to get the response; expected less than #{expected_delay}"
37
+ end
38
+
39
+ def test_reload_unknown
40
+ assert_raise(Qpid::NoPatientError) { @qpid.reload("3000999") }
41
+ end
42
+
43
+ def test_reload_error_tolerable
44
+ @qpid.client.next_response_nil = 1
45
+ assert_nothing_raised(Qpid::BadContentError) { @qpid.reload("3000888") }
46
+ end
47
+
48
+ def test_reload_error_nontolerable
49
+ @qpid.client.next_response_nil = 3
50
+ assert_raise(Qpid::BadContentError) { @qpid.reload("3000888") }
51
+ end
52
+
53
+ def test_reload_timeout
54
+ @qpid.client.delays["3000888"] = 10.0
55
+ @qpid.timeout_interval = 5.0
56
+ assert @qpid.reload("3000888") == false, "Should get false from reload() because we timed out."
57
+ end
58
+
59
+ def test_search
60
+ check_search_items 3, @qpid.search("3000888", Qpid.type(:PAT))
61
+ check_search_items 1, @qpid.search("3000888", Qpid.type(:RAD))
62
+ check_search_items 1, @qpid.search("3000888", Qpid.type(:PAT), Qpid.fromdate(Date.new(2007, 7, 1)))
63
+ end
64
+
65
+ def check_search_items(expected_length, items)
66
+ assert_equal expected_length, items.length, "Should have gotten a list of #{expected_length} SearchItem(s)"
67
+ items.each {|i| assert_kind_of Qpid::SearchItem, i, "Items returned from search should be of kind Qpid::SearchItem" }
68
+ end
69
+
70
+ def test_search_unknown
71
+ assert_raise(Qpid::NoPatientError) { @qpid.search("3000999", Qpid.type(:PAT)) }
72
+ end
73
+
74
+ def test_report
75
+ searchitem = @qpid.search("3000888", Qpid.type(:PAT), Qpid.fromdate(Date.new(2007, 7, 1)))[0]
76
+ report = @qpid.report("3000888", searchitem)
77
+ assert_not_nil report, "Should not get a nil report when have a valid response"
78
+ assert_match(/CASE: BS-07-W17636/, report, "We should see the expected text in the report")
79
+ assert_no_match(/(<\/?tr>|<\/?td>|<\/?pre>)/, report, "Should not see tags in the returned report")
80
+ end
81
+
82
+ def test_other_report
83
+ searchitem = @qpid.search("3000888", Qpid.type(:RAD))[0]
84
+ report = @qpid.report("3000888", searchitem)
85
+ assert_not_nil report, "Should not get a nil report when have a valid response"
86
+ assert_match(/Exam Number: 301654345/, report, "We should see the expected text in the report")
87
+ assert_no_match(/(<\/?tr>|<\/?td>|<\/?pre>)/, report, "Should not see tags in the returned report")
88
+ end
89
+
90
+ def test_report_unknown
91
+ searchitem = @qpid.search("3000888", Qpid.type(:PAT), Qpid.fromdate(Date.new(2007, 7, 1)))[0]
92
+ assert_raise(Qpid::NoPatientError) { @qpid.report("3000999", searchitem) }
93
+ end
94
+
95
+ def test_report_now_nil
96
+ assert_raise(Qpid::BadContentError) {
97
+ @qpid.client.next_response_nil = 1
98
+ searchitem = @qpid.search("3000888", Qpid.type(:RAD))[0]
99
+ @qpid.report("3000888", searchitem)
100
+ }
101
+ end
102
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qpid
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 2
10
+ version: 0.1.2
11
+ platform: ruby
12
+ authors:
13
+ - Tarik K. Alkasab
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2009-10-21 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: happymapper
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 19
30
+ segments:
31
+ - 0
32
+ - 3
33
+ - 0
34
+ version: 0.3.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: httpclient
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 119
46
+ segments:
47
+ - 2
48
+ - 1
49
+ - 5
50
+ - 2
51
+ version: 2.1.5.2
52
+ type: :runtime
53
+ version_requirements: *id002
54
+ - !ruby/object:Gem::Dependency
55
+ name: hoe
56
+ prerelease: false
57
+ requirement: &id003 !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 5
63
+ segments:
64
+ - 2
65
+ - 3
66
+ - 3
67
+ version: 2.3.3
68
+ type: :development
69
+ version_requirements: *id003
70
+ description: The QPID (Queriable Patient Inference Dossier, developed at Massachusetts General Hospital) aggregates and permits semantic searching of electronic medical record systems. The Qpid gem provides a ruby interface to the web service which QPID-based systems offer for searching and accessing medical record information.
71
+ email:
72
+ - tarik.alkasab@gmail.com
73
+ executables: []
74
+
75
+ extensions: []
76
+
77
+ extra_rdoc_files:
78
+ - History.txt
79
+ - Manifest.txt
80
+ files:
81
+ - History.txt
82
+ - Manifest.txt
83
+ - README.rdoc
84
+ - Rakefile
85
+ - lib/qpid.rb
86
+ - script/console
87
+ - script/destroy
88
+ - script/generate
89
+ - test/test_helper.rb
90
+ - test/test_qpid.rb
91
+ has_rdoc: true
92
+ homepage: http://github.com/talkasab/qpid
93
+ licenses: []
94
+
95
+ post_install_message:
96
+ rdoc_options:
97
+ - --main
98
+ - README.rdoc
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ hash: 3
116
+ segments:
117
+ - 0
118
+ version: "0"
119
+ requirements: []
120
+
121
+ rubyforge_project: qpid
122
+ rubygems_version: 1.3.7
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: Interface to medical record search systems based on the QPID (Queriable Patient Inference Dossier) system.
126
+ test_files:
127
+ - test/test_helper.rb
128
+ - test/test_qpid.rb