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.
- sum/__init__.py +1 -0
- sum/boilerplate/.env.example +124 -0
- sum/boilerplate/.gitea/workflows/ci.yml +33 -0
- sum/boilerplate/.gitea/workflows/deploy-production.yml +98 -0
- sum/boilerplate/.gitea/workflows/deploy-staging.yml +113 -0
- sum/boilerplate/.github/workflows/ci.yml +36 -0
- sum/boilerplate/.github/workflows/deploy-production.yml +102 -0
- sum/boilerplate/.github/workflows/deploy-staging.yml +115 -0
- sum/boilerplate/.gitignore +45 -0
- sum/boilerplate/README.md +259 -0
- sum/boilerplate/manage.py +34 -0
- sum/boilerplate/project_name/__init__.py +5 -0
- sum/boilerplate/project_name/home/__init__.py +5 -0
- sum/boilerplate/project_name/home/apps.py +20 -0
- sum/boilerplate/project_name/home/management/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/populate_demo_content.py +644 -0
- sum/boilerplate/project_name/home/management/commands/seed.py +129 -0
- sum/boilerplate/project_name/home/management/commands/seed_showroom.py +1661 -0
- sum/boilerplate/project_name/home/migrations/__init__.py +3 -0
- sum/boilerplate/project_name/home/models.py +13 -0
- sum/boilerplate/project_name/settings/__init__.py +5 -0
- sum/boilerplate/project_name/settings/base.py +348 -0
- sum/boilerplate/project_name/settings/local.py +78 -0
- sum/boilerplate/project_name/settings/production.py +106 -0
- sum/boilerplate/project_name/urls.py +33 -0
- sum/boilerplate/project_name/wsgi.py +16 -0
- sum/boilerplate/pytest.ini +5 -0
- sum/boilerplate/requirements.txt +25 -0
- sum/boilerplate/static/client/.gitkeep +3 -0
- sum/boilerplate/templates/overrides/.gitkeep +3 -0
- sum/boilerplate/tests/__init__.py +3 -0
- sum/boilerplate/tests/test_health.py +51 -0
- sum/cli.py +42 -0
- sum/commands/__init__.py +10 -0
- sum/commands/backup.py +308 -0
- sum/commands/check.py +128 -0
- sum/commands/init.py +265 -0
- sum/commands/promote.py +758 -0
- sum/commands/run.py +96 -0
- sum/commands/themes.py +56 -0
- sum/commands/update.py +301 -0
- sum/config.py +61 -0
- sum/docs/USER_GUIDE.md +663 -0
- sum/exceptions.py +45 -0
- sum/setup/__init__.py +17 -0
- sum/setup/auth.py +184 -0
- sum/setup/database.py +58 -0
- sum/setup/deps.py +73 -0
- sum/setup/git_ops.py +463 -0
- sum/setup/infrastructure.py +576 -0
- sum/setup/orchestrator.py +354 -0
- sum/setup/remote_themes.py +371 -0
- sum/setup/scaffold.py +500 -0
- sum/setup/seed.py +110 -0
- sum/setup/site_orchestrator.py +441 -0
- sum/setup/venv.py +89 -0
- sum/system_config.py +330 -0
- sum/themes_registry.py +180 -0
- sum/utils/__init__.py +25 -0
- sum/utils/django.py +97 -0
- sum/utils/environment.py +76 -0
- sum/utils/output.py +78 -0
- sum/utils/project.py +110 -0
- sum/utils/prompts.py +36 -0
- sum/utils/validation.py +313 -0
- sum_cli-3.0.0.dist-info/METADATA +127 -0
- sum_cli-3.0.0.dist-info/RECORD +72 -0
- sum_cli-3.0.0.dist-info/WHEEL +5 -0
- sum_cli-3.0.0.dist-info/entry_points.txt +2 -0
- sum_cli-3.0.0.dist-info/licenses/LICENSE +29 -0
- sum_cli-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Setup orchestration for CLI project initialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from sum.config import SetupConfig
|
|
13
|
+
from sum.exceptions import SetupError
|
|
14
|
+
from sum.setup.auth import SuperuserManager
|
|
15
|
+
from sum.setup.database import DatabaseManager
|
|
16
|
+
from sum.setup.deps import DependencyManager
|
|
17
|
+
from sum.setup.scaffold import scaffold_project, validate_project_structure
|
|
18
|
+
from sum.setup.seed import ContentSeeder
|
|
19
|
+
from sum.setup.venv import VenvManager
|
|
20
|
+
from sum.utils.django import DjangoCommandExecutor
|
|
21
|
+
from sum.utils.environment import ExecutionMode, find_monorepo_root
|
|
22
|
+
from sum.utils.output import OutputFormatter
|
|
23
|
+
from sum.utils.project import safe_rmtree
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SetupResult:
|
|
28
|
+
"""Result of a setup operation."""
|
|
29
|
+
|
|
30
|
+
success: bool
|
|
31
|
+
project_path: Path
|
|
32
|
+
credentials_path: Path | None = None
|
|
33
|
+
url: str = "http://127.0.0.1:8000/"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SetupStep(str, Enum):
|
|
37
|
+
"""Ordered, typed identifiers for setup steps."""
|
|
38
|
+
|
|
39
|
+
SCAFFOLD = "Scaffolding structure"
|
|
40
|
+
VALIDATE = "Validating structure"
|
|
41
|
+
CREATE_VENV = "Creating virtualenv"
|
|
42
|
+
INSTALL_DEPS = "Installing dependencies"
|
|
43
|
+
MIGRATE = "Running migrations"
|
|
44
|
+
SEED = "Seeding homepage"
|
|
45
|
+
CREATE_SUPERUSER = "Creating superuser"
|
|
46
|
+
START_SERVER = "Starting server"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
StepFunction = Callable[[SetupConfig], Path | None]
|
|
50
|
+
StepDefinition = tuple[SetupStep, StepFunction]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SetupOrchestrator:
|
|
54
|
+
"""Orchestrates the full project setup flow.
|
|
55
|
+
|
|
56
|
+
Owns ALL 8 steps — init command is a thin wrapper around this.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, project_path: Path, mode: ExecutionMode) -> None:
|
|
60
|
+
self.project_path = project_path
|
|
61
|
+
self.mode = mode
|
|
62
|
+
|
|
63
|
+
# Initialize components (reused across setup steps)
|
|
64
|
+
self.venv_manager = VenvManager()
|
|
65
|
+
self.deps_manager = DependencyManager(venv_manager=self.venv_manager)
|
|
66
|
+
self.django_executor: DjangoCommandExecutor | None = None
|
|
67
|
+
|
|
68
|
+
def run_full_setup(self, config: SetupConfig) -> SetupResult:
|
|
69
|
+
"""Run complete setup based on configuration.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
config: The setup configuration with flags and options.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
SetupError: If any setup step fails.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
SetupResult with success=True, project path, and optional credentials path.
|
|
79
|
+
"""
|
|
80
|
+
# Build step list dynamically based on config
|
|
81
|
+
steps = self._build_step_list(config)
|
|
82
|
+
total_steps = len(steps)
|
|
83
|
+
|
|
84
|
+
credentials_path: Path | None = None
|
|
85
|
+
|
|
86
|
+
for step_num, (step_name, step_func) in enumerate(steps, 1):
|
|
87
|
+
self._show_progress(step_num, total_steps, step_name.value, "⏳")
|
|
88
|
+
try:
|
|
89
|
+
result = step_func(config)
|
|
90
|
+
if step_name is SetupStep.CREATE_SUPERUSER and result:
|
|
91
|
+
credentials_path = result
|
|
92
|
+
self._show_progress(step_num, total_steps, step_name.value, "✅")
|
|
93
|
+
except SetupError:
|
|
94
|
+
self._show_progress(step_num, total_steps, step_name.value, "❌")
|
|
95
|
+
raise
|
|
96
|
+
except Exception as e:
|
|
97
|
+
# Wrap unexpected exceptions
|
|
98
|
+
self._show_progress(step_num, total_steps, step_name.value, "❌")
|
|
99
|
+
raise SetupError(f"Unexpected error in '{step_name.value}': {e}") from e
|
|
100
|
+
|
|
101
|
+
return SetupResult(
|
|
102
|
+
success=True,
|
|
103
|
+
project_path=self.project_path,
|
|
104
|
+
credentials_path=credentials_path,
|
|
105
|
+
url=f"http://127.0.0.1:{config.port}/",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _build_step_list(self, config: SetupConfig) -> list[StepDefinition]:
|
|
109
|
+
"""Build list of steps to execute based on config.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
config: The setup configuration with flags and options.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of (step_name, step_function) tuples to execute.
|
|
116
|
+
"""
|
|
117
|
+
# Always include scaffold and validate
|
|
118
|
+
steps: list[StepDefinition] = [
|
|
119
|
+
(SetupStep.SCAFFOLD, self._scaffold),
|
|
120
|
+
(SetupStep.VALIDATE, self._validate),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Venv and deps (unless skipped)
|
|
124
|
+
if not config.skip_venv:
|
|
125
|
+
steps.append((SetupStep.CREATE_VENV, self._setup_venv))
|
|
126
|
+
steps.append((SetupStep.INSTALL_DEPS, self._install_deps))
|
|
127
|
+
|
|
128
|
+
# Quick mode stops here
|
|
129
|
+
if config.quick:
|
|
130
|
+
return steps
|
|
131
|
+
|
|
132
|
+
# DB operations
|
|
133
|
+
if not config.skip_migrations:
|
|
134
|
+
steps.append((SetupStep.MIGRATE, self._migrate))
|
|
135
|
+
|
|
136
|
+
if not config.skip_seed:
|
|
137
|
+
steps.append((SetupStep.SEED, self._seed_content))
|
|
138
|
+
|
|
139
|
+
if not config.skip_superuser:
|
|
140
|
+
steps.append((SetupStep.CREATE_SUPERUSER, self._create_superuser))
|
|
141
|
+
|
|
142
|
+
# Server (only if requested)
|
|
143
|
+
if config.run_server:
|
|
144
|
+
steps.append((SetupStep.START_SERVER, self._start_server))
|
|
145
|
+
|
|
146
|
+
return steps
|
|
147
|
+
|
|
148
|
+
def _show_progress(self, step: int, total: int, message: str, status: str) -> None:
|
|
149
|
+
"""Display progress indicator.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
step: Current step number (1-indexed).
|
|
153
|
+
total: Total number of steps.
|
|
154
|
+
message: The step description.
|
|
155
|
+
status: Status emoji (⏳, ✅, ❌).
|
|
156
|
+
"""
|
|
157
|
+
OutputFormatter.progress(step, total, message, status)
|
|
158
|
+
|
|
159
|
+
def _get_django_executor(self) -> DjangoCommandExecutor:
|
|
160
|
+
"""Get or create the Django command executor (lazy initialization).
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The DjangoCommandExecutor instance.
|
|
164
|
+
"""
|
|
165
|
+
if self.django_executor is None:
|
|
166
|
+
self.django_executor = DjangoCommandExecutor(self.project_path, self.mode)
|
|
167
|
+
return self.django_executor
|
|
168
|
+
|
|
169
|
+
def _setup_venv(self, config: SetupConfig) -> None:
|
|
170
|
+
"""Create virtualenv.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
config: The setup configuration.
|
|
174
|
+
"""
|
|
175
|
+
self.venv_manager.create(self.project_path)
|
|
176
|
+
|
|
177
|
+
def _install_deps(self, config: SetupConfig) -> None:
|
|
178
|
+
"""Install dependencies.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
config: The setup configuration.
|
|
182
|
+
"""
|
|
183
|
+
self.deps_manager.install(self.project_path)
|
|
184
|
+
|
|
185
|
+
def _migrate(self, config: SetupConfig) -> None:
|
|
186
|
+
"""Run database migrations.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
config: The setup configuration.
|
|
190
|
+
"""
|
|
191
|
+
db_manager = DatabaseManager(self._get_django_executor())
|
|
192
|
+
db_manager.migrate()
|
|
193
|
+
|
|
194
|
+
def _seed_content(self, config: SetupConfig) -> None:
|
|
195
|
+
"""Seed site content.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
config: The setup configuration.
|
|
199
|
+
"""
|
|
200
|
+
seeder = ContentSeeder(self._get_django_executor())
|
|
201
|
+
if config.seed_site:
|
|
202
|
+
seed_site = config.seed_site.lower()
|
|
203
|
+
if seed_site == "sage-and-stone":
|
|
204
|
+
self._copy_seed_assets(profile="sage-stone")
|
|
205
|
+
seeder.seed_profile("sage-stone")
|
|
206
|
+
return
|
|
207
|
+
raise SetupError(f"Unknown seed site: {config.seed_site}")
|
|
208
|
+
seeder.seed_homepage(preset=config.seed_preset)
|
|
209
|
+
|
|
210
|
+
def _copy_seed_assets(self, *, profile: str) -> None:
|
|
211
|
+
repo_root = find_monorepo_root(self.project_path)
|
|
212
|
+
if repo_root is None:
|
|
213
|
+
raise SetupError(
|
|
214
|
+
"Cannot seed a demo site outside the monorepo: monorepo root not found."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
source_seeders_dir = repo_root / "seeders"
|
|
218
|
+
source_content_dir = repo_root / "content" / profile
|
|
219
|
+
source_seed_command = (
|
|
220
|
+
repo_root
|
|
221
|
+
/ "core"
|
|
222
|
+
/ "sum_core"
|
|
223
|
+
/ "test_project"
|
|
224
|
+
/ "home"
|
|
225
|
+
/ "management"
|
|
226
|
+
/ "commands"
|
|
227
|
+
/ "seed.py"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if not source_seeders_dir.is_dir():
|
|
231
|
+
raise SetupError(f"Missing seeder source directory: {source_seeders_dir}")
|
|
232
|
+
if not source_content_dir.is_dir():
|
|
233
|
+
raise SetupError(f"Missing content profile directory: {source_content_dir}")
|
|
234
|
+
if not source_seed_command.is_file():
|
|
235
|
+
raise SetupError(f"Missing seed management command: {source_seed_command}")
|
|
236
|
+
|
|
237
|
+
self._replace_tree(source_seeders_dir, self.project_path / "seeders")
|
|
238
|
+
self._replace_tree(source_content_dir, self.project_path / "content" / profile)
|
|
239
|
+
|
|
240
|
+
package_dir = self._find_project_package_dir(self.project_path)
|
|
241
|
+
command_dir = package_dir / "home" / "management" / "commands"
|
|
242
|
+
if not command_dir.is_dir():
|
|
243
|
+
raise SetupError(
|
|
244
|
+
"Cannot locate target Django command directory under project package: "
|
|
245
|
+
f"{command_dir}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
target_seed_command = command_dir / "seed.py"
|
|
249
|
+
if target_seed_command.exists():
|
|
250
|
+
if target_seed_command.is_dir():
|
|
251
|
+
raise SetupError(
|
|
252
|
+
f"Refusing to overwrite directory with seed.py file: {target_seed_command}"
|
|
253
|
+
)
|
|
254
|
+
target_seed_command.unlink()
|
|
255
|
+
|
|
256
|
+
shutil.copy2(source_seed_command, target_seed_command)
|
|
257
|
+
|
|
258
|
+
def _replace_tree(self, source: Path, target: Path) -> None:
|
|
259
|
+
if target.exists():
|
|
260
|
+
if target.is_dir():
|
|
261
|
+
safe_rmtree(
|
|
262
|
+
target, tmp_root=self.project_path, repo_root=self.project_path
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
raise SetupError(f"Refusing to overwrite non-directory path: {target}")
|
|
266
|
+
|
|
267
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
shutil.copytree(source, target)
|
|
269
|
+
|
|
270
|
+
def _find_project_package_dir(self, project_root: Path) -> Path:
|
|
271
|
+
candidates: list[Path] = []
|
|
272
|
+
for child in project_root.iterdir():
|
|
273
|
+
if not child.is_dir():
|
|
274
|
+
continue
|
|
275
|
+
if (child / "home" / "management" / "commands").is_dir():
|
|
276
|
+
candidates.append(child)
|
|
277
|
+
|
|
278
|
+
if not candidates:
|
|
279
|
+
raise SetupError(
|
|
280
|
+
"Cannot determine project package directory: expected a top-level "
|
|
281
|
+
"package containing home/management/commands/."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if len(candidates) > 1:
|
|
285
|
+
raise SetupError(
|
|
286
|
+
"Multiple package directories contain home/management/commands/: "
|
|
287
|
+
+ ", ".join(sorted(str(path) for path in candidates))
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return candidates[0]
|
|
291
|
+
|
|
292
|
+
def _create_superuser(self, config: SetupConfig) -> Path:
|
|
293
|
+
"""Create superuser and return credentials path.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
config: The setup configuration.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Path to the credentials file (.env.local).
|
|
300
|
+
"""
|
|
301
|
+
auth_manager = SuperuserManager(self._get_django_executor(), self.project_path)
|
|
302
|
+
result = auth_manager.create(
|
|
303
|
+
username=config.superuser_username,
|
|
304
|
+
email=config.superuser_email,
|
|
305
|
+
password=config.superuser_password,
|
|
306
|
+
)
|
|
307
|
+
return result.credentials_path
|
|
308
|
+
|
|
309
|
+
def _scaffold(self, config: SetupConfig) -> None:
|
|
310
|
+
"""Scaffold project structure.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
config: The setup configuration.
|
|
314
|
+
"""
|
|
315
|
+
scaffold_project(
|
|
316
|
+
# project_path name should be the validated project slug from init.
|
|
317
|
+
project_name=self.project_path.name,
|
|
318
|
+
clients_dir=self.project_path.parent,
|
|
319
|
+
theme_slug=config.theme_slug,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def _validate(self, config: SetupConfig) -> None:
|
|
323
|
+
"""Validate project structure.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
config: The setup configuration.
|
|
327
|
+
"""
|
|
328
|
+
validate_project_structure(self.project_path)
|
|
329
|
+
|
|
330
|
+
def _start_server(self, config: SetupConfig) -> None:
|
|
331
|
+
"""Start development server in background.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
config: The setup configuration.
|
|
335
|
+
|
|
336
|
+
Note:
|
|
337
|
+
If venv was skipped (skip_venv=True), falls back to sys.executable.
|
|
338
|
+
This allows server start to work even without a virtualenv.
|
|
339
|
+
"""
|
|
340
|
+
import sys
|
|
341
|
+
|
|
342
|
+
# Use venv python if available, otherwise fall back to system python
|
|
343
|
+
if self.venv_manager.exists(self.project_path):
|
|
344
|
+
python = self.venv_manager.get_python_executable(self.project_path)
|
|
345
|
+
else:
|
|
346
|
+
python = Path(sys.executable)
|
|
347
|
+
|
|
348
|
+
# Start server as background process
|
|
349
|
+
subprocess.Popen(
|
|
350
|
+
[str(python), "manage.py", "runserver", f"127.0.0.1:{config.port}"],
|
|
351
|
+
cwd=self.project_path,
|
|
352
|
+
stdout=subprocess.DEVNULL,
|
|
353
|
+
stderr=subprocess.DEVNULL,
|
|
354
|
+
)
|