├── .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 |
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 |
--------------------------------------------------------------------------------