Skip to content

Commit

Permalink
Add boto dependency as s3 extra (#46)
Browse files Browse the repository at this point in the history
* Gracefully fail if boto is not present

* Add s3 extra

* Aggregate coverage runs

* Version bump
  • Loading branch information
di authored Oct 11, 2017
1 parent fa5faeb commit e02d2e4
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 10 deletions.
7 changes: 5 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,11 @@ Vladiate comes with the following input types:

*class* ``S3File``

Read from a file in S3. Uses the `boto <https://github.com/boto/boto>`_
library. Optionally can specify either a full path, or a bucket/key pair.
Read from a file in S3. Optionally can specify either a full path, or a
bucket/key pair.

Requires the `boto <https://github.com/boto/boto>`_ library, which should be
installed via ``pip install vladiate[s3]``.

:``path=None``:
A full S3 filepath (e.g., ``s3://foo.bar/path/to/file.csv``)
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand

__version__ = '0.0.18'
__version__ = '0.0.19'


class PyTest(TestCommand):
Expand Down Expand Up @@ -60,7 +60,8 @@ def readme():
packages=find_packages(exclude=['examples', 'tests']),
include_package_data=True,
zip_safe=False,
install_requires=['boto'],
install_requires=[],
extras_require={'s3': ['boto']},
tests_require=['pretend', 'pytest', 'flake8'],
cmdclass={'test': PyTest},
entry_points={
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ basepython = python3.6
commands = flake8

[testenv]
commands = coverage run --source=vladiate setup.py test
commands = coverage run --append --source=vladiate setup.py test
deps =
pytest
coverage
Expand Down
9 changes: 9 additions & 0 deletions vladiate/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@ class BadValidatorException(Exception):
def __init__(self, extra):
message = 'Row does not contain the following unique_with fields: {}'
super(BadValidatorException, self).__init__(message.format(extra))


class MissingExtraException(Exception):
''' Thrown when an extra dependency is missing '''
def __init__(self):
super(MissingExtraException, self).__init__(
'The `s3` extra is required to use the `S3File` class. Install'
' it via `pip install vladiate[s3]`.'
)
14 changes: 12 additions & 2 deletions vladiate/inputs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import io
import boto
try:
from urlparse import urlparse
except:
Expand All @@ -9,6 +8,8 @@
except:
from io import StringIO

from vladiate.exceptions import MissingExtraException


class VladInput(object):
''' A generic input class '''
Expand Down Expand Up @@ -41,6 +42,15 @@ class S3File(VladInput):
''' Read from a file in S3 '''

def __init__(self, path=None, bucket=None, key=None):
try:
import boto # noqa
self.boto = boto
except:
# 2.7 workaround, should just be `raise Exception() from None`
exc = MissingExtraException()
exc.__context__ = None
raise exc

if path and not any((bucket, key)):
self.path = path
parse_result = urlparse(path)
Expand All @@ -57,7 +67,7 @@ def __init__(self, path=None, bucket=None, key=None):
)

def open(self):
s3 = boto.connect_s3()
s3 = self.boto.connect_s3()
bucket = s3.get_bucket(self.bucket)
key = bucket.new_key(self.key)
contents = key.get_contents_as_string()
Expand Down
35 changes: 32 additions & 3 deletions vladiate/test/test_inputs.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import pytest
from pretend import stub, call, call_recorder

from ..exceptions import MissingExtraException
from ..inputs import S3File, StringIO, String, VladInput
from ..vlad import Vlad


def mock_boto(result):
try:
import builtins
except:
import __builtin__ as builtins
realimport = builtins.__import__

def badimport(name, *args, **kwargs):
if name == 'boto':
return result()
return realimport(name, *args, **kwargs)

builtins.__import__ = badimport


@pytest.mark.parametrize('kwargs', [
({'path': 's3://some.bucket/some/s3/key.csv'}),
({'bucket': 'some.bucket', 'key': '/some/s3/key.csv'}),
])
def test_s3_input_works(kwargs):
mock_boto(lambda: stub())
S3File(**kwargs)


Expand All @@ -21,6 +38,7 @@ def test_s3_input_works(kwargs):
({'key': '/some/s3/key.csv'}),
])
def test_s3_input_fails(kwargs):
mock_boto(lambda: stub())
with pytest.raises(ValueError):
S3File(**kwargs)

Expand All @@ -35,7 +53,7 @@ def test_string_input_works(kwargs):
assert Vlad(source=source, validators=validators).validate()


def test_open_s3file(monkeypatch):
def test_open_s3file():
new_key = call_recorder(lambda *args, **kwargs: stub(
get_contents_as_string=lambda: 'contents'.encode()
))
Expand All @@ -44,9 +62,10 @@ def test_open_s3file(monkeypatch):

mock_boto = stub(connect_s3=lambda: stub(get_bucket=get_bucket))

monkeypatch.setattr('vladiate.inputs.boto', mock_boto)
s3file = S3File('s3://some.bucket/some/s3/key.csv')
s3file.boto = mock_boto

result = S3File('s3://some.bucket/some/s3/key.csv').open()
result = s3file.open()

assert get_bucket.calls == [
call('some.bucket')
Expand Down Expand Up @@ -77,3 +96,13 @@ def __init__(self):

with pytest.raises(NotImplementedError):
repr(PartiallyImplemented())


def test_s3file_raises_when_no_boto():
def import_result():
raise ImportError

mock_boto(import_result)

with pytest.raises(MissingExtraException):
S3File()

0 comments on commit e02d2e4

Please sign in to comment.