django-deploy-toolkit 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.
@@ -0,0 +1,4 @@
1
+ """django-deploy-toolkit: Auto-generate Gunicorn & Nginx configs for Django projects."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Antu Saha"
@@ -0,0 +1,409 @@
1
+ """Click-based CLI for django-deploy-toolkit."""
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ from . import __version__
11
+ from .detector import ProjectDetector
12
+ from .generators.nginx import NginxGenerator
13
+ from .generators.service import ServiceGenerator
14
+ from .generators.socket import SocketGenerator
15
+ from .installer import Installer
16
+ from .reporter import Reporter
17
+ from .rollback import RollbackManager
18
+ from .utils import (
19
+ check_gunicorn_installed,
20
+ check_nginx_installed,
21
+ check_platform,
22
+ check_systemd_available,
23
+ )
24
+ from .validators import ConfigValidator
25
+
26
+ console = Console()
27
+
28
+
29
+ def _configure_logging(verbose):
30
+ """Set up logging level based on the verbose flag."""
31
+ log_level = logging.DEBUG if verbose else logging.WARNING
32
+ logging.basicConfig(
33
+ level=log_level,
34
+ format="%(levelname)s [%(name)s] %(message)s",
35
+ force=True,
36
+ )
37
+ logging.getLogger("django_deploy_toolkit").setLevel(log_level)
38
+
39
+
40
+ @click.group(invoke_without_command=True)
41
+ @click.version_option(version=__version__, prog_name="django-deploy-toolkit")
42
+ @click.option(
43
+ "--project-path",
44
+ type=click.Path(exists=True, file_okay=False),
45
+ default=None,
46
+ help="Override the detected project path.",
47
+ )
48
+ @click.option(
49
+ "--project-name",
50
+ type=str,
51
+ default=None,
52
+ help="Override the detected project name.",
53
+ )
54
+ @click.option(
55
+ "--no-confirm",
56
+ is_flag=True,
57
+ default=False,
58
+ help="Skip the confirmation prompt.",
59
+ )
60
+ @click.option(
61
+ "--verbose/--no-verbose",
62
+ default=False,
63
+ help="Show or hide detailed output.",
64
+ )
65
+ @click.pass_context
66
+ def main(ctx, project_path, project_name, no_confirm, verbose):
67
+ """django-deploy-toolkit: Auto-generate Gunicorn & Nginx configs for Django projects."""
68
+ _configure_logging(verbose)
69
+ check_platform()
70
+
71
+ ctx.ensure_object(dict)
72
+ ctx.obj["project_path"] = project_path
73
+ ctx.obj["project_name"] = project_name
74
+ ctx.obj["no_confirm"] = no_confirm
75
+ ctx.obj["verbose"] = verbose
76
+
77
+ # If no subcommand, show help
78
+ if ctx.invoked_subcommand is None:
79
+ click.echo(ctx.get_help())
80
+
81
+
82
+ @main.command()
83
+ @click.option(
84
+ "--dry-run",
85
+ is_flag=True,
86
+ default=False,
87
+ help="Show what would be done without actually doing it.",
88
+ )
89
+ @click.option(
90
+ "--verbose/--no-verbose",
91
+ default=False,
92
+ help="Show or hide detailed output.",
93
+ )
94
+ @click.pass_context
95
+ def setup(ctx, dry_run, verbose):
96
+ """Full interactive setup: detect, validate, generate, and install."""
97
+ verbose = verbose or ctx.obj.get("verbose", False)
98
+ _configure_logging(verbose)
99
+ project_path = ctx.obj["project_path"]
100
+ project_name = ctx.obj["project_name"]
101
+ no_confirm = ctx.obj["no_confirm"]
102
+
103
+ console.print(
104
+ "\n[bold cyan]🚀 django-deploy-toolkit — Setup[/bold cyan]\n"
105
+ )
106
+
107
+ # --- Pre-flight checks ---
108
+ warnings = []
109
+
110
+ if not check_systemd_available():
111
+ warnings.append(
112
+ "Systemd not available. Are you running inside Docker? "
113
+ "The setup may not work correctly."
114
+ )
115
+
116
+ if not check_nginx_installed():
117
+ warnings.append(
118
+ "Nginx is not installed. Install it with: sudo apt install nginx"
119
+ )
120
+
121
+ for warning in warnings:
122
+ console.print(f"[yellow]⚠ {warning}[/yellow]")
123
+
124
+ if warnings and not dry_run:
125
+ if not no_confirm and not click.confirm(
126
+ "\nWarnings detected. Continue anyway?", default=True
127
+ ):
128
+ raise SystemExit("Setup cancelled.")
129
+
130
+ # --- Detection ---
131
+ console.print("[bold]Detecting project configuration...[/bold]\n")
132
+ detector = ProjectDetector(project_path=project_path)
133
+ config = detector.detect_all()
134
+
135
+ # Apply overrides
136
+ if project_name:
137
+ config["project_name"] = project_name
138
+ if project_path:
139
+ config["project_path"] = os.path.abspath(project_path)
140
+
141
+ # --- Warnings about detected values ---
142
+ if config.get("python_path"):
143
+ if not detector.is_virtualenv():
144
+ console.print(
145
+ "[yellow]⚠ Python path points to system Python, not a virtualenv. "
146
+ "Consider activating your project's virtual environment.[/yellow]\n"
147
+ )
148
+
149
+ if not check_gunicorn_installed(config["python_path"]):
150
+ console.print(
151
+ "[yellow]⚠ Gunicorn not found in the detected Python environment. "
152
+ "Install it with: pip install gunicorn[/yellow]\n"
153
+ )
154
+
155
+ # --- Validation ---
156
+ validator = ConfigValidator(config, no_confirm=no_confirm)
157
+ config = validator.validate_and_prompt()
158
+ sources = validator.get_sources()
159
+
160
+ # --- Report ---
161
+ reporter = Reporter(config, sources)
162
+
163
+ if dry_run:
164
+ reporter.print_dry_run_header()
165
+
166
+ reporter.print_settings_table()
167
+
168
+ # --- Installation ---
169
+ installer = Installer(config, dry_run=dry_run, overwrite=False)
170
+ results = installer.install()
171
+
172
+ reporter.print_results_table(results)
173
+
174
+ # Check if any step failed
175
+ failed = [r for r in results if r[1] == "failed"]
176
+ if failed:
177
+ failed_step = failed[0][0]
178
+ error = failed[0][2]
179
+ reporter.print_failure(error, failed_step)
180
+ raise SystemExit(1)
181
+ else:
182
+ if not dry_run:
183
+ reporter.print_success()
184
+ else:
185
+ console.print(
186
+ "[bold yellow]Dry run complete. "
187
+ "No changes were made.[/bold yellow]\n"
188
+ )
189
+
190
+ # Always remind the user to update server_name
191
+ server_ip = config.get("server_ip", "_")
192
+ nginx_path = f"/etc/nginx/sites-available/{config['project_name']}"
193
+ console.print(
194
+ f"[bold cyan]📌 Important:[/bold cyan] Your Nginx [bold]server_name[/bold] "
195
+ f"is currently set to [yellow]{server_ip}[/yellow].\n"
196
+ f" If you have a domain name, update it in the Nginx config:\n\n"
197
+ f" [dim]sudo nano {nginx_path}[/dim]\n\n"
198
+ f" Change [yellow]server_name {server_ip};[/yellow] → "
199
+ f"[green]server_name yourdomain.com;[/green]\n"
200
+ f" Then reload Nginx: [dim]sudo systemctl reload nginx[/dim]\n"
201
+ )
202
+
203
+
204
+ @main.command()
205
+ @click.option(
206
+ "--verbose/--no-verbose",
207
+ default=False,
208
+ help="Show or hide detailed output.",
209
+ )
210
+ @click.pass_context
211
+ def detect(ctx, verbose):
212
+ """Run detection only and print results without installing."""
213
+ verbose = verbose or ctx.obj.get("verbose", False)
214
+ _configure_logging(verbose)
215
+ project_path = ctx.obj["project_path"]
216
+
217
+ console.print(
218
+ "\n[bold cyan]🔍 django-deploy-toolkit — Detection[/bold cyan]\n"
219
+ )
220
+
221
+ detector = ProjectDetector(project_path=project_path)
222
+ config = detector.detect_all()
223
+
224
+ # Build sources
225
+ sources = {}
226
+ for key, value in config.items():
227
+ sources[key] = "auto" if value is not None else "missing"
228
+
229
+ reporter = Reporter(config, sources)
230
+ reporter.print_detection_results()
231
+
232
+
233
+ @main.command()
234
+ @click.option(
235
+ "--output-dir",
236
+ type=click.Path(file_okay=False),
237
+ default=".",
238
+ help="Directory to write generated files to.",
239
+ )
240
+ @click.option(
241
+ "--verbose/--no-verbose",
242
+ default=False,
243
+ help="Show or hide detailed output.",
244
+ )
245
+ @click.pass_context
246
+ def generate(ctx, output_dir, verbose):
247
+ """Generate config files to the current directory without installing."""
248
+ verbose = verbose or ctx.obj.get("verbose", False)
249
+ _configure_logging(verbose)
250
+ project_path = ctx.obj["project_path"]
251
+ project_name = ctx.obj["project_name"]
252
+ no_confirm = ctx.obj["no_confirm"]
253
+
254
+ console.print(
255
+ "\n[bold cyan]📝 django-deploy-toolkit — Generate[/bold cyan]\n"
256
+ )
257
+
258
+ # --- Detection ---
259
+ detector = ProjectDetector(project_path=project_path)
260
+ config = detector.detect_all()
261
+
262
+ if project_name:
263
+ config["project_name"] = project_name
264
+ if project_path:
265
+ config["project_path"] = os.path.abspath(project_path)
266
+
267
+ # --- Validation ---
268
+ validator = ConfigValidator(config, no_confirm=no_confirm)
269
+ config = validator.validate_and_prompt()
270
+
271
+ # --- Generate files ---
272
+ output_dir = os.path.abspath(output_dir)
273
+ os.makedirs(output_dir, exist_ok=True)
274
+
275
+ pname = config["project_name"]
276
+
277
+ socket_path = os.path.join(output_dir, f"{pname}.socket")
278
+ service_path = os.path.join(output_dir, f"{pname}.service")
279
+ nginx_path = os.path.join(output_dir, f"{pname}.nginx.conf")
280
+
281
+ SocketGenerator(config).write(socket_path)
282
+ console.print(f"[green]✓ Written:[/green] {socket_path}")
283
+
284
+ ServiceGenerator(config).write(service_path)
285
+ console.print(f"[green]✓ Written:[/green] {service_path}")
286
+
287
+ NginxGenerator(config).write(nginx_path)
288
+ console.print(f"[green]✓ Written:[/green] {nginx_path}")
289
+
290
+ console.print(
291
+ f"\n[bold green]Files generated in {output_dir}[/bold green]\n"
292
+ )
293
+
294
+
295
+ @main.command()
296
+ @click.option(
297
+ "--name",
298
+ type=str,
299
+ required=True,
300
+ help="Project name to rollback.",
301
+ )
302
+ @click.pass_context
303
+ def rollback(ctx, name):
304
+ """Rollback the last install for a given project.
305
+
306
+ Removes the generated config files and disables the services.
307
+ """
308
+ no_confirm = ctx.obj["no_confirm"]
309
+
310
+ console.print(
311
+ f"\n[bold red]🔄 django-deploy-toolkit — Rollback for '{name}'[/bold red]\n"
312
+ )
313
+
314
+ socket_path = f"/etc/systemd/system/{name}.socket"
315
+ service_path = f"/etc/systemd/system/{name}.service"
316
+ nginx_available = f"/etc/nginx/sites-available/{name}"
317
+ nginx_enabled = f"/etc/nginx/sites-enabled/{name}"
318
+
319
+ files_to_check = [
320
+ ("Socket file", socket_path),
321
+ ("Service file", service_path),
322
+ ("Nginx config", nginx_available),
323
+ ("Nginx symlink", nginx_enabled),
324
+ ]
325
+
326
+ found = []
327
+ for label, path in files_to_check:
328
+ if os.path.exists(path) or os.path.islink(path):
329
+ found.append((label, path))
330
+ console.print(f" Found: {label} at {path}")
331
+
332
+ if not found:
333
+ console.print(
334
+ f"[yellow]No deployment files found for project '{name}'.[/yellow]\n"
335
+ )
336
+ return
337
+
338
+ if not no_confirm:
339
+ if not click.confirm(
340
+ "\nRemove these files and disable services?", default=False
341
+ ):
342
+ console.print("[dim]Rollback cancelled.[/dim]")
343
+ return
344
+
345
+ from .utils import is_root, run_system_command
346
+
347
+ use_sudo = not is_root()
348
+
349
+ # Stop and disable services
350
+ for unit in [f"{name}.service", f"{name}.socket"]:
351
+ try:
352
+ run_system_command(
353
+ ["systemctl", "stop", unit],
354
+ use_sudo=use_sudo,
355
+ )
356
+ console.print(f" [green]Stopped:[/green] {unit}")
357
+ except RuntimeError:
358
+ pass
359
+
360
+ try:
361
+ run_system_command(
362
+ ["systemctl", "disable", unit],
363
+ use_sudo=use_sudo,
364
+ )
365
+ console.print(f" [green]Disabled:[/green] {unit}")
366
+ except RuntimeError:
367
+ pass
368
+
369
+ # Remove files
370
+ for label, path in found:
371
+ try:
372
+ if use_sudo:
373
+ run_system_command(["rm", "-f", path], use_sudo=True)
374
+ else:
375
+ if os.path.islink(path):
376
+ os.unlink(path)
377
+ else:
378
+ os.remove(path)
379
+ console.print(f" [green]Removed:[/green] {path}")
380
+ except (OSError, RuntimeError) as e:
381
+ console.print(f" [red]Failed to remove {path}: {e}[/red]")
382
+
383
+ # Reload
384
+ try:
385
+ run_system_command(
386
+ ["systemctl", "daemon-reload"],
387
+ use_sudo=use_sudo,
388
+ )
389
+ console.print(" [green]Reloaded systemd daemon[/green]")
390
+ except RuntimeError:
391
+ pass
392
+
393
+ try:
394
+ run_system_command(["nginx", "-t"], use_sudo=use_sudo)
395
+ run_system_command(
396
+ ["systemctl", "reload", "nginx"],
397
+ use_sudo=use_sudo,
398
+ )
399
+ console.print(" [green]Reloaded Nginx[/green]")
400
+ except RuntimeError:
401
+ pass
402
+
403
+ console.print(
404
+ f"\n[bold green]Rollback for '{name}' complete.[/bold green]\n"
405
+ )
406
+
407
+
408
+ if __name__ == "__main__":
409
+ main()