djboost 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- djboost/__init__.py +1 -0
- djboost/cli.py +37 -0
- djboost/commands/__init__.py +1 -0
- djboost/commands/add_cicd.py +16 -0
- djboost/commands/create_app.py +130 -0
- djboost/commands/create_project.py +6 -0
- djboost/commands/remove_cicd.py +34 -0
- djboost/generator.py +880 -0
- djboost-0.1.0.dist-info/METADATA +203 -0
- djboost-0.1.0.dist-info/RECORD +14 -0
- djboost-0.1.0.dist-info/WHEEL +5 -0
- djboost-0.1.0.dist-info/entry_points.txt +2 -0
- djboost-0.1.0.dist-info/licenses/LICENSE +21 -0
- djboost-0.1.0.dist-info/top_level.txt +1 -0
djboost/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# init
|
djboost/cli.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from djboost.commands.create_project import create_project_command
|
|
3
|
+
from djboost.commands.create_app import create_app_command
|
|
4
|
+
from djboost.commands.add_cicd import add_cicd_command
|
|
5
|
+
from djboost.commands.remove_cicd import remove_cicd_command
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="djboost — Django project generator CLI")
|
|
8
|
+
create = typer.Typer(help="Create a new project or app")
|
|
9
|
+
add = typer.Typer(help="Add integrations to your project")
|
|
10
|
+
remove = typer.Typer(help="Remove integrations from your project")
|
|
11
|
+
|
|
12
|
+
app.add_typer(create, name="create")
|
|
13
|
+
app.add_typer(add, name="add")
|
|
14
|
+
app.add_typer(remove, name="remove")
|
|
15
|
+
|
|
16
|
+
create.command("project")(create_project_command)
|
|
17
|
+
create.command("app")(create_app_command)
|
|
18
|
+
add.command("cicd")(add_cicd_command)
|
|
19
|
+
remove.command("cicd")(remove_cicd_command)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def version_callback(value: bool):
|
|
23
|
+
if value:
|
|
24
|
+
typer.echo("djboost version 0.1.0")
|
|
25
|
+
raise typer.Exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback()
|
|
29
|
+
def main(
|
|
30
|
+
version: bool = typer.Option(
|
|
31
|
+
None, "--version", "-v",
|
|
32
|
+
callback=version_callback,
|
|
33
|
+
is_eager=True,
|
|
34
|
+
help="Show the version and exit."
|
|
35
|
+
)
|
|
36
|
+
):
|
|
37
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# init
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich import print
|
|
3
|
+
from djboost.generator import generate_github_actions, generate_gitlab_ci, check_virtual_environment
|
|
4
|
+
|
|
5
|
+
def add_cicd_command(provider: str = typer.Argument(..., help="The CI/CD provider to add (github or gitlab)")):
|
|
6
|
+
check_virtual_environment()
|
|
7
|
+
provider = provider.lower()
|
|
8
|
+
if provider == "github":
|
|
9
|
+
generate_github_actions()
|
|
10
|
+
print("[green]GitHub Actions workflow created successfully![/green]")
|
|
11
|
+
elif provider == "gitlab":
|
|
12
|
+
generate_gitlab_ci()
|
|
13
|
+
print("[green]GitLab CI pipeline created successfully![/green]")
|
|
14
|
+
else:
|
|
15
|
+
print(f"[red]Error: Unsupported provider '{provider}'. Supported providers are: github, gitlab.[/red]")
|
|
16
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print
|
|
7
|
+
from djboost.generator import check_virtual_environment, validate_name
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_project_name():
|
|
11
|
+
if not Path("manage.py").exists():
|
|
12
|
+
print("[red]Error: manage.py not found. Are you in the project root?[/red]")
|
|
13
|
+
raise typer.Exit(1)
|
|
14
|
+
|
|
15
|
+
content = Path("manage.py").read_text(encoding="utf-8")
|
|
16
|
+
match = re.search(r"['\"]DJANGO_SETTINGS_MODULE['\"],\s*['\"]([^.]+)\.settings['\"]", content)
|
|
17
|
+
if match:
|
|
18
|
+
return match.group(1)
|
|
19
|
+
|
|
20
|
+
print("[red]Error: Could not determine project name from manage.py[/red]")
|
|
21
|
+
raise typer.Exit(1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def update_settings(project_name: str, app_name: str):
|
|
25
|
+
settings_path = Path(project_name) / "settings.py"
|
|
26
|
+
if not settings_path.exists():
|
|
27
|
+
print(f"[yellow]Warning: Could not find settings.py at {settings_path}. Skipping.[/yellow]")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
content = settings_path.read_text(encoding="utf-8")
|
|
31
|
+
app_string = f"'apps.{app_name}',"
|
|
32
|
+
|
|
33
|
+
if app_string in content or f'"apps.{app_name}",' in content:
|
|
34
|
+
print(f"[yellow]App '{app_name}' is already in INSTALLED_APPS[/yellow]")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if "INSTALLED_APPS = [" in content:
|
|
38
|
+
content = re.sub(
|
|
39
|
+
r"(INSTALLED_APPS\s*=\s*\[.*?)(\n?\])",
|
|
40
|
+
rf"\1\n {app_string}\2",
|
|
41
|
+
content,
|
|
42
|
+
flags=re.DOTALL
|
|
43
|
+
)
|
|
44
|
+
settings_path.write_text(content, encoding="utf-8")
|
|
45
|
+
print(f"[green]✔ Added '{app_string}' to INSTALLED_APPS[/green]")
|
|
46
|
+
else:
|
|
47
|
+
print("[yellow]Warning: Could not find INSTALLED_APPS in settings.py[/yellow]")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def update_urls(project_name: str, app_name: str):
|
|
51
|
+
urls_path = Path(project_name) / "urls.py"
|
|
52
|
+
if not urls_path.exists():
|
|
53
|
+
print(f"[yellow]Warning: Could not find urls.py at {urls_path}. Skipping.[/yellow]")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
content = urls_path.read_text(encoding="utf-8")
|
|
57
|
+
|
|
58
|
+
if f"apps.{app_name}.urls" in content:
|
|
59
|
+
print(f"[yellow]App '{app_name}' is already mapped in urls.py[/yellow]")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if "include" not in content:
|
|
63
|
+
content = re.sub(r"(from django\.urls import.*?path)", r"\1, include", content)
|
|
64
|
+
|
|
65
|
+
if "urlpatterns = [" in content:
|
|
66
|
+
content = content.replace(
|
|
67
|
+
"urlpatterns = [",
|
|
68
|
+
f"urlpatterns = [\n path('api/{app_name}/', include('apps.{app_name}.urls')),"
|
|
69
|
+
)
|
|
70
|
+
urls_path.write_text(content, encoding="utf-8")
|
|
71
|
+
print(f"[green]✔ Mapped /api/{app_name}/ in {project_name}/urls.py[/green]")
|
|
72
|
+
else:
|
|
73
|
+
print("[yellow]Warning: Could not find urlpatterns in urls.py[/yellow]")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_app_urls(app_name: str):
|
|
77
|
+
urls_path = Path("apps") / app_name / "urls.py"
|
|
78
|
+
content = f"""from django.urls import path
|
|
79
|
+
from . import views
|
|
80
|
+
|
|
81
|
+
app_name = '{app_name}'
|
|
82
|
+
|
|
83
|
+
urlpatterns = [
|
|
84
|
+
# path('', views.MyView.as_view(), name='my-view'),
|
|
85
|
+
]
|
|
86
|
+
"""
|
|
87
|
+
urls_path.write_text(content, encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_app_command(name: str = typer.Argument(..., help="The name of the Django app to create")):
|
|
91
|
+
check_virtual_environment()
|
|
92
|
+
validate_name(name, "app name")
|
|
93
|
+
|
|
94
|
+
if not Path("manage.py").exists():
|
|
95
|
+
print("[red]Error: manage.py not found. Run this command from your Django project root.[/red]")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
app_path = Path("apps") / name
|
|
99
|
+
if app_path.exists():
|
|
100
|
+
print(f"[red]Error: App '{name}' already exists at apps/{name}.[/red]")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
Path("apps").mkdir(exist_ok=True)
|
|
104
|
+
|
|
105
|
+
print(f"[cyan]Creating app '{name}'...[/cyan]")
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
[sys.executable, "manage.py", "startapp", name, f"apps/{name}"],
|
|
108
|
+
capture_output=True, text=True
|
|
109
|
+
)
|
|
110
|
+
if result.returncode != 0:
|
|
111
|
+
print(f"[red]Error creating app:\n{result.stderr}[/red]")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
(Path(f"apps/{name}") / "__init__.py").touch()
|
|
115
|
+
|
|
116
|
+
# Fix apps.py name
|
|
117
|
+
apps_py_path = Path(f"apps/{name}/apps.py")
|
|
118
|
+
if apps_py_path.exists():
|
|
119
|
+
apps_content = apps_py_path.read_text(encoding="utf-8")
|
|
120
|
+
apps_content = re.sub(rf"name\s*=\s*['\"]{name}['\"]", f"name = 'apps.{name}'", apps_content)
|
|
121
|
+
apps_py_path.write_text(apps_content, encoding="utf-8")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
project_name = get_project_name()
|
|
125
|
+
update_settings(project_name, name)
|
|
126
|
+
update_urls(project_name, name)
|
|
127
|
+
create_app_urls(name)
|
|
128
|
+
print(f"[bold green]✅ App '{name}' created and configured successfully![/bold green]")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"[red]Error during auto-configuration: {str(e)}[/red]")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import typer
|
|
4
|
+
from rich import print
|
|
5
|
+
from djboost.generator import check_virtual_environment
|
|
6
|
+
|
|
7
|
+
def remove_cicd_command(provider: str = typer.Argument(..., help="The CI/CD provider to remove (github or gitlab)")):
|
|
8
|
+
check_virtual_environment()
|
|
9
|
+
provider = provider.lower()
|
|
10
|
+
if provider == "github":
|
|
11
|
+
github_dir = ".github"
|
|
12
|
+
if os.path.exists(github_dir):
|
|
13
|
+
try:
|
|
14
|
+
shutil.rmtree(github_dir)
|
|
15
|
+
print("[green]GitHub Actions workflow removed successfully![/green]")
|
|
16
|
+
except Exception as e:
|
|
17
|
+
print(f"[red]Failed to remove GitHub Actions workflow: {e}[/red]")
|
|
18
|
+
else:
|
|
19
|
+
print("[yellow]GitHub Actions workflow is not present in this project.[/yellow]")
|
|
20
|
+
|
|
21
|
+
elif provider == "gitlab":
|
|
22
|
+
gitlab_file = ".gitlab-ci.yml"
|
|
23
|
+
if os.path.exists(gitlab_file):
|
|
24
|
+
try:
|
|
25
|
+
os.remove(gitlab_file)
|
|
26
|
+
print("[green]GitLab CI pipeline removed successfully![/green]")
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"[red]Failed to remove GitLab CI pipeline: {e}[/red]")
|
|
29
|
+
else:
|
|
30
|
+
print("[yellow]GitLab CI pipeline is not present in this project.[/yellow]")
|
|
31
|
+
|
|
32
|
+
else:
|
|
33
|
+
print(f"[red]Error: Unsupported provider '{provider}'. Supported providers are: github, gitlab.[/red]")
|
|
34
|
+
raise typer.Exit(code=1)
|
djboost/generator.py
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from rich import print
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_virtual_environment():
|
|
13
|
+
"""Check if user is inside a virtual environment."""
|
|
14
|
+
in_venv = (
|
|
15
|
+
hasattr(sys, 'real_prefix') or
|
|
16
|
+
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
|
|
17
|
+
)
|
|
18
|
+
if not in_venv:
|
|
19
|
+
print("[bold red]⚠ You are not inside a virtual environment![/bold red]")
|
|
20
|
+
print("[yellow]Please create and activate a virtual environment first:[/yellow]")
|
|
21
|
+
print()
|
|
22
|
+
print(" [cyan]python -m venv env[/cyan]")
|
|
23
|
+
print()
|
|
24
|
+
print(" [cyan]# Windows:[/cyan]")
|
|
25
|
+
print(" [cyan]env\\Scripts\\activate[/cyan]")
|
|
26
|
+
print()
|
|
27
|
+
print(" [cyan]# Mac/Linux:[/cyan]")
|
|
28
|
+
print(" [cyan]source env/bin/activate[/cyan]")
|
|
29
|
+
print()
|
|
30
|
+
import typer
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_name(name: str, label: str = "name"):
|
|
35
|
+
"""Validate that name is a valid Python identifier."""
|
|
36
|
+
if not name.isidentifier():
|
|
37
|
+
print(f"[red]Error: '{name}' is not a valid {label}. Use only letters, numbers, and underscores. Must not start with a number.[/red]")
|
|
38
|
+
import typer
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
if name[0].isdigit():
|
|
41
|
+
print(f"[red]Error: '{name}' must not start with a digit.[/red]")
|
|
42
|
+
import typer
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def update_settings_file(settings_path: str, name: str):
|
|
47
|
+
with open(settings_path, "r", encoding="utf-8") as f:
|
|
48
|
+
content = f.read()
|
|
49
|
+
|
|
50
|
+
# Extract the original SECRET_KEY generated by Django
|
|
51
|
+
match = re.search(r"SECRET_KEY\s*=\s*(['\"].*?['\"])", content)
|
|
52
|
+
secret_key = match.group(1) if match else "'your-secret-key-here'"
|
|
53
|
+
|
|
54
|
+
# 1. Add imports at the top
|
|
55
|
+
content = content.replace(
|
|
56
|
+
"from pathlib import Path",
|
|
57
|
+
"from pathlib import Path\nfrom datetime import timedelta\nfrom celery.schedules import crontab\nfrom decouple import config"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# 2. Update SECRET_KEY, DEBUG, ALLOWED_HOSTS
|
|
61
|
+
content = re.sub(r"SECRET_KEY = .*", "SECRET_KEY = config('SECRET_KEY')", content)
|
|
62
|
+
content = re.sub(r"DEBUG = .*", "DEBUG = config('DEBUG', default=False, cast=bool)", content)
|
|
63
|
+
content = re.sub(
|
|
64
|
+
r"ALLOWED_HOSTS = .*",
|
|
65
|
+
"ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost', cast=lambda v: [s.strip() for s in v.split(',')])",
|
|
66
|
+
content
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# 3. Update INSTALLED_APPS
|
|
70
|
+
apps_addition = """ 'corsheaders',
|
|
71
|
+
'rest_framework',
|
|
72
|
+
'rest_framework_simplejwt',
|
|
73
|
+
'rest_framework_simplejwt.token_blacklist',
|
|
74
|
+
'channels',
|
|
75
|
+
'channels_redis',
|
|
76
|
+
'drf_spectacular',"""
|
|
77
|
+
content = re.sub(
|
|
78
|
+
r"['\"]django\.contrib\.staticfiles['\"],",
|
|
79
|
+
f"'daphne',\n 'django.contrib.staticfiles',\n{apps_addition}",
|
|
80
|
+
content
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# 4. Update MIDDLEWARE
|
|
84
|
+
content = content.replace(
|
|
85
|
+
"MIDDLEWARE = [",
|
|
86
|
+
"MIDDLEWARE = [\n 'corsheaders.middleware.CorsMiddleware',\n 'whitenoise.middleware.WhiteNoiseMiddleware',"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# 5. Update DATABASES
|
|
90
|
+
db_config = """DATABASES = {
|
|
91
|
+
'default': {
|
|
92
|
+
'ENGINE': config("DB_ENGINE", default="django.db.backends.sqlite3"),
|
|
93
|
+
'NAME': config('DB_NAME', default=BASE_DIR / 'db.sqlite3'),
|
|
94
|
+
'USER': config('DB_USER', default=''),
|
|
95
|
+
'PASSWORD': config('DB_PASSWORD', default=''),
|
|
96
|
+
'HOST': config('DB_HOST', default='localhost'),
|
|
97
|
+
'PORT': config('DB_PORT', default=5432, cast=int),
|
|
98
|
+
'CONN_MAX_AGE': config('CONN_MAX_AGE', default=600, cast=int),
|
|
99
|
+
}
|
|
100
|
+
}"""
|
|
101
|
+
content = re.sub(r"DATABASES\s*=\s*\{.*?\}\s*\}", db_config, content, flags=re.DOTALL)
|
|
102
|
+
|
|
103
|
+
# 6. Update WSGI / ASGI
|
|
104
|
+
content = re.sub(
|
|
105
|
+
rf"WSGI_APPLICATION\s*=\s*['\"]{name}\.wsgi\.application['\"]",
|
|
106
|
+
f"WSGI_APPLICATION = '{name}.wsgi.application'\nASGI_APPLICATION = '{name}.asgi.application'",
|
|
107
|
+
content
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 6.5 Update Static / Media Files
|
|
111
|
+
static_media_config = """STATIC_URL = '/static/'
|
|
112
|
+
MEDIA_URL = '/media/'
|
|
113
|
+
|
|
114
|
+
STATICFILES_DIRS = [BASE_DIR / 'static']
|
|
115
|
+
STATIC_ROOT = BASE_DIR / 'assets'
|
|
116
|
+
MEDIA_ROOT = BASE_DIR / 'media'"""
|
|
117
|
+
content = re.sub(r"STATIC_URL\s*=\s*['\"]static/['\"]", static_media_config, content)
|
|
118
|
+
|
|
119
|
+
# 7. Append custom configurations at the end
|
|
120
|
+
custom_configs = """
|
|
121
|
+
|
|
122
|
+
# ── Caching Configuration (Redis) ─────────────────────────────────────────────
|
|
123
|
+
CACHES = {
|
|
124
|
+
"default": {
|
|
125
|
+
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
|
126
|
+
"LOCATION": config("REDIS_URL", default="redis://127.0.0.1:6379/1"),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ── Security & Performance Settings ───────────────────────────────────────────
|
|
131
|
+
SECURE_BROWSER_XSS_FILTER = True
|
|
132
|
+
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
133
|
+
X_FRAME_OPTIONS = 'DENY'
|
|
134
|
+
STORAGES = {
|
|
135
|
+
"staticfiles": {
|
|
136
|
+
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ── Channels Configuration ────────────────────────────────────────────────────
|
|
141
|
+
CHANNEL_LAYERS = {
|
|
142
|
+
"default": {
|
|
143
|
+
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
|
144
|
+
"CONFIG": {
|
|
145
|
+
"hosts": [(config("REDIS_HOST", default="127.0.0.1"), config("REDIS_PORT", default=6379, cast=int))],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ── REST Framework & JWT Settings ─────────────────────────────────────────────
|
|
151
|
+
REST_FRAMEWORK = {
|
|
152
|
+
'DEFAULT_AUTHENTICATION_CLASSES': (
|
|
153
|
+
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
|
154
|
+
),
|
|
155
|
+
'EXCEPTION_HANDLER': 'core.utils.custom_exception_handler',
|
|
156
|
+
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
|
157
|
+
'PAGE_SIZE': 10,
|
|
158
|
+
'DEFAULT_THROTTLE_CLASSES': [
|
|
159
|
+
'rest_framework.throttling.AnonRateThrottle',
|
|
160
|
+
'rest_framework.throttling.UserRateThrottle'
|
|
161
|
+
],
|
|
162
|
+
'DEFAULT_THROTTLE_RATES': {
|
|
163
|
+
'anon': '100/day',
|
|
164
|
+
'user': '1000/day'
|
|
165
|
+
},
|
|
166
|
+
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
SPECTACULAR_SETTINGS = {
|
|
170
|
+
'TITLE': 'API Documentation',
|
|
171
|
+
'DESCRIPTION': 'Project API Documentation',
|
|
172
|
+
'VERSION': '1.0.0',
|
|
173
|
+
'SERVE_INCLUDE_SCHEMA': False,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
SIMPLE_JWT = {
|
|
177
|
+
'ACCESS_TOKEN_LIFETIME': timedelta(days=10),
|
|
178
|
+
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
|
|
179
|
+
'ROTATE_REFRESH_TOKENS': True,
|
|
180
|
+
'BLACKLIST_AFTER_ROTATION': True,
|
|
181
|
+
'ALGORITHM': 'HS256',
|
|
182
|
+
'SIGNING_KEY': SECRET_KEY,
|
|
183
|
+
'AUTH_HEADER_TYPES': ('Bearer',),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# ── CORS & CSRF Settings ──────────────────────────────────────────────────────
|
|
187
|
+
CSRF_TRUSTED_ORIGINS = [i.strip() for i in config("CSRF_TRUSTED_ORIGINS", "").split(",") if i]
|
|
188
|
+
CORS_ALLOWED_ORIGINS = [i.strip() for i in config("CORS_ALLOWED_ORIGINS", "").split(",") if i]
|
|
189
|
+
CORS_ALLOW_CREDENTIALS = True
|
|
190
|
+
CORS_ALLOW_HEADERS = ['accept', 'accept-encoding', 'authorization', 'content-type', 'dnt', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with']
|
|
191
|
+
CORS_ALLOW_METHODS = ['DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT']
|
|
192
|
+
|
|
193
|
+
# ── Email Settings ────────────────────────────────────────────────────────────
|
|
194
|
+
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=True, cast=bool)
|
|
195
|
+
EMAIL_HOST = config('EMAIL_HOST', default='')
|
|
196
|
+
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
|
197
|
+
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
|
198
|
+
EMAIL_PORT = config('EMAIL_PORT', default=465, cast=int)
|
|
199
|
+
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
|
|
200
|
+
|
|
201
|
+
# ── Logging Configuration ─────────────────────────────────────────────────────
|
|
202
|
+
LOGGING = {
|
|
203
|
+
"version": 1,
|
|
204
|
+
"disable_existing_loggers": False,
|
|
205
|
+
"formatters": {
|
|
206
|
+
"verbose": {
|
|
207
|
+
"format": "[{levelname}] {asctime} {name}: {message}",
|
|
208
|
+
"style": "{",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
"handlers": {
|
|
212
|
+
"console": {
|
|
213
|
+
"class": "logging.StreamHandler",
|
|
214
|
+
"formatter": "verbose",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
"loggers": {
|
|
218
|
+
"django": {
|
|
219
|
+
"handlers": ["console"],
|
|
220
|
+
"level": "INFO",
|
|
221
|
+
},
|
|
222
|
+
"celery": {
|
|
223
|
+
"handlers": ["console"],
|
|
224
|
+
"level": "INFO",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# ── Celery & Background Tasks ─────────────────────────────────────────────────
|
|
230
|
+
CELERY_BROKER_URL = config("CELERY_BROKER_URL", default="redis://127.0.0.1:6379/0")
|
|
231
|
+
CELERY_RESULT_BACKEND = config("CELERY_RESULT_BACKEND", default="redis://127.0.0.1:6379/0")
|
|
232
|
+
CELERY_ACCEPT_CONTENT = ["json"]
|
|
233
|
+
CELERY_TASK_SERIALIZER = "json"
|
|
234
|
+
CELERY_RESULT_SERIALIZER = "json"
|
|
235
|
+
CELERY_TIMEZONE = TIME_ZONE
|
|
236
|
+
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
|
237
|
+
CELERY_TASK_TIME_LIMIT = 5 * 60
|
|
238
|
+
CELERY_TASK_SOFT_TIME_LIMIT = 60
|
|
239
|
+
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
|
240
|
+
|
|
241
|
+
# Celery Beat Schedule
|
|
242
|
+
CELERY_BEAT_SCHEDULE = {
|
|
243
|
+
"sample_task": {
|
|
244
|
+
"task": "core.tasks.sample_task",
|
|
245
|
+
"schedule": crontab(minute="*/15"),
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
"""
|
|
249
|
+
custom_configs = custom_configs.replace("core.utils.", f"{name}.utils.")
|
|
250
|
+
custom_configs = custom_configs.replace("core.tasks.", f"{name}.tasks.")
|
|
251
|
+
content += custom_configs
|
|
252
|
+
|
|
253
|
+
with open(settings_path, "w", encoding="utf-8") as f:
|
|
254
|
+
f.write(content)
|
|
255
|
+
|
|
256
|
+
return secret_key
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def create_directories():
|
|
260
|
+
os.makedirs("apps", exist_ok=True)
|
|
261
|
+
os.makedirs("static", exist_ok=True)
|
|
262
|
+
os.makedirs("media", exist_ok=True)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def install_dependencies():
|
|
266
|
+
packages = [
|
|
267
|
+
"Django",
|
|
268
|
+
"djangorestframework",
|
|
269
|
+
"djangorestframework-simplejwt",
|
|
270
|
+
"django-cors-headers",
|
|
271
|
+
"python-decouple",
|
|
272
|
+
"psycopg2-binary",
|
|
273
|
+
"Pillow",
|
|
274
|
+
"celery",
|
|
275
|
+
"redis",
|
|
276
|
+
"daphne",
|
|
277
|
+
"channels",
|
|
278
|
+
"channels-redis",
|
|
279
|
+
"whitenoise",
|
|
280
|
+
"drf-spectacular",
|
|
281
|
+
"pytest",
|
|
282
|
+
"pytest-django",
|
|
283
|
+
"pytest-cov",
|
|
284
|
+
"pre-commit",
|
|
285
|
+
"black",
|
|
286
|
+
"flake8",
|
|
287
|
+
"isort",
|
|
288
|
+
]
|
|
289
|
+
print("[cyan]📦 Installing dependencies...[/cyan]")
|
|
290
|
+
result = subprocess.run(
|
|
291
|
+
[sys.executable, "-m", "pip", "install"] + packages,
|
|
292
|
+
capture_output=True, text=True
|
|
293
|
+
)
|
|
294
|
+
if result.returncode != 0:
|
|
295
|
+
print(f"[red]Error installing dependencies:\n{result.stderr}[/red]")
|
|
296
|
+
import typer
|
|
297
|
+
raise typer.Exit(1)
|
|
298
|
+
print("[green]✔ Dependencies installed.[/green]")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def generate_env_file(secret_key: str, name: str):
|
|
302
|
+
env_content = f"""# -----------------------------
|
|
303
|
+
# Django Secret Key & Debug
|
|
304
|
+
# -----------------------------
|
|
305
|
+
DEBUG=True
|
|
306
|
+
SECRET_KEY={secret_key}
|
|
307
|
+
|
|
308
|
+
# ALLOWED HOSTS
|
|
309
|
+
ALLOWED_HOSTS=localhost,127.0.0.1
|
|
310
|
+
|
|
311
|
+
# CORS SETTINGS------------------------------#
|
|
312
|
+
# CSRF trusted origins
|
|
313
|
+
CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:8000
|
|
314
|
+
|
|
315
|
+
# CORS allowed origins
|
|
316
|
+
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:8000
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# -----------------------------
|
|
320
|
+
# Database: #'django.db.backends.postgresql', # or 'django.db.backends.mysql', # or 'django.db.backends.sqlite3'
|
|
321
|
+
# -----------------------------
|
|
322
|
+
# DB_ENGINE=django.db.backends.postgresql
|
|
323
|
+
# DB_NAME={name}_db
|
|
324
|
+
# DB_USER={name}_user
|
|
325
|
+
# DB_PASSWORD=your-db-password
|
|
326
|
+
# DB_HOST=localhost
|
|
327
|
+
# DB_PORT=5432
|
|
328
|
+
# CONN_MAX_AGE=600
|
|
329
|
+
|
|
330
|
+
# -----------------------------
|
|
331
|
+
# Redis Configuration
|
|
332
|
+
# -----------------------------
|
|
333
|
+
REDIS_HOST=localhost
|
|
334
|
+
REDIS_PORT=6379
|
|
335
|
+
REDIS_URL=redis://localhost:6379/1
|
|
336
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
337
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
|
338
|
+
|
|
339
|
+
# -----------------------------
|
|
340
|
+
# Email / SMTP Settings
|
|
341
|
+
# -----------------------------
|
|
342
|
+
EMAIL_USE_SSL=True
|
|
343
|
+
EMAIL_HOST=smtp.gmail.com
|
|
344
|
+
EMAIL_HOST_USER=your-email@gmail.com
|
|
345
|
+
EMAIL_HOST_PASSWORD=your-app-password
|
|
346
|
+
EMAIL_PORT=465
|
|
347
|
+
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
|
348
|
+
"""
|
|
349
|
+
with open(".env", "w", encoding="utf-8") as f:
|
|
350
|
+
f.write(env_content)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def generate_docker_files(name: str):
|
|
354
|
+
dockerfile_content = """FROM python:3.11-slim
|
|
355
|
+
|
|
356
|
+
ENV PYTHONDONTWRITEBYTECODE 1
|
|
357
|
+
ENV PYTHONUNBUFFERED 1
|
|
358
|
+
|
|
359
|
+
WORKDIR /app
|
|
360
|
+
|
|
361
|
+
RUN apt-get update \\
|
|
362
|
+
&& apt-get install -y --no-install-recommends gcc libpq-dev \\
|
|
363
|
+
&& apt-get clean \\
|
|
364
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
365
|
+
|
|
366
|
+
COPY requirements.txt /app/
|
|
367
|
+
RUN pip install --upgrade pip
|
|
368
|
+
RUN pip install -r requirements.txt
|
|
369
|
+
|
|
370
|
+
COPY . /app/
|
|
371
|
+
"""
|
|
372
|
+
with open("Dockerfile", "w", encoding="utf-8") as f:
|
|
373
|
+
f.write(dockerfile_content)
|
|
374
|
+
|
|
375
|
+
docker_compose_content = f"""version: '3.8'
|
|
376
|
+
|
|
377
|
+
services:
|
|
378
|
+
db:
|
|
379
|
+
image: postgres:15
|
|
380
|
+
volumes:
|
|
381
|
+
- postgres_data:/var/lib/postgresql/data
|
|
382
|
+
environment:
|
|
383
|
+
- POSTGRES_DB={name}_db
|
|
384
|
+
- POSTGRES_USER={name}_user
|
|
385
|
+
- POSTGRES_PASSWORD={name}@1234!
|
|
386
|
+
ports:
|
|
387
|
+
- "5432:5432"
|
|
388
|
+
|
|
389
|
+
redis:
|
|
390
|
+
image: redis:alpine
|
|
391
|
+
ports:
|
|
392
|
+
- "6379:6379"
|
|
393
|
+
|
|
394
|
+
web:
|
|
395
|
+
build: .
|
|
396
|
+
command: daphne -b 0.0.0.0 -p 8000 {name}.asgi:application
|
|
397
|
+
volumes:
|
|
398
|
+
- .:/app
|
|
399
|
+
ports:
|
|
400
|
+
- "8000:8000"
|
|
401
|
+
env_file:
|
|
402
|
+
- .env
|
|
403
|
+
environment:
|
|
404
|
+
- DB_HOST=db
|
|
405
|
+
- REDIS_HOST=redis
|
|
406
|
+
- REDIS_URL=redis://redis:6379/1
|
|
407
|
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
408
|
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
409
|
+
depends_on:
|
|
410
|
+
- db
|
|
411
|
+
- redis
|
|
412
|
+
|
|
413
|
+
celery:
|
|
414
|
+
build: .
|
|
415
|
+
command: celery -A {name} worker -l info
|
|
416
|
+
volumes:
|
|
417
|
+
- .:/app
|
|
418
|
+
env_file:
|
|
419
|
+
- .env
|
|
420
|
+
environment:
|
|
421
|
+
- DB_HOST=db
|
|
422
|
+
- REDIS_HOST=redis
|
|
423
|
+
- REDIS_URL=redis://redis:6379/1
|
|
424
|
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
|
425
|
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
|
426
|
+
depends_on:
|
|
427
|
+
- db
|
|
428
|
+
- redis
|
|
429
|
+
- web
|
|
430
|
+
|
|
431
|
+
volumes:
|
|
432
|
+
postgres_data:
|
|
433
|
+
"""
|
|
434
|
+
with open("docker-compose.yml", "w", encoding="utf-8") as f:
|
|
435
|
+
f.write(docker_compose_content)
|
|
436
|
+
|
|
437
|
+
dockerignore_content = """.env
|
|
438
|
+
.venv
|
|
439
|
+
venv/
|
|
440
|
+
__pycache__/
|
|
441
|
+
*.pyc
|
|
442
|
+
db.sqlite3
|
|
443
|
+
media/
|
|
444
|
+
static/
|
|
445
|
+
"""
|
|
446
|
+
with open(".dockerignore", "w", encoding="utf-8") as f:
|
|
447
|
+
f.write(dockerignore_content)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def freeze_requirements():
|
|
451
|
+
print("[cyan]📄 Freezing requirements...[/cyan]")
|
|
452
|
+
result = subprocess.run(
|
|
453
|
+
[sys.executable, "-m", "pip", "freeze", "--local"],
|
|
454
|
+
capture_output=True, text=True
|
|
455
|
+
)
|
|
456
|
+
with open("requirements.txt", "w", encoding="utf-8") as f:
|
|
457
|
+
f.write(result.stdout)
|
|
458
|
+
print("[green]✔ requirements.txt created.[/green]")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def create_utils_file(name: str):
|
|
462
|
+
utils_content = """from rest_framework.views import exception_handler
|
|
463
|
+
|
|
464
|
+
# Custom Message
|
|
465
|
+
def custom_exception_handler(exc, context):
|
|
466
|
+
\"\"\"
|
|
467
|
+
Global DRF exception handler.
|
|
468
|
+
Always return only first error message.
|
|
469
|
+
\"\"\"
|
|
470
|
+
|
|
471
|
+
response = exception_handler(exc, context)
|
|
472
|
+
|
|
473
|
+
if response is None:
|
|
474
|
+
return response
|
|
475
|
+
|
|
476
|
+
data = response.data
|
|
477
|
+
|
|
478
|
+
# Case 1: {"detail": "..."}
|
|
479
|
+
if isinstance(data, dict) and "detail" in data:
|
|
480
|
+
message = data["detail"]
|
|
481
|
+
|
|
482
|
+
# Case 2: serializer errors {"field": ["error"]}
|
|
483
|
+
elif isinstance(data, dict):
|
|
484
|
+
first_error = list(data.values())[0]
|
|
485
|
+
|
|
486
|
+
if isinstance(first_error, list):
|
|
487
|
+
message = first_error[0]
|
|
488
|
+
else:
|
|
489
|
+
message = first_error
|
|
490
|
+
|
|
491
|
+
# Case 3: ["error"]
|
|
492
|
+
elif isinstance(data, list):
|
|
493
|
+
message = data[0]
|
|
494
|
+
|
|
495
|
+
else:
|
|
496
|
+
message = "Something went wrong."
|
|
497
|
+
|
|
498
|
+
# Clean up technical messages
|
|
499
|
+
message = str(message)
|
|
500
|
+
if "JSON parse error" in message:
|
|
501
|
+
message = "Invalid JSON format in request body."
|
|
502
|
+
|
|
503
|
+
# Standardize response to include success: false
|
|
504
|
+
response.data = {
|
|
505
|
+
"success": False,
|
|
506
|
+
"message": message
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return response
|
|
510
|
+
"""
|
|
511
|
+
with open(f"{name}/utils.py", "w", encoding="utf-8") as f:
|
|
512
|
+
f.write(utils_content)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def create_celery_file(name: str):
|
|
516
|
+
celery_content = f"""import os
|
|
517
|
+
from celery import Celery
|
|
518
|
+
|
|
519
|
+
# Set the default Django settings module for the 'celery' program.
|
|
520
|
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{name}.settings')
|
|
521
|
+
|
|
522
|
+
app = Celery('{name}')
|
|
523
|
+
|
|
524
|
+
# Using a string here means the worker doesn't have to serialize
|
|
525
|
+
# the configuration object to child processes.
|
|
526
|
+
# - namespace='CELERY' means all celery-related configuration keys
|
|
527
|
+
# should have a `CELERY_` prefix.
|
|
528
|
+
app.config_from_object('django.conf:settings', namespace='CELERY')
|
|
529
|
+
|
|
530
|
+
# Load task modules from all registered Django apps.
|
|
531
|
+
app.autodiscover_tasks()
|
|
532
|
+
|
|
533
|
+
@app.task(bind=True, ignore_result=True)
|
|
534
|
+
def debug_task(self):
|
|
535
|
+
print(f'Request: {{self.request!r}}')
|
|
536
|
+
"""
|
|
537
|
+
with open(f"{name}/celery.py", "w", encoding="utf-8") as f:
|
|
538
|
+
f.write(celery_content)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def update_init_file(name: str):
|
|
542
|
+
init_content = """from .celery import app as celery_app
|
|
543
|
+
|
|
544
|
+
__all__ = ('celery_app',)
|
|
545
|
+
"""
|
|
546
|
+
with open(f"{name}/__init__.py", "w", encoding="utf-8") as f:
|
|
547
|
+
f.write(init_content)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def update_urls_file(name: str):
|
|
551
|
+
urls_content = """from django.contrib import admin
|
|
552
|
+
from django.urls import path, include
|
|
553
|
+
from django.conf.urls.static import static
|
|
554
|
+
from django.conf import settings
|
|
555
|
+
from django.http import JsonResponse
|
|
556
|
+
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# ── Root / Health-check endpoint ──────────────────────────────────────────────
|
|
560
|
+
def root_view(request):
|
|
561
|
+
return JsonResponse({
|
|
562
|
+
"message": "API Server Running",
|
|
563
|
+
"status": "ok"
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ── URL Patterns ──────────────────────────────────────────────────────────────
|
|
568
|
+
urlpatterns = [
|
|
569
|
+
# Root / Health-check
|
|
570
|
+
path('', root_view, name='home'),
|
|
571
|
+
|
|
572
|
+
# Django Admin Panel
|
|
573
|
+
# Optional: Change 'admin/' to 'em-secure-admin/' in production for security
|
|
574
|
+
path('admin/', admin.site.urls),
|
|
575
|
+
|
|
576
|
+
# API Documentation
|
|
577
|
+
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
|
578
|
+
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
|
579
|
+
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
|
580
|
+
|
|
581
|
+
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
582
|
+
"""
|
|
583
|
+
with open(f"{name}/urls.py", "w", encoding="utf-8") as f:
|
|
584
|
+
f.write(urls_content)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def generate_gitignore():
|
|
588
|
+
gitignore_content = """# ===========================
|
|
589
|
+
# Python / Django / Virtual Env
|
|
590
|
+
# ===========================
|
|
591
|
+
__pycache__/
|
|
592
|
+
*.py[cod]
|
|
593
|
+
*$py.class
|
|
594
|
+
*.so
|
|
595
|
+
|
|
596
|
+
env/
|
|
597
|
+
venv/
|
|
598
|
+
ENV/
|
|
599
|
+
.venv/
|
|
600
|
+
pyvenv.cfg
|
|
601
|
+
|
|
602
|
+
*.log
|
|
603
|
+
*.pot
|
|
604
|
+
*.pyc
|
|
605
|
+
*.pyo
|
|
606
|
+
*.pyd
|
|
607
|
+
|
|
608
|
+
# ===========================
|
|
609
|
+
# Database (PostgreSQL / MySQL)
|
|
610
|
+
# ===========================
|
|
611
|
+
*.dump
|
|
612
|
+
*.sql
|
|
613
|
+
*.backup
|
|
614
|
+
backups/
|
|
615
|
+
dumps/
|
|
616
|
+
db.sqlite3
|
|
617
|
+
db.sqlite3-journal
|
|
618
|
+
*.sqlite3
|
|
619
|
+
*.db
|
|
620
|
+
|
|
621
|
+
# ===========================
|
|
622
|
+
# Django migrations (optional)
|
|
623
|
+
# ===========================
|
|
624
|
+
# Ignore migrations if you handle them via CI/CD
|
|
625
|
+
# */migrations/*.py
|
|
626
|
+
# */migrations/*.pyc
|
|
627
|
+
# !*/migrations/__init__.py
|
|
628
|
+
|
|
629
|
+
# ===========================
|
|
630
|
+
# Static files / Media
|
|
631
|
+
# ===========================
|
|
632
|
+
/static/
|
|
633
|
+
/staticfiles/
|
|
634
|
+
/media/
|
|
635
|
+
/mediafiles/
|
|
636
|
+
/assets/
|
|
637
|
+
|
|
638
|
+
# ===========================
|
|
639
|
+
# Secrets / Environment variables
|
|
640
|
+
# ===========================
|
|
641
|
+
.env
|
|
642
|
+
.env.*
|
|
643
|
+
secret_key.txt
|
|
644
|
+
.secret_key
|
|
645
|
+
*.key
|
|
646
|
+
*.pem
|
|
647
|
+
*.crt
|
|
648
|
+
*.cer
|
|
649
|
+
credentials.json
|
|
650
|
+
secrets.json
|
|
651
|
+
|
|
652
|
+
# ===========================
|
|
653
|
+
# Caches / Temp
|
|
654
|
+
# ===========================
|
|
655
|
+
.cache/
|
|
656
|
+
.pytest_cache/
|
|
657
|
+
.mypy_cache/
|
|
658
|
+
django_cache/
|
|
659
|
+
tmp/
|
|
660
|
+
temp/
|
|
661
|
+
|
|
662
|
+
# ===========================
|
|
663
|
+
# Celery
|
|
664
|
+
# ===========================
|
|
665
|
+
celerybeat-schedule*
|
|
666
|
+
celerybeat-schedule
|
|
667
|
+
celerybeat-schedule-shm
|
|
668
|
+
celerybeat-schedule-wal
|
|
669
|
+
*.pid
|
|
670
|
+
|
|
671
|
+
# ===========================
|
|
672
|
+
# IDEs / Editors
|
|
673
|
+
# ===========================
|
|
674
|
+
.vscode/
|
|
675
|
+
.idea/
|
|
676
|
+
*.iml
|
|
677
|
+
*.sublime-project
|
|
678
|
+
*.sublime-workspace
|
|
679
|
+
*.swp
|
|
680
|
+
*~
|
|
681
|
+
|
|
682
|
+
# ===========================
|
|
683
|
+
# OS / System
|
|
684
|
+
# ===========================
|
|
685
|
+
.DS_Store
|
|
686
|
+
Thumbs.db
|
|
687
|
+
*.bak
|
|
688
|
+
*.orig
|
|
689
|
+
*.rej
|
|
690
|
+
|
|
691
|
+
# ===========================
|
|
692
|
+
# Docker
|
|
693
|
+
# ===========================
|
|
694
|
+
.docker/
|
|
695
|
+
docker-compose.override.yml
|
|
696
|
+
|
|
697
|
+
# ===========================
|
|
698
|
+
# Node / Frontend
|
|
699
|
+
# ===========================
|
|
700
|
+
node_modules/
|
|
701
|
+
dist/
|
|
702
|
+
build/
|
|
703
|
+
*.map
|
|
704
|
+
|
|
705
|
+
# ===========================
|
|
706
|
+
# Testing / Coverage
|
|
707
|
+
# ===========================
|
|
708
|
+
.coverage
|
|
709
|
+
htmlcov/
|
|
710
|
+
.tox/
|
|
711
|
+
.nox/
|
|
712
|
+
"""
|
|
713
|
+
with open(".gitignore", "w", encoding="utf-8") as f:
|
|
714
|
+
f.write(gitignore_content)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def generate_pytest_ini(name: str):
|
|
718
|
+
content = f"""[pytest]
|
|
719
|
+
DJANGO_SETTINGS_MODULE = {name}.settings
|
|
720
|
+
python_files = tests.py test_*.py *_tests.py
|
|
721
|
+
addopts = --cov=. --cov-report=html
|
|
722
|
+
"""
|
|
723
|
+
with open("pytest.ini", "w", encoding="utf-8") as f:
|
|
724
|
+
f.write(content)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def generate_pre_commit_config():
|
|
728
|
+
content = """repos:
|
|
729
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
730
|
+
rev: v4.4.0
|
|
731
|
+
hooks:
|
|
732
|
+
- id: trailing-whitespace
|
|
733
|
+
- id: end-of-file-fixer
|
|
734
|
+
- id: check-yaml
|
|
735
|
+
- repo: https://github.com/psf/black
|
|
736
|
+
rev: 23.3.0
|
|
737
|
+
hooks:
|
|
738
|
+
- id: black
|
|
739
|
+
- repo: https://github.com/PyCQA/isort
|
|
740
|
+
rev: 5.12.0
|
|
741
|
+
hooks:
|
|
742
|
+
- id: isort
|
|
743
|
+
- repo: https://github.com/PyCQA/flake8
|
|
744
|
+
rev: 6.0.0
|
|
745
|
+
hooks:
|
|
746
|
+
- id: flake8
|
|
747
|
+
"""
|
|
748
|
+
with open(".pre-commit-config.yaml", "w", encoding="utf-8") as f:
|
|
749
|
+
f.write(content)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def generate_github_actions():
|
|
753
|
+
os.makedirs(".github/workflows", exist_ok=True)
|
|
754
|
+
content = """name: Django CI
|
|
755
|
+
|
|
756
|
+
on:
|
|
757
|
+
push:
|
|
758
|
+
branches: [ "main" ]
|
|
759
|
+
pull_request:
|
|
760
|
+
branches: [ "main" ]
|
|
761
|
+
|
|
762
|
+
jobs:
|
|
763
|
+
build:
|
|
764
|
+
runs-on: ubuntu-latest
|
|
765
|
+
steps:
|
|
766
|
+
- uses: actions/checkout@v3
|
|
767
|
+
- name: Set up Python 3.11
|
|
768
|
+
uses: actions/setup-python@v3
|
|
769
|
+
with:
|
|
770
|
+
python-version: "3.11"
|
|
771
|
+
- name: Install dependencies
|
|
772
|
+
run: |
|
|
773
|
+
python -m pip install --upgrade pip
|
|
774
|
+
pip install -r requirements.txt
|
|
775
|
+
- name: Lint with flake8
|
|
776
|
+
run: |
|
|
777
|
+
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
778
|
+
- name: Run Tests
|
|
779
|
+
run: |
|
|
780
|
+
pytest
|
|
781
|
+
"""
|
|
782
|
+
with open(".github/workflows/main.yml", "w", encoding="utf-8") as f:
|
|
783
|
+
f.write(content)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def generate_gitlab_ci():
|
|
787
|
+
content = """image: python:3.11-slim
|
|
788
|
+
|
|
789
|
+
stages:
|
|
790
|
+
- test
|
|
791
|
+
|
|
792
|
+
variables:
|
|
793
|
+
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
|
794
|
+
|
|
795
|
+
cache:
|
|
796
|
+
paths:
|
|
797
|
+
- .cache/pip
|
|
798
|
+
|
|
799
|
+
test_project:
|
|
800
|
+
stage: test
|
|
801
|
+
before_script:
|
|
802
|
+
- python -m pip install --upgrade pip
|
|
803
|
+
- pip install -r requirements.txt
|
|
804
|
+
script:
|
|
805
|
+
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
806
|
+
- pytest
|
|
807
|
+
"""
|
|
808
|
+
with open(".gitlab-ci.yml", "w", encoding="utf-8") as f:
|
|
809
|
+
f.write(content)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def create_project(name: str):
|
|
813
|
+
check_virtual_environment()
|
|
814
|
+
validate_name(name, "project name")
|
|
815
|
+
|
|
816
|
+
# Check if project already exists
|
|
817
|
+
if Path(name).exists():
|
|
818
|
+
print(f"[red]Error: A directory named '{name}' already exists. Choose a different name or delete it first.[/red]")
|
|
819
|
+
import typer
|
|
820
|
+
raise typer.Exit(1)
|
|
821
|
+
|
|
822
|
+
# Check if current directory already has a manage.py (already a Django project)
|
|
823
|
+
if Path("manage.py").exists():
|
|
824
|
+
print("[red]Error: manage.py already exists here. Are you already inside a Django project?[/red]")
|
|
825
|
+
import typer
|
|
826
|
+
raise typer.Exit(1)
|
|
827
|
+
|
|
828
|
+
print(f"[green]🚀 Creating Django project: {name}[/green]")
|
|
829
|
+
|
|
830
|
+
# Install Django first so django-admin is available
|
|
831
|
+
print("[cyan]📦 Installing Django first...[/cyan]")
|
|
832
|
+
result = subprocess.run(
|
|
833
|
+
[sys.executable, "-m", "pip", "install", "Django"],
|
|
834
|
+
capture_output=True, text=True
|
|
835
|
+
)
|
|
836
|
+
if result.returncode != 0:
|
|
837
|
+
print(f"[red]Failed to install Django:\n{result.stderr}[/red]")
|
|
838
|
+
import typer
|
|
839
|
+
raise typer.Exit(1)
|
|
840
|
+
|
|
841
|
+
result = subprocess.run(
|
|
842
|
+
[sys.executable, "-m", "django", "startproject", name, "."],
|
|
843
|
+
capture_output=True, text=True
|
|
844
|
+
)
|
|
845
|
+
if result.returncode != 0:
|
|
846
|
+
print(f"[red]Failed to create Django project:\n{result.stderr}[/red]")
|
|
847
|
+
import typer
|
|
848
|
+
raise typer.Exit(1)
|
|
849
|
+
|
|
850
|
+
secret_key = update_settings_file(f"{name}/settings.py", name)
|
|
851
|
+
create_utils_file(name)
|
|
852
|
+
create_celery_file(name)
|
|
853
|
+
update_init_file(name)
|
|
854
|
+
update_urls_file(name)
|
|
855
|
+
create_directories()
|
|
856
|
+
install_dependencies()
|
|
857
|
+
generate_env_file(secret_key, name)
|
|
858
|
+
freeze_requirements()
|
|
859
|
+
generate_docker_files(name)
|
|
860
|
+
generate_gitignore()
|
|
861
|
+
generate_pytest_ini(name)
|
|
862
|
+
generate_pre_commit_config()
|
|
863
|
+
|
|
864
|
+
# Initialize git and install pre-commit
|
|
865
|
+
try:
|
|
866
|
+
subprocess.run(["git", "init"], check=True, capture_output=True)
|
|
867
|
+
subprocess.run(
|
|
868
|
+
[sys.executable, "-m", "pre_commit", "install"],
|
|
869
|
+
check=True, capture_output=True
|
|
870
|
+
)
|
|
871
|
+
except Exception:
|
|
872
|
+
pass
|
|
873
|
+
|
|
874
|
+
print()
|
|
875
|
+
print(f"[bold green]✅ Project '{name}' created successfully![/bold green]")
|
|
876
|
+
print()
|
|
877
|
+
print("[cyan]Next steps:[/cyan]")
|
|
878
|
+
print(f" 1. Update your [bold].env[/bold] file with DB credentials")
|
|
879
|
+
print(f" 2. Run [bold]python manage.py migrate[/bold]")
|
|
880
|
+
print(f" 3. Run [bold]python manage.py runserver[/bold]")
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: djboost
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Django project generator CLI — production-ready Django projects in one command
|
|
5
|
+
Author: Munjur Alom
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/munjurdev/djboost
|
|
8
|
+
Project-URL: Repository, https://github.com/munjurdev/djboost
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/munjurdev/djboost/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: typer>=0.9.0
|
|
23
|
+
Requires-Dist: rich>=13.0.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# djboost 🚀
|
|
27
|
+
|
|
28
|
+
`djboost` is a CLI tool that generates a fully-configured, production-ready Django project in one command. No more repetitive boilerplate setup.
|
|
29
|
+
|
|
30
|
+
With a single command you get Django REST Framework, JWT Authentication, Celery, Redis, WebSockets (Channels), Docker, Swagger docs, and more — all pre-wired and ready to go.
|
|
31
|
+
|
|
32
|
+
## ✨ Features
|
|
33
|
+
|
|
34
|
+
- **API Ready** — Django REST Framework + Simple JWT pre-configured
|
|
35
|
+
- **API Docs** — Auto-generated Swagger UI and ReDoc via `drf-spectacular`
|
|
36
|
+
- **Async Tasks** — Celery + Redis integrated out of the box
|
|
37
|
+
- **WebSockets** — Django Channels with Daphne and Redis channel layers
|
|
38
|
+
- **Environment Variables** — `python-decouple` with a generated `.env` file
|
|
39
|
+
- **Database** — Pre-configured for PostgreSQL (SQLite default for dev)
|
|
40
|
+
- **CORS & Security** — `django-cors-headers` + standard security headers
|
|
41
|
+
- **Docker** — Ready-to-use `Dockerfile` and `docker-compose.yml`
|
|
42
|
+
- **Static Files** — Whitenoise configured for efficient static file serving
|
|
43
|
+
- **Code Quality** — `pre-commit` with `black`, `flake8`, and `isort`
|
|
44
|
+
- **Testing** — `pytest` + `pytest-django` with coverage pre-configured
|
|
45
|
+
- **CI/CD** — Modular GitHub Actions and GitLab CI pipelines
|
|
46
|
+
- **Custom Exception Handling** — Global DRF exception handler included
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## � Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install djboost
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 🚀 Quick Start
|
|
59
|
+
|
|
60
|
+
### Step 1 — Create a virtual environment and activate it
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -m venv env
|
|
64
|
+
|
|
65
|
+
# Windows
|
|
66
|
+
env\Scripts\activate
|
|
67
|
+
|
|
68
|
+
# Mac / Linux
|
|
69
|
+
source env/bin/activate
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Step 2 — Install djboost
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install djboost
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Step 3 — Navigate to an empty folder and create your project
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Create a project with a custom name
|
|
82
|
+
djboost create project myproject
|
|
83
|
+
|
|
84
|
+
# Or use the default name 'core'
|
|
85
|
+
djboost create project
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This single command will automatically:
|
|
89
|
+
|
|
90
|
+
1. Install Django and run `startproject`
|
|
91
|
+
2. Update `settings.py` with all advanced configurations
|
|
92
|
+
3. Generate `.env`, `Dockerfile`, `docker-compose.yml`, and `.gitignore`
|
|
93
|
+
4. Generate `pytest.ini` and `.pre-commit-config.yaml`
|
|
94
|
+
5. Create `/apps`, `/static`, and `/media` directories
|
|
95
|
+
6. Install all required dependencies
|
|
96
|
+
7. Initialize a `git` repository and set up `pre-commit` hooks
|
|
97
|
+
8. Freeze dependencies into `requirements.txt`
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🧱 Creating Apps
|
|
102
|
+
|
|
103
|
+
Apps are created inside the `apps/` directory to keep your project root clean. Settings and URLs are auto-configured.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Navigate to your project root first
|
|
107
|
+
cd myproject
|
|
108
|
+
|
|
109
|
+
# Create a new app
|
|
110
|
+
djboost create app users
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This will:
|
|
114
|
+
- Create `apps/users/` with standard Django app structure
|
|
115
|
+
- Auto-add `'apps.users'` to `INSTALLED_APPS`
|
|
116
|
+
- Auto-map `api/users/` in `urls.py`
|
|
117
|
+
- Create a starter `urls.py` inside the app
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## � Managing CI/CD Pipelines
|
|
122
|
+
|
|
123
|
+
CI/CD is modular — add or remove it any time after project creation.
|
|
124
|
+
|
|
125
|
+
### Add a pipeline
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
djboost add cicd github # GitHub Actions
|
|
129
|
+
djboost add cicd gitlab # GitLab CI
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Remove a pipeline
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
djboost remove cicd github
|
|
136
|
+
djboost remove cicd gitlab
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 🏃 Running Your Project
|
|
142
|
+
|
|
143
|
+
### Locally
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Apply migrations
|
|
147
|
+
python manage.py migrate
|
|
148
|
+
|
|
149
|
+
# Start the dev server
|
|
150
|
+
python manage.py runserver
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### API Documentation
|
|
154
|
+
|
|
155
|
+
Once your server is running:
|
|
156
|
+
|
|
157
|
+
| Interface | URL |
|
|
158
|
+
|---|---|
|
|
159
|
+
| Swagger UI | `http://127.0.0.1:8000/api/schema/swagger-ui/` |
|
|
160
|
+
| ReDoc | `http://127.0.0.1:8000/api/schema/redoc/` |
|
|
161
|
+
| OpenAPI Schema | `http://127.0.0.1:8000/api/schema/` |
|
|
162
|
+
|
|
163
|
+
### Testing
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pytest
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### With Docker
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
docker-compose up --build
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This spins up PostgreSQL, Redis, Celery worker, and a Daphne ASGI server together.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## ⚙️ CLI Reference
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
djboost --help
|
|
183
|
+
djboost --version
|
|
184
|
+
|
|
185
|
+
djboost create project [NAME] Create a new Django project
|
|
186
|
+
djboost create app NAME Create a new app inside apps/
|
|
187
|
+
|
|
188
|
+
djboost add cicd github|gitlab Add a CI/CD pipeline
|
|
189
|
+
djboost remove cicd github|gitlab Remove a CI/CD pipeline
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 🐍 Requirements
|
|
195
|
+
|
|
196
|
+
- Python 3.10+
|
|
197
|
+
- Virtual environment (required — djboost will warn you if not activated)
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 📄 License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
djboost/__init__.py,sha256=dP-7015-lW7jRYSq9V1Gamk6IBR_QApOw1KMCvi7WcE,8
|
|
2
|
+
djboost/cli.py,sha256=iQaCfkNuRKqQQMZ3m4WmUbasNJNrOXmvUFPxHa3AhiU,1159
|
|
3
|
+
djboost/generator.py,sha256=HJucjKldl_qSJjEYEDUT4DLnQ6JXAl6otqmnIus9Iik,26048
|
|
4
|
+
djboost/commands/__init__.py,sha256=dP-7015-lW7jRYSq9V1Gamk6IBR_QApOw1KMCvi7WcE,8
|
|
5
|
+
djboost/commands/add_cicd.py,sha256=I28rZcuZ8ZOb67w7SP7m4wOyYeG8RW181U_POVan768,759
|
|
6
|
+
djboost/commands/create_app.py,sha256=rr4kpm6iNq-uJcHH-6WZ5QozO-ryddQIhGWClqrdX9Q,4693
|
|
7
|
+
djboost/commands/create_project.py,sha256=FcNODKMGlIKYrsce94u58rRhkYkV4xZsclfNp6o2NI0,192
|
|
8
|
+
djboost/commands/remove_cicd.py,sha256=arkMkZUHit4YcDTDMV7BT3YoSCC3WmG7vMVYx7BVLRs,1447
|
|
9
|
+
djboost-0.1.0.dist-info/licenses/LICENSE,sha256=y5cRw4FX_ySuzWEy0aq14MhDvK-cKxrNsCHvkmoQ2CQ,1087
|
|
10
|
+
djboost-0.1.0.dist-info/METADATA,sha256=Q4701Sgx04kSsqU8LoUbENA3UhtHrUzD3eLR-97h5AY,5280
|
|
11
|
+
djboost-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
djboost-0.1.0.dist-info/entry_points.txt,sha256=i_aCUH4pIdn_LZDe5SQZATzpaj4pWAPkmZjA17mVL-I,44
|
|
13
|
+
djboost-0.1.0.dist-info/top_level.txt,sha256=mLgii1ayliYMsRglYvmbFtc-PVL2pcaDKqcmFKVkrxQ,8
|
|
14
|
+
djboost-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 munjurdev
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
djboost
|