quickscale-cli 0.84.0__tar.gz → 0.86.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.86.0/PKG-INFO +43 -0
- quickscale_cli-0.86.0/README.md +19 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/pyproject.toml +2 -2
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/backups_contract.py +3 -0
- quickscale_cli-0.86.0/src/quickscale_cli/billing_contract.py +183 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/apply_command.py +197 -7
- quickscale_cli-0.86.0/src/quickscale_cli/commands/implied_module_defaults.py +23 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/module_commands.py +10 -3
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/module_config.py +126 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/module_wiring_specs.py +54 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/plan_command.py +56 -3
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/status_command.py +12 -6
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/module_catalog.py +14 -7
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/config_schema.py +81 -12
- quickscale_cli-0.84.0/PKG-INFO +0 -62
- quickscale_cli-0.84.0/README.md +0 -38
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/analytics_contract.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/auth_contract.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/beta_migration.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/deployment_commands.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/development_commands.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/dr_commands.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/remove_command.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/crm_contract.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/main.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/notifications_contract.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/__init__.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/delta.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/state_schema.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/social_contract.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/__init__.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/dependency_utils.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/docker_utils.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/module_dependency_sync.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/module_wiring_manager.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/project_identity.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/project_manager.py +0 -0
- {quickscale_cli-0.84.0 → quickscale_cli-0.86.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.86.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.86.0,<0.87.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,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.86.0"
|
|
4
4
|
description = "Command-line interface for QuickScale Django project generator"
|
|
5
5
|
requires-python = ">=3.13,<3.15"
|
|
6
6
|
readme = "README.md"
|
|
@@ -32,7 +32,7 @@ packages = [{include = "quickscale_cli", from = "src"}]
|
|
|
32
32
|
[tool.poetry.dependencies]
|
|
33
33
|
python = ">=3.13,<3.15"
|
|
34
34
|
click = "^8.3.1"
|
|
35
|
-
quickscale-core = "^0.
|
|
35
|
+
quickscale-core = "^0.86.0"
|
|
36
36
|
|
|
37
37
|
[tool.poetry.group.dev.dependencies]
|
|
38
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.86.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.84.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/apply_command.py
RENAMED
|
@@ -21,6 +21,14 @@ 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_CURRENCY,
|
|
26
|
+
DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR,
|
|
27
|
+
DEFAULT_BILLING_SECRET_KEY_ENV_VAR,
|
|
28
|
+
DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR,
|
|
29
|
+
resolve_billing_module_options,
|
|
30
|
+
validate_billing_module_options,
|
|
31
|
+
)
|
|
24
32
|
from quickscale_cli.backups_contract import (
|
|
25
33
|
BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION,
|
|
26
34
|
BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION,
|
|
@@ -39,6 +47,9 @@ from quickscale_cli.notifications_contract import (
|
|
|
39
47
|
resolve_notifications_module_options,
|
|
40
48
|
validate_notifications_module_options,
|
|
41
49
|
)
|
|
50
|
+
from quickscale_cli.commands.implied_module_defaults import (
|
|
51
|
+
get_implied_module_default_configs,
|
|
52
|
+
)
|
|
42
53
|
from quickscale_cli.module_catalog import (
|
|
43
54
|
find_not_ready_modules,
|
|
44
55
|
get_module_readiness_reason,
|
|
@@ -63,6 +74,7 @@ from quickscale_cli.utils.module_dependency_sync import (
|
|
|
63
74
|
from quickscale_cli.utils.module_wiring_manager import regenerate_managed_wiring
|
|
64
75
|
from quickscale_cli.schema.config_schema import (
|
|
65
76
|
ConfigValidationError,
|
|
77
|
+
ModuleConfig,
|
|
66
78
|
QuickScaleConfig,
|
|
67
79
|
generate_yaml,
|
|
68
80
|
validate_config,
|
|
@@ -470,13 +482,13 @@ def _start_docker(
|
|
|
470
482
|
|
|
471
483
|
|
|
472
484
|
def _abort_for_not_ready_modules(module_names: list[str], *, source: str) -> None:
|
|
473
|
-
"""Abort apply when
|
|
485
|
+
"""Abort apply when non-public-ready modules appear in config or applied state."""
|
|
474
486
|
not_ready = find_not_ready_modules(module_names)
|
|
475
487
|
if not not_ready:
|
|
476
488
|
return
|
|
477
489
|
|
|
478
490
|
click.secho(
|
|
479
|
-
f"\n❌ {source} references
|
|
491
|
+
f"\n❌ {source} references modules that are not public-ready:",
|
|
480
492
|
fg="red",
|
|
481
493
|
err=True,
|
|
482
494
|
bold=True,
|
|
@@ -680,6 +692,21 @@ def _sanitize_loaded_module_configs(qs_config: QuickScaleConfig) -> list[str]:
|
|
|
680
692
|
return sanitized_modules
|
|
681
693
|
|
|
682
694
|
|
|
695
|
+
def _materialize_implied_module_configs(qs_config: QuickScaleConfig) -> list[str]:
|
|
696
|
+
"""Add explicit module config blocks required by selected modules."""
|
|
697
|
+
implied_configs = get_implied_module_default_configs(qs_config.modules.keys())
|
|
698
|
+
if not implied_configs:
|
|
699
|
+
return []
|
|
700
|
+
|
|
701
|
+
for module_name, options in implied_configs.items():
|
|
702
|
+
qs_config.modules[module_name] = ModuleConfig(
|
|
703
|
+
name=module_name,
|
|
704
|
+
options=sanitize_module_options(module_name, options),
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
return list(implied_configs.keys())
|
|
708
|
+
|
|
709
|
+
|
|
683
710
|
def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
684
711
|
"""Load and validate configuration from file."""
|
|
685
712
|
if not config_path.exists():
|
|
@@ -698,16 +725,24 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
|
698
725
|
qs_config = validate_config(yaml_content)
|
|
699
726
|
original_data = yaml.safe_load(yaml_content) or {}
|
|
700
727
|
original_modules = original_data.get("modules") or {}
|
|
701
|
-
_sanitize_loaded_module_configs(qs_config)
|
|
728
|
+
sanitized_modules = _sanitize_loaded_module_configs(qs_config)
|
|
729
|
+
implied_modules = _materialize_implied_module_configs(qs_config)
|
|
702
730
|
normalized_yaml = generate_yaml(qs_config)
|
|
703
731
|
normalized_data = yaml.safe_load(normalized_yaml) or {}
|
|
704
732
|
normalized_modules = normalized_data.get("modules") or {}
|
|
705
733
|
if normalized_modules != original_modules:
|
|
706
734
|
config_path.write_text(normalized_yaml)
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
735
|
+
if sanitized_modules:
|
|
736
|
+
click.secho(
|
|
737
|
+
"✅ Sanitized legacy module config keys in quickscale.yml",
|
|
738
|
+
fg="green",
|
|
739
|
+
)
|
|
740
|
+
if implied_modules:
|
|
741
|
+
click.secho(
|
|
742
|
+
"✅ Added implied module config to quickscale.yml: "
|
|
743
|
+
+ ", ".join(implied_modules),
|
|
744
|
+
fg="green",
|
|
745
|
+
)
|
|
711
746
|
_validate_module_prerequisites(qs_config)
|
|
712
747
|
return qs_config
|
|
713
748
|
except ConfigValidationError as e:
|
|
@@ -720,6 +755,64 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
|
720
755
|
|
|
721
756
|
def _validate_module_prerequisites(qs_config: QuickScaleConfig) -> None:
|
|
722
757
|
"""Validate actionable module-specific prerequisites before apply proceeds."""
|
|
758
|
+
if "orgs" in qs_config.modules and "auth" not in qs_config.modules:
|
|
759
|
+
click.secho(
|
|
760
|
+
"\n❌ Organizations requires the auth module before apply can continue:",
|
|
761
|
+
fg="red",
|
|
762
|
+
err=True,
|
|
763
|
+
)
|
|
764
|
+
click.echo(
|
|
765
|
+
" • Add 'auth' under modules in quickscale.yml before applying orgs.",
|
|
766
|
+
err=True,
|
|
767
|
+
)
|
|
768
|
+
click.echo(
|
|
769
|
+
"\n💡 Organizations relies on the QuickScale auth login flow and "
|
|
770
|
+
"django-allauth account adapter hooks. Re-run 'quickscale plan "
|
|
771
|
+
"--reconfigure --configure-modules' or edit quickscale.yml to add "
|
|
772
|
+
"auth, then re-run 'quickscale apply'.",
|
|
773
|
+
err=True,
|
|
774
|
+
)
|
|
775
|
+
raise click.Abort()
|
|
776
|
+
|
|
777
|
+
billing_config = qs_config.modules.get("billing")
|
|
778
|
+
if billing_config is not None:
|
|
779
|
+
billing_issues = validate_billing_module_options(billing_config.options or {})
|
|
780
|
+
if billing_issues:
|
|
781
|
+
click.secho(
|
|
782
|
+
"\n❌ Billing module configuration is incomplete for apply:",
|
|
783
|
+
fg="red",
|
|
784
|
+
err=True,
|
|
785
|
+
)
|
|
786
|
+
for issue in billing_issues:
|
|
787
|
+
click.echo(f" • {issue}", err=True)
|
|
788
|
+
click.echo(
|
|
789
|
+
"\n💡 Re-run 'quickscale plan --reconfigure --configure-modules' or "
|
|
790
|
+
"edit quickscale.yml to correct the billing option values. "
|
|
791
|
+
"Billing apply requires valid env-var references and a supported "
|
|
792
|
+
"currency code.",
|
|
793
|
+
err=True,
|
|
794
|
+
)
|
|
795
|
+
raise click.Abort()
|
|
796
|
+
|
|
797
|
+
if "auth" not in qs_config.modules:
|
|
798
|
+
click.secho(
|
|
799
|
+
"\n❌ Billing requires the auth module before apply can continue:",
|
|
800
|
+
fg="red",
|
|
801
|
+
err=True,
|
|
802
|
+
)
|
|
803
|
+
click.echo(
|
|
804
|
+
" • Add 'auth' under modules in quickscale.yml before applying billing.",
|
|
805
|
+
err=True,
|
|
806
|
+
)
|
|
807
|
+
click.echo(
|
|
808
|
+
"\n💡 Billing relies on the QuickScale auth login flow and "
|
|
809
|
+
"AUTH_USER_MODEL. Re-run 'quickscale plan --reconfigure "
|
|
810
|
+
"--configure-modules' or edit quickscale.yml to add auth, then "
|
|
811
|
+
"re-run 'quickscale apply'.",
|
|
812
|
+
err=True,
|
|
813
|
+
)
|
|
814
|
+
raise click.Abort()
|
|
815
|
+
|
|
723
816
|
_abort_for_not_ready_modules(
|
|
724
817
|
list(qs_config.modules.keys()), source="quickscale.yml"
|
|
725
818
|
)
|
|
@@ -1004,6 +1097,95 @@ def _sync_analytics_env_example(
|
|
|
1004
1097
|
return True
|
|
1005
1098
|
|
|
1006
1099
|
|
|
1100
|
+
def _render_billing_env_example_block(
|
|
1101
|
+
options: Mapping[str, Any] | None,
|
|
1102
|
+
) -> str:
|
|
1103
|
+
"""Render the managed billing section for `.env.example`."""
|
|
1104
|
+
resolved = resolve_billing_module_options(options)
|
|
1105
|
+
publishable_key_env_var = str(
|
|
1106
|
+
resolved.get(
|
|
1107
|
+
"publishable_key_env_var",
|
|
1108
|
+
DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR,
|
|
1109
|
+
)
|
|
1110
|
+
or DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR
|
|
1111
|
+
).strip()
|
|
1112
|
+
secret_key_env_var = str(
|
|
1113
|
+
resolved.get(
|
|
1114
|
+
"secret_key_env_var",
|
|
1115
|
+
DEFAULT_BILLING_SECRET_KEY_ENV_VAR,
|
|
1116
|
+
)
|
|
1117
|
+
or DEFAULT_BILLING_SECRET_KEY_ENV_VAR
|
|
1118
|
+
).strip()
|
|
1119
|
+
webhook_secret_env_var = str(
|
|
1120
|
+
resolved.get(
|
|
1121
|
+
"webhook_secret_env_var",
|
|
1122
|
+
DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR,
|
|
1123
|
+
)
|
|
1124
|
+
or DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR
|
|
1125
|
+
).strip()
|
|
1126
|
+
billing_currency = str(
|
|
1127
|
+
resolved.get("billing_currency", DEFAULT_BILLING_CURRENCY)
|
|
1128
|
+
or DEFAULT_BILLING_CURRENCY
|
|
1129
|
+
).strip()
|
|
1130
|
+
|
|
1131
|
+
return "\n".join(
|
|
1132
|
+
[
|
|
1133
|
+
"# QuickScale Billing (managed)",
|
|
1134
|
+
"# Runtime Stripe variables for the backend billing module.",
|
|
1135
|
+
f"# Billing currency from quickscale.yml: {billing_currency}",
|
|
1136
|
+
f"{publishable_key_env_var}=",
|
|
1137
|
+
f"{secret_key_env_var}=",
|
|
1138
|
+
f"{webhook_secret_env_var}=",
|
|
1139
|
+
"# End QuickScale Billing",
|
|
1140
|
+
]
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def _sync_billing_env_example(
|
|
1145
|
+
output_path: Path,
|
|
1146
|
+
qs_config: QuickScaleConfig,
|
|
1147
|
+
) -> bool:
|
|
1148
|
+
"""Keep `.env.example` aligned with the billing env-var names."""
|
|
1149
|
+
billing_config = qs_config.modules.get("billing")
|
|
1150
|
+
env_example_path = output_path / ".env.example"
|
|
1151
|
+
if billing_config is None or not env_example_path.exists():
|
|
1152
|
+
return True
|
|
1153
|
+
|
|
1154
|
+
start_marker = "# QuickScale Billing (managed)"
|
|
1155
|
+
end_marker = "# End QuickScale Billing"
|
|
1156
|
+
rendered_block = _render_billing_env_example_block(billing_config.options or {})
|
|
1157
|
+
|
|
1158
|
+
try:
|
|
1159
|
+
content = env_example_path.read_text()
|
|
1160
|
+
except OSError as e:
|
|
1161
|
+
click.secho(
|
|
1162
|
+
f"⚠️ Failed to read .env.example for billing wiring: {e}",
|
|
1163
|
+
fg="yellow",
|
|
1164
|
+
)
|
|
1165
|
+
return False
|
|
1166
|
+
|
|
1167
|
+
if start_marker in content and end_marker in content:
|
|
1168
|
+
before, remainder = content.split(start_marker, maxsplit=1)
|
|
1169
|
+
_, after = remainder.split(end_marker, maxsplit=1)
|
|
1170
|
+
replacement = rendered_block + after
|
|
1171
|
+
updated_content = before + replacement
|
|
1172
|
+
else:
|
|
1173
|
+
suffix = "" if content.endswith("\n") else "\n"
|
|
1174
|
+
updated_content = content + suffix + "\n" + rendered_block + "\n"
|
|
1175
|
+
|
|
1176
|
+
try:
|
|
1177
|
+
env_example_path.write_text(updated_content)
|
|
1178
|
+
except OSError as e:
|
|
1179
|
+
click.secho(
|
|
1180
|
+
f"⚠️ Failed to update .env.example for billing wiring: {e}",
|
|
1181
|
+
fg="yellow",
|
|
1182
|
+
)
|
|
1183
|
+
return False
|
|
1184
|
+
|
|
1185
|
+
click.secho("✅ Updated .env.example with billing env vars", fg="green")
|
|
1186
|
+
return True
|
|
1187
|
+
|
|
1188
|
+
|
|
1007
1189
|
def _normalize_backups_gitignore_entry(local_directory: str) -> str | None:
|
|
1008
1190
|
"""Return a safe repo-relative ignore entry for backup artifacts."""
|
|
1009
1191
|
if any(
|
|
@@ -2179,6 +2361,14 @@ def _execute_apply_steps(
|
|
|
2179
2361
|
reason="Unable to update .env.example with the configured analytics env-var names.",
|
|
2180
2362
|
)
|
|
2181
2363
|
|
|
2364
|
+
if not _sync_billing_env_example(ctx.output_path, ctx.qs_config):
|
|
2365
|
+
_abort_after_post_embed_failure(
|
|
2366
|
+
ctx,
|
|
2367
|
+
embedded_modules,
|
|
2368
|
+
failed_step="billing env example sync",
|
|
2369
|
+
reason="Unable to update .env.example with the configured billing env-var names.",
|
|
2370
|
+
)
|
|
2371
|
+
|
|
2182
2372
|
if not _sync_project_module_dependencies_for_apply(
|
|
2183
2373
|
ctx.output_path,
|
|
2184
2374
|
ctx.qs_config,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Helpers for materializing implicit module configs in CLI flows."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from quickscale_cli.commands.module_config import get_default_notifications_config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_implied_module_default_configs(
|
|
10
|
+
module_names: Collection[str],
|
|
11
|
+
) -> dict[str, dict[str, Any]]:
|
|
12
|
+
"""Return module config blocks that should be made explicit automatically."""
|
|
13
|
+
names = set(module_names)
|
|
14
|
+
implied_configs: dict[str, dict[str, Any]] = {}
|
|
15
|
+
|
|
16
|
+
if "billing" in names and "orgs" not in names:
|
|
17
|
+
implied_configs["orgs"] = {}
|
|
18
|
+
names.add("orgs")
|
|
19
|
+
|
|
20
|
+
if "orgs" in names and "notifications" not in names:
|
|
21
|
+
implied_configs["notifications"] = get_default_notifications_config()
|
|
22
|
+
|
|
23
|
+
return implied_configs
|
{quickscale_cli-0.84.0 → quickscale_cli-0.86.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"
|