Skip to content

Commit 7a52032

Browse files
committed
Complete testing s3 coverage provider methods.
1 parent e55615e commit 7a52032

3 files changed

Lines changed: 140 additions & 15 deletions

File tree

bravo_api/core/s3_coverage_provider.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@
88
import re
99
import boto3
1010
from botocore.exceptions import ClientError
11+
from functools import reduce
1112

1213

1314
class S3CoverageProvider(CoverageProvider):
1415

1516
def __init__(self, src):
1617
self.client = boto3.client('s3')
1718
self.src = src
19+
split_url = urlparse(self.src)
20+
self.bucket = split_url.netloc
21+
self.prefix = split_url.path.lstrip('/')
1822
self.validate_source()
19-
# self.catalog = self.discover_files()
23+
self.catalog = self.discover_files()
2024

2125
def validate_source(self):
2226
"""
@@ -30,8 +34,8 @@ def validate_source(self):
3034

3135
# Check that objects exists in bucket at prefix.
3236
try:
33-
obj_resp = self.client.list_objects_v2(Bucket=split_url.netloc,
34-
Prefix=split_url.path.lstrip('/'),
37+
obj_resp = self.client.list_objects_v2(Bucket=self.bucket,
38+
Prefix=self.prefix,
3539
MaxKeys=10)
3640
except ClientError as err:
3741
msg = err.response['Error']['Message']
@@ -52,16 +56,38 @@ def validate_source(self):
5256
def evaluate_chrom_representation(self):
5357
"""
5458
All chromosomes expected to be represented in all coverage bins
59+
Return list of messages to the contrary.
60+
"""
61+
messages = []
62+
for bin_name, cov_bin in self.catalog.items():
63+
missing_chroms = [chrom for chrom in self._chroms if chrom not in cov_bin.keys()]
64+
if(missing_chroms):
65+
messages.append(f'Coverage {bin_name} missing chroms: {missing_chroms}')
66+
return(messages)
67+
68+
@staticmethod
69+
def simple_dict_flat(acc, d):
5570
"""
56-
msgs = []
57-
return(msgs)
71+
Helper function for reducing list of non-nested dicts to list of values
72+
"""
73+
acc.extend(list(d.values()))
74+
return(acc)
5875

5976
def evaluate_chrom_readability(self):
6077
"""
61-
All coverage files expected to be readable
78+
All coverage files expected to be readable.
79+
Return list of messages to the contrary.
6280
"""
63-
msgs = []
64-
return(msgs)
81+
messages = []
82+
all_paths = reduce(S3CoverageProvider.simple_dict_flat, self.catalog.values(), [])
83+
84+
for path in all_paths:
85+
try:
86+
self.client.head_object(Bucket=self.bucket, Key=path)
87+
except ClientError as err:
88+
err_msg = err.response['Error']['Message']
89+
messages.append(f'{self.bucket}/{path}: {err_msg}')
90+
return(messages)
6591

6692
def evaluate_catalog(self):
6793
warn_msgs = []
@@ -72,9 +98,31 @@ def evaluate_catalog(self):
7298
def discover_files(self):
7399
"""
74100
Find and organize the chrN.bin_X.YZ.tar.gz coverage files into a dictionary organized by
75-
bin then chromosome.
101+
bin then chromosome. Form of bin-chr-file index will be:
102+
{
103+
'bin_0.25': {'chr1': 's3://bucket/prefix/chr1.bin_0.25.tsv.gz',
104+
...,
105+
'chr22': 's3://bucket/prefix/chr22.bin_0.25.tsv.gz'},
106+
....
107+
'bin_full': {...}
108+
}
76109
"""
110+
split_url = urlparse(self.src)
111+
bucket = split_url.netloc
112+
prefix = split_url.path.lstrip('/')
113+
114+
obj_resp = self.client.list_objects_v2(Bucket=bucket, Prefix=prefix)
115+
all_keys = [item['Key'] for item in obj_resp['Contents']]
116+
117+
chr_patt = re.compile(r'.*chr([0-9X]{1,2})')
77118
result = {}
119+
for bin_name in self._bins:
120+
result[bin_name] = {}
121+
bin_keys = [item for item in all_keys if item.startswith(f'{prefix}/{bin_name}')]
122+
123+
for key in bin_keys:
124+
chr = chr_patt.match(key).group(1)
125+
result[bin_name][chr] = key
78126
return(result)
79127

80128
def lookup_coverage_path(self, cov_bin, chrom):

tests/core/conftest.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
def expected_bins():
3838
return(['bin_0.25', 'bin_0.50', 'bin_0.75', 'bin_1.00', 'full'])
3939

40+
@pytest.fixture(scope="session")
41+
def incomplete_bins(expected_bins):
42+
return(expected_bins[1:])
43+
4044

4145
@pytest.fixture(scope="session")
4246
def expected_chroms():
@@ -75,18 +79,46 @@ def s3(aws_credentials):
7579

7680

7781
@pytest.fixture(scope="session")
78-
def sham_cov_bucket(s3, expected_bins, expected_chroms):
82+
def sham_cov_url(s3, expected_bins, expected_chroms):
83+
"""
84+
Create sham bucket with all bins and chroms stubbed out.
85+
Return url to of coverage prefix containing bins.
86+
"""
7987
bucket_name = 'test-'.join([random.choice(string.ascii_lowercase) for i in range(8)])
88+
prefix = 'coverage'
8089
s3.create_bucket(Bucket=bucket_name,
8190
CreateBucketConfiguration={'LocationConstraint': 'atlantis'})
8291

8392
for cbin in expected_bins:
8493
for chrom in expected_chroms:
8594
s3.put_object(Bucket=bucket_name,
86-
Key=f'coverage/{cbin}/chr{chrom}.{cbin}.tsv.gz',
95+
Key=f'{prefix}/{cbin}/chr{chrom}.{cbin}.tsv.gz',
96+
Body='sham coverage content')
97+
s3.put_object(Bucket=bucket_name,
98+
Key=f'{prefix}/{cbin}/chr{chrom}.{cbin}.tsv.gz.tbi',
99+
Body='sham index content')
100+
101+
return(f's3://{bucket_name}/{prefix}')
102+
103+
104+
@pytest.fixture(scope="session")
105+
def sham_incomplete_cov_url(s3, incomplete_bins, expected_chroms):
106+
"""
107+
Create sham bucket missing an entire bin of chroms stubs.
108+
Return url to of coverage prefix containing bins.
109+
"""
110+
bucket_name = 'test-'.join([random.choice(string.ascii_lowercase) for i in range(8)])
111+
prefix = 'coverage'
112+
s3.create_bucket(Bucket=bucket_name,
113+
CreateBucketConfiguration={'LocationConstraint': 'atlantis'})
114+
115+
for cbin in incomplete_bins:
116+
for chrom in expected_chroms:
117+
s3.put_object(Bucket=bucket_name,
118+
Key=f'{prefix}/{cbin}/chr{chrom}.{cbin}.tsv.gz',
87119
Body='sham coverage content')
88120
s3.put_object(Bucket=bucket_name,
89-
Key=f'coverage/{cbin}/chr{chrom}.{cbin}.tsv.gz.tbi',
121+
Key=f'{prefix}/{cbin}/chr{chrom}.{cbin}.tsv.gz.tbi',
90122
Body='sham index content')
91123

92-
return(bucket_name)
124+
return(f's3://{bucket_name}/{prefix}')

tests/core/test_s3_coverage.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from botocore.exceptions import ClientError
55

66

7-
def test_smokes(sham_cov_bucket):
8-
cp = S3CoverageProvider(f's3://{sham_cov_bucket}')
7+
def test_smokes(sham_cov_url):
8+
cp = S3CoverageProvider(sham_cov_url)
99
assert(isinstance(cp, CoverageProvider))
1010

1111

@@ -31,3 +31,48 @@ def test_missing_source_error(mocker):
3131
MaxKeys=10)
3232
# Verify informative error thrown
3333
assert(err.match(fake_msg))
34+
35+
36+
def test_catalog_discovery_full(s3, sham_cov_url, expected_bins, expected_chroms):
37+
cp = S3CoverageProvider(sham_cov_url)
38+
39+
# All bins are represented
40+
assert(set(cp.catalog.keys()) == set(expected_bins))
41+
42+
# All chroms are represented in each bin
43+
for catalog_bin in cp.catalog.values():
44+
assert(set(catalog_bin.keys()) == set(expected_chroms))
45+
46+
47+
def test_catalog_readability(sham_cov_url):
48+
cp = S3CoverageProvider(sham_cov_url)
49+
messages = cp.evaluate_chrom_readability()
50+
assert(len(messages) == 0)
51+
52+
53+
def test_catalog_readability_denied(mocker, sham_cov_url):
54+
cp = S3CoverageProvider(sham_cov_url)
55+
56+
mock_method = mocker.patch.object(cp.client, 'head_object')
57+
fake_err = ClientError({'Error': {'Code': '403', 'Message': 'Forbidden'}}, 'head_object')
58+
mock_method.side_effect = fake_err
59+
60+
messages = cp.evaluate_chrom_readability()
61+
62+
assert(len(messages) == mock_method.call_count)
63+
64+
65+
def test_evaluate_chrom_representation(sham_cov_url):
66+
cp = S3CoverageProvider(sham_cov_url)
67+
messages = cp.evaluate_chrom_representation()
68+
assert(len(messages) == 0)
69+
70+
71+
def test_evaluate_chrom_representation_incomplete(
72+
sham_incomplete_cov_url, incomplete_bins, expected_bins
73+
):
74+
cp = S3CoverageProvider(sham_incomplete_cov_url)
75+
messages = cp.evaluate_chrom_representation()
76+
77+
num_messages_expected = len(expected_bins) - len(incomplete_bins)
78+
assert(len(messages) == num_messages_expected)

0 commit comments

Comments
 (0)