Skip to content

Commit 64f96f8

Browse files
authored
Merge pull request #343 from dfarrow0/dfarrow/covid-hosp-facility3
facility fix and new endpoint
2 parents dd287a2 + 4bd0706 commit 64f96f8

File tree

10 files changed

+302
-4
lines changed

10 files changed

+302
-4
lines changed

integrations/acquisition/covid_hosp/facility/test_scenarios.py

+76
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,79 @@ def test_acquire_dataset(self):
8585
'450822', Epidata.range(20200101, 20210101))
8686
self.assertEqual(response['result'], 1)
8787
self.assertEqual(len(response['epidata']), 1)
88+
89+
def test_facility_lookup(self):
90+
"""Lookup facilities using various filters."""
91+
92+
# only mock out network calls to external hosts
93+
mock_network = MagicMock()
94+
mock_network.fetch_metadata.return_value = \
95+
self.test_utils.load_sample_metadata()
96+
mock_network.fetch_dataset.return_value = \
97+
self.test_utils.load_sample_dataset()
98+
99+
# acquire sample data into local database
100+
with self.subTest(name='first acquisition'):
101+
acquired = Update.run(network=mock_network)
102+
self.assertTrue(acquired)
103+
104+
# texas ground truth, sorted by `hospital_pk`
105+
# see sample data at testdata/acquisition/covid_hosp/facility/dataset.csv
106+
texas_hospitals = [{
107+
'hospital_pk': '450771',
108+
'state': 'TX',
109+
'ccn': '450771',
110+
'hospital_name': 'TEXAS HEALTH PRESBYTERIAN HOSPITAL PLANO',
111+
'address': '6200 W PARKER RD',
112+
'city': 'PLANO',
113+
'zip': '75093',
114+
'hospital_subtype': 'Short Term',
115+
'fips_code': '48085',
116+
'is_metro_micro': 1,
117+
}, {
118+
'hospital_pk': '450822',
119+
'state': 'TX',
120+
'ccn': '450822',
121+
'hospital_name': 'MEDICAL CITY LAS COLINAS',
122+
'address': '6800 N MACARTHUR BLVD',
123+
'city': 'IRVING',
124+
'zip': '75039',
125+
'hospital_subtype': 'Short Term',
126+
'fips_code': '48113',
127+
'is_metro_micro': 1,
128+
}, {
129+
'hospital_pk': '451329',
130+
'state': 'TX',
131+
'ccn': '451329',
132+
'hospital_name': 'RANKIN HOSPITAL MEDICAL CLINIC',
133+
'address': '1611 SPUR 576',
134+
'city': 'RANKIN',
135+
'zip': '79778',
136+
'hospital_subtype': 'Critical Access Hospitals',
137+
'fips_code': '48461',
138+
'is_metro_micro': 0,
139+
}]
140+
141+
with self.subTest(name='by state'):
142+
response = Epidata.covid_hosp_facility_lookup(state='tx')
143+
self.assertEqual(response['epidata'], texas_hospitals)
144+
145+
with self.subTest(name='by ccn'):
146+
response = Epidata.covid_hosp_facility_lookup(ccn='450771')
147+
self.assertEqual(response['epidata'], texas_hospitals[0:1])
148+
149+
with self.subTest(name='by city'):
150+
response = Epidata.covid_hosp_facility_lookup(city='irving')
151+
self.assertEqual(response['epidata'], texas_hospitals[1:2])
152+
153+
with self.subTest(name='by zip'):
154+
response = Epidata.covid_hosp_facility_lookup(zip='79778')
155+
self.assertEqual(response['epidata'], texas_hospitals[2:3])
156+
157+
with self.subTest(name='by fips_code'):
158+
response = Epidata.covid_hosp_facility_lookup(fips_code='48085')
159+
self.assertEqual(response['epidata'], texas_hospitals[0:1])
160+
161+
with self.subTest(name='no results'):
162+
response = Epidata.covid_hosp_facility_lookup(state='not a state')
163+
self.assertEqual(response['result'], -2)

src/acquisition/covid_hosp/common/utils.py

+30
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,36 @@ def int_from_date(date):
3737

3838
return int(date.replace('-', ''))
3939

40+
def parse_bool(value):
41+
"""Convert a string to a boolean.
42+
43+
Parameters
44+
----------
45+
value : str
46+
Boolean-like value, like "true" or "false".
47+
48+
Returns
49+
-------
50+
bool
51+
If the string contains some version of "true" or "false".
52+
None
53+
If the string is None or empty.
54+
55+
Raises
56+
------
57+
CovidHospException
58+
If the string constains something other than a version of "true" or
59+
"false".
60+
"""
61+
62+
if not value:
63+
return None
64+
if value.lower() == 'true':
65+
return True
66+
if value.lower() == 'false':
67+
return False
68+
raise CovidHospException(f'cannot convert "{value}" to bool')
69+
4070
def get_entry(obj, *path):
4171
"""Get a deeply nested field from an arbitrary object.
4272

src/acquisition/covid_hosp/facility/database.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Database(BaseDatabase):
2323
('zip', str),
2424
('hospital_subtype', str),
2525
('fips_code', str),
26-
('is_metro_micro', bool),
26+
('is_metro_micro', Utils.parse_bool),
2727
('total_beds_7_day_avg', float),
2828
('all_adult_hospital_beds_7_day_avg', float),
2929
('all_adult_hospital_inpatient_beds_7_day_avg', float),

src/client/delphi_epidata.R

+23-1
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,27 @@ Epidata <- (function() {
590590
return(.request(params))
591591
}
592592

593+
# Lookup COVID hospitalization facility identifiers
594+
covid_hosp_facility_lookup <- function(state, ccn, city, zip, fips_code) {
595+
# Set up request
596+
params <- list(source = 'covid_hosp_facility_lookup')
597+
if(!missing(state)) {
598+
params$state <- state
599+
} else if(!missing(ccn)) {
600+
params$ccn <- ccn
601+
} else if(!missing(city)) {
602+
params$city <- city
603+
} else if(!missing(zip)) {
604+
params$zip <- zip
605+
} else if(!missing(fips_code)) {
606+
params$fips_code <- fips_code
607+
} else {
608+
stop('one of `state`, `ccn`, `city`, `zip`, or `fips_code` is required')
609+
}
610+
# Make the API call
611+
return(.request(params))
612+
}
613+
593614
# Export the public methods
594615
return(list(
595616
range = range,
@@ -618,6 +639,7 @@ Epidata <- (function() {
618639
covidcast = covidcast,
619640
covidcast_meta = covidcast_meta,
620641
covid_hosp = covid_hosp,
621-
covid_hosp_facility = covid_hosp_facility
642+
covid_hosp_facility = covid_hosp_facility,
643+
covid_hosp_facility_lookup = covid_hosp_facility_lookup
622644
))
623645
})()

src/client/delphi_epidata.coffee

+20
Original file line numberDiff line numberDiff line change
@@ -445,5 +445,25 @@ class Epidata
445445
# Make the API call
446446
_request(callback, params)
447447

448+
# Lookup COVID hospitalization facility identifiers
449+
@covid_hosp_facility_lookup: (state, ccn, city, zip, fips_code) ->
450+
# Set up request
451+
params =
452+
'source': 'covid_hosp_facility'
453+
if state?
454+
params.state = state
455+
else if ccn?
456+
params.ccn = ccn
457+
else if city?
458+
params.city = city
459+
else if zip?
460+
params.zip = zip
461+
else if fips_code?
462+
params.fips_code = fips_code
463+
else
464+
throw { msg: 'one of `state`, `ccn`, `city`, `zip`, or `fips_code` is required' }
465+
# Make the API call
466+
_request(callback, params)
467+
448468
# Export the API to the global environment
449469
(exports ? window).Epidata = Epidata

src/client/delphi_epidata.js

+29
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,35 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
660660
} // Make the API call
661661

662662

663+
return _request(callback, params);
664+
} // Lookup COVID hospitalization facility identifiers
665+
666+
}, {
667+
key: "covid_hosp_facility_lookup",
668+
value: function covid_hosp_facility_lookup(state, ccn, city, zip, fips_code) {
669+
var params; // Set up request
670+
671+
params = {
672+
'source': 'covid_hosp_facility'
673+
};
674+
675+
if (state != null) {
676+
params.state = state;
677+
} else if (ccn != null) {
678+
params.ccn = ccn;
679+
} else if (city != null) {
680+
params.city = city;
681+
} else if (zip != null) {
682+
params.zip = zip;
683+
} else if (fips_code != null) {
684+
params.fips_code = fips_code;
685+
} else {
686+
throw {
687+
msg: 'one of `state`, `ccn`, `city`, `zip`, or `fips_code` is required'
688+
};
689+
} // Make the API call
690+
691+
663692
return _request(callback, params);
664693
}
665694
}]);

src/client/delphi_epidata.py

+22
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,25 @@ def covid_hosp_facility(
643643
params['publication_dates'] = Epidata._list(publication_dates)
644644
# Make the API call
645645
return Epidata._request(params)
646+
647+
# Lookup COVID hospitalization facility identifiers
648+
@staticmethod
649+
def covid_hosp_facility_lookup(
650+
state=None, ccn=None, city=None, zip=None, fips_code=None):
651+
"""Lookup COVID hospitalization facility identifiers."""
652+
# Set up request
653+
params = {'source': 'covid_hosp_facility_lookup'}
654+
if state is not None:
655+
params['state'] = state
656+
elif ccn is not None:
657+
params['ccn'] = ccn
658+
elif city is not None:
659+
params['city'] = city
660+
elif zip is not None:
661+
params['zip'] = zip
662+
elif fips_code is not None:
663+
params['fips_code'] = fips_code
664+
else:
665+
raise Exception('one of `state`, `ccn`, `city`, `zip`, or `fips_code` is required')
666+
# Make the API call
667+
return Epidata._request(params)

src/ddl/covid_hosp.sql

+6-2
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,10 @@ CREATE TABLE `covid_hosp_facility` (
10431043
UNIQUE KEY (`hospital_pk`, `collection_week`, `publication_date`),
10441044
-- for fast lookup of a time-series for a given hospital and publication date
10451045
KEY (`publication_date`, `hospital_pk`, `collection_week`),
1046-
-- for fast lookup of all hospital identifiers in a given state
1047-
KEY (`state`, `hospital_pk`)
1046+
-- for fast lookup of hospitals in a given location
1047+
KEY (`state`, `hospital_pk`),
1048+
KEY (`ccn`, `hospital_pk`),
1049+
KEY (`city`, `hospital_pk`),
1050+
KEY (`zip`, `hospital_pk`),
1051+
KEY (`fips_code`, `hospital_pk`)
10481052
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

src/server/api.php

+72
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,67 @@ function get_covid_hosp_facility($hospital_pks, $collection_weeks, $publication_
14461446
return count($epidata) === 0 ? null : $epidata;
14471447
}
14481448

1449+
// queries the `covid_hosp_facility` table for hospital discovery
1450+
// $state (optional): 2-letter state abbreviation
1451+
// $ccn (optional): cms certification number (ccn) of the given facility
1452+
// $city (optional): name of
1453+
// $zip (optional): 2-letter state abbreviation
1454+
// $fips_code (optional): 2-letter state abbreviation
1455+
// note: exactly one of the above parameters should be non-null. if more than
1456+
// one is non-null, then only the first filter will be used.
1457+
function get_covid_hosp_facility_lookup($state, $ccn, $city, $zip, $fips_code) {
1458+
$epidata = array();
1459+
$table = '`covid_hosp_facility` c';
1460+
$fields = implode(', ', array(
1461+
'c.`hospital_pk`',
1462+
'MAX(c.`state`) `state`',
1463+
'MAX(c.`ccn`) `ccn`',
1464+
'MAX(c.`hospital_name`) `hospital_name`',
1465+
'MAX(c.`address`) `address`',
1466+
'MAX(c.`city`) `city`',
1467+
'MAX(c.`zip`) `zip`',
1468+
'MAX(c.`hospital_subtype`) `hospital_subtype`',
1469+
'MAX(c.`fips_code`) `fips_code`',
1470+
'MAX(c.`is_metro_micro`) `is_metro_micro`',
1471+
));
1472+
// basic query info
1473+
$group = 'c.`hospital_pk`';
1474+
$order = "c.`hospital_pk` ASC";
1475+
// build the filter
1476+
// these are all fast because the table has indexes on each of these fields
1477+
$condition = 'FALSE';
1478+
if ($state !== null) {
1479+
$condition = filter_strings('c.`state`', $state);
1480+
} else if ($ccn !== null) {
1481+
$condition = filter_strings('c.`ccn`', $ccn);
1482+
} else if ($city !== null) {
1483+
$condition = filter_strings('c.`city`', $city);
1484+
} else if ($zip !== null) {
1485+
$condition = filter_strings('c.`zip`', $zip);
1486+
} else if ($fips_code !== null) {
1487+
$condition = filter_strings('c.`fips_code`', $fips_code);
1488+
}
1489+
// final query using specific issues
1490+
$query = "SELECT {$fields} FROM {$table} WHERE ({$condition}) GROUP BY {$group} ORDER BY {$order}";
1491+
// get the data from the database
1492+
$fields_string = array(
1493+
'hospital_pk',
1494+
'state',
1495+
'ccn',
1496+
'hospital_name',
1497+
'address',
1498+
'city',
1499+
'zip',
1500+
'hospital_subtype',
1501+
'fips_code',
1502+
);
1503+
$fields_int = array('is_metro_micro');
1504+
$fields_float = null;
1505+
execute_query($query, $epidata, $fields_string, $fields_int, $fields_float);
1506+
// return the data
1507+
return count($epidata) === 0 ? null : $epidata;
1508+
}
1509+
14491510
// queries a bunch of epidata tables
14501511
function get_meta() {
14511512
// query and return metadata
@@ -1956,6 +2017,17 @@ function meta_delphi() {
19562017
$epidata = get_covid_hosp_facility($hospital_pks, $collection_weeks, $publication_dates);
19572018
store_result($data, $epidata);
19582019
}
2020+
} else if($source === 'covid_hosp_facility_lookup') {
2021+
if(require_any($data, array('state', 'ccn', 'city', 'zip', 'fips_code'))) {
2022+
$state = isset($_REQUEST['state']) ? extract_values($_REQUEST['state'], 'str') : null;
2023+
$ccn = isset($_REQUEST['ccn']) ? extract_values($_REQUEST['ccn'], 'str') : null;
2024+
$city = isset($_REQUEST['city']) ? extract_values($_REQUEST['city'], 'str') : null;
2025+
$zip = isset($_REQUEST['zip']) ? extract_values($_REQUEST['zip'], 'str') : null;
2026+
$fips_code = isset($_REQUEST['fips_code']) ? extract_values($_REQUEST['fips_code'], 'str') : null;
2027+
// get the data
2028+
$epidata = get_covid_hosp_facility_lookup($state, $ccn, $city, $zip, $fips_code);
2029+
store_result($data, $epidata);
2030+
}
19592031
} else {
19602032
$data['message'] = 'no data source specified';
19612033
}

tests/acquisition/covid_hosp/common/test_utils.py

+23
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ def test_int_from_date(self):
4343

4444
self.assertEqual(Utils.int_from_date('2020-11-17'), 20201117)
4545

46+
def test_parse_bool(self):
47+
"""Parse a boolean value from a string."""
48+
49+
with self.subTest(name='None'):
50+
self.assertIsNone(Utils.parse_bool(None))
51+
52+
with self.subTest(name='empty'):
53+
self.assertIsNone(Utils.parse_bool(''))
54+
55+
with self.subTest(name='true'):
56+
self.assertTrue(Utils.parse_bool('true'))
57+
self.assertTrue(Utils.parse_bool('True'))
58+
self.assertTrue(Utils.parse_bool('tRuE'))
59+
60+
with self.subTest(name='false'):
61+
self.assertFalse(Utils.parse_bool('false'))
62+
self.assertFalse(Utils.parse_bool('False'))
63+
self.assertFalse(Utils.parse_bool('fAlSe'))
64+
65+
with self.subTest(name='exception'):
66+
with self.assertRaises(CovidHospException):
67+
Utils.parse_bool('maybe')
68+
4669
def test_get_entry_success(self):
4770
"""Get a deeply nested field from an arbitrary object."""
4871

0 commit comments

Comments
 (0)