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
sum/commands/run.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Run command for starting SUM projects with virtualenv awareness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from sum.setup.venv import VenvManager
|
|
11
|
+
from sum.utils.environment import (
|
|
12
|
+
ExecutionMode,
|
|
13
|
+
detect_mode,
|
|
14
|
+
find_monorepo_root,
|
|
15
|
+
resolve_project_path,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_available_port(start_port: int = 8000, max_attempts: int = 10) -> int:
|
|
20
|
+
"""Find an available port starting from start_port.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
port = find_available_port(8000, max_attempts=5)
|
|
24
|
+
# Returns the first available port between 8000 and 8004
|
|
25
|
+
"""
|
|
26
|
+
for port in range(start_port, start_port + max_attempts):
|
|
27
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
28
|
+
try:
|
|
29
|
+
sock.bind(("127.0.0.1", port))
|
|
30
|
+
except OSError:
|
|
31
|
+
continue
|
|
32
|
+
return port
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
f"No available ports found in range {start_port}-{start_port + max_attempts - 1}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.command()
|
|
39
|
+
@click.argument("project", required=False)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--port",
|
|
42
|
+
default=8000,
|
|
43
|
+
type=int,
|
|
44
|
+
help="Development server port (default: 8000)",
|
|
45
|
+
)
|
|
46
|
+
def run(project: str | None, port: int) -> None:
|
|
47
|
+
"""Start development server for a project."""
|
|
48
|
+
try:
|
|
49
|
+
project_path = resolve_project_path(project)
|
|
50
|
+
except FileNotFoundError as exc:
|
|
51
|
+
raise click.ClickException(str(exc)) from exc
|
|
52
|
+
project_name = project_path.name
|
|
53
|
+
mode = detect_mode(project_path)
|
|
54
|
+
|
|
55
|
+
venv_manager = VenvManager()
|
|
56
|
+
if not venv_manager.exists(project_path):
|
|
57
|
+
raise click.ClickException(
|
|
58
|
+
f"Virtualenv not found at {project_path / '.venv'}. Run 'sum init --full' or create manually."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
python_exe = venv_manager.get_python_executable(project_path)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
actual_port = find_available_port(port)
|
|
65
|
+
except RuntimeError as exc:
|
|
66
|
+
raise click.ClickException(str(exc)) from exc
|
|
67
|
+
|
|
68
|
+
if actual_port != port:
|
|
69
|
+
click.echo(f"⚠️ Port {port} in use, using {actual_port} instead")
|
|
70
|
+
|
|
71
|
+
click.echo(f"🚀 Starting {project_name}...")
|
|
72
|
+
click.echo()
|
|
73
|
+
click.echo(f"Using virtualenv: {project_path / '.venv'}")
|
|
74
|
+
click.echo(f"Mode: {mode.value}")
|
|
75
|
+
click.echo(f"Python: {python_exe}")
|
|
76
|
+
click.echo()
|
|
77
|
+
|
|
78
|
+
env = os.environ.copy()
|
|
79
|
+
if mode is ExecutionMode.MONOREPO:
|
|
80
|
+
repo_root = find_monorepo_root(project_path)
|
|
81
|
+
if repo_root is not None:
|
|
82
|
+
core_path = repo_root / "core"
|
|
83
|
+
existing = env.get("PYTHONPATH", "")
|
|
84
|
+
if existing:
|
|
85
|
+
env["PYTHONPATH"] = f"{existing}{os.pathsep}{core_path}"
|
|
86
|
+
else:
|
|
87
|
+
env["PYTHONPATH"] = str(core_path)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
subprocess.run(
|
|
91
|
+
[str(python_exe), "manage.py", "runserver", f"127.0.0.1:{actual_port}"],
|
|
92
|
+
cwd=project_path,
|
|
93
|
+
env=env,
|
|
94
|
+
)
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
click.echo("\n👋 Server stopped")
|
sum/commands/themes.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Themes command for listing available themes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from sum.themes_registry import list_themes
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_themes_list() -> int:
|
|
11
|
+
"""
|
|
12
|
+
List all available themes.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
0 on success, 1 on failure
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
themes = list_themes()
|
|
19
|
+
|
|
20
|
+
if not themes:
|
|
21
|
+
print("No themes available.")
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
print("Available themes:")
|
|
25
|
+
print()
|
|
26
|
+
for theme in themes:
|
|
27
|
+
print(f" {theme.slug}")
|
|
28
|
+
print(f" Name: {theme.name}")
|
|
29
|
+
print(f" Description: {theme.description}")
|
|
30
|
+
print(f" Version: {theme.version}")
|
|
31
|
+
print()
|
|
32
|
+
|
|
33
|
+
return 0
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(f"[FAIL] Failed to list themes: {e}", file=sys.stderr)
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Click command wrapper
|
|
40
|
+
_missing_click: bool = False
|
|
41
|
+
try:
|
|
42
|
+
import click
|
|
43
|
+
|
|
44
|
+
@click.command(name="themes")
|
|
45
|
+
def themes() -> None:
|
|
46
|
+
"""List available themes."""
|
|
47
|
+
result = run_themes_list()
|
|
48
|
+
if result != 0:
|
|
49
|
+
raise SystemExit(result)
|
|
50
|
+
|
|
51
|
+
except ImportError:
|
|
52
|
+
_missing_click = True
|
|
53
|
+
|
|
54
|
+
def themes() -> None: # type: ignore[misc]
|
|
55
|
+
"""Fallback when click is not installed."""
|
|
56
|
+
raise RuntimeError("Click is required for CLI commands")
|
sum/commands/update.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# pyright: reportImplicitStringConcatenation=false, reportUnusedCallResult=false
|
|
2
|
+
|
|
3
|
+
"""Update command implementation.
|
|
4
|
+
|
|
5
|
+
Updates an existing site with latest code changes:
|
|
6
|
+
- Git pull
|
|
7
|
+
- Pip install requirements
|
|
8
|
+
- Run migrations
|
|
9
|
+
- Collect static files
|
|
10
|
+
- Restart service
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shlex
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
|
|
20
|
+
from sum.exceptions import SetupError
|
|
21
|
+
from sum.setup.database import DatabaseManager
|
|
22
|
+
from sum.setup.deps import DependencyManager
|
|
23
|
+
from sum.setup.venv import VenvManager
|
|
24
|
+
from sum.system_config import ConfigurationError, SystemConfig, get_system_config
|
|
25
|
+
from sum.utils.django import DjangoCommandExecutor
|
|
26
|
+
from sum.utils.environment import ExecutionMode
|
|
27
|
+
from sum.utils.output import OutputFormatter
|
|
28
|
+
|
|
29
|
+
click_module: ModuleType | None
|
|
30
|
+
try:
|
|
31
|
+
import click as click_module
|
|
32
|
+
except ImportError: # pragma: no cover
|
|
33
|
+
click_module = None
|
|
34
|
+
|
|
35
|
+
click: ModuleType | None = click_module
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_git_pull(app_dir: Path, config: SystemConfig) -> None:
|
|
39
|
+
"""Pull latest changes from git remote.
|
|
40
|
+
|
|
41
|
+
Runs git as the deploy user to handle safe.directory permissions.
|
|
42
|
+
"""
|
|
43
|
+
deploy_user = config.defaults.deploy_user
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["sudo", "-u", deploy_user, "git", "pull", "--ff-only"],
|
|
47
|
+
cwd=app_dir,
|
|
48
|
+
check=True,
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
if result.stdout:
|
|
53
|
+
OutputFormatter.info(result.stdout.strip())
|
|
54
|
+
except subprocess.CalledProcessError as exc:
|
|
55
|
+
raise SetupError(f"Git pull failed: {exc.stderr}") from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _restart_service(site_slug: str, config: SystemConfig) -> None:
|
|
59
|
+
"""Restart the systemd service for the site."""
|
|
60
|
+
service_name = config.get_systemd_service_name(site_slug)
|
|
61
|
+
try:
|
|
62
|
+
subprocess.run(
|
|
63
|
+
["systemctl", "restart", service_name],
|
|
64
|
+
check=True,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
)
|
|
67
|
+
except subprocess.CalledProcessError as exc:
|
|
68
|
+
raise SetupError(f"Failed to restart service: {exc.stderr}") from exc
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _run_remote_update(
|
|
72
|
+
site_slug: str,
|
|
73
|
+
config: SystemConfig,
|
|
74
|
+
skip_migrations: bool = False,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Run update on a remote (production) server via SSH."""
|
|
77
|
+
ssh_host = config.production.ssh_host
|
|
78
|
+
site_dir = config.get_site_dir(site_slug, target="prod")
|
|
79
|
+
app_dir = site_dir / "app"
|
|
80
|
+
venv_python = site_dir / "venv" / "bin" / "python"
|
|
81
|
+
service_name = config.get_systemd_service_name(site_slug)
|
|
82
|
+
|
|
83
|
+
# Quote paths for safe shell interpolation
|
|
84
|
+
q_app_dir = shlex.quote(str(app_dir))
|
|
85
|
+
q_venv_python = shlex.quote(str(venv_python))
|
|
86
|
+
|
|
87
|
+
# Build remote commands
|
|
88
|
+
commands = [
|
|
89
|
+
f"cd {q_app_dir} && git pull --ff-only",
|
|
90
|
+
f"{q_venv_python} -m pip install -r {q_app_dir}/requirements.txt -q",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
if not skip_migrations:
|
|
94
|
+
commands.append(
|
|
95
|
+
f"cd {q_app_dir} && {q_venv_python} manage.py migrate --noinput"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
commands.extend(
|
|
99
|
+
[
|
|
100
|
+
f"cd {q_app_dir} && {q_venv_python} manage.py collectstatic --noinput",
|
|
101
|
+
f"sudo systemctl restart {service_name}",
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Execute each command via SSH with timeout
|
|
106
|
+
for cmd in commands:
|
|
107
|
+
OutputFormatter.info(f"Running: {cmd[:60]}...")
|
|
108
|
+
try:
|
|
109
|
+
subprocess.run(
|
|
110
|
+
["ssh", ssh_host, cmd],
|
|
111
|
+
check=True,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
timeout=300, # 5 minute timeout for long operations
|
|
115
|
+
)
|
|
116
|
+
except subprocess.TimeoutExpired as exc:
|
|
117
|
+
raise SetupError(
|
|
118
|
+
f"Remote command timed out after 5 minutes: {cmd[:60]}..."
|
|
119
|
+
) from exc
|
|
120
|
+
except subprocess.CalledProcessError as exc:
|
|
121
|
+
raise SetupError(f"Remote command failed: {exc.stderr}") from exc
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run_update(
|
|
125
|
+
site_name: str,
|
|
126
|
+
*,
|
|
127
|
+
target: str = "staging",
|
|
128
|
+
skip_migrations: bool = False,
|
|
129
|
+
) -> int:
|
|
130
|
+
"""Update an existing site with latest code.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
site_name: Name/slug of the site to update.
|
|
134
|
+
target: 'staging' or 'prod'.
|
|
135
|
+
skip_migrations: Skip running database migrations.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Exit code (0 for success, non-zero for failure).
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
config = get_system_config()
|
|
142
|
+
except ConfigurationError as exc:
|
|
143
|
+
OutputFormatter.error(str(exc))
|
|
144
|
+
return 1
|
|
145
|
+
|
|
146
|
+
site_dir = config.get_site_dir(site_name, target=target)
|
|
147
|
+
|
|
148
|
+
# For production, delegate to remote execution
|
|
149
|
+
if target == "prod":
|
|
150
|
+
OutputFormatter.header(f"Updating {site_name} on production")
|
|
151
|
+
print()
|
|
152
|
+
try:
|
|
153
|
+
_run_remote_update(site_name, config, skip_migrations)
|
|
154
|
+
except SetupError as exc:
|
|
155
|
+
OutputFormatter.error(str(exc))
|
|
156
|
+
return 1
|
|
157
|
+
OutputFormatter.success(f"Site {site_name} updated on production")
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
# For staging, run locally
|
|
161
|
+
app_dir = site_dir / "app"
|
|
162
|
+
venv_path = site_dir / "venv"
|
|
163
|
+
|
|
164
|
+
# Validate site exists
|
|
165
|
+
if not site_dir.exists():
|
|
166
|
+
OutputFormatter.error(f"Site not found: {site_dir}")
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
if not app_dir.exists():
|
|
170
|
+
OutputFormatter.error(f"App directory not found: {app_dir}")
|
|
171
|
+
return 1
|
|
172
|
+
|
|
173
|
+
OutputFormatter.header(f"Updating {site_name} on staging")
|
|
174
|
+
print()
|
|
175
|
+
|
|
176
|
+
total_steps = 5 if not skip_migrations else 4
|
|
177
|
+
current_step = 0
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Step 1: Git pull
|
|
181
|
+
current_step += 1
|
|
182
|
+
OutputFormatter.progress(current_step, total_steps, "Pulling latest code", "⏳")
|
|
183
|
+
_run_git_pull(app_dir, config)
|
|
184
|
+
OutputFormatter.progress(current_step, total_steps, "Code updated", "✅")
|
|
185
|
+
|
|
186
|
+
# Step 2: Install dependencies
|
|
187
|
+
current_step += 1
|
|
188
|
+
OutputFormatter.progress(
|
|
189
|
+
current_step, total_steps, "Installing dependencies", "⏳"
|
|
190
|
+
)
|
|
191
|
+
venv_manager = VenvManager(venv_path=venv_path)
|
|
192
|
+
deps_manager = DependencyManager(venv_manager=venv_manager)
|
|
193
|
+
deps_manager.install(app_dir)
|
|
194
|
+
OutputFormatter.progress(
|
|
195
|
+
current_step, total_steps, "Dependencies installed", "✅"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Create Django executor
|
|
199
|
+
django_executor = DjangoCommandExecutor(
|
|
200
|
+
app_dir,
|
|
201
|
+
ExecutionMode.STANDALONE,
|
|
202
|
+
python_path=venv_manager.get_python_executable(app_dir),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Step 3: Run migrations (optional)
|
|
206
|
+
if not skip_migrations:
|
|
207
|
+
current_step += 1
|
|
208
|
+
OutputFormatter.progress(
|
|
209
|
+
current_step, total_steps, "Running migrations", "⏳"
|
|
210
|
+
)
|
|
211
|
+
db_manager = DatabaseManager(django_executor)
|
|
212
|
+
db_manager.migrate()
|
|
213
|
+
OutputFormatter.progress(
|
|
214
|
+
current_step, total_steps, "Migrations complete", "✅"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Step 4: Collect static
|
|
218
|
+
current_step += 1
|
|
219
|
+
OutputFormatter.progress(
|
|
220
|
+
current_step, total_steps, "Collecting static files", "⏳"
|
|
221
|
+
)
|
|
222
|
+
django_executor.run_command(["collectstatic", "--noinput"])
|
|
223
|
+
OutputFormatter.progress(
|
|
224
|
+
current_step, total_steps, "Static files collected", "✅"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Step 5: Restart service
|
|
228
|
+
current_step += 1
|
|
229
|
+
OutputFormatter.progress(current_step, total_steps, "Restarting service", "⏳")
|
|
230
|
+
_restart_service(site_name, config)
|
|
231
|
+
OutputFormatter.progress(current_step, total_steps, "Service restarted", "✅")
|
|
232
|
+
|
|
233
|
+
except SetupError as exc:
|
|
234
|
+
OutputFormatter.error(str(exc))
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
print()
|
|
238
|
+
OutputFormatter.success(f"Site {site_name} updated successfully")
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _update_command(
|
|
243
|
+
site_name: str,
|
|
244
|
+
target: str,
|
|
245
|
+
skip_migrations: bool,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Update an existing site."""
|
|
248
|
+
result = run_update(
|
|
249
|
+
site_name,
|
|
250
|
+
target=target,
|
|
251
|
+
skip_migrations=skip_migrations,
|
|
252
|
+
)
|
|
253
|
+
if result != 0:
|
|
254
|
+
raise SystemExit(result)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _missing_click(*_args: object, **_kwargs: object) -> None:
|
|
258
|
+
raise RuntimeError("click is required to use the update command")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if click is None:
|
|
262
|
+
update = _missing_click
|
|
263
|
+
else:
|
|
264
|
+
|
|
265
|
+
@click.command(name="update")
|
|
266
|
+
@click.argument("site_name")
|
|
267
|
+
@click.option(
|
|
268
|
+
"--target",
|
|
269
|
+
type=click.Choice(["staging", "prod"], case_sensitive=False),
|
|
270
|
+
default="staging",
|
|
271
|
+
show_default=True,
|
|
272
|
+
help="Target environment to update.",
|
|
273
|
+
)
|
|
274
|
+
@click.option(
|
|
275
|
+
"--skip-migrations",
|
|
276
|
+
is_flag=True,
|
|
277
|
+
help="Skip running database migrations.",
|
|
278
|
+
)
|
|
279
|
+
def _click_update(
|
|
280
|
+
site_name: str,
|
|
281
|
+
target: str,
|
|
282
|
+
skip_migrations: bool,
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Update an existing site with latest code.
|
|
285
|
+
|
|
286
|
+
Pulls latest code from git, installs dependencies, runs migrations,
|
|
287
|
+
collects static files, and restarts the service.
|
|
288
|
+
|
|
289
|
+
\b
|
|
290
|
+
Examples:
|
|
291
|
+
sum-platform update acme
|
|
292
|
+
sum-platform update acme --target prod
|
|
293
|
+
sum-platform update acme --skip-migrations
|
|
294
|
+
"""
|
|
295
|
+
_update_command(
|
|
296
|
+
site_name,
|
|
297
|
+
target=target,
|
|
298
|
+
skip_migrations=skip_migrations,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
update = _click_update
|
sum/config.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TypedDict, Unpack
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SetupConfigArgs(TypedDict, total=False):
|
|
8
|
+
full: bool
|
|
9
|
+
quick: bool
|
|
10
|
+
ci: bool
|
|
11
|
+
no_prompt: bool
|
|
12
|
+
skip_venv: bool
|
|
13
|
+
skip_migrations: bool
|
|
14
|
+
skip_seed: bool
|
|
15
|
+
skip_superuser: bool
|
|
16
|
+
run_server: bool
|
|
17
|
+
port: int
|
|
18
|
+
superuser_username: str
|
|
19
|
+
superuser_email: str
|
|
20
|
+
superuser_password: str
|
|
21
|
+
seed_preset: str | None
|
|
22
|
+
seed_site: str | None
|
|
23
|
+
theme_slug: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SetupConfig:
|
|
28
|
+
"""Configuration for setup orchestration."""
|
|
29
|
+
|
|
30
|
+
full: bool = False
|
|
31
|
+
quick: bool = False
|
|
32
|
+
ci: bool = False
|
|
33
|
+
no_prompt: bool = False
|
|
34
|
+
|
|
35
|
+
skip_venv: bool = False
|
|
36
|
+
skip_migrations: bool = False
|
|
37
|
+
skip_seed: bool = False
|
|
38
|
+
skip_superuser: bool = False
|
|
39
|
+
|
|
40
|
+
run_server: bool = False
|
|
41
|
+
port: int = 8000
|
|
42
|
+
|
|
43
|
+
superuser_username: str = "admin"
|
|
44
|
+
superuser_email: str = "admin@example.com"
|
|
45
|
+
superuser_password: str = "admin"
|
|
46
|
+
|
|
47
|
+
seed_preset: str | None = None
|
|
48
|
+
seed_site: str | None = None
|
|
49
|
+
theme_slug: str = "theme_a"
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
"""Validate configuration."""
|
|
53
|
+
if self.full and self.quick:
|
|
54
|
+
raise ValueError("--full and --quick are mutually exclusive")
|
|
55
|
+
if self.ci:
|
|
56
|
+
self.no_prompt = True
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_cli_args(cls, **kwargs: Unpack[SetupConfigArgs]) -> SetupConfig:
|
|
60
|
+
"""Create a SetupConfig from CLI arguments."""
|
|
61
|
+
return cls(**kwargs)
|