Coverage for frappe_manager / metadata_manager.py: 50%
136 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:13 +0530
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:13 +0530
1from pathlib import Path
3import tomlkit
4from pydantic import BaseModel, EmailStr, Field
6from frappe_manager import CLI_FM_CONFIG_PATH
7from frappe_manager.migration_manager.version import Version
8from frappe_manager.utils.helpers import get_current_fm_version
11class FMCloudflareConfig(BaseModel):
12 """Cloudflare DNS API credentials for DNS-01 challenge."""
14 email: EmailStr | None = Field(None, description="Cloudflare account email (required for Global API Key).")
15 api_token: str | None = Field(None, description="Cloudflare API Token (recommended - scoped permissions).")
16 api_key: str | None = Field(None, description="Cloudflare Global API Key (legacy - full account access).")
18 @property
19 def exists(self) -> bool:
20 """Check if any Cloudflare credentials are configured."""
21 return bool(self.api_token or self.api_key)
23 def get_toml_doc(self):
24 model_dict = self.model_dump(exclude_none=True)
25 toml_doc = tomlkit.document()
27 for key, value in model_dict.items():
28 if isinstance(value, Path):
29 toml_doc[key] = str(value.absolute())
30 else:
31 toml_doc[key] = value
32 return toml_doc
34 @classmethod
35 def import_from_toml_doc(cls, toml_doc):
36 config_object = cls(**toml_doc)
37 return config_object
40class FMLetsencryptConfig(BaseModel):
41 """Let's Encrypt configuration for certificate registration."""
43 email: EmailStr | None = Field(
44 None,
45 description="Email for Let's Encrypt certificate registration and notifications.",
46 )
48 @property
49 def exists(self) -> bool:
50 """Check if Let's Encrypt email is configured."""
51 return bool(self.email and self.email != "dummy@fm.fm")
53 def get_toml_doc(self):
54 model_dict = self.model_dump(exclude_none=True)
55 toml_doc = tomlkit.document()
57 for key, value in model_dict.items():
58 if isinstance(value, Path):
59 toml_doc[key] = str(value.absolute())
60 else:
61 toml_doc[key] = value
62 return toml_doc
64 @classmethod
65 def import_from_toml_doc(cls, toml_doc):
66 config_object = cls(**toml_doc)
67 return config_object
70class FMValidationConfig(BaseModel):
71 """Validation settings for Frappe Manager operations."""
73 enforce_domain_uniqueness: bool = Field(default=True, description="Enforce domain uniqueness across benches")
75 def get_toml_doc(self):
76 model_dict = self.model_dump(exclude_none=True)
77 toml_doc = tomlkit.document()
78 for key, value in model_dict.items():
79 toml_doc[key] = value
80 return toml_doc
82 @classmethod
83 def import_from_toml_doc(cls, toml_doc):
84 return cls(**toml_doc)
87class FMLogsConfig(BaseModel):
88 """Logging configuration for file and console output."""
90 file_level: str = Field(
91 default="DEBUG",
92 description="Log level for file logs (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
93 )
95 def get_toml_doc(self):
96 model_dict = self.model_dump(exclude_none=True)
97 toml_doc = tomlkit.document()
98 for key, value in model_dict.items():
99 toml_doc[key] = value
100 return toml_doc
102 @classmethod
103 def import_from_toml_doc(cls, toml_doc):
104 return cls(**toml_doc)
107class FMConfigManager(BaseModel):
108 root_path: Path
109 version: Version
110 cloudflare: FMCloudflareConfig = Field(default=FMCloudflareConfig())
111 ngrok_auth_token: str | None = Field(None, description="Ngrok authentication token")
112 validation: FMValidationConfig = Field(default=FMValidationConfig())
113 logs: FMLogsConfig = Field(default=FMLogsConfig())
115 def __init__(self, **data):
116 super().__init__(**data)
117 self._raw_config = {}
119 def get_system_migration_version(self) -> Version:
120 """Get version system is migrated to."""
121 if hasattr(self, "_raw_config") and "migration_state" in self._raw_config:
122 version_str = self._raw_config["migration_state"].get("system_migrated_to")
123 if version_str:
124 return Version(version_str)
125 return self.version
127 def set_system_migration_version(self, version: Version) -> None:
128 """Update system migration version."""
129 if not hasattr(self, "_raw_config"):
130 self._raw_config = {}
132 if "migration_state" not in self._raw_config:
133 self._raw_config["migration_state"] = {}
135 self._raw_config["migration_state"]["system_migrated_to"] = str(version.version)
136 self.export_to_toml()
138 def _ensure_migration_state(self) -> None:
139 """Ensure migration_state exists in config."""
140 if not hasattr(self, "_raw_config"):
141 self._raw_config = {}
143 if "migration_state" not in self._raw_config:
144 self._raw_config["migration_state"] = {
145 "system_migrated_to": str(self.version.version),
146 }
147 self.export_to_toml()
149 _config_data: dict | None = None
151 def export_to_toml(self, path: Path = CLI_FM_CONFIG_PATH) -> None:
152 exclude = {"root_path"}
154 if not self.cloudflare.exists:
155 exclude.add("cloudflare")
157 fm_config_dict = self.model_dump(exclude=exclude, exclude_none=True)
159 fm_config_dict["version"] = self.version.version
161 if hasattr(self, "_raw_config") and "migration_state" in self._raw_config:
162 fm_config_dict["migration_state"] = self._raw_config["migration_state"]
164 toml_doc = tomlkit.document()
166 for key, value in fm_config_dict.items():
167 toml_doc[key] = value
169 try:
170 with open(path, "w") as f:
171 f.write(tomlkit.dumps(toml_doc))
172 except Exception as e:
173 raise RuntimeError(f"Failed to write FM config to {path}: {e}") from e
175 @classmethod
176 def import_from_toml(cls, path: Path = CLI_FM_CONFIG_PATH) -> "FMConfigManager":
177 input_data = {}
179 input_data["version"] = Version(get_current_fm_version())
180 input_data["cloudflare"] = FMCloudflareConfig(email=None, api_key=None, api_token=None)
181 input_data["root_path"] = str(path)
182 input_data["ngrok_auth_token"] = None
183 input_data["validation"] = FMValidationConfig()
184 input_data["logs"] = FMLogsConfig()
186 raw_config_data = {}
188 if path.exists():
189 data = tomlkit.parse(path.read_text())
190 input_data["version"] = Version(data.get("version", get_current_fm_version()))
192 if "cloudflare" in data:
193 input_data["cloudflare"] = FMCloudflareConfig(**data["cloudflare"])
195 input_data["ngrok_auth_token"] = data.get("ngrok_auth_token", None)
197 if "validation" in data:
198 input_data["validation"] = FMValidationConfig(**data["validation"])
200 if "logs" in data:
201 input_data["logs"] = FMLogsConfig(**data["logs"])
203 if "migration_state" in data:
204 import json
206 raw_config_data["migration_state"] = json.loads(json.dumps(data["migration_state"]))
208 fm_config_instance = cls(**input_data)
209 fm_config_instance._raw_config = raw_config_data
210 return fm_config_instance