quickscale-cli 0.79.0__tar.gz → 0.84.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.
Files changed (39) hide show
  1. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/PKG-INFO +6 -4
  2. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/README.md +2 -1
  3. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/pyproject.toml +6 -4
  4. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/__init__.py +8 -1
  5. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/_version.py +1 -1
  6. quickscale_cli-0.84.0/src/quickscale_cli/analytics_contract.py +177 -0
  7. quickscale_cli-0.84.0/src/quickscale_cli/auth_contract.py +97 -0
  8. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/backups_contract.py +11 -2
  9. quickscale_cli-0.84.0/src/quickscale_cli/beta_migration.py +2451 -0
  10. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/apply_command.py +1019 -160
  11. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/development_commands.py +47 -11
  12. quickscale_cli-0.84.0/src/quickscale_cli/commands/dr_commands.py +1319 -0
  13. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/module_commands.py +296 -67
  14. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/module_config.py +351 -411
  15. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/module_wiring_specs.py +51 -39
  16. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/plan_command.py +180 -43
  17. quickscale_cli-0.84.0/src/quickscale_cli/commands/remove_command.py +704 -0
  18. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/status_command.py +127 -27
  19. quickscale_cli-0.84.0/src/quickscale_cli/crm_contract.py +43 -0
  20. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/main.py +2 -0
  21. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/module_catalog.py +29 -0
  22. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/notifications_contract.py +13 -3
  23. quickscale_cli-0.84.0/src/quickscale_cli/schema/__init__.py +56 -0
  24. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/schema/config_schema.py +141 -5
  25. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/schema/state_schema.py +7 -2
  26. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/social_contract.py +1 -1
  27. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/utils/dependency_utils.py +8 -25
  28. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/utils/docker_utils.py +14 -12
  29. quickscale_cli-0.84.0/src/quickscale_cli/utils/module_dependency_sync.py +364 -0
  30. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/utils/module_wiring_manager.py +31 -10
  31. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/utils/project_identity.py +54 -12
  32. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/utils/project_manager.py +8 -2
  33. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/utils/railway_utils.py +30 -6
  34. quickscale_cli-0.79.0/src/quickscale_cli/commands/remove_command.py +0 -264
  35. quickscale_cli-0.79.0/src/quickscale_cli/schema/__init__.py +0 -40
  36. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/__init__.py +0 -0
  37. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/commands/deployment_commands.py +0 -0
  38. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/schema/delta.py +0 -0
  39. {quickscale_cli-0.79.0 → quickscale_cli-0.84.0}/src/quickscale_cli/utils/__init__.py +0 -0
@@ -1,20 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickscale-cli
3
- Version: 0.79.0
3
+ Version: 0.84.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.14,<4.0
9
+ Requires-Python: >=3.13,<3.15
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
13
14
  Classifier: Programming Language :: Python :: 3.14
14
15
  Classifier: Framework :: Django
15
16
  Classifier: Framework :: Django :: 6.0
16
17
  Requires-Dist: click (>=8.3.1,<9.0.0)
17
- Requires-Dist: quickscale-core (>=0.79.0,<0.80.0)
18
+ Requires-Dist: quickscale-core (>=0.84.0,<0.85.0)
18
19
  Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
19
20
  Project-URL: Homepage, https://github.com/Experto-AI/quickscale
20
21
  Project-URL: Repository, https://github.com/Experto-AI/quickscale
@@ -24,11 +25,12 @@ Description-Content-Type: text/markdown
24
25
 
25
26
  Command-line interface package for QuickScale.
26
27
 
27
- This package exposes the `quickscale` command used to plan, apply, inspect, and operate generated projects.
28
+ This package exposes the `quickscale` command used to plan, apply, inspect, recover, and operate generated projects.
28
29
 
29
30
  ## Command groups
30
31
 
31
32
  - Project lifecycle: `plan`, `apply`, `status`, `remove`
33
+ - Disaster recovery & promotion: `dr capture`, `dr plan`, `dr execute`, `dr report`
32
34
  - Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
33
35
  - Deployment: `deploy`
34
36
  - 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.79.0"
3
+ version = "0.84.0"
4
4
  description = "Command-line interface for QuickScale Django project generator"
5
- requires-python = ">=3.14,<4.0"
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.14"
33
+ python = ">=3.13,<3.15"
33
34
  click = "^8.3.1"
34
- quickscale-core = "^0.79.0"
35
+ quickscale-core = "^0.84.0"
35
36
 
36
37
  [tool.poetry.group.dev.dependencies]
37
38
  # Dev dependencies centralized in root pyproject.toml
@@ -43,6 +44,7 @@ build-backend = "poetry.core.masonry.api"
43
44
 
44
45
  [tool.pytest.ini_options]
45
46
  testpaths = ["tests"]
47
+ pythonpath = ["src"]
46
48
  python_files = ["test_*.py"]
47
49
  python_classes = ["Test*"]
48
50
  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.79.0"
2
+ __version__ = "0.84.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
+ ]
@@ -1,7 +1,7 @@
1
- """Shared backups-module configuration helpers.
1
+ """Shared module configuration helpers.
2
2
 
3
3
  This module centralizes the non-secret configuration contract used by the
4
- planner, apply flow, and state persistence for the backups module.
4
+ planner, apply flow, and state persistence for module compatibility helpers.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -10,6 +10,9 @@ from collections.abc import Mapping
10
10
  import re
11
11
  from typing import Any
12
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
13
16
  from quickscale_cli.notifications_contract import normalize_notifications_module_options
14
17
  from quickscale_cli.social_contract import normalize_social_module_options
15
18
 
@@ -107,8 +110,14 @@ def sanitize_module_options(
107
110
  options: Mapping[str, Any] | None,
108
111
  ) -> dict[str, Any]:
109
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)
110
117
  if module_name == "backups":
111
118
  return normalize_backups_module_options(options)
119
+ if module_name == "crm":
120
+ return normalize_crm_module_options(options)
112
121
  if module_name == "notifications":
113
122
  return normalize_notifications_module_options(options)
114
123
  if module_name == "social":