Skip to content

Commit

Permalink
Merge pull request #3 from grampajoe/downtime
Browse files Browse the repository at this point in the history
Implement a down command
  • Loading branch information
grampajoe committed Feb 16, 2015
2 parents ea0fdb6 + f0bb1a5 commit 4590ef3
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 7 deletions.
15 changes: 13 additions & 2 deletions README.rst
Expand Up @@ -43,7 +43,8 @@ Log in with the `Heroku toolbelt`_ , then do this:
$ pip install happy
$ happy up
Creating app...
Creating app... butt-man-123
Building... done
It's up! :) https://butt-man-123.herokuapp.com
That's it! You made a temporary app with all the fixins, which you can
Expand All @@ -54,7 +55,7 @@ Then, you can get rid of it like:
.. code:: text
$ happy down
Destroying app...
Destroying app butt-man-123... done
It's down. :(
.. _app.json manifest: https://devcenter.heroku.com/articles/app-json-schema
Expand All @@ -68,11 +69,21 @@ up

Brings up a Heroku app.

The app name is stored in a file called ``.happy`` in the working directory so
happy can find it later.

- ``--tarball-url``

(optional) URL of the tarball containing app.json. If this is not given,
happy tries to infer it from an ``app.json`` file in the current directory.

down
~~~~

Brings down a Heroku app.

The app name is read from a file called ``.happy`` in the working directory.

Running the tests
-----------------

Expand Down
10 changes: 10 additions & 0 deletions happy/__init__.py
Expand Up @@ -30,3 +30,13 @@ def wait(build_id):
if api.check_build_status(build_id):
break
sleep(3)


def delete(app_name):
"""Deletes a Heroku app.
:param app_name: Name of the Heroku app to delete.
"""
api = Heroku()

api.delete_app(app_name=app_name)
38 changes: 36 additions & 2 deletions happy/cli.py
Expand Up @@ -2,6 +2,7 @@
The command-line interface for happy!
"""
import json
import os
import subprocess
import sys

Expand Down Expand Up @@ -29,6 +30,20 @@ def _write_app_name(app_name):
f.write(str(app_name))


def _read_app_name():
"""Reads the app name from the .happy file."""
try:
with click.open_file('.happy', 'r') as f:
return f.read().strip()
except IOError:
return None


def _delete_app_name_file():
"""Deletes the .happy file. :("""
os.remove('.happy')


@click.group(name='happy')
def cli():
"""Quickly set up and tear down Heroku apps!"""
Expand All @@ -50,11 +65,30 @@ def up(tarball_url):

click.echo(app_name)

_write_app_name(app_name)

click.echo('Building... ', nl=False)

happy.wait(build_id)

_write_app_name(app_name)

click.echo('done')
click.echo("It's up! :) https://%s.herokuapp.com" % app_name)


@cli.command(name='down')
def down():
"""Brings down a Heroku app."""
app_name = _read_app_name()

if not app_name:
click.echo('No app is running.')
sys.exit(1)

click.echo('Destroying app %s... ' % app_name, nl=False)

happy.delete(app_name=app_name)

_delete_app_name_file()

click.echo('done')
click.echo("It's down. :(")
13 changes: 12 additions & 1 deletion happy/heroku.py
Expand Up @@ -65,7 +65,11 @@ def create_build(self, tarball_url):
return self.api_request('POST', '/app-setups', data=data)

def check_build_status(self, build_id):
"""Checks the status of an app-setups build."""
"""Checks the status of an app-setups build.
:param build_id: ID of the build to check.
:returns: ``True`` if succeeded, ``False`` if pending.
"""
data = self.api_request('GET', '/app-setups/%s' % build_id)

status = data.get('status')
Expand All @@ -76,3 +80,10 @@ def check_build_status(self, build_id):
return True
else:
raise BuildError(data.get('failure_message'))

def delete_app(self, app_name):
"""Deletes an app.
:param app_name: Name of the app to delete.
"""
self.api_request('DELETE', '/apps/%s' % app_name)
53 changes: 51 additions & 2 deletions tests/test_cli.py
Expand Up @@ -112,10 +112,59 @@ def test_up_prints_info(create, wait, runner):
with runner.isolated_filesystem():
result = runner.invoke(cli, ['up', '--tarball-url=example.com'])

expected = (
assert result.output == (
"Creating app... butt-man-123\n"
"Building... done\n"
"It's up! :) https://butt-man-123.herokuapp.com\n"
)

assert result.output == expected

@mock.patch('happy.delete')
def test_down(delete, runner):
"""`happy.down` should delete the app."""
with runner.isolated_filesystem():
with open('.happy', 'w') as f:
f.write('butt-man-123')

result = runner.invoke(cli, ['down'])

delete.assert_called_with(app_name='butt-man-123')
assert result.exit_code == 0


@mock.patch('happy.delete')
def test_down_deletes_app_name_file(delete, runner):
"""`happy.down` should delete the .happy file."""
with runner.isolated_filesystem():
with open('.happy', 'w') as f:
f.write('butt-man-123')

runner.invoke(cli, ['down'])

with pytest.raises(IOError):
open('.happy', 'r')


@mock.patch('happy.delete')
def test_down_no_app(delete, runner):
"""With no app to delete, down should fail."""
with runner.isolated_filesystem():
result = runner.invoke(cli, ['down'])

assert delete.called is False
assert result.exit_code == 1


@mock.patch('happy.delete')
def test_down_prints_info(delete, runner):
"""`happy.down` should print status info."""
with runner.isolated_filesystem():
with open('.happy', 'w') as f:
f.write('butt-man-123')

result = runner.invoke(cli, ['down'])

assert result.output == (
"Destroying app butt-man-123... done\n"
"It's down. :(\n"
)
7 changes: 7 additions & 0 deletions tests/test_happy.py
Expand Up @@ -54,3 +54,10 @@ def test_wait(heroku):
mock.call('12345'),
mock.call('12345'),
])


def test_delete(heroku):
"""Should delete the app."""
happy.delete(app_name='butt-man-123')

heroku.delete_app.assert_called_with(app_name='butt-man-123')
13 changes: 13 additions & 0 deletions tests/test_heroku.py
Expand Up @@ -146,3 +146,16 @@ def test_heroku_check_build_status_failed(api_request):
heroku.check_build_status('123')

assert 'oops' in str(exc.value)


@mock.patch.object(Heroku, 'api_request')
def test_heroku_delete_app(api_request):
"""Heroku.delete_app should delete an app."""
heroku = Heroku()

heroku.delete_app(app_name='butt-man-123')

api_request.assert_called_with(
'DELETE',
'/apps/butt-man-123',
)

0 comments on commit 4590ef3

Please sign in to comment.