Coverage for rfpy/conf/settings.py: 100%
85 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 10:52 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 10:52 +0000
1import os
2import re
3import sys
4import time
5import uuid
6from enum import Enum
7from pathlib import Path
8from typing import Tuple, Any, Generator, Self
11from pydantic_settings import SettingsConfigDict, BaseSettings
12from pydantic import EmailStr, model_validator
15class RunMode(str, Enum):
16 production = "production"
17 development = "development"
18 test = "test"
21class Mailer(str, Enum):
22 postmark = "postmark"
23 logfile = "logfile"
26LIVE_SETTINGS_PATH = "/etc/postrfp/rfpy-settings.env"
27LOCAL_SETTINGS_PATH = "./rfpy-settings.env"
30class AppSettings(BaseSettings):
31 model_config = SettingsConfigDict(
32 env_prefix="rfpy_", env_file=LIVE_SETTINGS_PATH, env_file_encoding="utf-8"
33 )
35 run_mode: RunMode = RunMode.production
37 db_name: str = "rfp"
38 db_host: str = "localhost"
39 db_user: str = "root"
40 db_password: str = ""
41 db_driver: str = "mysqldb"
42 crypt_key: str = ""
43 webapp_hostname: str = "localhost"
44 data_directory: str = "/var/supplierselect/"
45 # jwt_expiry_hours: int = 72 # Default to 3 days
46 jwt_expiry_minutes: int = 20 # Default to 20 minutes
47 jwt_refresh_token_expiry_hours:int = 168 # Default to 7 days
49 max_failed_login_attempts: int = 5
50 lockout_duration_minutes: int = 20
52 remote_events_url: str = "DUMMY_LOG_SERVER"
54 mailer: Mailer = Mailer.logfile
55 email_from_address: EmailStr = "[email protected]"
56 email_to_override: EmailStr = "[email protected]"
57 postmark_api_key: str = "POSTMARK_API_TEST"
58 postmark_logging_key: str = "POSTMARK_API_TEST"
59 system_name: str = "PostRFP"
60 template_dir: str = "conf/postmarktmpls/txt/"
61 fanout_realm_id: str | None = None
62 fanout_realm_key: str | None = None
64 @property
65 def sqlalchemy_dsn(self) -> str:
66 return (
67 f"mysql+{self.db_driver}://{self.db_user}:{self.db_password}"
68 f"@{self.db_host}/{self.db_name}?charset=utf8mb4"
69 )
71 @property
72 def cache_dir(self) -> Path:
73 return Path(self.data_directory) / "cache" / self.db_name
75 @property
76 def attachments_dir(self) -> Path:
77 return Path(self.data_directory) / "attachments" / self.db_name
79 @model_validator(mode="after")
80 def validate_dirs(self) -> Self:
81 """
82 Check that the data directory exists, that correct permissions are set
83 and that cache and attachment subdirectories exists
84 """
85 data_directory = Path(self.data_directory)
86 if not data_directory.is_dir():
87 m = f"\n !!! {data_directory} directory not found - required for cache/ and attachments/"
88 sys.exit(m)
90 for folder in ["cache", "attachments"]:
91 app_directory = data_directory / folder
92 app_directory.mkdir(exist_ok=True)
93 self.test_usable(app_directory)
95 return self
97 def random_cache_file_path(self) -> Tuple[str, Path]:
98 fname = "%s.tmp" % uuid.uuid4().hex
99 return fname, self.cache_dir / fname
101 def conn_string(self, db_name=None):
102 name = self.db_name if db_name is None else db_name
103 return re.sub(r"(?<=\/)\w+(?=\?)", name, self.sqlalchemy_dsn)
105 @classmethod
106 def test_usable(cls, dir_name):
107 try:
108 ts = str(time.time())
109 test_file_path = os.path.join(dir_name, "test.txt")
110 with open(test_file_path, "w") as tw:
111 tw.write(ts)
112 with open(test_file_path, "r") as tr:
113 tr.read()
114 os.remove(test_file_path)
115 except IOError as ioe:
116 sys.exit(
117 "Test read/write to data directory %s failed with %s" % (dir_name, ioe)
118 )
120 def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
121 for key, value in self.model_config.items():
122 yield (key, value)