├── .gitignore ├── README.md ├── djangohero ├── __init__.py └── djangohero.py ├── logo └── djangohero.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #DjangoHero 2 | djangohero logo 3 | 4 | ## Description 5 | 6 | DjangoHero is the fastest way to set up a Django project on the cloud (using Heroku). Starting a Django project using DjangoHero will 7 | 8 | 1. Set up the Django project with a folder structure that follows best practices. 9 | 2. Make the Django project live on the cloud (Heroku) immediately. 10 | 3. Provision a database on the cloud (Heroku postgres) and connect your appication to it. 11 | 12 | Best part : all of this happens with a single command under a minute without any input from your side. This is as good as plug and play gets. 13 | 14 | ## Example 15 | 16 | To start a Heroku app called `my_app` running an empty Django project called `my_django_project`, complete with a 17 | database, we need to run the following command. Try it out, it works like a charm. 18 | 19 | ``` 20 | djangohero deploy --app=my_app --database my_django_project 21 | ``` 22 | 23 | ## Why is this useful? 24 | 25 | Creating a Django app on Heroku usually requires the following steps (you don't have to read all of them, just notice how many there are): 26 | 27 | 1. Create a Django project using `django-admin startproject`. 28 | 2. Modify the Django project to conform to best practices outlined in 29 | [Two Scoops of Django](https://www.twoscoopspress.com/products/two-scoops-of-django-1-8) or whatever 30 | project structure suits your team best. 31 | 3. Create a settings file for Heroku and add Heroku specific database settings and static file settings. 32 | 4. For serving static files from Heroku, modify `wsgi.py` to use whitenoise. 33 | 5. Initialize a Git repository for the project. 34 | 6. Create the heroku app. 35 | 7. Add the app url to `ALLOWED_HOSTS` in the Django settings. 36 | 8. Set heroku config vars for `DJANGO_SETTINGS_MODULE` and `DJANGO_SECRET_KEY`. If you are using best practices for Django, 37 | you'd need these. 38 | 9. Create `requirements.txt` containing required Python packages. 39 | 10. Create a `Procfile` containing information on the web process and `runtime.txt` containing information on the 40 | preferred Python runtime. 41 | 11. Create a Heroku Postgres addon to set up a database. 42 | 12. Commit the changes to Git and push the changes to Heroku. 43 | 13. Scale the app. 44 | 45 | These boring steps have to be repeated for every project before we can get it to go live on the cloud. This package solves the problem by automating the setup process and making the application live on the cloud from birth. 46 | 47 | ## Installation 48 | 49 | The following steps are recommended. 50 | 51 | ###Git clone this repo 52 | ``` 53 | git clone https://github.com/gutfeeling/djangohero.git 54 | ``` 55 | ###Install with pip 56 | ``` 57 | pip install -e djangohero 58 | ``` 59 | 60 | ## Usage 61 | 62 | Installing djangohero will make the program `djangohero` available on your system. Here's how to use it. 63 | 64 | ### Syntax 65 | 66 | ``` 67 | djangohero deploy --app= --region= --scale= --database --database_type= --python= --container_name= 68 | ``` 69 | 70 | ### Explanation of options, flags and arguments 71 | 72 | **--app** : Name of the Heroku app; defaults to Heroku's imaginative app names. 73 | 74 | **--region** : [Geographical location of the app](https://devcenter.heroku.com/articles/regions); defaults to us. 75 | 76 | **--scale** : [Scale of the dynos](https://devcenter.heroku.com/articles/scaling); defaults to 1. 77 | 78 | **--database** : If this flag is present, a [database](https://devcenter.heroku.com/articles/heroku-postgresql) is created. 79 | 80 | **--database_type** : [Name of the Heroku Postgres' plan tier](https://devcenter.heroku.com/articles/heroku-postgres-plans#plan-tiers); defaults to hobby-dev. 81 | 82 | **--python** : [Python runtime (either 2 or 3)](https://devcenter.heroku.com/articles/python-runtimes#supported-python-runtimes); 83 | defaults to Python 3. 84 | 85 | **--container_name** : Name of the container folder; defaults to app name. 86 | 87 | **django_project_name** : Name of the Django project. 88 | 89 | ## Folder structure of the app 90 | 91 | The created app uses the following folder structure for the Django project in accordance with 92 | [Two Scoops of Django](https://www.twoscoopspress.com/products/two-scoops-of-django-1-8) and 93 | Heroku's requirements. The folder structure can be 94 | changed without much trouble as well by changing the template. 95 | ``` 96 | container_name 97 | ├── django_project_name 98 | │   ├── django_project_name 99 | │   │   ├── __init__.py 100 | │   │   ├── settings 101 | │   │   │   ├── __init__.py 102 | │   │   │   ├── settings_base.py 103 | │   │   │   └── settings_heroku.py 104 | │   │   ├── urls.py 105 | │   │   └── wsgi 106 | │   │   ├── __init__.py 107 | │   │   ├── wsgi_base.py 108 | │   │   └── wsgi_heroku.py 109 | │   └── manage.py 110 | ├── Procfile 111 | ├── requirements.txt 112 | └── runtime.txt 113 | ``` 114 | 115 | ## A note about migrations 116 | 117 | Because of the project structure, you need to execute the following commands for migrations 118 | 119 | ``` 120 | heroku run django_project_name/manage.py makemigrations 121 | heroku run django_project_name/manage.py migrate 122 | ``` 123 | 124 | ## Compatibility 125 | Works on both Python 2 and 3. 126 | 127 | ## Requirements 128 | 129 | You should have the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) and Git installed for this to work. You need 130 | to logged into Heroku at the time of running `djangohero`. You can login to Heroku by using the following command. 131 | 132 | ``` 133 | heroku login 134 | ``` 135 | 136 | ## Additional comments 137 | 138 | There's another project called [heroku-django-template](https://github.com/heroku/heroku-django-template) that provides a 139 | Heroku template. It does not fit my use case as the template 140 | there does not conform to the best practices outlined in 141 | [Two Scoops of Django](https://www.twoscoopspress.com/products/two-scoops-of-django-1-8) 142 | and it doesn't try to automate the other command line things. But it might fit yours, do check it out. 143 | 144 | If you think similar stuff can be done for other languages that work on Heroku, we could create a unified project containing all the code. 145 | Drop me a line at dibyachakravorty@gmail.com 146 | 147 | Finally, your contributions and suggestions to improve this project is most welcome. 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /djangohero/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gutfeeling/djangohero/72116a622550717efb13fcd895a670aced5674bd/djangohero/__init__.py -------------------------------------------------------------------------------- /djangohero/djangohero.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import subprocess 5 | import sys 6 | import re 7 | import random 8 | import shutil 9 | import os 10 | 11 | try: 12 | FileNotFoundError 13 | except NameError: 14 | FileNotFoundError = OSError 15 | 16 | class DjangoHero(object): 17 | 18 | def __init__(self, args): 19 | self.args = args 20 | self.steps = 10 21 | self.requirements_text = ("dj-database-url==0.4.1\n" 22 | "Django==1.10.4\n" 23 | "gunicorn==19.6.0\n" 24 | "psycopg2==2.7.4\n" 25 | "whitenoise==3.2\n" 26 | "Unipath==1.1") 27 | 28 | def create_root_directory(self, direction): 29 | if direction == "execute": 30 | if self.args.container_name is None and self.args.app is None: 31 | raise ValueError("app_name and container_name cannot both be" 32 | " empty.") 33 | elif self.args.container_name is None: 34 | self.container_name = self.args.app 35 | else: 36 | self.container_name = self.args.container_name 37 | os.makedirs(self.container_name) 38 | os.chdir(self.container_name) 39 | print("Created container folder.") 40 | elif direction == "revert": 41 | os.chdir("..") 42 | shutil.rmtree(self.container_name) 43 | print("Deleted container folder.") 44 | 45 | def initialize_git(self, direction): 46 | if direction == "execute": 47 | try: 48 | command = ["git", "init"] 49 | output_bytestring = subprocess.check_output(command) 50 | output= output_bytestring.decode("utf-8") 51 | print(output.strip()) 52 | except FileNotFoundError as e: 53 | print("Could not initialize a git repo. Is git installed?") 54 | raise(e) 55 | elif direction == "revert": 56 | pass 57 | 58 | def create_django_project_from_template(self, direction): 59 | if direction == "execute": 60 | try: 61 | command = ["django-admin", "startproject", 62 | "--template={0}".format(self.args.template), 63 | "--name=Procfile", self.args.django_project_name] 64 | output_bytestring = subprocess.check_output(command) 65 | print("Created Django project from specified template.") 66 | except FileNotFoundError as e: 67 | print("Could not create Django project. Is Django installed and" 68 | " accessible?") 69 | raise(e) 70 | elif direction == "revert": 71 | pass 72 | 73 | def create_requirements_file_and_procfile(self, direction): 74 | if direction == "execute": 75 | with open("requirements.txt", "w") as requirements_file: 76 | requirements_file.write(self.requirements_text) 77 | with open("Procfile", "w") as procfile: 78 | procfile.write("web: sh -c 'cd {0} && gunicorn" 79 | " {0}.wsgi.wsgi_heroku --log-file -'".format( 80 | self.args.django_project_name) 81 | ) 82 | print("Created requirements file and Procfile.") 83 | elif direction == "revert": 84 | pass 85 | 86 | def create_runtime_file(self, direction): 87 | if direction == "execute": 88 | if self.args.python == "3": 89 | with open("runtime.txt", "w") as runtime_file: 90 | runtime_file.write("python-3.6.0") 91 | elif self.args.python == "2": 92 | with open("runtime.txt", "w") as runtime_file: 93 | runtime_file.write("python-2.7.13") 94 | else: 95 | raise ValueError("Python version can be 2 or 3.") 96 | if direction == "revert": 97 | pass 98 | 99 | def app_exists(self, app_name): 100 | try: 101 | app_info_bytestring = subprocess.check_output(["heroku", "apps"]) 102 | app_info = app_info_bytestring.decode("utf-8").split("\n") 103 | for line in app_info: 104 | items = line.split() 105 | if len(items) > 0 and items[0] == app_name: 106 | return True 107 | return False 108 | except FileNotFoundError as e: 109 | print("Could not find the Heroku CLI. Is it installed?") 110 | raise(e) 111 | 112 | def get_app_name(self, line_with_url): 113 | items = line_with_url.split() 114 | for item in items: 115 | match = re.match(re.compile("http(s?)://(.+)\.herokuapp.com(/?)$"), 116 | item) 117 | if match is not None: 118 | return match.group(2) 119 | 120 | def create_app(self, command): 121 | try: 122 | line_with_url_bytestring = subprocess.check_output(command) 123 | line_with_url= line_with_url_bytestring.decode("utf-8") 124 | print(line_with_url.strip()) 125 | return self.get_app_name(line_with_url) 126 | except subprocess.CalledProcessError as e: 127 | print("Could not create the Heroku app. Is your quota full" 128 | " or the name taken?") 129 | raise(e) 130 | 131 | def create_heroku_app(self, direction): 132 | if direction == "execute": 133 | command = ["heroku", "create"] 134 | self.app_name = self.args.app 135 | self.delete_app_on_error = True 136 | if self.app_name is None: 137 | self.app_name = self.create_app(command) 138 | elif not self.app_exists(self.app_name): 139 | command += [self.app_name] 140 | if self.args.region is not None: 141 | command += ["--region", self.args.region] 142 | self.app_name = self.create_app(command) 143 | else: 144 | print("Heroku app with that name exists. Skipping creation.") 145 | command = ["heroku", "git:remote", "-a", self.app_name] 146 | output_bytestring = subprocess.check_output(command) 147 | self.delete_app_on_error = False 148 | 149 | elif direction == "revert": 150 | if self.delete_app_on_error: 151 | command = ["heroku", "apps:destroy", "--app", self.app_name, 152 | "--confirm", self.app_name] 153 | output_bytestring = subprocess.check_output(command) 154 | 155 | def add_django_settings_module_config_var(self, direction): 156 | if direction == "execute": 157 | output_bytestring = subprocess.check_output( 158 | ["heroku", "config:set", "DJANGO_SETTINGS_MODULE" 159 | "={0}.settings.settings_heroku".format( 160 | self.args.django_project_name)] 161 | ) 162 | print(output_bytestring.decode("utf-8").strip()) 163 | 164 | elif direction == "revert": 165 | command = ["heroku", "config:unset", "DJANGO_SETTINGS_MODULE"] 166 | output_bytestring = subprocess.check_output(command) 167 | 168 | 169 | 170 | def add_secret_key_config_var(self, direction): 171 | if direction == "execute": 172 | secret_key = "".join([random.SystemRandom().choice( 173 | "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") 174 | for i in range(50)]) 175 | output_bytestring = subprocess.check_output(["heroku", 176 | "config:set", "DJANGO_SECRET_KEY={0}".format(secret_key)]) 177 | print(output_bytestring.decode("utf-8").strip()) 178 | elif direction == "revert": 179 | command = ["heroku", "config:unset", "DJANGO_SECRET_KEY"] 180 | output_bytestring = subprocess.check_output(command) 181 | 182 | def database_exists(self): 183 | command = ["heroku", "config:get", "DATABASE_URL"] 184 | output_bytestring = subprocess.check_output(command) 185 | output = output_bytestring.decode("utf-8") 186 | if output.strip() != "": 187 | return True 188 | return False 189 | 190 | def create_database(self, direction): 191 | if self.args.database: 192 | if direction == "execute": 193 | if self.database_exists(): 194 | raise ValueError("You cannot use the --database flag" 195 | " if a database already exists") 196 | command = ["heroku", "addons:create", 197 | "heroku-postgresql:{0}".format( 198 | self.args.database_type)] 199 | output_bytestring = subprocess.check_output(command) 200 | output= output_bytestring.decode("utf-8") 201 | print(output.strip()) 202 | 203 | elif direction == "revert": 204 | command = ["heroku", "addons:destroy", "DATABASE", 205 | "--confirm", self.app_name] 206 | output_bytestring = subprocess.check_output(command) 207 | 208 | 209 | def add_allowed_hosts_settings_var(self, direction): 210 | if direction == "execute": 211 | with open("./{0}/{0}/settings/settings_heroku.py".format( 212 | self.args.django_project_name), "r") as heroku_settings_file: 213 | lines = heroku_settings_file.readlines() 214 | with open("./{0}/{0}/settings/settings_heroku.py".format( 215 | self.args.django_project_name), "a") as heroku_settings_file: 216 | newline = "ALLOWED_HOSTS = ['{0}.herokuapp.com']\n".format( 217 | self.app_name) 218 | if not lines[-1].endswith("\n"): 219 | newline = "\n" + newline 220 | heroku_settings_file.write(newline) 221 | print("Added alllowed host to settings file.") 222 | elif direction == "revert": 223 | pass 224 | 225 | def commit_changes_to_git_and_push(self, direction): 226 | if direction == "execute": 227 | try: 228 | command1 = ["git", "add", "."] 229 | command2 = ["git", "commit", "-m", "First commit"] 230 | command3 = ["git", "push", "heroku", "master"] 231 | output1_bytestring = subprocess.check_output(command1) 232 | output2_bytestring = subprocess.check_output(command2) 233 | output3_bytestring = subprocess.check_output(command3) 234 | print("Commited changes to git and pushed to Heroku.") 235 | except subprocess.CalledProcessError as e: 236 | print("Could not commit changes and push to Heroku." 237 | " Did some data already exist in the app?") 238 | raise(e) 239 | elif direction == "revert": 240 | pass 241 | 242 | def scale_app(self, direction): 243 | if direction == "execute": 244 | command = ["heroku", "ps:scale", "web={0}".format( 245 | self.args.scale)] 246 | output_bytestring = subprocess.check_output(command) 247 | print("\nYour app is now live on Heroku!.".format( 248 | self.app_name)) 249 | os.chdir("..") 250 | elif direction == "revert": 251 | pass 252 | 253 | 254 | def deploy(self): 255 | self.pipeline = [self.create_root_directory, 256 | self.initialize_git, 257 | self.create_django_project_from_template, 258 | self.create_requirements_file_and_procfile, 259 | self.create_runtime_file, 260 | self.create_heroku_app, 261 | self.add_django_settings_module_config_var, 262 | self.add_secret_key_config_var, 263 | self.create_database, 264 | self.add_allowed_hosts_settings_var, 265 | self.commit_changes_to_git_and_push, 266 | self.scale_app] 267 | for step in range(len(self.pipeline)): 268 | try: 269 | self.pipeline[step]("execute") 270 | except Exception as e: 271 | print("\n!!!\n") 272 | print("An error occured.") 273 | print("The full traceback will appear after cleanup.") 274 | self.current_step = step 275 | self.cleanup() 276 | raise(e) 277 | 278 | def cleanup(self): 279 | for step in range(self.current_step - 1, -1 , -1): 280 | self.pipeline[step]("revert") 281 | 282 | def deploy(args): 283 | 284 | dh = DjangoHero(args) 285 | dh.deploy() 286 | 287 | parser = argparse.ArgumentParser() 288 | subparsers = parser.add_subparsers() 289 | 290 | deploy_parser = subparsers.add_parser("deploy") 291 | 292 | deploy_parser.add_argument("--app", default = None) 293 | deploy_parser.add_argument("--region", default = None) 294 | deploy_parser.add_argument("--template", 295 | default = "https://github.com/gutfeeling/djangohero_default_template/" 296 | "archive/master.zip") 297 | deploy_parser.add_argument("--database", action = "store_true") 298 | deploy_parser.add_argument("--database_type", default = "hobby-dev") 299 | deploy_parser.add_argument("--python", default = "3") 300 | deploy_parser.add_argument("--container_name", default = None) 301 | deploy_parser.add_argument("--scale", default = "1") 302 | deploy_parser.add_argument("django_project_name") 303 | 304 | deploy_parser.set_defaults(func = deploy) 305 | 306 | def main(): 307 | args = parser.parse_args() 308 | args.func(args) 309 | 310 | if __name__ == "__main__": 311 | main() 312 | -------------------------------------------------------------------------------- /logo/djangohero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gutfeeling/djangohero/72116a622550717efb13fcd895a670aced5674bd/logo/djangohero.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | # setup parameters 6 | setup(name = "djangohero", 7 | version = "0.1.0", 8 | description = "Deploy a new Django app to Heroku with a one liner", 9 | long_description = open("README.md").read(), 10 | author = "Dibya", 11 | packages = ["djangohero"], 12 | package_data = {"djangohero" : []}, 13 | include_package_data = True, 14 | author_email = "dibyachakravorty@gmail.com", 15 | entry_points={ 16 | "console_scripts": ["djangohero = djangohero.djangohero:main"]}, 17 | install_requires=[ 18 | "Django == 1.10.4", 19 | ], 20 | ) 21 | --------------------------------------------------------------------------------