├── cli ├── __init__.py ├── console.py ├── script.py └── cli.py ├── djang-setup-demo.gif ├── requirements.txt ├── .gitignore ├── pyproject.toml ├── readme.md ├── setup.py └── LICENSE /cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cli/console.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | console = Console() -------------------------------------------------------------------------------- /djang-setup-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulanii/djang_setup/HEAD/djang-setup-demo.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astor==0.8.1 2 | black==24.10.0 3 | click==8.1.8 4 | rich==13.9.4 5 | django-environ==0.11.2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .python-version 3 | **__pycache__/ 4 | **/*.pyc 5 | **/*.pyo 6 | **/project/ 7 | **/app/ 8 | django_cli_tool/ 9 | **.egg-info/ 10 | djnago_cli_venv 11 | dist 12 | tests 13 | .env 14 | tests/ -------------------------------------------------------------------------------- /cli/script.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | try: 4 | # For when running as part of the package 5 | from .console import console 6 | from .cli import Cli 7 | except ImportError: 8 | # For when running directly 9 | from console import console 10 | from cli import Cli 11 | 12 | def main(): 13 | if os.name == 'nt': 14 | subprocess.run('cls', shell=True) # Windows 15 | else: 16 | subprocess.run('clear', shell=True) # Linux/MacOS 17 | # subprocess.run(["clear"]) 18 | 19 | console.rule("[bold red]Welcome to the Django project creator!") 20 | 21 | project_name = console.input("Enter the [bold red]Django project[/] name: ") 22 | app_name = console.input("Enter the [bold red]Django app[/] name: ") 23 | 24 | django_cli = Cli(project_name, app_name) 25 | django_cli.run_setup() 26 | 27 | 28 | if __name__ == "__main__": 29 | main() -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=6.3"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "djang-setup" 7 | version = "0.0.7" 8 | description = "A CLI tool to set up Django projects for you" 9 | readme = {file = "readme.md", content-type = "text/markdown"} 10 | requires-python = ">=3.6" 11 | authors = [ 12 | { name = "Yassine", email = "yassine@yassinecodes.dev" } 13 | ] 14 | license = { text = "MIT" } 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "Framework :: Django", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent" 20 | ] 21 | dependencies = [ 22 | "astor==0.8.1", 23 | "black==24.10.0", 24 | "click==8.1.8", 25 | "rich==13.9.4", 26 | "django-environ==0.11.2", 27 | ] 28 | 29 | [project.urls] 30 | Homepage = "https://github.com/fulanii/django_setup" 31 | 32 | [project.scripts] 33 | djang-setup = "cli.script:main" -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Django Setup 2 | A cli tool to setup django for you 3 | 4 | ## Features 5 | * install django if not already installed 6 | * creates django project 7 | * creates django app 8 | * creates settings folder 9 | * creates settings files: `base.py`, `developmemt.py`, `production.py` 10 | * creates `.gitignore`, `.env.dev`, `.env,prod`, and `requirements.txt` 11 | * updates `INSTALLED_APPS`, `DEBUG`, `ALLOWED_HOST` and `BASE_DIR` 12 | * creates `app_name/urls.py` 13 | * add `app_name/urls.py` to `project_name/urls.py` urlpatterns uisng `include()` 14 | * update prod settings in prod file 15 | * update django to use either env.dev or env.prod based on env var 16 | 17 | ## Usage 18 | 1. Set up a virtual environment: 19 | ```bash 20 | python -m venv venv 21 | source venv/bin/activate # On Windows: venv\Scripts\activate 22 | ``` 23 | 24 | 2. install the package 25 | ```bash 26 | pip install djang-setup 27 | ``` 28 | 29 | 3. run it and follow the prompt 30 | ```bash 31 | djang-setup 32 | ``` 33 | 34 | ![domo](./djang-setup-demo.gif) 35 | 36 | 37 | ## Support 38 | * Star the project :) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='djang-setup', 5 | version='0.0.7', 6 | include_package_data=True, 7 | install_requires=[ 8 | "astor==0.8.1", 9 | "black==24.10.0", 10 | "click==8.1.8", 11 | "rich==13.9.4", 12 | "django-environ==0.11.2" 13 | ], 14 | entry_points={ 15 | 'console_scripts': [ 16 | 'djang-setup=cli.script:main', 17 | ], 18 | }, 19 | packages=find_packages(include=['cli', 'cli.*']), 20 | author='Yassine', 21 | author_email='yassine@yassinecodes.dev', 22 | description='A CLI tool to set up Django projects for you', 23 | long_description=open('readme.md').read(), 24 | long_description_content_type='text/markdown', 25 | url='https://github.com/fulanii/djnago_setup', 26 | classifiers=[ 27 | 'Programming Language :: Python :: 3', 28 | 'Framework :: Django', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Operating System :: OS Independent', 31 | ], 32 | python_requires='>=3.6', 33 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yassine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import astor 5 | import ast 6 | 7 | try: 8 | # For when running as part of the package 9 | from .console import console 10 | except ImportError: 11 | # For when running directly 12 | from console import console 13 | 14 | 15 | class Cli: 16 | def __init__(self, project_name, app_name): 17 | self.django_project_name = project_name 18 | self.django_app_name = app_name 19 | self.project_root = os.path.join(os.getcwd(), self.django_project_name) 20 | self.project_configs = os.path.join(self.project_root, self.django_project_name) 21 | self.settings_folder = os.path.join(self.project_configs, "settings") 22 | self.settings_file = os.path.join(self.project_configs, "settings.py") 23 | 24 | def _create_project(self) -> bool: 25 | """ 26 | Create a new Django project, 27 | return True if successful, False otherwise. 28 | """ 29 | 30 | # check if a project already exists 31 | if not os.path.exists(self.project_root): 32 | try: 33 | import django 34 | except ImportError: 35 | subprocess.run( 36 | [sys.executable, "-m", "pip", "install", "--upgrade", "django"], 37 | check=True, 38 | ) 39 | 40 | try: 41 | subprocess.run( 42 | ["django-admin", "startproject", self.django_project_name], 43 | check=True, 44 | ) 45 | console.print( 46 | f"\nDjango project '{self.django_project_name}' created successfully! ✅", 47 | style="bold on blue", 48 | ) 49 | return True 50 | except Exception as e: 51 | return False 52 | 53 | else: 54 | console.print(f"\nDjango project already exists. ❌", style="bold red") 55 | return False 56 | 57 | def _create_app(self) -> bool: 58 | """Create a new Django app, return True if successful, False otherwise.""" 59 | try: 60 | os.chdir(self.project_root) 61 | subprocess.run( 62 | [ 63 | sys.executable, 64 | os.path.join(self.project_root, "manage.py"), 65 | "startapp", 66 | self.django_app_name, 67 | ], 68 | check=True, 69 | ) 70 | console.print( 71 | f"\nDjango app '{self.django_app_name}' created successfully! ✅", 72 | style="bold on blue", 73 | ) 74 | return True 75 | except Exception as e: 76 | # print("An error occurred while creating the Django app." + str(e)) # for debugging 77 | return False 78 | 79 | def _create_project_util_files(self) -> bool: 80 | """ 81 | Creates: 82 | .gitignore, 83 | requirements.txt, 84 | README.md, 85 | .env.dev, 86 | .env.prod, 87 | 88 | returns: True if successful, False otherwise. 89 | """ 90 | os.chdir(self.project_root) 91 | try: 92 | with open(".gitignore", "w") as file: 93 | file.write("*.pyc\n") 94 | file.write("__pycache__/\n") 95 | file.write("*.sqlite3\n") 96 | file.write("db.sqlite3\n") 97 | file.write("env\n") 98 | file.write(".env.dev\n") 99 | file.write(".env.prod\n") 100 | file.write(".vscode\n") 101 | file.write(".idea\n") 102 | file.write("*.DS_Store\n") 103 | 104 | open("requirements.txt", "a").close() 105 | open("README.md", "a").close() 106 | open(".env.dev", "a").close() 107 | with open(".env.prod", "w") as file: 108 | file.write("DEBUG=False\n") 109 | file.write("ALLOWED_HOSTS='*' # Add your domains in production separated by commas\n") 110 | file.write("SECRET_KEY='' # generate and add new secret key using Django shell\n") 111 | 112 | console.print( 113 | "\nCreated requirements.txt, Readme, and .env files successfully! ✅", 114 | style="bold on blue", 115 | ) 116 | return True 117 | except FileExistsError as e: 118 | # print(f"An error occurred while creating the project utility files. {e}") # for debugging 119 | return False 120 | 121 | def _create_settings(self) -> bool: 122 | """ 123 | Creates a settings folder of the Django project. 124 | settings/base.py: Base settings 125 | settings/develoment.py: Development settings 126 | settings/production.py: Production settings 127 | 128 | returns: True if successful, False otherwise. 129 | """ 130 | 131 | # cd into project folder 132 | os.chdir(self.project_configs) 133 | 134 | # create folder called settings 135 | os.makedirs("settings", exist_ok=True) 136 | 137 | # move into new folder 138 | os.chdir(self.settings_folder) 139 | 140 | # move settings.py into new settings folder and rename it to base.py 141 | os.rename(self.settings_file, os.path.join(self.settings_folder, "base.py")) 142 | 143 | try: 144 | open("__init__.py", "a").close() 145 | open("development.py", "a").close() 146 | open("production.py", "a").close() 147 | 148 | console.print( 149 | f"\nDjango project '{self.django_project_name}' Settings folder and files created successfully! ✅", 150 | style="bold on blue", 151 | ) 152 | return True 153 | except FileExistsError as e: 154 | # print(F"An error occurred while creating the settings folder. {e}") # for debugging 155 | return False 156 | 157 | def _update_base_setting(self) -> bool: 158 | """ 159 | Fill the base settings file with the necessary configurations. 160 | returns: True if successful, False otherwise. 161 | """ 162 | try: 163 | new_code = """ 164 | env = environ.Env() 165 | ENVIRONMENT = os.getenv('SETTING_FILE_PATH') 166 | 167 | # Load environment-specific .env file 168 | if ENVIRONMENT == 'project.settings.production': 169 | environ.Env.read_env('.env.prod') 170 | elif ENVIRONMENT == "project.settings.development": 171 | environ.Env.read_env('.env.dev') 172 | """ 173 | 174 | # cd into project settings folder 175 | os.chdir(self.settings_folder) 176 | 177 | # open base.py file 178 | with open("base.py", "r") as file: 179 | tree = ast.parse(file.read()) 180 | 181 | # Create a new import node 182 | os_import = ast.parse("import os").body[0] 183 | environ_impoort = ast.parse("import environ").body[0] 184 | 185 | # Find the last import statement 186 | last_import_index = -1 187 | for index, node in enumerate(tree.body): 188 | if isinstance(node, (ast.Import, ast.ImportFrom)): 189 | last_import_index = index 190 | 191 | # Insert the new import after the last import statement 192 | tree.body.insert(last_import_index + 1, os_import) 193 | tree.body.insert(last_import_index + 2, environ_impoort) 194 | 195 | for node in ast.walk(tree): 196 | if isinstance(node, ast.Assign): 197 | if node.targets[0].id == "INSTALLED_APPS": 198 | node.value.elts.append(ast.Constant(s=self.django_app_name)) 199 | 200 | if node.targets[0].id == "ALLOWED_HOSTS": 201 | node.value.elts.append(ast.Constant(s="*")) 202 | 203 | if node.targets[0].id == "BASE_DIR": 204 | # Create the AST for Path(__file__).resolve().parent.parent.parent 205 | node.value = ast.Call( 206 | func=ast.Attribute( 207 | value=ast.Call( 208 | func=ast.Name(id="Path", ctx=ast.Load()), # Path() 209 | args=[ast.Name(id="__file__", ctx=ast.Load())], # __file__ 210 | keywords=[], 211 | ), 212 | attr="resolve", # resolve() 213 | ctx=ast.Load(), 214 | ), 215 | args=[], 216 | keywords=[], 217 | ) 218 | 219 | # Add `.parent.parent.parent` to the result 220 | node.value = ast.Attribute( 221 | value=ast.Attribute( 222 | value=ast.Attribute( 223 | value=node.value, attr="parent", ctx=ast.Load() # first parent 224 | ), 225 | attr="parent", # second parent 226 | ctx=ast.Load(), 227 | ), 228 | attr="parent", # third parent 229 | ctx=ast.Load(), 230 | ) 231 | 232 | new_nodes = ast.parse(new_code).body 233 | for i, new_node in enumerate(new_nodes): 234 | tree.body.insert(last_import_index + 3 + i, new_node) 235 | 236 | # Iterate over the AST nodes and add a blank line after assignments 237 | for index, node in enumerate(tree.body): 238 | # Check if the node is an assignment (Store, Assignment or AugAssign) 239 | if isinstance(node, ast.Assign): 240 | # Insert a 'pass' node to simulate a blank line 241 | tree.body.insert(index + 1, "\n") 242 | 243 | # write the changes to the file, with indentation and spaces 244 | with open("base.py", "w") as file: 245 | file.write(astor.to_source(tree)) 246 | 247 | # run black to format the code on base.py 248 | subprocess.run(["black", "base.py"], check=True) 249 | console.print( 250 | f"\nUpdated settings/base.py successfully! ✅", style="bold on blue" 251 | ) 252 | return True 253 | except Exception as e: 254 | return False 255 | 256 | def _update_dev_setting(self) -> bool: 257 | """ 258 | Fill the development settings file with the necessary configurations. 259 | returns: True if successful, False otherwise. 260 | """ 261 | try: 262 | # cd into project settings folder 263 | os.chdir(self.settings_folder) 264 | 265 | # open development.py file 266 | with open("development.py", "w") as file: 267 | file.write("from .base import *") 268 | 269 | console.print( 270 | f"\nUpdated settings/development.py successfully! ✅", 271 | style="bold on blue", 272 | ) 273 | return True 274 | except Exception as e: 275 | # print(f"An error occurred while updating the development settings file. {e}") # for debugging 276 | return False 277 | 278 | def _update_prod_setting(self) -> bool: 279 | """ 280 | Fill the production settings file with the necessary configurations. 281 | returns: True if successful, False otherwise. 282 | """ 283 | 284 | try: 285 | # cd into project settings folder 286 | os.chdir(self.settings_folder) 287 | 288 | # open development.py file 289 | with open("production.py", "w") as file: 290 | file.write("from .base import *\n") 291 | file.write("import os\n\n") 292 | file.write("DEBUG = env('DEBUG')\n") 293 | file.write("SECRET_KEY = env('SECRET_KEY')\n") 294 | file.write( 295 | "ALLOWED_HOSTS = env('ALLOWED_HOSTS').split(',')\n" 296 | ) 297 | file.write( 298 | "DATABASES = {} # Add your production database settings here\n" 299 | ) 300 | 301 | console.print( 302 | f"\nUpdated settings/production.py successfully! ✅", style="bold on blue" 303 | ) 304 | return True 305 | except Exception as e: 306 | # print(f"An error occurred while updating the production settings file. {e}") # for debugging 307 | return False 308 | 309 | def _create_app_urls_file(self) -> bool: 310 | """ 311 | create a urls.py file in the app folder. 312 | returns: True if successful, False otherwise. 313 | """ 314 | 315 | try: 316 | # cd into the app folder 317 | os.chdir(os.path.join(self.project_root, self.django_app_name)) 318 | 319 | # create urls.py file 320 | open("urls.py", "w").close() 321 | 322 | console.print( 323 | f"\nCreated '{self.django_app_name}/urls.py' successfully! ✅", 324 | style="bold on blue", 325 | ) 326 | return True 327 | except Exception as e: 328 | return False 329 | 330 | def _add_app_urls_to_project_urls(self) -> bool: 331 | """ 332 | Add the app urls to the project urls file. 333 | returns: True if successful, False otherwise. 334 | """ 335 | os.chdir(self.project_configs) 336 | 337 | try: 338 | with open("urls.py", "r") as file: 339 | tree = ast.parse(file.read()) 340 | for node in ast.walk(tree): 341 | if isinstance(node, ast.ImportFrom): 342 | if node.module == "django.urls": 343 | # add the include function to the import statement, if it doesn't exist 344 | if not any(alias.name == "include" for alias in node.names): 345 | node.names.append(ast.alias(name="include", asname=None)) 346 | 347 | for node in ast.walk(tree): 348 | if not any(isinstance(node, ast.Assign) for node in ast.walk(tree)): 349 | if isinstance(node, ast.Assign): 350 | if node.targets[0].id == "urlpatterns": 351 | node.value.elts.append( 352 | ast.Call( 353 | func=ast.Attribute( 354 | value=ast.Name(id="path", ctx=ast.Load()), 355 | attr="include", 356 | ctx=ast.Load(), 357 | ), 358 | args=[ 359 | ast.Constant( 360 | s=f"{self.django_app_name}.urls", kind=None 361 | ) 362 | ], 363 | keywords=[], 364 | ) 365 | ) 366 | 367 | with open("urls.py", "w") as file: 368 | file.write(astor.to_source(tree)) 369 | 370 | subprocess.run(["black", "urls.py"], check=True) 371 | console.print(f"\nAdded app urls to project urls.py successfully! ✅", style="bold on blue") 372 | return True 373 | except Exception as e: 374 | return False 375 | 376 | def _update_settings_path(self): 377 | """ 378 | Updates manage.py setting path 379 | return True if successful False otherwise 380 | """ 381 | try: 382 | os.chdir(self.project_root) 383 | 384 | with open("manage.py", "r") as file: 385 | tree = ast.parse(file.read()) 386 | 387 | # Check if "from django.conf import settings" is already imported 388 | import_already_exists = any( 389 | isinstance(node, ast.ImportFrom) 390 | and node.module == "django.conf" 391 | and any(alias.name == "settings" for alias in node.names) 392 | for node in tree.body 393 | ) 394 | 395 | # if not import_already_exists: 396 | # env_import = ast.parse("from django.conf import settings").body[0] 397 | # last_import_index = -1 398 | # for index, node in enumerate(tree.body): 399 | # if isinstance(node, (ast.Import, ast.ImportFrom)): 400 | # last_import_index = index 401 | 402 | # # Insert the new import after the last import statement 403 | # tree.body.insert(last_import_index + 1, env_import) 404 | 405 | # Find and update the `os.environ.setdefault` call 406 | for node in tree.body: 407 | if isinstance(node, ast.FunctionDef) and node.name == "main": 408 | for stmt in node.body: 409 | if ( 410 | isinstance(stmt, ast.Expr) 411 | and isinstance(stmt.value, ast.Call) 412 | and isinstance(stmt.value.func, ast.Attribute) 413 | and isinstance(stmt.value.func.value, ast.Attribute) 414 | and isinstance(stmt.value.func.value.value, ast.Name) 415 | and stmt.value.func.value.value.id == "os" 416 | and stmt.value.func.value.attr == "environ" 417 | and stmt.value.func.attr == "setdefault" 418 | ): 419 | # Update the second argument of the call 420 | stmt.value.args[1] = ast.parse( 421 | 'os.getenv("SETTING_FILE_PATH")' 422 | ).body[0].value 423 | 424 | 425 | # write the changes to the file, with indentation and spaces 426 | with open("manage.py", "w") as file: 427 | file.write(astor.to_source(tree)) 428 | 429 | subprocess.run(["black", "manage.py"], check=True) 430 | console.print(f"\nUpdated manage.py successfully! ✅", style="bold on blue") 431 | return True 432 | except Exception as e: 433 | return False 434 | 435 | def run_setup(self): 436 | """Main method that creates everything""" 437 | steps = [ 438 | (self._create_project), 439 | (self._create_app), 440 | (self._create_settings), 441 | (self._update_base_setting), 442 | (self._update_dev_setting), 443 | (self._update_prod_setting), 444 | (self._create_project_util_files), 445 | (self._create_app_urls_file), 446 | (self._add_app_urls_to_project_urls), 447 | (self._update_settings_path), 448 | ] 449 | success = True 450 | 451 | for step in steps: 452 | result = step() 453 | if not result: 454 | success = False 455 | break 456 | 457 | if success: 458 | console.print(f"\nMake sure you set the env 'SETTING_FILE_PATH' to '{self.django_project_name}.settings.development' (for your development enviroment)\nor '{self.django_project_name}.settings.production' (for your production enviroment) before running the server.", style="bold white on yellow") 459 | --------------------------------------------------------------------------------