quickscale-cli 0.83.0__tar.gz → 0.85.0__tar.gz
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.
- quickscale_cli-0.85.0/PKG-INFO +43 -0
- quickscale_cli-0.85.0/README.md +19 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/pyproject.toml +5 -4
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/backups_contract.py +3 -0
- quickscale_cli-0.85.0/src/quickscale_cli/billing_contract.py +183 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/apply_command.py +42 -2
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/development_commands.py +33 -8
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_commands.py +10 -3
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_config.py +116 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/plan_command.py +8 -3
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/status_command.py +12 -6
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/module_catalog.py +9 -7
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/config_schema.py +81 -12
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/dependency_utils.py +8 -25
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/docker_utils.py +12 -10
- quickscale_cli-0.83.0/PKG-INFO +0 -61
- quickscale_cli-0.83.0/README.md +0 -38
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/analytics_contract.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/auth_contract.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/beta_migration.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/deployment_commands.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/dr_commands.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_wiring_specs.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/remove_command.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/crm_contract.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/main.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/notifications_contract.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/__init__.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/delta.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/state_schema.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/social_contract.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/__init__.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/module_dependency_sync.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/module_wiring_manager.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/project_identity.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/project_manager.py +0 -0
- {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/railway_utils.py +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quickscale-cli
|
|
3
|
+
Version: 0.85.0
|
|
4
|
+
Summary: Command-line interface for QuickScale Django project generator
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Keywords: django,cli,project-generator,saas
|
|
7
|
+
Author: Experto AI
|
|
8
|
+
Author-email: victor@experto.ai
|
|
9
|
+
Requires-Python: >=3.13,<3.15
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Django :: 6.0
|
|
17
|
+
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
18
|
+
Requires-Dist: quickscale-core (>=0.85.0,<0.86.0)
|
|
19
|
+
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
20
|
+
Project-URL: Homepage, https://github.com/Experto-AI/quickscale
|
|
21
|
+
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# quickscale-cli
|
|
25
|
+
|
|
26
|
+
Command-line interface for the [QuickScale](https://github.com/Experto-AI/quickscale) Django project generator.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install quickscale-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or install the full bundle:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install quickscale
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
See the [main repository README](../README.md) and [docs/](../docs/) for full documentation.
|
|
43
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# quickscale-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for the [QuickScale](https://github.com/Experto-AI/quickscale) Django project generator.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install quickscale-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install the full bundle:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install quickscale
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Documentation
|
|
18
|
+
|
|
19
|
+
See the [main repository README](../README.md) and [docs/](../docs/) for full documentation.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.85.0"
|
|
4
4
|
description = "Command-line interface for QuickScale Django project generator"
|
|
5
|
-
requires-python = ">=3.
|
|
5
|
+
requires-python = ">=3.13,<3.15"
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
authors = [{name = "Experto AI", email = "victor@experto.ai"}]
|
|
@@ -11,6 +11,7 @@ classifiers = [
|
|
|
11
11
|
"Development Status :: 3 - Alpha",
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
14
15
|
"Programming Language :: Python :: 3.14",
|
|
15
16
|
"Framework :: Django",
|
|
16
17
|
"Framework :: Django :: 6.0",
|
|
@@ -29,9 +30,9 @@ quickscale = "quickscale_cli.main:cli"
|
|
|
29
30
|
packages = [{include = "quickscale_cli", from = "src"}]
|
|
30
31
|
|
|
31
32
|
[tool.poetry.dependencies]
|
|
32
|
-
python = ">=3.
|
|
33
|
+
python = ">=3.13,<3.15"
|
|
33
34
|
click = "^8.3.1"
|
|
34
|
-
quickscale-core = "^0.
|
|
35
|
+
quickscale-core = "^0.85.0"
|
|
35
36
|
|
|
36
37
|
[tool.poetry.group.dev.dependencies]
|
|
37
38
|
# Dev dependencies centralized in root pyproject.toml
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by scripts/version_tool.sh
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.85.0"
|
|
@@ -10,6 +10,7 @@ from collections.abc import Mapping
|
|
|
10
10
|
import re
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
+
from quickscale_cli.billing_contract import normalize_billing_module_options
|
|
13
14
|
from quickscale_cli.analytics_contract import normalize_analytics_module_options
|
|
14
15
|
from quickscale_cli.auth_contract import normalize_auth_module_options
|
|
15
16
|
from quickscale_cli.crm_contract import normalize_crm_module_options
|
|
@@ -116,6 +117,8 @@ def sanitize_module_options(
|
|
|
116
117
|
return normalize_auth_module_options(options)
|
|
117
118
|
if module_name == "backups":
|
|
118
119
|
return normalize_backups_module_options(options)
|
|
120
|
+
if module_name == "billing":
|
|
121
|
+
return normalize_billing_module_options(options)
|
|
119
122
|
if module_name == "crm":
|
|
120
123
|
return normalize_crm_module_options(options)
|
|
121
124
|
if module_name == "notifications":
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Shared billing-module configuration helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR = "STRIPE_PUBLISHABLE_KEY"
|
|
10
|
+
DEFAULT_BILLING_SECRET_KEY_ENV_VAR = "STRIPE_SECRET_KEY"
|
|
11
|
+
DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR = "QUICKSCALE_BILLING_WEBHOOK_SECRET"
|
|
12
|
+
DEFAULT_BILLING_CURRENCY = "usd"
|
|
13
|
+
|
|
14
|
+
BILLING_ENV_VAR_OPTION_NAMES = (
|
|
15
|
+
"publishable_key_env_var",
|
|
16
|
+
"secret_key_env_var",
|
|
17
|
+
"webhook_secret_env_var",
|
|
18
|
+
)
|
|
19
|
+
BILLING_MODULE_OPTION_KEYS = frozenset(
|
|
20
|
+
{
|
|
21
|
+
"enabled",
|
|
22
|
+
*BILLING_ENV_VAR_OPTION_NAMES,
|
|
23
|
+
"billing_currency",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
BILLING_SUPPORTED_CURRENCIES = (
|
|
27
|
+
"aud",
|
|
28
|
+
"brl",
|
|
29
|
+
"cad",
|
|
30
|
+
"chf",
|
|
31
|
+
"czk",
|
|
32
|
+
"dkk",
|
|
33
|
+
"eur",
|
|
34
|
+
"gbp",
|
|
35
|
+
"hkd",
|
|
36
|
+
"huf",
|
|
37
|
+
"inr",
|
|
38
|
+
"jpy",
|
|
39
|
+
"mxn",
|
|
40
|
+
"myr",
|
|
41
|
+
"nok",
|
|
42
|
+
"nzd",
|
|
43
|
+
"php",
|
|
44
|
+
"pln",
|
|
45
|
+
"ron",
|
|
46
|
+
"sek",
|
|
47
|
+
"sgd",
|
|
48
|
+
"thb",
|
|
49
|
+
"try",
|
|
50
|
+
"usd",
|
|
51
|
+
"zar",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_BILLING_ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def default_billing_module_options() -> dict[str, Any]:
|
|
58
|
+
"""Return the default planner/apply contract for billing."""
|
|
59
|
+
return {
|
|
60
|
+
"enabled": True,
|
|
61
|
+
"publishable_key_env_var": DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR,
|
|
62
|
+
"secret_key_env_var": DEFAULT_BILLING_SECRET_KEY_ENV_VAR,
|
|
63
|
+
"webhook_secret_env_var": DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR,
|
|
64
|
+
"billing_currency": DEFAULT_BILLING_CURRENCY,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalize_billing_module_options(
|
|
69
|
+
options: Mapping[str, Any] | None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""Return billing options with normalized env-var and currency values."""
|
|
72
|
+
normalized = dict(options or {})
|
|
73
|
+
|
|
74
|
+
for option_name in BILLING_ENV_VAR_OPTION_NAMES:
|
|
75
|
+
if option_name in normalized:
|
|
76
|
+
normalized[option_name] = str(normalized[option_name]).strip()
|
|
77
|
+
|
|
78
|
+
if "billing_currency" in normalized:
|
|
79
|
+
normalized["billing_currency"] = (
|
|
80
|
+
str(normalized["billing_currency"]).strip().lower()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return normalized
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolve_billing_module_options(
|
|
87
|
+
options: Mapping[str, Any] | None,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
"""Merge billing options with defaults and normalized overrides."""
|
|
90
|
+
resolved = default_billing_module_options()
|
|
91
|
+
resolved.update(normalize_billing_module_options(options))
|
|
92
|
+
for option_name in BILLING_ENV_VAR_OPTION_NAMES:
|
|
93
|
+
resolved[option_name] = str(resolved[option_name]).strip()
|
|
94
|
+
resolved["billing_currency"] = str(resolved["billing_currency"]).strip().lower()
|
|
95
|
+
return resolved
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate_billing_env_var_reference(option_name: str, value: Any) -> str | None:
|
|
99
|
+
"""Validate a billing env-var reference field."""
|
|
100
|
+
candidate = str(value).strip()
|
|
101
|
+
if not candidate:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
qualified_option = f"modules.billing.{option_name}"
|
|
105
|
+
if not _BILLING_ENV_VAR_NAME_PATTERN.fullmatch(candidate):
|
|
106
|
+
return (
|
|
107
|
+
f"{qualified_option} must be an environment variable name matching "
|
|
108
|
+
"^[A-Z][A-Z0-9_]*$"
|
|
109
|
+
)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_billing_currency(value: Any) -> str | None:
|
|
114
|
+
"""Validate the configured billing currency code."""
|
|
115
|
+
candidate = str(value).strip().lower()
|
|
116
|
+
if not candidate:
|
|
117
|
+
return "modules.billing.billing_currency cannot be blank"
|
|
118
|
+
|
|
119
|
+
if candidate not in BILLING_SUPPORTED_CURRENCIES:
|
|
120
|
+
return (
|
|
121
|
+
"modules.billing.billing_currency must be one of the supported "
|
|
122
|
+
"QuickScale billing currency codes: "
|
|
123
|
+
+ ", ".join(BILLING_SUPPORTED_CURRENCIES)
|
|
124
|
+
)
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def validate_billing_module_options(options: Mapping[str, Any] | None) -> list[str]:
|
|
129
|
+
"""Return validation issues for billing module options."""
|
|
130
|
+
resolved = resolve_billing_module_options(options)
|
|
131
|
+
issues: list[str] = []
|
|
132
|
+
|
|
133
|
+
if not isinstance(resolved.get("enabled", True), bool):
|
|
134
|
+
issues.append("modules.billing.enabled must be a boolean")
|
|
135
|
+
|
|
136
|
+
for option_name in BILLING_ENV_VAR_OPTION_NAMES:
|
|
137
|
+
issue = validate_billing_env_var_reference(
|
|
138
|
+
option_name,
|
|
139
|
+
resolved.get(option_name, ""),
|
|
140
|
+
)
|
|
141
|
+
if issue:
|
|
142
|
+
issues.append(issue)
|
|
143
|
+
|
|
144
|
+
currency_issue = validate_billing_currency(resolved.get("billing_currency", ""))
|
|
145
|
+
if currency_issue:
|
|
146
|
+
issues.append(currency_issue)
|
|
147
|
+
|
|
148
|
+
return issues
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def billing_production_targeted(options: Mapping[str, Any] | None) -> bool:
|
|
152
|
+
"""Return whether billing is configured for live runtime ownership."""
|
|
153
|
+
resolved = resolve_billing_module_options(options)
|
|
154
|
+
if not bool(resolved.get("enabled", True)):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
if validate_billing_currency(resolved.get("billing_currency", "")):
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
for option_name in BILLING_ENV_VAR_OPTION_NAMES:
|
|
161
|
+
candidate = str(resolved.get(option_name, "")).strip()
|
|
162
|
+
if not candidate or validate_billing_env_var_reference(option_name, candidate):
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
__all__ = [
|
|
169
|
+
"BILLING_ENV_VAR_OPTION_NAMES",
|
|
170
|
+
"BILLING_MODULE_OPTION_KEYS",
|
|
171
|
+
"BILLING_SUPPORTED_CURRENCIES",
|
|
172
|
+
"DEFAULT_BILLING_CURRENCY",
|
|
173
|
+
"DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR",
|
|
174
|
+
"DEFAULT_BILLING_SECRET_KEY_ENV_VAR",
|
|
175
|
+
"DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR",
|
|
176
|
+
"billing_production_targeted",
|
|
177
|
+
"default_billing_module_options",
|
|
178
|
+
"normalize_billing_module_options",
|
|
179
|
+
"resolve_billing_module_options",
|
|
180
|
+
"validate_billing_currency",
|
|
181
|
+
"validate_billing_env_var_reference",
|
|
182
|
+
"validate_billing_module_options",
|
|
183
|
+
]
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/apply_command.py
RENAMED
|
@@ -21,6 +21,7 @@ from quickscale_cli.analytics_contract import (
|
|
|
21
21
|
resolve_analytics_module_options,
|
|
22
22
|
validate_analytics_module_options,
|
|
23
23
|
)
|
|
24
|
+
from quickscale_cli.billing_contract import validate_billing_module_options
|
|
24
25
|
from quickscale_cli.backups_contract import (
|
|
25
26
|
BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION,
|
|
26
27
|
BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION,
|
|
@@ -470,13 +471,13 @@ def _start_docker(
|
|
|
470
471
|
|
|
471
472
|
|
|
472
473
|
def _abort_for_not_ready_modules(module_names: list[str], *, source: str) -> None:
|
|
473
|
-
"""Abort apply when
|
|
474
|
+
"""Abort apply when non-public-ready modules appear in config or applied state."""
|
|
474
475
|
not_ready = find_not_ready_modules(module_names)
|
|
475
476
|
if not not_ready:
|
|
476
477
|
return
|
|
477
478
|
|
|
478
479
|
click.secho(
|
|
479
|
-
f"\n❌ {source} references
|
|
480
|
+
f"\n❌ {source} references modules that are not public-ready:",
|
|
480
481
|
fg="red",
|
|
481
482
|
err=True,
|
|
482
483
|
bold=True,
|
|
@@ -720,6 +721,45 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
|
720
721
|
|
|
721
722
|
def _validate_module_prerequisites(qs_config: QuickScaleConfig) -> None:
|
|
722
723
|
"""Validate actionable module-specific prerequisites before apply proceeds."""
|
|
724
|
+
billing_config = qs_config.modules.get("billing")
|
|
725
|
+
if billing_config is not None:
|
|
726
|
+
billing_issues = validate_billing_module_options(billing_config.options or {})
|
|
727
|
+
if billing_issues:
|
|
728
|
+
click.secho(
|
|
729
|
+
"\n❌ Billing module configuration is incomplete for apply:",
|
|
730
|
+
fg="red",
|
|
731
|
+
err=True,
|
|
732
|
+
)
|
|
733
|
+
for issue in billing_issues:
|
|
734
|
+
click.echo(f" • {issue}", err=True)
|
|
735
|
+
click.echo(
|
|
736
|
+
"\n💡 Re-run 'quickscale plan --reconfigure --configure-modules' or "
|
|
737
|
+
"edit quickscale.yml to correct the billing option values. "
|
|
738
|
+
"Billing apply requires valid env-var references and a supported "
|
|
739
|
+
"currency code.",
|
|
740
|
+
err=True,
|
|
741
|
+
)
|
|
742
|
+
raise click.Abort()
|
|
743
|
+
|
|
744
|
+
if "auth" not in qs_config.modules:
|
|
745
|
+
click.secho(
|
|
746
|
+
"\n❌ Billing requires the auth module before apply can continue:",
|
|
747
|
+
fg="red",
|
|
748
|
+
err=True,
|
|
749
|
+
)
|
|
750
|
+
click.echo(
|
|
751
|
+
" • Add 'auth' under modules in quickscale.yml before applying billing.",
|
|
752
|
+
err=True,
|
|
753
|
+
)
|
|
754
|
+
click.echo(
|
|
755
|
+
"\n💡 Billing relies on the QuickScale auth login flow and "
|
|
756
|
+
"AUTH_USER_MODEL. Re-run 'quickscale plan --reconfigure "
|
|
757
|
+
"--configure-modules' or edit quickscale.yml to add auth, then "
|
|
758
|
+
"re-run 'quickscale apply'.",
|
|
759
|
+
err=True,
|
|
760
|
+
)
|
|
761
|
+
raise click.Abort()
|
|
762
|
+
|
|
723
763
|
_abort_for_not_ready_modules(
|
|
724
764
|
list(qs_config.modules.keys()), source="quickscale.yml"
|
|
725
765
|
)
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/development_commands.py
RENAMED
|
@@ -10,6 +10,7 @@ import click
|
|
|
10
10
|
|
|
11
11
|
from quickscale_cli.schema.config_schema import QuickScaleConfig
|
|
12
12
|
from quickscale_cli.utils.docker_utils import (
|
|
13
|
+
DockerComposePluginRequiredError,
|
|
13
14
|
get_docker_compose_command,
|
|
14
15
|
get_port_from_env,
|
|
15
16
|
is_docker_running,
|
|
@@ -74,8 +75,32 @@ def _show_port_conflict_error(port: int | str) -> None:
|
|
|
74
75
|
)
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
def _show_compose_v2_required_error() -> None:
|
|
79
|
+
"""Display a user-friendly error when Docker Compose v2 is unavailable."""
|
|
80
|
+
click.secho(
|
|
81
|
+
"❌ Error: Docker Compose v2 plugin is required",
|
|
82
|
+
fg="red",
|
|
83
|
+
err=True,
|
|
84
|
+
)
|
|
85
|
+
click.echo(
|
|
86
|
+
"💡 Tip: Install or update Docker so the 'docker compose' command is "
|
|
87
|
+
"available. QuickScale keeps the generated 'docker-compose.yml' file "
|
|
88
|
+
"name, but the CLI uses the Docker Compose v2 plugin.",
|
|
89
|
+
err=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _require_docker_compose_command() -> list[str]:
|
|
94
|
+
"""Return the Docker Compose v2 command or exit with remediation text."""
|
|
95
|
+
try:
|
|
96
|
+
return get_docker_compose_command()
|
|
97
|
+
except DockerComposePluginRequiredError:
|
|
98
|
+
_show_compose_v2_required_error()
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
|
|
77
102
|
def _run_docker_compose_up(compose_cmd: list, build: bool, no_cache: bool) -> None:
|
|
78
|
-
"""Execute docker
|
|
103
|
+
"""Execute docker compose up with appropriate flags."""
|
|
79
104
|
if build or no_cache:
|
|
80
105
|
cmd = compose_cmd + ["--progress", "plain", "up", "-d"]
|
|
81
106
|
else:
|
|
@@ -101,7 +126,7 @@ def _run_docker_compose_up(compose_cmd: list, build: bool, no_cache: bool) -> No
|
|
|
101
126
|
|
|
102
127
|
|
|
103
128
|
def _handle_up_error(error: subprocess.CalledProcessError) -> None:
|
|
104
|
-
"""Handle docker
|
|
129
|
+
"""Handle docker compose up errors with user-friendly messages."""
|
|
105
130
|
error_output = error.stderr if error.stderr else ""
|
|
106
131
|
stdout_output = error.stdout if error.stdout else ""
|
|
107
132
|
full_output = error_output + stdout_output
|
|
@@ -122,7 +147,7 @@ def _handle_up_error(error: subprocess.CalledProcessError) -> None:
|
|
|
122
147
|
click.echo(
|
|
123
148
|
f"\n💡 To resolve this issue, try one of the following:\n"
|
|
124
149
|
f" 1. Stop existing containers: quickscale down\n"
|
|
125
|
-
f" 2. Remove orphaned containers: docker
|
|
150
|
+
f" 2. Remove orphaned containers: docker compose down --remove-orphans\n"
|
|
126
151
|
f" 3. Find and kill the process: lsof -ti:{conflict_port} | xargs kill -9\n"
|
|
127
152
|
f" 4. Find process details: sudo lsof -i:{conflict_port}\n"
|
|
128
153
|
f" 5. Or use: sudo fuser -k {conflict_port}/tcp",
|
|
@@ -272,14 +297,14 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
272
297
|
)
|
|
273
298
|
click.secho(" quickscale down && quickscale up --build\n", fg="cyan")
|
|
274
299
|
|
|
275
|
-
# Check if required port is available BEFORE calling docker
|
|
300
|
+
# Check if required port is available BEFORE calling docker compose.
|
|
276
301
|
port = get_port_from_env()
|
|
277
302
|
if not is_port_available(port):
|
|
278
303
|
_show_port_conflict_error(port)
|
|
279
304
|
sys.exit(1)
|
|
280
305
|
|
|
281
306
|
try:
|
|
282
|
-
compose_cmd =
|
|
307
|
+
compose_cmd = _require_docker_compose_command()
|
|
283
308
|
_run_docker_compose_up(compose_cmd, should_build, no_cache)
|
|
284
309
|
|
|
285
310
|
# Update build timestamp if build was performed
|
|
@@ -326,7 +351,7 @@ def down(volumes: bool) -> None:
|
|
|
326
351
|
_validate_project_and_docker()
|
|
327
352
|
|
|
328
353
|
try:
|
|
329
|
-
compose_cmd =
|
|
354
|
+
compose_cmd = _require_docker_compose_command()
|
|
330
355
|
cmd = compose_cmd + ["down", "--remove-orphans"]
|
|
331
356
|
|
|
332
357
|
if volumes:
|
|
@@ -468,7 +493,7 @@ def logs(service: str | None, follow: bool, tail: str | None, timestamps: bool)
|
|
|
468
493
|
_validate_project_and_docker()
|
|
469
494
|
|
|
470
495
|
try:
|
|
471
|
-
compose_cmd =
|
|
496
|
+
compose_cmd = _require_docker_compose_command()
|
|
472
497
|
cmd = compose_cmd + ["logs"]
|
|
473
498
|
|
|
474
499
|
if follow:
|
|
@@ -503,7 +528,7 @@ def ps() -> None:
|
|
|
503
528
|
_validate_project_and_docker()
|
|
504
529
|
|
|
505
530
|
try:
|
|
506
|
-
compose_cmd =
|
|
531
|
+
compose_cmd = _require_docker_compose_command()
|
|
507
532
|
cmd = compose_cmd + ["ps"]
|
|
508
533
|
subprocess.run(cmd, check=True)
|
|
509
534
|
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_commands.py
RENAMED
|
@@ -208,8 +208,12 @@ def _sync_state_module_version(project_path: Path, module: str, version: str) ->
|
|
|
208
208
|
state_manager.save(state)
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
def _validate_module_readiness(
|
|
212
|
-
|
|
211
|
+
def _validate_module_readiness(
|
|
212
|
+
module: str,
|
|
213
|
+
*,
|
|
214
|
+
execution_mode: ModuleExecutionMode = STANDALONE_MODULE_EXECUTION_MODE,
|
|
215
|
+
) -> bool:
|
|
216
|
+
"""Reject non-public modules from standalone embed/update flows."""
|
|
213
217
|
readiness_reason = get_module_readiness_reason(module)
|
|
214
218
|
if readiness_reason is None:
|
|
215
219
|
return True
|
|
@@ -416,7 +420,10 @@ def embed_module(
|
|
|
416
420
|
if not _validate_module_not_exists(project_path, module):
|
|
417
421
|
return False
|
|
418
422
|
|
|
419
|
-
if not _validate_module_readiness(
|
|
423
|
+
if not _validate_module_readiness(
|
|
424
|
+
module,
|
|
425
|
+
execution_mode=execution_mode,
|
|
426
|
+
):
|
|
420
427
|
return False
|
|
421
428
|
|
|
422
429
|
branch = f"splits/{module}-module"
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_config.py
RENAMED
|
@@ -21,6 +21,11 @@ from quickscale_cli.analytics_contract import (
|
|
|
21
21
|
resolve_analytics_module_options,
|
|
22
22
|
validate_analytics_module_options,
|
|
23
23
|
)
|
|
24
|
+
from quickscale_cli.billing_contract import (
|
|
25
|
+
default_billing_module_options,
|
|
26
|
+
resolve_billing_module_options,
|
|
27
|
+
validate_billing_module_options,
|
|
28
|
+
)
|
|
24
29
|
from quickscale_cli.auth_contract import (
|
|
25
30
|
default_auth_module_options,
|
|
26
31
|
resolve_auth_module_options,
|
|
@@ -1722,6 +1727,116 @@ def apply_analytics_configuration(
|
|
|
1722
1727
|
)
|
|
1723
1728
|
|
|
1724
1729
|
|
|
1730
|
+
# ============================================================================
|
|
1731
|
+
# BILLING MODULE CONFIGURATION
|
|
1732
|
+
# ============================================================================
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def get_default_billing_config() -> dict[str, Any]:
|
|
1736
|
+
"""Return default configuration for the billing module."""
|
|
1737
|
+
return default_billing_module_options()
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def _raise_for_invalid_billing_config(config: Mapping[str, Any]) -> None:
|
|
1741
|
+
"""Abort with actionable messaging when billing config is invalid."""
|
|
1742
|
+
issues = validate_billing_module_options(config)
|
|
1743
|
+
if not issues:
|
|
1744
|
+
return
|
|
1745
|
+
|
|
1746
|
+
click.secho("\n❌ Invalid billing module configuration:", fg="red", err=True)
|
|
1747
|
+
for issue in issues:
|
|
1748
|
+
click.echo(f" • {issue}", err=True)
|
|
1749
|
+
raise click.Abort()
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
def configure_billing_module(
|
|
1753
|
+
non_interactive: bool = False,
|
|
1754
|
+
existing_config: Mapping[str, Any] | None = None,
|
|
1755
|
+
) -> dict[str, Any]:
|
|
1756
|
+
"""Configure billing module settings interactively or with defaults."""
|
|
1757
|
+
defaults = resolve_billing_module_options(existing_config)
|
|
1758
|
+
|
|
1759
|
+
if non_interactive:
|
|
1760
|
+
click.echo("\n⚙️ Using default billing module configuration...")
|
|
1761
|
+
click.echo(" • Runtime: " + ("Enabled" if defaults["enabled"] else "Disabled"))
|
|
1762
|
+
click.echo(
|
|
1763
|
+
" • Publishable key env var: " + str(defaults["publishable_key_env_var"])
|
|
1764
|
+
)
|
|
1765
|
+
click.echo(" • Secret key env var: " + str(defaults["secret_key_env_var"]))
|
|
1766
|
+
click.echo(
|
|
1767
|
+
" • Webhook secret env var: " + str(defaults["webhook_secret_env_var"])
|
|
1768
|
+
)
|
|
1769
|
+
click.echo(" • Billing currency: " + str(defaults["billing_currency"]))
|
|
1770
|
+
_raise_for_invalid_billing_config(defaults)
|
|
1771
|
+
return defaults
|
|
1772
|
+
|
|
1773
|
+
click.echo("\n⚙️ Configuring billing module...")
|
|
1774
|
+
click.echo(
|
|
1775
|
+
"Billing is public-ready in quickscale plan, quickscale.yml, and "
|
|
1776
|
+
"quickscale apply flows. These settings configure the managed Stripe "
|
|
1777
|
+
"runtime, while quickscale apply still requires auth before billing "
|
|
1778
|
+
"can be enabled.\n"
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
config = resolve_billing_module_options(
|
|
1782
|
+
{
|
|
1783
|
+
"enabled": click.confirm(
|
|
1784
|
+
"Enable the billing runtime?",
|
|
1785
|
+
default=bool(defaults["enabled"]),
|
|
1786
|
+
),
|
|
1787
|
+
"publishable_key_env_var": click.prompt(
|
|
1788
|
+
"Stripe publishable key environment variable",
|
|
1789
|
+
default=str(defaults["publishable_key_env_var"]),
|
|
1790
|
+
show_default=True,
|
|
1791
|
+
).strip(),
|
|
1792
|
+
"secret_key_env_var": click.prompt(
|
|
1793
|
+
"Stripe secret key environment variable",
|
|
1794
|
+
default=str(defaults["secret_key_env_var"]),
|
|
1795
|
+
show_default=True,
|
|
1796
|
+
).strip(),
|
|
1797
|
+
"webhook_secret_env_var": click.prompt(
|
|
1798
|
+
"Stripe webhook secret environment variable",
|
|
1799
|
+
default=str(defaults["webhook_secret_env_var"]),
|
|
1800
|
+
show_default=True,
|
|
1801
|
+
).strip(),
|
|
1802
|
+
"billing_currency": click.prompt(
|
|
1803
|
+
"Billing currency (ISO 4217)",
|
|
1804
|
+
default=str(defaults["billing_currency"]),
|
|
1805
|
+
show_default=True,
|
|
1806
|
+
).strip(),
|
|
1807
|
+
}
|
|
1808
|
+
)
|
|
1809
|
+
|
|
1810
|
+
_raise_for_invalid_billing_config(config)
|
|
1811
|
+
return config
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def apply_billing_configuration(
|
|
1815
|
+
project_path: Path,
|
|
1816
|
+
config: dict[str, Any],
|
|
1817
|
+
*,
|
|
1818
|
+
execution_mode: ModuleExecutionMode = STANDALONE_MODULE_EXECUTION_MODE,
|
|
1819
|
+
) -> None:
|
|
1820
|
+
"""Apply billing module configuration via managed wiring files."""
|
|
1821
|
+
resolved = resolve_billing_module_options(config)
|
|
1822
|
+
_raise_for_invalid_billing_config(resolved)
|
|
1823
|
+
_regenerate_wiring_for_execution_mode(
|
|
1824
|
+
project_path,
|
|
1825
|
+
"billing",
|
|
1826
|
+
resolved,
|
|
1827
|
+
execution_mode=execution_mode,
|
|
1828
|
+
)
|
|
1829
|
+
|
|
1830
|
+
click.echo("\n📋 Configuration applied:")
|
|
1831
|
+
click.echo(" • Runtime: " + ("Enabled" if resolved["enabled"] else "Disabled"))
|
|
1832
|
+
click.echo(
|
|
1833
|
+
" • Publishable key env var: " + str(resolved["publishable_key_env_var"])
|
|
1834
|
+
)
|
|
1835
|
+
click.echo(" • Secret key env var: " + str(resolved["secret_key_env_var"]))
|
|
1836
|
+
click.echo(" • Webhook secret env var: " + str(resolved["webhook_secret_env_var"]))
|
|
1837
|
+
click.echo(" • Billing currency: " + str(resolved["billing_currency"]))
|
|
1838
|
+
|
|
1839
|
+
|
|
1725
1840
|
# ============================================================================
|
|
1726
1841
|
# SOCIAL MODULE CONFIGURATION
|
|
1727
1842
|
# ============================================================================
|
|
@@ -1870,6 +1985,7 @@ MODULE_CONFIGURATORS = {
|
|
|
1870
1985
|
"forms": (configure_forms_module, apply_forms_configuration),
|
|
1871
1986
|
"storage": (configure_storage_module, apply_storage_configuration),
|
|
1872
1987
|
"backups": (configure_backups_module, apply_backups_configuration),
|
|
1988
|
+
"billing": (configure_billing_module, apply_billing_configuration),
|
|
1873
1989
|
"notifications": (
|
|
1874
1990
|
configure_notifications_module,
|
|
1875
1991
|
apply_notifications_configuration,
|
|
@@ -52,7 +52,12 @@ def _get_module_choices(
|
|
|
52
52
|
def _format_module_choice(entry: ModuleCatalogEntry) -> str:
|
|
53
53
|
"""Format a catalog entry for interactive display."""
|
|
54
54
|
if not entry.ready:
|
|
55
|
-
|
|
55
|
+
if entry.name == "billing":
|
|
56
|
+
return (
|
|
57
|
+
f"{entry.name} - {entry.description} "
|
|
58
|
+
"(internal packaged Phase 1 foundation, not public-ready)"
|
|
59
|
+
)
|
|
60
|
+
return f"{entry.name} - {entry.description} (placeholder, not public-ready)"
|
|
56
61
|
if entry.experimental:
|
|
57
62
|
return f"{entry.name} - {entry.description} (experimental)"
|
|
58
63
|
return f"{entry.name} - {entry.description}"
|
|
@@ -171,7 +176,7 @@ def _select_modules(*, include_experimental: bool = False) -> list[str]:
|
|
|
171
176
|
|
|
172
177
|
if include_experimental and any(not entry.ready for entry in available_modules):
|
|
173
178
|
click.echo(
|
|
174
|
-
"\n
|
|
179
|
+
"\n Non-public modules are shown for visibility only and cannot be selected"
|
|
175
180
|
)
|
|
176
181
|
|
|
177
182
|
click.echo(
|
|
@@ -1051,7 +1056,7 @@ def _save_config_with_validation(yaml_content: str, output_path: Path) -> None:
|
|
|
1051
1056
|
@click.option(
|
|
1052
1057
|
"--include-experimental",
|
|
1053
1058
|
is_flag=True,
|
|
1054
|
-
help="Show
|
|
1059
|
+
help="Show non-public module inventory entries (currently teams) in the picker",
|
|
1055
1060
|
)
|
|
1056
1061
|
@click.option(
|
|
1057
1062
|
"--configure-modules",
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/status_command.py
RENAMED
|
@@ -120,13 +120,13 @@ def _load_module_manifests(
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def _abort_for_not_ready_modules(module_names: list[str], *, source: str) -> None:
|
|
123
|
-
"""Abort status when
|
|
123
|
+
"""Abort status when non-public-ready modules appear in config or applied state."""
|
|
124
124
|
not_ready = find_not_ready_modules(module_names)
|
|
125
125
|
if not not_ready:
|
|
126
126
|
return
|
|
127
127
|
|
|
128
128
|
click.secho(
|
|
129
|
-
f"\n❌ Unsupported
|
|
129
|
+
f"\n❌ Unsupported non-public-ready module state detected in {source}:",
|
|
130
130
|
fg="red",
|
|
131
131
|
err=True,
|
|
132
132
|
bold=True,
|
|
@@ -136,11 +136,17 @@ def _abort_for_not_ready_modules(module_names: list[str], *, source: str) -> Non
|
|
|
136
136
|
if reason is not None:
|
|
137
137
|
click.echo(f" • {reason}", err=True)
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
"\n💡
|
|
141
|
-
"
|
|
142
|
-
err=True,
|
|
139
|
+
guidance = (
|
|
140
|
+
"\n💡 Resolve the non-public-ready module state above before running "
|
|
141
|
+
"'quickscale status' again."
|
|
143
142
|
)
|
|
143
|
+
if "teams" in not_ready:
|
|
144
|
+
guidance += (
|
|
145
|
+
" Teams remains placeholder inventory and cannot participate in "
|
|
146
|
+
"public QuickScale workflows yet."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
click.echo(guidance, err=True)
|
|
144
150
|
raise click.Abort()
|
|
145
151
|
|
|
146
152
|
|
|
@@ -67,9 +67,8 @@ MODULE_CATALOG: tuple[ModuleCatalogEntry, ...] = (
|
|
|
67
67
|
),
|
|
68
68
|
ModuleCatalogEntry(
|
|
69
69
|
name="billing",
|
|
70
|
-
description="Stripe integration",
|
|
71
|
-
ready=
|
|
72
|
-
experimental=True,
|
|
70
|
+
description="Stripe integration with credits-first pricing and dashboard routes",
|
|
71
|
+
ready=True,
|
|
73
72
|
),
|
|
74
73
|
ModuleCatalogEntry(
|
|
75
74
|
name="teams",
|
|
@@ -114,13 +113,16 @@ def find_not_ready_modules(module_names: Iterable[str]) -> list[str]:
|
|
|
114
113
|
|
|
115
114
|
|
|
116
115
|
def get_module_readiness_reason(module_name: str) -> str | None:
|
|
117
|
-
"""Return an actionable readiness error for
|
|
116
|
+
"""Return an actionable readiness error for non-public-ready modules."""
|
|
118
117
|
entry = get_module_entry(module_name)
|
|
119
118
|
if entry is None or entry.ready:
|
|
120
119
|
return None
|
|
121
120
|
|
|
121
|
+
module_display_name = module_name.replace("_", " ").title()
|
|
122
122
|
return (
|
|
123
|
-
f"Module '{module_name}'
|
|
124
|
-
"
|
|
125
|
-
"
|
|
123
|
+
f"Module '{module_name}' remains placeholder inventory only and is not a "
|
|
124
|
+
"public-ready QuickScale module yet. "
|
|
125
|
+
f"{module_display_name} remains excluded from public quickscale plan, "
|
|
126
|
+
"quickscale.yml, quickscale apply, and quickscale status workflows until "
|
|
127
|
+
"it ships."
|
|
126
128
|
)
|
|
@@ -11,6 +11,12 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import yaml
|
|
13
13
|
|
|
14
|
+
from quickscale_cli.billing_contract import (
|
|
15
|
+
BILLING_ENV_VAR_OPTION_NAMES,
|
|
16
|
+
BILLING_MODULE_OPTION_KEYS,
|
|
17
|
+
validate_billing_currency,
|
|
18
|
+
validate_billing_env_var_reference,
|
|
19
|
+
)
|
|
14
20
|
from quickscale_cli.auth_contract import (
|
|
15
21
|
AUTH_AUTHENTICATION_METHOD_OPTION,
|
|
16
22
|
AUTH_AUTHENTICATION_METHOD_VALUES,
|
|
@@ -239,6 +245,67 @@ def _validate_auth_module_options(
|
|
|
239
245
|
)
|
|
240
246
|
|
|
241
247
|
|
|
248
|
+
def _validate_billing_module_options(
|
|
249
|
+
module_options: dict[str, Any],
|
|
250
|
+
yaml_content: str,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Validate billing config keys and value shapes before readiness gating."""
|
|
253
|
+
billing_option_keys = set(BILLING_MODULE_OPTION_KEYS)
|
|
254
|
+
|
|
255
|
+
for option_name in module_options:
|
|
256
|
+
if option_name not in billing_option_keys:
|
|
257
|
+
line = _find_line_number(yaml_content, option_name)
|
|
258
|
+
suggestion = _suggest_similar_key(option_name, billing_option_keys)
|
|
259
|
+
suggestion_text = (
|
|
260
|
+
f"Use modules.billing.{suggestion} instead."
|
|
261
|
+
if suggestion
|
|
262
|
+
else "Supported keys: "
|
|
263
|
+
+ ", ".join(
|
|
264
|
+
f"modules.billing.{key}" for key in sorted(billing_option_keys)
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
raise ConfigValidationError(
|
|
268
|
+
f"Unknown key '{option_name}' in modules.billing section",
|
|
269
|
+
line=line,
|
|
270
|
+
suggestion=suggestion_text,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if "enabled" in module_options and not isinstance(module_options["enabled"], bool):
|
|
274
|
+
raise ConfigValidationError(
|
|
275
|
+
"'modules.billing.enabled' must be a boolean (true/false)",
|
|
276
|
+
line=_find_line_number(yaml_content, "enabled"),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
for option_name in BILLING_ENV_VAR_OPTION_NAMES:
|
|
280
|
+
if option_name not in module_options:
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
issue = validate_billing_env_var_reference(
|
|
284
|
+
option_name,
|
|
285
|
+
module_options[option_name],
|
|
286
|
+
)
|
|
287
|
+
if issue:
|
|
288
|
+
raise ConfigValidationError(
|
|
289
|
+
issue,
|
|
290
|
+
line=_find_line_number(yaml_content, option_name),
|
|
291
|
+
suggestion=(
|
|
292
|
+
"Use uppercase environment-variable names such as "
|
|
293
|
+
"STRIPE_SECRET_KEY."
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if "billing_currency" in module_options:
|
|
298
|
+
currency_issue = validate_billing_currency(
|
|
299
|
+
module_options["billing_currency"],
|
|
300
|
+
)
|
|
301
|
+
if currency_issue:
|
|
302
|
+
raise ConfigValidationError(
|
|
303
|
+
currency_issue,
|
|
304
|
+
line=_find_line_number(yaml_content, "billing_currency"),
|
|
305
|
+
suggestion="Use a supported ISO 4217 code such as usd or eur.",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
242
309
|
def _validate_version(data: dict, yaml_content: str) -> None:
|
|
243
310
|
"""Validate the version field."""
|
|
244
311
|
if "version" not in data:
|
|
@@ -426,18 +493,6 @@ def _validate_modules_section(data: dict, yaml_content: str) -> dict[str, Module
|
|
|
426
493
|
or f"Available modules: {', '.join(sorted(AVAILABLE_MODULES))}",
|
|
427
494
|
)
|
|
428
495
|
|
|
429
|
-
readiness_reason = get_module_readiness_reason(module_name)
|
|
430
|
-
if readiness_reason is not None:
|
|
431
|
-
line = _find_line_number(yaml_content, module_name)
|
|
432
|
-
raise ConfigValidationError(
|
|
433
|
-
readiness_reason,
|
|
434
|
-
line=line,
|
|
435
|
-
suggestion=(
|
|
436
|
-
"Remove it from quickscale.yml and choose a shipped module: "
|
|
437
|
-
+ ", ".join(sorted(READY_MODULES))
|
|
438
|
-
),
|
|
439
|
-
)
|
|
440
|
-
|
|
441
496
|
if module_options is None:
|
|
442
497
|
module_options = {}
|
|
443
498
|
elif not isinstance(module_options, dict):
|
|
@@ -449,6 +504,20 @@ def _validate_modules_section(data: dict, yaml_content: str) -> dict[str, Module
|
|
|
449
504
|
|
|
450
505
|
if module_name == "auth":
|
|
451
506
|
_validate_auth_module_options(module_options, yaml_content)
|
|
507
|
+
if module_name == "billing":
|
|
508
|
+
_validate_billing_module_options(module_options, yaml_content)
|
|
509
|
+
|
|
510
|
+
readiness_reason = get_module_readiness_reason(module_name)
|
|
511
|
+
if readiness_reason is not None:
|
|
512
|
+
line = _find_line_number(yaml_content, module_name)
|
|
513
|
+
raise ConfigValidationError(
|
|
514
|
+
readiness_reason,
|
|
515
|
+
line=line,
|
|
516
|
+
suggestion=(
|
|
517
|
+
"Remove it from quickscale.yml and choose a shipped module: "
|
|
518
|
+
+ ", ".join(sorted(READY_MODULES))
|
|
519
|
+
),
|
|
520
|
+
)
|
|
452
521
|
|
|
453
522
|
modules[module_name] = ModuleConfig(name=module_name, options=module_options)
|
|
454
523
|
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/dependency_utils.py
RENAMED
|
@@ -42,13 +42,13 @@ class DependencyStatus(NamedTuple):
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def check_python_version() -> DependencyStatus:
|
|
45
|
-
"""Check Python version (3.
|
|
45
|
+
"""Check Python version (3.13+ required)."""
|
|
46
46
|
import sys
|
|
47
47
|
|
|
48
48
|
version = (
|
|
49
49
|
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
50
50
|
)
|
|
51
|
-
meets_requirement = sys.version_info >= (3,
|
|
51
|
+
meets_requirement = sys.version_info >= (3, 13)
|
|
52
52
|
|
|
53
53
|
return DependencyStatus(
|
|
54
54
|
name="Python",
|
|
@@ -151,7 +151,7 @@ def check_git_installed() -> DependencyStatus:
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def check_docker_installed() -> DependencyStatus:
|
|
154
|
-
"""Check if Docker and Docker Compose are installed."""
|
|
154
|
+
"""Check if Docker and the Docker Compose v2 plugin are installed."""
|
|
155
155
|
|
|
156
156
|
if _should_skip_dependency_checks():
|
|
157
157
|
return _skipped_dependency(
|
|
@@ -176,45 +176,28 @@ def check_docker_installed() -> DependencyStatus:
|
|
|
176
176
|
parts = version_str.split()
|
|
177
177
|
version = parts[2].rstrip(",") if len(parts) >= 3 else None
|
|
178
178
|
|
|
179
|
-
#
|
|
180
|
-
compose_available = False
|
|
179
|
+
# Docker-backed workflows require the Compose v2 plugin.
|
|
181
180
|
try:
|
|
182
|
-
# Try docker compose v2 (plugin)
|
|
183
181
|
subprocess.run(
|
|
184
182
|
["docker", "compose", "version"],
|
|
185
183
|
capture_output=True,
|
|
186
184
|
check=True,
|
|
187
185
|
timeout=2,
|
|
188
186
|
)
|
|
189
|
-
compose_available = True
|
|
190
187
|
except (
|
|
191
188
|
subprocess.SubprocessError,
|
|
192
189
|
FileNotFoundError,
|
|
193
190
|
subprocess.TimeoutExpired,
|
|
194
191
|
):
|
|
195
|
-
# Try docker-compose v1 (standalone)
|
|
196
|
-
try:
|
|
197
|
-
subprocess.run(
|
|
198
|
-
["docker-compose", "--version"],
|
|
199
|
-
capture_output=True,
|
|
200
|
-
check=True,
|
|
201
|
-
timeout=2,
|
|
202
|
-
)
|
|
203
|
-
compose_available = True
|
|
204
|
-
except (
|
|
205
|
-
subprocess.SubprocessError,
|
|
206
|
-
FileNotFoundError,
|
|
207
|
-
subprocess.TimeoutExpired,
|
|
208
|
-
):
|
|
209
|
-
pass
|
|
210
|
-
|
|
211
|
-
if not compose_available:
|
|
212
192
|
return DependencyStatus(
|
|
213
193
|
name="Docker",
|
|
214
194
|
installed=False,
|
|
215
195
|
version=version,
|
|
216
196
|
required=False,
|
|
217
|
-
purpose=
|
|
197
|
+
purpose=(
|
|
198
|
+
"Containerized development (quickscale up/down) - "
|
|
199
|
+
"Docker Compose v2 plugin not found"
|
|
200
|
+
),
|
|
218
201
|
)
|
|
219
202
|
|
|
220
203
|
return DependencyStatus(
|
|
@@ -7,6 +7,10 @@ import time
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class DockerComposePluginRequiredError(RuntimeError):
|
|
11
|
+
"""Raised when the Docker Compose v2 plugin is unavailable."""
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
def is_interactive() -> bool:
|
|
11
15
|
"""Check if running in an interactive terminal (has TTY)."""
|
|
12
16
|
return sys.stdout.isatty() and sys.stdin.isatty()
|
|
@@ -32,24 +36,22 @@ def find_docker_compose() -> Path | None:
|
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
def get_docker_compose_command() -> list[str]:
|
|
35
|
-
"""Get the
|
|
36
|
-
|
|
37
|
-
Tries 'docker compose' (v2 plugin) first, falls back to 'docker-compose' (v1 standalone).
|
|
38
|
-
Both are fully supported.
|
|
39
|
-
"""
|
|
40
|
-
# Try docker compose first (v2 plugin)
|
|
39
|
+
"""Get the Docker Compose v2 command."""
|
|
41
40
|
try:
|
|
42
41
|
subprocess.run(
|
|
43
42
|
["docker", "compose", "version"], capture_output=True, check=True, timeout=2
|
|
44
43
|
)
|
|
45
|
-
return ["docker", "compose"]
|
|
46
44
|
except (
|
|
47
45
|
subprocess.SubprocessError,
|
|
48
46
|
FileNotFoundError,
|
|
49
47
|
subprocess.TimeoutExpired,
|
|
50
|
-
):
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
) as error:
|
|
49
|
+
raise DockerComposePluginRequiredError(
|
|
50
|
+
"Docker Compose v2 is required. Install or update Docker so the "
|
|
51
|
+
"'docker compose' command is available."
|
|
52
|
+
) from error
|
|
53
|
+
|
|
54
|
+
return ["docker", "compose"]
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
def get_container_status(container_name: str) -> str | None:
|
quickscale_cli-0.83.0/PKG-INFO
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: quickscale-cli
|
|
3
|
-
Version: 0.83.0
|
|
4
|
-
Summary: Command-line interface for QuickScale Django project generator
|
|
5
|
-
License-Expression: Apache-2.0
|
|
6
|
-
Keywords: django,cli,project-generator,saas
|
|
7
|
-
Author: Experto AI
|
|
8
|
-
Author-email: victor@experto.ai
|
|
9
|
-
Requires-Python: >=3.12,<3.14
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
-
Classifier: Framework :: Django
|
|
15
|
-
Classifier: Framework :: Django :: 6.0
|
|
16
|
-
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
17
|
-
Requires-Dist: quickscale-core (>=0.83.0,<0.84.0)
|
|
18
|
-
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
19
|
-
Project-URL: Homepage, https://github.com/Experto-AI/quickscale
|
|
20
|
-
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
21
|
-
Description-Content-Type: text/markdown
|
|
22
|
-
|
|
23
|
-
# quickscale-cli
|
|
24
|
-
|
|
25
|
-
Command-line interface package for QuickScale.
|
|
26
|
-
|
|
27
|
-
This package exposes the `quickscale` command used to plan, apply, inspect, recover, and operate generated projects.
|
|
28
|
-
|
|
29
|
-
## Command groups
|
|
30
|
-
|
|
31
|
-
- Project lifecycle: `plan`, `apply`, `status`, `remove`
|
|
32
|
-
- Disaster recovery & promotion: `dr capture`, `dr plan`, `dr execute`, `dr report`
|
|
33
|
-
- Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
|
|
34
|
-
- Deployment: `deploy`
|
|
35
|
-
- Module workflows: `update`, `push`
|
|
36
|
-
|
|
37
|
-
## Relationship to other packages
|
|
38
|
-
|
|
39
|
-
- `quickscale-cli` is the command surface
|
|
40
|
-
- `quickscale-core` contains the underlying scaffolding and template logic
|
|
41
|
-
- `quickscale` installs both together for convenience
|
|
42
|
-
|
|
43
|
-
## Install
|
|
44
|
-
|
|
45
|
-
Usually installed via the meta-package:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pip install quickscale
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Standalone package installs can use this package's published metadata and dependency constraints.
|
|
52
|
-
|
|
53
|
-
## Documentation
|
|
54
|
-
|
|
55
|
-
- Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
|
|
56
|
-
- Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
|
|
57
|
-
- Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
|
|
58
|
-
- CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
|
|
59
|
-
|
|
60
|
-
This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.
|
|
61
|
-
|
quickscale_cli-0.83.0/README.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# quickscale-cli
|
|
2
|
-
|
|
3
|
-
Command-line interface package for QuickScale.
|
|
4
|
-
|
|
5
|
-
This package exposes the `quickscale` command used to plan, apply, inspect, recover, and operate generated projects.
|
|
6
|
-
|
|
7
|
-
## Command groups
|
|
8
|
-
|
|
9
|
-
- Project lifecycle: `plan`, `apply`, `status`, `remove`
|
|
10
|
-
- Disaster recovery & promotion: `dr capture`, `dr plan`, `dr execute`, `dr report`
|
|
11
|
-
- Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
|
|
12
|
-
- Deployment: `deploy`
|
|
13
|
-
- Module workflows: `update`, `push`
|
|
14
|
-
|
|
15
|
-
## Relationship to other packages
|
|
16
|
-
|
|
17
|
-
- `quickscale-cli` is the command surface
|
|
18
|
-
- `quickscale-core` contains the underlying scaffolding and template logic
|
|
19
|
-
- `quickscale` installs both together for convenience
|
|
20
|
-
|
|
21
|
-
## Install
|
|
22
|
-
|
|
23
|
-
Usually installed via the meta-package:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pip install quickscale
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Standalone package installs can use this package's published metadata and dependency constraints.
|
|
30
|
-
|
|
31
|
-
## Documentation
|
|
32
|
-
|
|
33
|
-
- Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
|
|
34
|
-
- Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
|
|
35
|
-
- Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
|
|
36
|
-
- CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
|
|
37
|
-
|
|
38
|
-
This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/deployment_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_wiring_specs.py
RENAMED
|
File without changes
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/remove_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/notifications_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/module_dependency_sync.py
RENAMED
|
File without changes
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/module_wiring_manager.py
RENAMED
|
File without changes
|
{quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/project_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|