quickscale-cli 0.76.0__tar.gz → 0.83.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.76.0 → quickscale_cli-0.83.0}/PKG-INFO +5 -4
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/README.md +2 -1
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/pyproject.toml +5 -4
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/__init__.py +8 -1
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/_version.py +1 -1
- quickscale_cli-0.83.0/src/quickscale_cli/analytics_contract.py +177 -0
- quickscale_cli-0.83.0/src/quickscale_cli/auth_contract.py +97 -0
- quickscale_cli-0.83.0/src/quickscale_cli/backups_contract.py +137 -0
- quickscale_cli-0.83.0/src/quickscale_cli/beta_migration.py +2451 -0
- quickscale_cli-0.83.0/src/quickscale_cli/commands/apply_command.py +2349 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/commands/development_commands.py +14 -3
- quickscale_cli-0.83.0/src/quickscale_cli/commands/dr_commands.py +1319 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/commands/module_commands.py +295 -57
- quickscale_cli-0.83.0/src/quickscale_cli/commands/module_config.py +1879 -0
- quickscale_cli-0.83.0/src/quickscale_cli/commands/module_wiring_specs.py +646 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/commands/plan_command.py +183 -43
- quickscale_cli-0.83.0/src/quickscale_cli/commands/remove_command.py +704 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/commands/status_command.py +127 -27
- quickscale_cli-0.83.0/src/quickscale_cli/crm_contract.py +43 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/main.py +2 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/module_catalog.py +44 -0
- quickscale_cli-0.83.0/src/quickscale_cli/notifications_contract.py +306 -0
- quickscale_cli-0.83.0/src/quickscale_cli/schema/__init__.py +56 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/schema/config_schema.py +141 -5
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/schema/state_schema.py +18 -4
- quickscale_cli-0.83.0/src/quickscale_cli/social_contract.py +483 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/utils/dependency_utils.py +5 -1
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/utils/docker_utils.py +12 -4
- quickscale_cli-0.83.0/src/quickscale_cli/utils/module_dependency_sync.py +364 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/utils/module_wiring_manager.py +38 -11
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/utils/project_identity.py +54 -12
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/utils/project_manager.py +8 -2
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/utils/railway_utils.py +80 -16
- quickscale_cli-0.76.0/src/quickscale_cli/commands/apply_command.py +0 -1045
- quickscale_cli-0.76.0/src/quickscale_cli/commands/module_config.py +0 -1334
- quickscale_cli-0.76.0/src/quickscale_cli/commands/module_wiring_specs.py +0 -291
- quickscale_cli-0.76.0/src/quickscale_cli/commands/remove_command.py +0 -264
- quickscale_cli-0.76.0/src/quickscale_cli/schema/__init__.py +0 -40
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/commands/deployment_commands.py +0 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/schema/delta.py +0 -0
- {quickscale_cli-0.76.0 → quickscale_cli-0.83.0}/src/quickscale_cli/utils/__init__.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickscale-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.83.0
|
|
4
4
|
Summary: Command-line interface for QuickScale Django project generator
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Keywords: django,cli,project-generator,saas
|
|
7
7
|
Author: Experto AI
|
|
8
8
|
Author-email: victor@experto.ai
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.12,<3.14
|
|
10
10
|
Classifier: Development Status :: 3 - Alpha
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
14
14
|
Classifier: Framework :: Django
|
|
15
15
|
Classifier: Framework :: Django :: 6.0
|
|
16
16
|
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
17
|
-
Requires-Dist: quickscale-core (>=0.
|
|
17
|
+
Requires-Dist: quickscale-core (>=0.83.0,<0.84.0)
|
|
18
18
|
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
19
19
|
Project-URL: Homepage, https://github.com/Experto-AI/quickscale
|
|
20
20
|
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
@@ -24,11 +24,12 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
Command-line interface package for QuickScale.
|
|
26
26
|
|
|
27
|
-
This package exposes the `quickscale` command used to plan, apply, inspect, and operate generated projects.
|
|
27
|
+
This package exposes the `quickscale` command used to plan, apply, inspect, recover, and operate generated projects.
|
|
28
28
|
|
|
29
29
|
## Command groups
|
|
30
30
|
|
|
31
31
|
- Project lifecycle: `plan`, `apply`, `status`, `remove`
|
|
32
|
+
- Disaster recovery & promotion: `dr capture`, `dr plan`, `dr execute`, `dr report`
|
|
32
33
|
- Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
|
|
33
34
|
- Deployment: `deploy`
|
|
34
35
|
- Module workflows: `update`, `push`
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Command-line interface package for QuickScale.
|
|
4
4
|
|
|
5
|
-
This package exposes the `quickscale` command used to plan, apply, inspect, and operate generated projects.
|
|
5
|
+
This package exposes the `quickscale` command used to plan, apply, inspect, recover, and operate generated projects.
|
|
6
6
|
|
|
7
7
|
## Command groups
|
|
8
8
|
|
|
9
9
|
- Project lifecycle: `plan`, `apply`, `status`, `remove`
|
|
10
|
+
- Disaster recovery & promotion: `dr capture`, `dr plan`, `dr execute`, `dr report`
|
|
10
11
|
- Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
|
|
11
12
|
- Deployment: `deploy`
|
|
12
13
|
- Module workflows: `update`, `push`
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.83.0"
|
|
4
4
|
description = "Command-line interface for QuickScale Django project generator"
|
|
5
|
-
requires-python = ">=3.
|
|
5
|
+
requires-python = ">=3.12,<3.14"
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
authors = [{name = "Experto AI", email = "victor@experto.ai"}]
|
|
@@ -29,9 +29,9 @@ quickscale = "quickscale_cli.main:cli"
|
|
|
29
29
|
packages = [{include = "quickscale_cli", from = "src"}]
|
|
30
30
|
|
|
31
31
|
[tool.poetry.dependencies]
|
|
32
|
-
python = "
|
|
32
|
+
python = ">=3.12,<3.14"
|
|
33
33
|
click = "^8.3.1"
|
|
34
|
-
quickscale-core = "^0.
|
|
34
|
+
quickscale-core = "^0.83.0"
|
|
35
35
|
|
|
36
36
|
[tool.poetry.group.dev.dependencies]
|
|
37
37
|
# Dev dependencies centralized in root pyproject.toml
|
|
@@ -43,6 +43,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
43
43
|
|
|
44
44
|
[tool.pytest.ini_options]
|
|
45
45
|
testpaths = ["tests"]
|
|
46
|
+
pythonpath = ["src"]
|
|
46
47
|
python_files = ["test_*.py"]
|
|
47
48
|
python_classes = ["Test*"]
|
|
48
49
|
python_functions = ["test_*"]
|
|
@@ -7,15 +7,22 @@ stays in sync with other packages when the release is bumped in one place.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import logging
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
__author__ = "Experto AI"
|
|
13
14
|
__email__ = "victor@experto.ai"
|
|
14
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
15
18
|
# Prefer an embedded package-level `_version.py` if it exists (build step).
|
|
16
19
|
try:
|
|
17
20
|
from ._version import __version__
|
|
18
|
-
except Exception:
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
logger.warning(
|
|
23
|
+
"Failed to import embedded CLI version; falling back to VERSION file or 0.0.0: %s",
|
|
24
|
+
exc,
|
|
25
|
+
)
|
|
19
26
|
_root = Path(__file__).resolve().parents[3]
|
|
20
27
|
_version_file = _root / "VERSION"
|
|
21
28
|
if _version_file.exists():
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by scripts/version_tool.sh
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.83.0"
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Shared analytics-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
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
ANALYTICS_PROVIDER_POSTHOG = "posthog"
|
|
11
|
+
ANALYTICS_PROVIDERS = (ANALYTICS_PROVIDER_POSTHOG,)
|
|
12
|
+
|
|
13
|
+
DEFAULT_ANALYTICS_POSTHOG_API_KEY_ENV_VAR = "POSTHOG_API_KEY"
|
|
14
|
+
DEFAULT_ANALYTICS_POSTHOG_HOST_ENV_VAR = "POSTHOG_HOST"
|
|
15
|
+
|
|
16
|
+
ANALYTICS_POSTHOG_DEFAULT_HOST = "https://us.i.posthog.com"
|
|
17
|
+
ANALYTICS_POSTHOG_EU_HOST = "https://eu.i.posthog.com"
|
|
18
|
+
|
|
19
|
+
ANALYTICS_EVENT_PAGEVIEW = "$pageview"
|
|
20
|
+
ANALYTICS_EVENT_FORM_SUBMIT = "form_submit"
|
|
21
|
+
ANALYTICS_EVENT_SOCIAL_LINK_CLICK = "social_link_click"
|
|
22
|
+
|
|
23
|
+
_ANALYTICS_ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def default_analytics_module_options() -> dict[str, Any]:
|
|
27
|
+
"""Return the default planner/apply contract for analytics."""
|
|
28
|
+
return {
|
|
29
|
+
"enabled": True,
|
|
30
|
+
"provider": ANALYTICS_PROVIDER_POSTHOG,
|
|
31
|
+
"posthog_api_key_env_var": DEFAULT_ANALYTICS_POSTHOG_API_KEY_ENV_VAR,
|
|
32
|
+
"posthog_host_env_var": DEFAULT_ANALYTICS_POSTHOG_HOST_ENV_VAR,
|
|
33
|
+
"posthog_host": ANALYTICS_POSTHOG_DEFAULT_HOST,
|
|
34
|
+
"exclude_debug": True,
|
|
35
|
+
"exclude_staff": False,
|
|
36
|
+
"anonymous_by_default": True,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_provider(value: Any) -> str:
|
|
41
|
+
return str(value).strip().lower()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _normalize_posthog_host(value: Any) -> str:
|
|
45
|
+
candidate = str(value).strip()
|
|
46
|
+
if not candidate:
|
|
47
|
+
return ""
|
|
48
|
+
if not candidate.startswith(("http://", "https://")):
|
|
49
|
+
candidate = "https://" + candidate.lstrip("/")
|
|
50
|
+
return candidate.rstrip("/")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_analytics_module_options(
|
|
54
|
+
options: Mapping[str, Any] | None,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
"""Return analytics options with normalized provider and host fields."""
|
|
57
|
+
normalized = dict(options or {})
|
|
58
|
+
|
|
59
|
+
if "provider" in normalized:
|
|
60
|
+
normalized["provider"] = _normalize_provider(normalized["provider"])
|
|
61
|
+
|
|
62
|
+
for option_name in ("posthog_api_key_env_var", "posthog_host_env_var"):
|
|
63
|
+
if option_name in normalized:
|
|
64
|
+
normalized[option_name] = str(normalized[option_name]).strip()
|
|
65
|
+
|
|
66
|
+
if "posthog_host" in normalized:
|
|
67
|
+
normalized["posthog_host"] = _normalize_posthog_host(normalized["posthog_host"])
|
|
68
|
+
|
|
69
|
+
return normalized
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_analytics_module_options(
|
|
73
|
+
options: Mapping[str, Any] | None,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""Merge analytics options with defaults and normalized overrides."""
|
|
76
|
+
resolved = default_analytics_module_options()
|
|
77
|
+
resolved.update(normalize_analytics_module_options(options))
|
|
78
|
+
resolved["provider"] = _normalize_provider(resolved["provider"])
|
|
79
|
+
resolved["posthog_api_key_env_var"] = str(
|
|
80
|
+
resolved["posthog_api_key_env_var"]
|
|
81
|
+
).strip()
|
|
82
|
+
resolved["posthog_host_env_var"] = str(resolved["posthog_host_env_var"]).strip()
|
|
83
|
+
resolved["posthog_host"] = _normalize_posthog_host(resolved["posthog_host"])
|
|
84
|
+
return resolved
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_analytics_env_var_reference(option_name: str, value: Any) -> str | None:
|
|
88
|
+
"""Validate an analytics env-var reference field."""
|
|
89
|
+
candidate = str(value).strip()
|
|
90
|
+
if not candidate:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
qualified_option = f"modules.analytics.{option_name}"
|
|
94
|
+
if not _ANALYTICS_ENV_VAR_NAME_PATTERN.fullmatch(candidate):
|
|
95
|
+
return (
|
|
96
|
+
f"{qualified_option} must be an environment variable name matching "
|
|
97
|
+
"^[A-Z][A-Z0-9_]*$"
|
|
98
|
+
)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_valid_posthog_host(value: str) -> bool:
|
|
103
|
+
candidate = value.strip()
|
|
104
|
+
if not candidate:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
parsed = urlsplit(candidate)
|
|
108
|
+
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def validate_analytics_module_options(options: Mapping[str, Any] | None) -> list[str]:
|
|
112
|
+
"""Return validation issues for analytics module options."""
|
|
113
|
+
resolved = resolve_analytics_module_options(options)
|
|
114
|
+
issues: list[str] = []
|
|
115
|
+
|
|
116
|
+
provider = str(resolved.get("provider", "")).strip().lower()
|
|
117
|
+
if provider not in ANALYTICS_PROVIDERS:
|
|
118
|
+
issues.append(
|
|
119
|
+
"modules.analytics.provider must be one of: "
|
|
120
|
+
+ ", ".join(ANALYTICS_PROVIDERS)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for option_name in ("posthog_api_key_env_var", "posthog_host_env_var"):
|
|
124
|
+
issue = validate_analytics_env_var_reference(
|
|
125
|
+
option_name,
|
|
126
|
+
resolved.get(option_name, ""),
|
|
127
|
+
)
|
|
128
|
+
if issue:
|
|
129
|
+
issues.append(issue)
|
|
130
|
+
|
|
131
|
+
if not _is_valid_posthog_host(str(resolved.get("posthog_host", ""))):
|
|
132
|
+
issues.append("modules.analytics.posthog_host must be an absolute http(s) URL")
|
|
133
|
+
|
|
134
|
+
for option_name in (
|
|
135
|
+
"enabled",
|
|
136
|
+
"exclude_debug",
|
|
137
|
+
"exclude_staff",
|
|
138
|
+
"anonymous_by_default",
|
|
139
|
+
):
|
|
140
|
+
if not isinstance(resolved.get(option_name), bool):
|
|
141
|
+
issues.append(f"modules.analytics.{option_name} must be a boolean")
|
|
142
|
+
|
|
143
|
+
return issues
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def analytics_production_targeted(options: Mapping[str, Any] | None) -> bool:
|
|
147
|
+
"""Return whether analytics is configured for live runtime capture."""
|
|
148
|
+
resolved = resolve_analytics_module_options(options)
|
|
149
|
+
if not bool(resolved.get("enabled", True)):
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
api_key_env_var = str(resolved.get("posthog_api_key_env_var", "")).strip()
|
|
153
|
+
return not bool(
|
|
154
|
+
validate_analytics_env_var_reference(
|
|
155
|
+
"posthog_api_key_env_var",
|
|
156
|
+
api_key_env_var,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = [
|
|
162
|
+
"ANALYTICS_EVENT_FORM_SUBMIT",
|
|
163
|
+
"ANALYTICS_EVENT_PAGEVIEW",
|
|
164
|
+
"ANALYTICS_EVENT_SOCIAL_LINK_CLICK",
|
|
165
|
+
"ANALYTICS_POSTHOG_DEFAULT_HOST",
|
|
166
|
+
"ANALYTICS_POSTHOG_EU_HOST",
|
|
167
|
+
"ANALYTICS_PROVIDER_POSTHOG",
|
|
168
|
+
"ANALYTICS_PROVIDERS",
|
|
169
|
+
"DEFAULT_ANALYTICS_POSTHOG_API_KEY_ENV_VAR",
|
|
170
|
+
"DEFAULT_ANALYTICS_POSTHOG_HOST_ENV_VAR",
|
|
171
|
+
"analytics_production_targeted",
|
|
172
|
+
"default_analytics_module_options",
|
|
173
|
+
"normalize_analytics_module_options",
|
|
174
|
+
"resolve_analytics_module_options",
|
|
175
|
+
"validate_analytics_env_var_reference",
|
|
176
|
+
"validate_analytics_module_options",
|
|
177
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Shared auth-module configuration helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
AUTH_REGISTRATION_ENABLED_OPTION = "registration_enabled"
|
|
9
|
+
AUTH_EMAIL_VERIFICATION_OPTION = "email_verification"
|
|
10
|
+
AUTH_AUTHENTICATION_METHOD_OPTION = "authentication_method"
|
|
11
|
+
AUTH_SESSION_COOKIE_AGE_OPTION = "session_cookie_age"
|
|
12
|
+
|
|
13
|
+
AUTH_EMAIL_VERIFICATION_VALUES = ("none", "optional", "mandatory")
|
|
14
|
+
AUTH_AUTHENTICATION_METHOD_VALUES = ("email", "username", "both")
|
|
15
|
+
CANONICAL_AUTH_MODULE_OPTION_KEYS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
AUTH_REGISTRATION_ENABLED_OPTION,
|
|
18
|
+
AUTH_EMAIL_VERIFICATION_OPTION,
|
|
19
|
+
AUTH_AUTHENTICATION_METHOD_OPTION,
|
|
20
|
+
AUTH_SESSION_COOKIE_AGE_OPTION,
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
LEGACY_AUTH_ALLOW_REGISTRATION_OPTION = "allow_registration"
|
|
25
|
+
LEGACY_AUTH_SOCIAL_PROVIDERS_OPTION = "social_providers"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_auth_module_options() -> dict[str, Any]:
|
|
29
|
+
"""Return the default planner/apply contract for auth."""
|
|
30
|
+
return {
|
|
31
|
+
AUTH_REGISTRATION_ENABLED_OPTION: True,
|
|
32
|
+
AUTH_EMAIL_VERIFICATION_OPTION: "none",
|
|
33
|
+
AUTH_AUTHENTICATION_METHOD_OPTION: "email",
|
|
34
|
+
AUTH_SESSION_COOKIE_AGE_OPTION: 1209600,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def format_auth_desired_config_contract() -> str:
|
|
39
|
+
"""Return concise remediation text for canonical auth desired config."""
|
|
40
|
+
return "\n".join(
|
|
41
|
+
[
|
|
42
|
+
"Canonical auth keys/value shapes:",
|
|
43
|
+
"modules.auth.registration_enabled: true|false",
|
|
44
|
+
"modules.auth.email_verification: none|optional|mandatory",
|
|
45
|
+
"modules.auth.authentication_method: email|username|both",
|
|
46
|
+
"modules.auth.session_cookie_age: <positive integer seconds> # optional",
|
|
47
|
+
(
|
|
48
|
+
"Remove legacy keys like modules.auth.allow_registration and "
|
|
49
|
+
"modules.auth.social_providers."
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normalize_auth_module_options(
|
|
56
|
+
options: Mapping[str, Any] | None,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""Return auth options with legacy keys normalized or removed."""
|
|
59
|
+
normalized = dict(options or {})
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
AUTH_REGISTRATION_ENABLED_OPTION not in normalized
|
|
63
|
+
and LEGACY_AUTH_ALLOW_REGISTRATION_OPTION in normalized
|
|
64
|
+
):
|
|
65
|
+
normalized[AUTH_REGISTRATION_ENABLED_OPTION] = normalized[
|
|
66
|
+
LEGACY_AUTH_ALLOW_REGISTRATION_OPTION
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
normalized.pop(LEGACY_AUTH_ALLOW_REGISTRATION_OPTION, None)
|
|
70
|
+
normalized.pop(LEGACY_AUTH_SOCIAL_PROVIDERS_OPTION, None)
|
|
71
|
+
return normalized
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def resolve_auth_module_options(
|
|
75
|
+
options: Mapping[str, Any] | None,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""Merge auth options with defaults and normalized overrides."""
|
|
78
|
+
resolved = default_auth_module_options()
|
|
79
|
+
resolved.update(normalize_auth_module_options(options))
|
|
80
|
+
return resolved
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"AUTH_AUTHENTICATION_METHOD_OPTION",
|
|
85
|
+
"AUTH_AUTHENTICATION_METHOD_VALUES",
|
|
86
|
+
"AUTH_EMAIL_VERIFICATION_OPTION",
|
|
87
|
+
"AUTH_EMAIL_VERIFICATION_VALUES",
|
|
88
|
+
"AUTH_REGISTRATION_ENABLED_OPTION",
|
|
89
|
+
"AUTH_SESSION_COOKIE_AGE_OPTION",
|
|
90
|
+
"CANONICAL_AUTH_MODULE_OPTION_KEYS",
|
|
91
|
+
"LEGACY_AUTH_ALLOW_REGISTRATION_OPTION",
|
|
92
|
+
"LEGACY_AUTH_SOCIAL_PROVIDERS_OPTION",
|
|
93
|
+
"default_auth_module_options",
|
|
94
|
+
"format_auth_desired_config_contract",
|
|
95
|
+
"normalize_auth_module_options",
|
|
96
|
+
"resolve_auth_module_options",
|
|
97
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Shared module configuration helpers.
|
|
2
|
+
|
|
3
|
+
This module centralizes the non-secret configuration contract used by the
|
|
4
|
+
planner, apply flow, and state persistence for module compatibility helpers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from quickscale_cli.analytics_contract import normalize_analytics_module_options
|
|
14
|
+
from quickscale_cli.auth_contract import normalize_auth_module_options
|
|
15
|
+
from quickscale_cli.crm_contract import normalize_crm_module_options
|
|
16
|
+
from quickscale_cli.notifications_contract import normalize_notifications_module_options
|
|
17
|
+
from quickscale_cli.social_contract import normalize_social_module_options
|
|
18
|
+
|
|
19
|
+
DEFAULT_BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR = "QUICKSCALE_BACKUPS_REMOTE_ACCESS_KEY_ID"
|
|
20
|
+
DEFAULT_BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR = (
|
|
21
|
+
"QUICKSCALE_BACKUPS_REMOTE_SECRET_ACCESS_KEY"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION = "remote_access_key_id_env_var"
|
|
25
|
+
BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION = "remote_secret_access_key_env_var"
|
|
26
|
+
|
|
27
|
+
_LEGACY_BACKUPS_SECRET_OPTIONS = {
|
|
28
|
+
"remote_access_key_id": DEFAULT_BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR,
|
|
29
|
+
"remote_secret_access_key": DEFAULT_BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_BACKUPS_ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
33
|
+
_LIKELY_AWS_ACCESS_KEY_ID_PATTERN = re.compile(r"^(?:AKIA|ASIA)[A-Z0-9]{16}$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_backups_module_options(
|
|
37
|
+
options: Mapping[str, Any] | None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Return backups options with legacy raw-secret keys removed.
|
|
40
|
+
|
|
41
|
+
Legacy raw credential values are converted into conventional environment-variable
|
|
42
|
+
references so downstream persistence layers never re-store the secret values.
|
|
43
|
+
"""
|
|
44
|
+
normalized = dict(options or {})
|
|
45
|
+
|
|
46
|
+
legacy_access_key_id = str(normalized.pop("remote_access_key_id", "")).strip()
|
|
47
|
+
legacy_secret_access_key = str(
|
|
48
|
+
normalized.pop("remote_secret_access_key", "")
|
|
49
|
+
).strip()
|
|
50
|
+
|
|
51
|
+
access_key_env_var = str(
|
|
52
|
+
normalized.get(BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION, "")
|
|
53
|
+
).strip()
|
|
54
|
+
secret_access_key_env_var = str(
|
|
55
|
+
normalized.get(BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION, "")
|
|
56
|
+
).strip()
|
|
57
|
+
|
|
58
|
+
if legacy_access_key_id and not access_key_env_var:
|
|
59
|
+
normalized[BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION] = (
|
|
60
|
+
DEFAULT_BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR
|
|
61
|
+
)
|
|
62
|
+
if legacy_secret_access_key and not secret_access_key_env_var:
|
|
63
|
+
normalized[BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION] = (
|
|
64
|
+
DEFAULT_BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return normalized
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def has_legacy_backups_secret_values(options: Mapping[str, Any] | None) -> bool:
|
|
71
|
+
"""Return whether backups options still include legacy raw-secret keys."""
|
|
72
|
+
if not options:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
for option_name in _LEGACY_BACKUPS_SECRET_OPTIONS:
|
|
76
|
+
if str(options.get(option_name, "")).strip():
|
|
77
|
+
return True
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_backups_env_var_reference(option_name: str, value: Any) -> str | None:
|
|
82
|
+
"""Validate a backups env-var reference field.
|
|
83
|
+
|
|
84
|
+
Returns an actionable error string when the value is not a safe environment
|
|
85
|
+
variable name or appears to be a literal credential value.
|
|
86
|
+
"""
|
|
87
|
+
candidate = str(value).strip()
|
|
88
|
+
if not candidate:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
qualified_option = f"modules.backups.{option_name}"
|
|
92
|
+
if not _BACKUPS_ENV_VAR_NAME_PATTERN.fullmatch(candidate):
|
|
93
|
+
return (
|
|
94
|
+
f"{qualified_option} must be an environment variable name matching "
|
|
95
|
+
"^[A-Z][A-Z0-9_]*$"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if option_name == BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION:
|
|
99
|
+
if _LIKELY_AWS_ACCESS_KEY_ID_PATTERN.fullmatch(candidate):
|
|
100
|
+
return (
|
|
101
|
+
f"{qualified_option} must reference an environment variable name, "
|
|
102
|
+
"not a literal AWS access key id"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def sanitize_module_options(
|
|
109
|
+
module_name: str,
|
|
110
|
+
options: Mapping[str, Any] | None,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""Return module options safe for config/state persistence."""
|
|
113
|
+
if module_name == "analytics":
|
|
114
|
+
return normalize_analytics_module_options(options)
|
|
115
|
+
if module_name == "auth":
|
|
116
|
+
return normalize_auth_module_options(options)
|
|
117
|
+
if module_name == "backups":
|
|
118
|
+
return normalize_backups_module_options(options)
|
|
119
|
+
if module_name == "crm":
|
|
120
|
+
return normalize_crm_module_options(options)
|
|
121
|
+
if module_name == "notifications":
|
|
122
|
+
return normalize_notifications_module_options(options)
|
|
123
|
+
if module_name == "social":
|
|
124
|
+
return normalize_social_module_options(dict(options or {}))
|
|
125
|
+
return dict(options or {})
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION",
|
|
130
|
+
"BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION",
|
|
131
|
+
"DEFAULT_BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR",
|
|
132
|
+
"DEFAULT_BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR",
|
|
133
|
+
"has_legacy_backups_secret_values",
|
|
134
|
+
"normalize_backups_module_options",
|
|
135
|
+
"sanitize_module_options",
|
|
136
|
+
"validate_backups_env_var_reference",
|
|
137
|
+
]
|