qpid 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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