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
@@ -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
+ )