sum-cli 3.0.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.
Files changed (72) hide show
  1. sum/__init__.py +1 -0
  2. sum/boilerplate/.env.example +124 -0
  3. sum/boilerplate/.gitea/workflows/ci.yml +33 -0
  4. sum/boilerplate/.gitea/workflows/deploy-production.yml +98 -0
  5. sum/boilerplate/.gitea/workflows/deploy-staging.yml +113 -0
  6. sum/boilerplate/.github/workflows/ci.yml +36 -0
  7. sum/boilerplate/.github/workflows/deploy-production.yml +102 -0
  8. sum/boilerplate/.github/workflows/deploy-staging.yml +115 -0
  9. sum/boilerplate/.gitignore +45 -0
  10. sum/boilerplate/README.md +259 -0
  11. sum/boilerplate/manage.py +34 -0
  12. sum/boilerplate/project_name/__init__.py +5 -0
  13. sum/boilerplate/project_name/home/__init__.py +5 -0
  14. sum/boilerplate/project_name/home/apps.py +20 -0
  15. sum/boilerplate/project_name/home/management/__init__.py +0 -0
  16. sum/boilerplate/project_name/home/management/commands/__init__.py +0 -0
  17. sum/boilerplate/project_name/home/management/commands/populate_demo_content.py +644 -0
  18. sum/boilerplate/project_name/home/management/commands/seed.py +129 -0
  19. sum/boilerplate/project_name/home/management/commands/seed_showroom.py +1661 -0
  20. sum/boilerplate/project_name/home/migrations/__init__.py +3 -0
  21. sum/boilerplate/project_name/home/models.py +13 -0
  22. sum/boilerplate/project_name/settings/__init__.py +5 -0
  23. sum/boilerplate/project_name/settings/base.py +348 -0
  24. sum/boilerplate/project_name/settings/local.py +78 -0
  25. sum/boilerplate/project_name/settings/production.py +106 -0
  26. sum/boilerplate/project_name/urls.py +33 -0
  27. sum/boilerplate/project_name/wsgi.py +16 -0
  28. sum/boilerplate/pytest.ini +5 -0
  29. sum/boilerplate/requirements.txt +25 -0
  30. sum/boilerplate/static/client/.gitkeep +3 -0
  31. sum/boilerplate/templates/overrides/.gitkeep +3 -0
  32. sum/boilerplate/tests/__init__.py +3 -0
  33. sum/boilerplate/tests/test_health.py +51 -0
  34. sum/cli.py +42 -0
  35. sum/commands/__init__.py +10 -0
  36. sum/commands/backup.py +308 -0
  37. sum/commands/check.py +128 -0
  38. sum/commands/init.py +265 -0
  39. sum/commands/promote.py +758 -0
  40. sum/commands/run.py +96 -0
  41. sum/commands/themes.py +56 -0
  42. sum/commands/update.py +301 -0
  43. sum/config.py +61 -0
  44. sum/docs/USER_GUIDE.md +663 -0
  45. sum/exceptions.py +45 -0
  46. sum/setup/__init__.py +17 -0
  47. sum/setup/auth.py +184 -0
  48. sum/setup/database.py +58 -0
  49. sum/setup/deps.py +73 -0
  50. sum/setup/git_ops.py +463 -0
  51. sum/setup/infrastructure.py +576 -0
  52. sum/setup/orchestrator.py +354 -0
  53. sum/setup/remote_themes.py +371 -0
  54. sum/setup/scaffold.py +500 -0
  55. sum/setup/seed.py +110 -0
  56. sum/setup/site_orchestrator.py +441 -0
  57. sum/setup/venv.py +89 -0
  58. sum/system_config.py +330 -0
  59. sum/themes_registry.py +180 -0
  60. sum/utils/__init__.py +25 -0
  61. sum/utils/django.py +97 -0
  62. sum/utils/environment.py +76 -0
  63. sum/utils/output.py +78 -0
  64. sum/utils/project.py +110 -0
  65. sum/utils/prompts.py +36 -0
  66. sum/utils/validation.py +313 -0
  67. sum_cli-3.0.0.dist-info/METADATA +127 -0
  68. sum_cli-3.0.0.dist-info/RECORD +72 -0
  69. sum_cli-3.0.0.dist-info/WHEEL +5 -0
  70. sum_cli-3.0.0.dist-info/entry_points.txt +2 -0
  71. sum_cli-3.0.0.dist-info/licenses/LICENSE +29 -0
  72. sum_cli-3.0.0.dist-info/top_level.txt +1 -0
sum/setup/auth.py ADDED
@@ -0,0 +1,184 @@
1
+ """Superuser management for CLI setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from sum.exceptions import SuperuserError
12
+ from sum.utils.django import DjangoCommandExecutor
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class SuperuserResult:
19
+ """Result of a superuser creation operation.
20
+
21
+ Note:
22
+ credentials_path points to .env.local regardless of the created flag.
23
+ When created=False (user already existed), the file may not exist or
24
+ may contain different credentials. Only trust credentials_path when
25
+ created=True.
26
+ """
27
+
28
+ success: bool
29
+ username: str
30
+ credentials_path: Path
31
+ created: bool # True if newly created, False if already existed
32
+
33
+
34
+ def _escape_env_value(value: str) -> str:
35
+ """Escape value for .env file (wrap in quotes if needed).
36
+
37
+ Args:
38
+ value: The raw value to escape.
39
+
40
+ Returns:
41
+ The value wrapped in quotes with proper escaping.
42
+ """
43
+ # Always quote to handle spaces, #, etc.
44
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
45
+ return f'"{escaped}"'
46
+
47
+
48
+ class SuperuserManager:
49
+ """Manages Django superuser creation."""
50
+
51
+ def __init__(
52
+ self, django_executor: DjangoCommandExecutor, project_path: Path
53
+ ) -> None:
54
+ self.django = django_executor
55
+ self.project_path = project_path
56
+
57
+ def user_exists(self, username: str) -> bool:
58
+ """Check if a specific username already exists.
59
+
60
+ Args:
61
+ username: The username to check.
62
+
63
+ Returns:
64
+ True if the user exists, False otherwise.
65
+
66
+ Raises:
67
+ SuperuserError: If the Django shell command fails or returns unexpected output.
68
+ """
69
+ # Use JSON serialization to safely pass username and prevent injection
70
+ username_json = json.dumps(username)
71
+ result = self.django.run_command(
72
+ [
73
+ "shell",
74
+ "-v",
75
+ "0",
76
+ "-c",
77
+ f"import json; "
78
+ f"from django.contrib.auth import get_user_model; "
79
+ f"username = json.loads({username_json!r}); "
80
+ f"print(get_user_model().objects.filter(username=username).exists())",
81
+ ],
82
+ check=False,
83
+ )
84
+
85
+ # Check return code before parsing output
86
+ if result.returncode != 0:
87
+ raise SuperuserError(
88
+ f"Failed to check if user '{username}' exists: {result.stderr}"
89
+ )
90
+
91
+ # Validate output before returning
92
+ output = result.stdout.strip().lower()
93
+ if output not in {"true", "false"}:
94
+ raise SuperuserError(
95
+ f"Unexpected output when checking if user '{username}' exists: "
96
+ f"{result.stdout!r}"
97
+ )
98
+
99
+ return output == "true"
100
+
101
+ def create(
102
+ self,
103
+ username: str = "admin",
104
+ email: str = "admin@example.com",
105
+ password: str = "admin",
106
+ ) -> SuperuserResult:
107
+ """Create superuser and store credentials.
108
+
109
+ Idempotency: If user already exists, skips both creation and .env.local
110
+ writing to avoid creating a credentials file that doesn't match reality.
111
+
112
+ Args:
113
+ username: The username for the superuser (default: "admin").
114
+ email: The email for the superuser (default: "admin@example.com").
115
+ password: The password for the superuser (default: "admin").
116
+
117
+ Raises:
118
+ SuperuserError: If superuser creation fails.
119
+
120
+ Returns:
121
+ SuperuserResult with success=True, username, credentials path,
122
+ and created flag indicating if the user was newly created.
123
+ """
124
+ # Check FIRST - don't attempt creation if user exists
125
+ if self.user_exists(username):
126
+ logger.info(f"User '{username}' already exists, skipping creation")
127
+ return SuperuserResult(
128
+ success=True,
129
+ username=username,
130
+ credentials_path=self.project_path / ".env.local",
131
+ created=False,
132
+ )
133
+
134
+ # User doesn't exist - create it
135
+ result = self.django.run_command(
136
+ [
137
+ "createsuperuser",
138
+ "--noinput",
139
+ "--username",
140
+ username,
141
+ "--email",
142
+ email,
143
+ ],
144
+ env={"DJANGO_SUPERUSER_PASSWORD": password},
145
+ check=False,
146
+ )
147
+
148
+ if result.returncode != 0:
149
+ details = result.stderr or result.stdout or "Unknown error"
150
+ raise SuperuserError(f"Creation failed: {details}")
151
+
152
+ # Only write .env.local if we actually created the user
153
+ self._save_credentials(username, email, password)
154
+
155
+ return SuperuserResult(
156
+ success=True,
157
+ username=username,
158
+ credentials_path=self.project_path / ".env.local",
159
+ created=True,
160
+ )
161
+
162
+ def _save_credentials(self, username: str, email: str, password: str) -> None:
163
+ """Save credentials to .env.local with proper escaping.
164
+
165
+ Args:
166
+ username: The superuser username.
167
+ email: The superuser email.
168
+ password: The superuser password.
169
+ """
170
+ env_local = self.project_path / ".env.local"
171
+ date_str = datetime.now().strftime("%Y-%m-%d")
172
+
173
+ content = f"""# .env.local
174
+ # Auto-generated by sum init on {date_str}
175
+ # DO NOT COMMIT THIS FILE
176
+ DJANGO_SUPERUSER_USERNAME={_escape_env_value(username)}
177
+ DJANGO_SUPERUSER_EMAIL={_escape_env_value(email)}
178
+ DJANGO_SUPERUSER_PASSWORD={_escape_env_value(password)}
179
+ """
180
+ # Write the file
181
+ env_local.write_text(content)
182
+
183
+ # Set restrictive permissions (owner read/write only) for security
184
+ env_local.chmod(0o600)
sum/setup/database.py ADDED
@@ -0,0 +1,58 @@
1
+ """Database management for CLI setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from sum.exceptions import MigrationError
8
+ from sum.utils.django import DjangoCommandExecutor
9
+
10
+
11
+ @dataclass
12
+ class MigrationResult:
13
+ """Result of a migration operation."""
14
+
15
+ success: bool
16
+ output: str
17
+
18
+
19
+ class DatabaseManager:
20
+ """Manages database operations."""
21
+
22
+ def __init__(self, django_executor: DjangoCommandExecutor) -> None:
23
+ self.django = django_executor
24
+
25
+ def migrate(self) -> MigrationResult:
26
+ """Run database migrations.
27
+
28
+ Raises:
29
+ MigrationError: If migration fails.
30
+
31
+ Returns:
32
+ MigrationResult with success=True and output.
33
+ """
34
+ result = self.django.run_command(["migrate", "--noinput"], check=False)
35
+
36
+ if result.returncode != 0:
37
+ message = result.stderr or result.stdout or "Unknown error"
38
+ raise MigrationError(f"Migration failed: {message}")
39
+
40
+ return MigrationResult(success=True, output=result.stdout)
41
+
42
+ def check_migrations(self) -> bool:
43
+ """Check if migrations are up to date.
44
+
45
+ Returns:
46
+ True if migrations are up to date, False otherwise.
47
+ """
48
+ result = self.django.run_command(["migrate", "--check"], check=False)
49
+ return result.returncode == 0
50
+
51
+ def get_migration_status(self) -> str:
52
+ """Get detailed migration status.
53
+
54
+ Returns:
55
+ Output from showmigrations --plan command.
56
+ """
57
+ result = self.django.run_command(["showmigrations", "--plan"], check=False)
58
+ return result.stdout
sum/setup/deps.py ADDED
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from sum.exceptions import DependencyError
7
+ from sum.setup.venv import VenvManager
8
+ from sum.utils.output import OutputFormatter
9
+
10
+
11
+ class DependencyManager:
12
+ """Manage dependency installation for CLI projects."""
13
+
14
+ def __init__(self, venv_manager: VenvManager | None = None) -> None:
15
+ self.venv_manager = venv_manager or VenvManager()
16
+
17
+ def install(self, project_path: Path) -> None:
18
+ """Install dependencies from requirements.txt using the venv pip."""
19
+ requirements = project_path / "requirements.txt"
20
+ OutputFormatter.progress(1, 1, f"Installing dependencies for {project_path}")
21
+
22
+ if not requirements.exists():
23
+ OutputFormatter.error(f"requirements.txt not found at {requirements}")
24
+ raise DependencyError(f"requirements.txt not found at {requirements}")
25
+
26
+ if not self.venv_manager.exists(project_path):
27
+ OutputFormatter.error("Virtualenv not found for dependency install")
28
+ raise DependencyError("Virtualenv not found for dependency installation")
29
+
30
+ python = self.venv_manager.get_python_executable(project_path)
31
+ try:
32
+ subprocess.run(
33
+ [str(python), "-m", "pip", "install", "-r", str(requirements)],
34
+ check=True,
35
+ capture_output=True,
36
+ text=True,
37
+ )
38
+ except FileNotFoundError as exc:
39
+ OutputFormatter.error("Virtualenv python not found for pip install")
40
+ raise DependencyError(f"Virtualenv python not found at {python}") from exc
41
+ except subprocess.CalledProcessError as exc:
42
+ OutputFormatter.error("pip install failed")
43
+ details = exc.stderr or exc.stdout or "Unknown error"
44
+ raise DependencyError(f"pip install failed: {details}") from exc
45
+
46
+ OutputFormatter.success("Dependencies installed")
47
+
48
+ def verify(self, project_path: Path) -> bool:
49
+ """Verify key dependencies are installed in the virtualenv."""
50
+ OutputFormatter.progress(1, 1, f"Verifying dependencies for {project_path}")
51
+
52
+ if not self.venv_manager.exists(project_path):
53
+ OutputFormatter.error("Virtualenv not found for dependency verification")
54
+ raise DependencyError("Virtualenv not found for dependency verification")
55
+
56
+ python = self.venv_manager.get_python_executable(project_path)
57
+ required = ["django", "wagtail"]
58
+ missing: list[str] = []
59
+ for package in required:
60
+ result = subprocess.run(
61
+ [str(python), "-c", f"import {package}"],
62
+ capture_output=True,
63
+ text=True,
64
+ )
65
+ if result.returncode != 0:
66
+ missing.append(package)
67
+
68
+ if missing:
69
+ OutputFormatter.error(f"Missing packages: {', '.join(sorted(missing))}")
70
+ return False
71
+
72
+ OutputFormatter.success("Dependencies verified")
73
+ return True