diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..94deffdd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+SHELL := /bin/bash
+
+init:
+ python setup.py develop
+ pip install -r requirements.txt
+
+test:
+ nosetests ./tests/
+
+travis:
+ nosetests ./tests/
diff --git a/README.md b/README.md
index 962983d9..204f20bc 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,6 @@ Imagine you have the following code that you want to test:
```python
import boto
from boto.s3.key import Key
-conn = boto.connect_s3()
class MyModel(object):
def __init__(self, name, value):
@@ -17,6 +16,7 @@ class MyModel(object):
self.value = value
def save(self):
+ conn = boto.connect_s3()
bucket = conn.get_bucket('mybucket')
k = Key(bucket)
k.key = self.name
diff --git a/moto/__init__.py b/moto/__init__.py
new file mode 100644
index 00000000..8da1c85e
--- /dev/null
+++ b/moto/__init__.py
@@ -0,0 +1,2 @@
+from .ec2 import mock_ec2
+from .s3 import mock_s3
diff --git a/moto/__init__.pyc b/moto/__init__.pyc
new file mode 100644
index 00000000..0969fbd6
Binary files /dev/null and b/moto/__init__.pyc differ
diff --git a/moto/core/__init__.py b/moto/core/__init__.py
new file mode 100644
index 00000000..a4f19aed
--- /dev/null
+++ b/moto/core/__init__.py
@@ -0,0 +1 @@
+from .models import BaseBackend
\ No newline at end of file
diff --git a/moto/core/__init__.pyc b/moto/core/__init__.pyc
new file mode 100644
index 00000000..3679b007
Binary files /dev/null and b/moto/core/__init__.pyc differ
diff --git a/moto/core/models.py b/moto/core/models.py
new file mode 100644
index 00000000..f24974d5
--- /dev/null
+++ b/moto/core/models.py
@@ -0,0 +1,40 @@
+import functools
+import re
+
+from httpretty import HTTPretty
+
+
+class BaseBackend(object):
+ base_url = None
+
+ def reset(self):
+ self = self.__class__()
+
+ @property
+ def urls(self):
+ backend_module = self.__class__.__module__
+ backend_urls_module_name = backend_module.replace("models", "urls")
+ backend_urls_module = __import__(backend_urls_module_name, fromlist=['urls'])
+ urls = backend_urls_module.urls
+ return urls
+
+ def decorator(self, func):
+ @functools.wraps(func)
+ def wrapper(*args, **kw):
+ self.reset()
+
+ HTTPretty.reset()
+ HTTPretty.enable()
+
+ for method in HTTPretty.METHODS:
+ for key, value in self.urls.iteritems():
+ HTTPretty.register_uri(
+ method=method,
+ uri=re.compile(self.base_url + key),
+ body=value,
+ )
+ try:
+ return func(*args, **kw)
+ finally:
+ HTTPretty.disable()
+ return wrapper
diff --git a/moto/core/models.pyc b/moto/core/models.pyc
new file mode 100644
index 00000000..0eaf077a
Binary files /dev/null and b/moto/core/models.pyc differ
diff --git a/moto/ec2/__init__.py b/moto/ec2/__init__.py
new file mode 100644
index 00000000..f64708fe
--- /dev/null
+++ b/moto/ec2/__init__.py
@@ -0,0 +1,2 @@
+from .models import ec2_backend
+mock_ec2 = ec2_backend.decorator
\ No newline at end of file
diff --git a/moto/ec2/__init__.pyc b/moto/ec2/__init__.pyc
new file mode 100644
index 00000000..aad71507
Binary files /dev/null and b/moto/ec2/__init__.pyc differ
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
new file mode 100644
index 00000000..6d6013d4
--- /dev/null
+++ b/moto/ec2/models.py
@@ -0,0 +1,44 @@
+from boto.ec2.instance import Instance, InstanceState, Reservation
+
+from moto.core import BaseBackend
+from .utils import random_instance_id, random_reservation_id
+
+
+class MockEC2(BaseBackend):
+ base_url = "https://ec2.us-east-1.amazonaws.com"
+
+ def __init__(self):
+ self.reservations = {}
+
+ def add_instance(self):
+ new_instance = Instance()
+ new_instance.id = random_instance_id()
+ new_instance._state = InstanceState(0, "pending")
+
+ new_reservation = Reservation()
+ new_reservation.id = random_reservation_id()
+ new_reservation.instances = [new_instance]
+ self.reservations[new_reservation.id] = new_reservation
+ return new_reservation
+
+ def terminate_instances(self, instance_ids):
+ terminated_instances = []
+ for instance in self.all_instances():
+ if instance.id in instance_ids:
+ instance._state = InstanceState(32, 'shutting-down')
+ terminated_instances.append(instance)
+
+ return terminated_instances
+
+ def all_instances(self):
+ instances = []
+ for reservation in self.all_reservations():
+ for instance in reservation.instances:
+ instances.append(instance)
+ return instances
+
+ def all_reservations(self):
+ return self.reservations.values()
+
+
+ec2_backend = MockEC2()
\ No newline at end of file
diff --git a/moto/ec2/models.pyc b/moto/ec2/models.pyc
new file mode 100644
index 00000000..f7d51b6a
Binary files /dev/null and b/moto/ec2/models.pyc differ
diff --git a/moto/ec2/responses.py b/moto/ec2/responses.py
new file mode 100644
index 00000000..d77c828c
--- /dev/null
+++ b/moto/ec2/responses.py
@@ -0,0 +1,224 @@
+from urlparse import parse_qs
+
+from jinja2 import Template
+
+from .models import ec2_backend
+
+
+def instances(uri, body, headers):
+ querystring = parse_qs(body)
+ action = querystring['Action'][0]
+
+ if action == 'DescribeInstances':
+ template = Template(EC2_DESCRIBE_INSTANCES)
+ return template.render(reservations=ec2_backend.all_reservations())
+ elif action == 'RunInstances':
+ new_reservation = ec2_backend.add_instance()
+ template = Template(EC2_RUN_INSTANCES)
+ return template.render(reservation=new_reservation)
+ elif action == 'TerminateInstances':
+ instance_ids = querystring.get('InstanceId.1')[0]
+ instances = ec2_backend.terminate_instances(instance_ids)
+ template = Template(EC2_TERMINATE_INSTANCES)
+ return template.render(instances=instances)
+ else:
+ raise ValueError("Not implemented", action)
+
+
+EC2_RUN_INSTANCES = """
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ {{ reservation.id }}
+ 111122223333
+
+ -
+ sg-245f6a01
+ default
+
+
+
+ {% for instance in reservation.instances %}
+ -
+ {{ instance.id }}
+ ami-60a54009
+
+
0
+ pending
+
+
+
+
+ 0
+ m1.small
+ 2007-08-07T11:51:50.000Z
+
+ us-east-1b
+
+ default
+
+
+ enabled
+
+ true
+
+ -
+ sg-245f6a01
+ default
+
+
+ paravirtual
+
+ xen
+ false
+
+ {% endfor %}
+
+ """
+
+EC2_DESCRIBE_INSTANCES = """
+ fdcdcab1-ae5c-489e-9c33-4637c5dda355
+
+ {% for reservation in reservations %}
+ -
+ {{ reservation.id }}
+ 111122223333
+
+
-
+ sg-1a2b3c4d
+ my-security-group
+
+
+
+ {% for instance in reservation.instances %}
+ -
+ {{ instance.id }}
+ ami-1a2b3c4d
+
+
16
+ {{ instance.state }}
+
+
+
+
+ gsg-keypair
+ 0
+
+ c1.medium
+ YYYY-MM-DDTHH:MM:SS+0000
+
+ us-west-2a
+
+ default
+
+ windows
+
+ disabled
+
+ subnet-1a2b3c4d
+ vpc-1a2b3c4d
+ 10.0.0.12
+ 46.51.219.63
+ true
+
+ -
+ sg-1a2b3c4d
+ my-security-group
+
+
+ x86_64
+ ebs
+ /dev/sda1
+
+ -
+ /dev/sda1
+
+ vol-1a2b3c4d
+ attached
+ YYYY-MM-DDTHH:MM:SS.SSSZ
+ true
+
+
+
+ hvm
+ ABCDE1234567890123
+
+ -
+ Name
+ Windows Instance
+
+
+ xen
+
+ -
+ eni-1a2b3c4d
+ subnet-1a2b3c4d
+ vpc-1a2b3c4d
+ Primary network interface
+ 111122223333
+ in-use
+ 10.0.0.12
+ 1b:2b:3c:4d:5e:6f
+ true
+
+
-
+ sg-1a2b3c4d
+ my-security-group
+
+
+
+ eni-attach-1a2b3c4d
+ 0
+ attached
+ YYYY-MM-DDTHH:MM:SS+0000
+ true
+
+
+ 46.51.219.63
+ 111122223333
+
+
+ -
+ 10.0.0.12
+ true
+
+ 46.51.219.63
+ 111122223333
+
+
+ -
+ 10.0.0.14
+ false
+
+ 46.51.221.177
+ 111122223333
+
+
+
+
+
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+"""
+
+EC2_TERMINATE_INSTANCES = """
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ {% for instance in instances %}
+ -
+ {{ instance.id }}
+
+
32
+ shutting-down
+
+
+ 16
+ running
+
+
+ {% endfor %}
+
+"""
\ No newline at end of file
diff --git a/moto/ec2/responses.pyc b/moto/ec2/responses.pyc
new file mode 100644
index 00000000..83c92271
Binary files /dev/null and b/moto/ec2/responses.pyc differ
diff --git a/moto/ec2/urls.py b/moto/ec2/urls.py
new file mode 100644
index 00000000..0d12059e
--- /dev/null
+++ b/moto/ec2/urls.py
@@ -0,0 +1,5 @@
+from .responses import instances
+
+urls = {
+ '/': instances,
+}
diff --git a/moto/ec2/urls.pyc b/moto/ec2/urls.pyc
new file mode 100644
index 00000000..5a13c4fe
Binary files /dev/null and b/moto/ec2/urls.pyc differ
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
new file mode 100644
index 00000000..190ff4cd
--- /dev/null
+++ b/moto/ec2/utils.py
@@ -0,0 +1,15 @@
+import random
+
+
+def random_id(prefix=''):
+ size = 8
+ chars = range(10) + ['a', 'b', 'c', 'd', 'e', 'f']
+
+ instance_tag = ''.join(unicode(random.choice(chars)) for x in range(size))
+ return '{}-{}'.format(prefix, instance_tag)
+
+def random_instance_id():
+ return random_id(prefix='i')
+
+def random_reservation_id():
+ return random_id(prefix='r')
diff --git a/moto/ec2/utils.pyc b/moto/ec2/utils.pyc
new file mode 100644
index 00000000..c5395cf9
Binary files /dev/null and b/moto/ec2/utils.pyc differ
diff --git a/moto/s3/__init__.py b/moto/s3/__init__.py
new file mode 100644
index 00000000..441b5996
--- /dev/null
+++ b/moto/s3/__init__.py
@@ -0,0 +1,2 @@
+from .models import s3_backend
+mock_s3 = s3_backend.decorator
\ No newline at end of file
diff --git a/moto/s3/__init__.pyc b/moto/s3/__init__.pyc
new file mode 100644
index 00000000..d172de9a
Binary files /dev/null and b/moto/s3/__init__.pyc differ
diff --git a/moto/s3/models.py b/moto/s3/models.py
new file mode 100644
index 00000000..c24fb89c
--- /dev/null
+++ b/moto/s3/models.py
@@ -0,0 +1,57 @@
+# from boto.s3.bucket import Bucket
+# from boto.s3.key import Key
+import md5
+
+from moto.core import BaseBackend
+
+
+class FakeKey(object):
+ def __init__(self, name, value):
+ self.name = name
+ self.value = value
+
+ @property
+ def etag(self):
+ value_md5 = md5.new()
+ value_md5.update(self.value)
+ return '"{0}"'.format(value_md5.hexdigest())
+
+class FakeBucket(object):
+ def __init__(self, name):
+ self.name = name
+ self.keys = []
+
+
+class MockS3(BaseBackend):
+ base_url = "https://(.+).s3.amazonaws.com"
+
+ def __init__(self):
+ self.buckets = {}
+
+ def create_bucket(self, bucket_name):
+ new_bucket = FakeBucket(name=bucket_name)
+ self.buckets[bucket_name] = new_bucket
+ return new_bucket
+
+ def get_bucket(self, bucket_name):
+ return self.buckets.get(bucket_name)
+
+ def set_key(self, bucket_name, key_name, value):
+ bucket = self.buckets[bucket_name]
+ new_key = FakeKey(name=key_name, value=value)
+ bucket.keys.append(new_key)
+
+ return new_key
+
+ def get_key(self, bucket_name, key_name):
+ bucket = self.buckets[bucket_name]
+ found_key = None
+ for key in bucket.keys:
+ if key.name == key_name:
+ found_key = key
+ break
+
+ return found_key
+
+
+s3_backend = MockS3()
diff --git a/moto/s3/models.pyc b/moto/s3/models.pyc
new file mode 100644
index 00000000..d515c95f
Binary files /dev/null and b/moto/s3/models.pyc differ
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
new file mode 100644
index 00000000..57a44dd0
--- /dev/null
+++ b/moto/s3/responses.py
@@ -0,0 +1,72 @@
+from jinja2 import Template
+
+from .models import s3_backend
+
+def bucket_response(uri, body, headers):
+ hostname = uri.hostname
+ bucket_name = hostname.replace(".s3.amazonaws.com", "")
+
+ if uri.method == 'GET':
+ bucket = s3_backend.get_bucket(bucket_name)
+ if bucket:
+ template = Template(S3_BUCKET_GET_RESPONSE)
+ return template.render(bucket=bucket)
+ else:
+ return "", dict(status=404)
+ else:
+ new_bucket = s3_backend.create_bucket(bucket_name)
+ template = Template(S3_BUCKET_CREATE_RESPONSE)
+ return template.render(bucket=new_bucket)
+
+
+def key_response(uri_info, body, headers):
+
+ key_name = uri_info.path.lstrip('/')
+ hostname = uri_info.hostname
+ bucket_name = hostname.replace(".s3.amazonaws.com", "")
+
+ if uri_info.method == 'GET':
+ key = s3_backend.get_key(bucket_name, key_name)
+ if key:
+ return key.value
+ else:
+ return "", dict(status=404)
+
+ if uri_info.method == 'PUT':
+ if body:
+ new_key = s3_backend.set_key(bucket_name, key_name, body)
+ return S3_OBJECT_RESPONSE, dict(etag=new_key.etag)
+ key = s3_backend.get_key(bucket_name, key_name)
+ if key:
+ return "", dict(etag=key.etag)
+ else:
+ return ""
+ elif uri_info.method == 'HEAD':
+ key = s3_backend.get_key(bucket_name, key_name)
+ return S3_OBJECT_RESPONSE, dict(etag=key.etag)
+ else:
+ import pdb;pdb.set_trace()
+
+
+S3_BUCKET_GET_RESPONSE = """\
+ {{ bucket.name }}\
+ notes/\
+ /\
+ 1000\
+ AKIAIOSFODNN7EXAMPLE\
+ 2006-03-01T12:00:00.183Z\
+ Iuyz3d3P0aTou39dzbqaEXAMPLE=\
+ """
+
+S3_BUCKET_CREATE_RESPONSE = """
+
+ {{ bucket.name }}
+
+"""
+
+S3_OBJECT_RESPONSE = """
+
+ "asdlfkdalsjfsalfkjsadlfjsdjkk"
+ 2006-03-01T12:00:00.183Z
+
+ """
\ No newline at end of file
diff --git a/moto/s3/responses.pyc b/moto/s3/responses.pyc
new file mode 100644
index 00000000..b1f545e1
Binary files /dev/null and b/moto/s3/responses.pyc differ
diff --git a/moto/s3/urls.py b/moto/s3/urls.py
new file mode 100644
index 00000000..b7b20d82
--- /dev/null
+++ b/moto/s3/urls.py
@@ -0,0 +1,6 @@
+from .responses import bucket_response, key_response
+
+urls = {
+ '/$': bucket_response,
+ '/(.+)': key_response,
+}
diff --git a/moto/s3/urls.pyc b/moto/s3/urls.pyc
new file mode 100644
index 00000000..4ab59047
Binary files /dev/null and b/moto/s3/urls.pyc differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..430bd583
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+boto
+httpretty
+Jinja2
+mock
+nose
+sure
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..4bfde779
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+import sys
+from setuptools import setup, find_packages
+
+setup(
+ name='moto',
+ version='0.0.1',
+ description='Moto is a library that allows your python tests to easily mock out the boto library',
+ author='Steve Pulec',
+ author_email='spulec@gmail',
+ url='https://github.com/spulec/moto',
+ packages=find_packages()
+)
\ No newline at end of file
diff --git a/tests/__init__.pyc b/tests/__init__.pyc
new file mode 100644
index 00000000..a328373b
Binary files /dev/null and b/tests/__init__.pyc differ
diff --git a/tests/test_ec2/test_ec2.py b/tests/test_ec2/test_ec2.py
new file mode 100644
index 00000000..90e7e156
--- /dev/null
+++ b/tests/test_ec2/test_ec2.py
@@ -0,0 +1,29 @@
+import boto
+from boto.ec2.instance import Reservation
+from sure import expect
+
+from moto import mock_ec2
+
+
+
+@mock_ec2
+def test_instance_launch_and_terminate():
+ conn = boto.connect_ec2('the_key', 'the_secret')
+ reservation = conn.run_instances('')
+ reservation.should.be.a(Reservation)
+ reservation.instances.should.have.length_of(1)
+ instance = reservation.instances[0]
+
+ reservations = conn.get_all_instances()
+ reservations.should.have.length_of(1)
+ reservations[0].id.should.equal(reservation.id)
+ instances = reservations[0].instances
+ instances.should.have.length_of(1)
+ instances[0].id.should.equal(instance.id)
+ instances[0].state.should.equal('pending')
+
+ conn.terminate_instances(instances[0].id)
+
+ reservations = conn.get_all_instances()
+ instance = reservations[0].instances[0]
+ instance.state.should.equal('shutting-down')
diff --git a/tests/test_ec2/test_ec2.pyc b/tests/test_ec2/test_ec2.pyc
new file mode 100644
index 00000000..50053ae6
Binary files /dev/null and b/tests/test_ec2/test_ec2.pyc differ
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
new file mode 100644
index 00000000..0f844476
--- /dev/null
+++ b/tests/test_s3/test_s3.py
@@ -0,0 +1,30 @@
+import boto
+from boto.s3.key import Key
+
+from moto import mock_s3
+
+
+class MyModel(object):
+ def __init__(self, name, value):
+ self.name = name
+ self.value = value
+
+ def save(self):
+ conn = boto.connect_s3('the_key', 'the_secret')
+ bucket = conn.get_bucket('mybucket')
+ k = Key(bucket)
+ k.key = self.name
+ k.set_contents_from_string(self.value)
+
+
+@mock_s3
+def test_my_model_save():
+ # Create Bucket so that test can run
+ conn = boto.connect_s3('the_key', 'the_secret')
+ conn.create_bucket('mybucket')
+ ####################################
+
+ model_instance = MyModel('steve', 'is awesome')
+ model_instance.save()
+
+ assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome'
diff --git a/tests/test_s3/test_s3.pyc b/tests/test_s3/test_s3.pyc
new file mode 100644
index 00000000..8fe1e7de
Binary files /dev/null and b/tests/test_s3/test_s3.pyc differ