We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
development server no longer hot-reloads on file changes
## Description

After #6563, `make dev` no longer hot-reloads on file changes, because the immutable `SDConfig` doesn't carry the `env` attribute from `config.py`.  (This regression was last encountered in #5594.)

## Steps to Reproduce

Same as #5594.

## Expected Behavior

Same as #5594.

## Actual Behavior

Same as #5594.

## Comments

The fix in <https://github.com/freedomofpress/securedrop/issues/6669#issuecomment-1526129678> is trivial, but I want to take a moment to see if there's an easy way to test for this regression (fool me once, fool me twice, etc.).

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/sdconfig.py`
Content:
```
1 from dataclasses import dataclass
2 from importlib import import_module
3 from pathlib import Path
4 from typing import Dict, List, Optional
5 
6 FALLBACK_LOCALE = "en_US"
7 
8 DEFAULT_SECUREDROP_ROOT = Path(__file__).absolute().parent
9 
10 
11 @dataclass(frozen=True)
12 class _FlaskAppConfig:
13     """Config fields that are common to the Journalist and Source interfaces."""
14 
15     SESSION_COOKIE_NAME: str
16     SECRET_KEY: str
17 
18     DEBUG: bool
19     TESTING: bool
20     WTF_CSRF_ENABLED: bool
21 
22     # Use MAX_CONTENT_LENGTH to mimic the behavior of Apache's LimitRequestBody
23     # in the development environment. See #1714.
24     MAX_CONTENT_LENGTH: int
25 
26     # This is recommended for performance, and also resolves #369
27     USE_X_SENDFILE: bool
28 
29 
30 @dataclass(frozen=True)
31 class JournalistInterfaceConfig(_FlaskAppConfig):
32     # Additional config for JI Redis sessions
33     SESSION_SIGNER_SALT: str = "js_session"
34     SESSION_KEY_PREFIX: str = "js_session:"
35     SESSION_LIFETIME: int = 2 * 60 * 60
36     SESSION_RENEW_COUNT: int = 5
37 
38 
39 @dataclass(frozen=True)
40 class SourceInterfaceConfig(_FlaskAppConfig):
41     pass
42 
43 
44 @dataclass(frozen=True)
45 class SecureDropConfig:
46     JOURNALIST_APP_FLASK_CONFIG_CLS: JournalistInterfaceConfig
47     SOURCE_APP_FLASK_CONFIG_CLS: SourceInterfaceConfig
48 
49     GPG_KEY_DIR: Path
50     JOURNALIST_KEY: str
51     SCRYPT_GPG_PEPPER: str
52     SCRYPT_ID_PEPPER: str
53     SCRYPT_PARAMS: Dict[str, int]
54 
55     SECUREDROP_DATA_ROOT: Path
56 
57     DATABASE_FILE: Path  # Path to the sqlite DB file
58 
59     SECUREDROP_ROOT: Path
60     STATIC_DIR: Path
61     TRANSLATION_DIRS: Path
62     SOURCE_TEMPLATES_DIR: Path
63     JOURNALIST_TEMPLATES_DIR: Path
64     NOUNS: Path
65     ADJECTIVES: Path
66 
67     DEFAULT_LOCALE: str
68     SUPPORTED_LOCALES: List[str]
69 
70     SESSION_EXPIRATION_MINUTES: float
71 
72     RQ_WORKER_NAME: str
73 
74     @property
75     def TEMP_DIR(self) -> Path:
76         # We use a directory under the SECUREDROP_DATA_ROOT instead of `/tmp` because
77         # we need to expose this directory via X-Send-File, and want to minimize the
78         # potential for exposing unintended files.
79         return self.SECUREDROP_DATA_ROOT / "tmp"
80 
81     @property
82     def STORE_DIR(self) -> Path:
83         return self.SECUREDROP_DATA_ROOT / "store"
84 
85     @property
86     def DATABASE_URI(self) -> str:
87         return f"sqlite:///{self.DATABASE_FILE}"
88 
89     @classmethod
90     def get_current(cls) -> "SecureDropConfig":
91         global _current_config
92         if _current_config is None:
93             # Retrieve the config by parsing it from ./config.py
94             _current_config = _parse_config_from_file(config_module_name="config")
95         return _current_config
96 
97 
98 _current_config: Optional[SecureDropConfig] = None
99 
100 
101 def _parse_config_from_file(config_module_name: str) -> SecureDropConfig:
102     """Parse the config from a config.py file."""
103     config_from_local_file = import_module(config_module_name)
104 
105     # Parse the local config; as there are SD instances with very old config files
106     # the parsing logic here has to assume some values might be missing, and hence
107     # set default values for such config entries
108     final_default_locale = getattr(config_from_local_file, "DEFAULT_LOCALE", FALLBACK_LOCALE)
109     final_supp_locales = getattr(config_from_local_file, "SUPPORTED_LOCALES", [FALLBACK_LOCALE])
110     final_sess_expiration_mins = getattr(config_from_local_file, "SESSION_EXPIRATION_MINUTES", 120)
111 
112     final_worker_name = getattr(config_from_local_file, "RQ_WORKER_NAME", "default")
113 
114     final_scrypt_params = getattr(
115         config_from_local_file, "SCRYPT_PARAMS", dict(N=2**14, r=8, p=1)
116     )
117 
118     try:
119         final_securedrop_root = Path(config_from_local_file.SECUREDROP_ROOT)
120     except AttributeError:
121         final_securedrop_root = DEFAULT_SECUREDROP_ROOT
122 
123     try:
124         final_securedrop_data_root = Path(config_from_local_file.SECUREDROP_DATA_ROOT)
125     except AttributeError:
126         final_securedrop_data_root = Path("/var/lib/securedrop")
127 
128     try:
129         final_db_file = Path(config_from_local_file.DATABASE_FILE)
130     except AttributeError:
131         final_db_file = final_securedrop_data_root / "db.sqlite"
132 
133     try:
134         final_gpg_key_dir = Path(config_from_local_file.GPG_KEY_DIR)
135     except AttributeError:
136         final_gpg_key_dir = final_securedrop_data_root / "keys"
137 
138     try:
139         final_nouns = Path(config_from_local_file.NOUNS)
140     except AttributeError:
141         final_nouns = final_securedrop_root / "dictionaries" / "nouns.txt"
142 
143     try:
144         final_adjectives = Path(config_from_local_file.ADJECTIVES)
145     except AttributeError:
146         final_adjectives = final_securedrop_root / "dictionaries" / "adjectives.txt"
147 
148     try:
149         final_static_dir = Path(config_from_local_file.STATIC_DIR)  # type: ignore
150     except AttributeError:
151         final_static_dir = final_securedrop_root / "static"
152 
153     try:
154         final_transl_dir = Path(config_from_local_file.TRANSLATION_DIRS)  # type: ignore
155     except AttributeError:
156         final_transl_dir = final_securedrop_root / "translations"
157 
158     try:
159         final_source_tmpl_dir = Path(config_from_local_file.SOURCE_TEMPLATES_DIR)
160     except AttributeError:
161         final_source_tmpl_dir = final_securedrop_root / "source_templates"
162 
163     try:
164         final_journ_tmpl_dir = Path(config_from_local_file.JOURNALIST_TEMPLATES_DIR)
165     except AttributeError:
166         final_journ_tmpl_dir = final_securedrop_root / "journalist_templates"
167 
168     # Parse the Flask configurations
169     journ_flask_config = config_from_local_file.JournalistInterfaceFlaskConfig
170     parsed_journ_flask_config = JournalistInterfaceConfig(
171         SECRET_KEY=journ_flask_config.SECRET_KEY,
172         SESSION_COOKIE_NAME=getattr(journ_flask_config, "SESSION_COOKIE_NAME", "js"),
173         DEBUG=getattr(journ_flask_config, "DEBUG", False),
174         TESTING=getattr(journ_flask_config, "TESTING", False),
175         WTF_CSRF_ENABLED=getattr(journ_flask_config, "WTF_CSRF_ENABLED", True),
176         MAX_CONTENT_LENGTH=getattr(journ_flask_config, "MAX_CONTENT_LENGTH", 524288000),
177         USE_X_SENDFILE=getattr(journ_flask_config, "USE_X_SENDFILE", False),
178     )
179     source_flask_config = config_from_local_file.SourceInterfaceFlaskConfig
180     parsed_source_flask_config = SourceInterfaceConfig(
181         SECRET_KEY=source_flask_config.SECRET_KEY,
182         SESSION_COOKIE_NAME=getattr(journ_flask_config, "SESSION_COOKIE_NAME", "ss"),
183         DEBUG=getattr(journ_flask_config, "DEBUG", False),
184         TESTING=getattr(journ_flask_config, "TESTING", False),
185         WTF_CSRF_ENABLED=getattr(journ_flask_config, "WTF_CSRF_ENABLED", True),
186         MAX_CONTENT_LENGTH=getattr(journ_flask_config, "MAX_CONTENT_LENGTH", 524288000),
187         USE_X_SENDFILE=getattr(journ_flask_config, "USE_X_SENDFILE", False),
188     )
189 
190     return SecureDropConfig(
191         JOURNALIST_APP_FLASK_CONFIG_CLS=parsed_journ_flask_config,
192         SOURCE_APP_FLASK_CONFIG_CLS=parsed_source_flask_config,
193         GPG_KEY_DIR=final_gpg_key_dir,
194         JOURNALIST_KEY=config_from_local_file.JOURNALIST_KEY,
195         SCRYPT_GPG_PEPPER=config_from_local_file.SCRYPT_GPG_PEPPER,
196         SCRYPT_ID_PEPPER=config_from_local_file.SCRYPT_ID_PEPPER,
197         SCRYPT_PARAMS=final_scrypt_params,
198         SECUREDROP_DATA_ROOT=final_securedrop_data_root,
199         SECUREDROP_ROOT=final_securedrop_root,
200         DATABASE_FILE=final_db_file,
201         STATIC_DIR=final_static_dir,
202         TRANSLATION_DIRS=final_transl_dir,
203         SOURCE_TEMPLATES_DIR=final_source_tmpl_dir,
204         JOURNALIST_TEMPLATES_DIR=final_journ_tmpl_dir,
205         NOUNS=final_nouns,
206         ADJECTIVES=final_adjectives,
207         DEFAULT_LOCALE=final_default_locale,
208         SUPPORTED_LOCALES=final_supp_locales,
209         SESSION_EXPIRATION_MINUTES=final_sess_expiration_mins,
210         RQ_WORKER_NAME=final_worker_name,
211     )
212
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/sdconfig.py b/securedrop/sdconfig.py
--- a/securedrop/sdconfig.py
+++ b/securedrop/sdconfig.py
@@ -71,6 +71,8 @@
 
     RQ_WORKER_NAME: str
 
+    env: str = "prod"
+
     @property
     def TEMP_DIR(self) -> Path:
         # We use a directory under the SECUREDROP_DATA_ROOT instead of `/tmp` because
@@ -115,6 +117,8 @@
         config_from_local_file, "SCRYPT_PARAMS", dict(N=2**14, r=8, p=1)
     )
 
+    env = getattr(config_from_local_file, "env", "prod")
+
     try:
         final_securedrop_root = Path(config_from_local_file.SECUREDROP_ROOT)
     except AttributeError:
@@ -188,6 +192,7 @@
     )
 
     return SecureDropConfig(
+        env=env,
         JOURNALIST_APP_FLASK_CONFIG_CLS=parsed_journ_flask_config,
         SOURCE_APP_FLASK_CONFIG_CLS=parsed_source_flask_config,
         GPG_KEY_DIR=final_gpg_key_dir,

{"golden_diff": "diff --git a/securedrop/sdconfig.py b/securedrop/sdconfig.py\n--- a/securedrop/sdconfig.py\n+++ b/securedrop/sdconfig.py\n@@ -71,6 +71,8 @@\n \n     RQ_WORKER_NAME: str\n \n+    env: str = \"prod\"\n+\n     @property\n     def TEMP_DIR(self) -> Path:\n         # We use a directory under the SECUREDROP_DATA_ROOT instead of `/tmp` because\n@@ -115,6 +117,8 @@\n         config_from_local_file, \"SCRYPT_PARAMS\", dict(N=2**14, r=8, p=1)\n     )\n \n+    env = getattr(config_from_local_file, \"env\", \"prod\")\n+\n     try:\n         final_securedrop_root = Path(config_from_local_file.SECUREDROP_ROOT)\n     except AttributeError:\n@@ -188,6 +192,7 @@\n     )\n \n     return SecureDropConfig(\n+        env=env,\n         JOURNALIST_APP_FLASK_CONFIG_CLS=parsed_journ_flask_config,\n         SOURCE_APP_FLASK_CONFIG_CLS=parsed_source_flask_config,\n         GPG_KEY_DIR=final_gpg_key_dir,\n", "issue": "development server no longer hot-reloads on file changes\n## Description\r\n\r\nAfter #6563, `make dev` no longer hot-reloads on file changes, because the immutable `SDConfig` doesn't carry the `env` attribute from `config.py`.  (This regression was last encountered in #5594.)\r\n\r\n## Steps to Reproduce\r\n\r\nSame as #5594.\r\n\r\n## Expected Behavior\r\n\r\nSame as #5594.\r\n\r\n## Actual Behavior\r\n\r\nSame as #5594.\r\n\r\n## Comments\r\n\r\nThe fix in <https://github.com/freedomofpress/securedrop/issues/6669#issuecomment-1526129678> is trivial, but I want to take a moment to see if there's an easy way to test for this regression (fool me once, fool me twice, etc.).\n", "before_files": [{"content": "from dataclasses import dataclass\nfrom importlib import import_module\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nFALLBACK_LOCALE = \"en_US\"\n\nDEFAULT_SECUREDROP_ROOT = Path(__file__).absolute().parent\n\n\n@dataclass(frozen=True)\nclass _FlaskAppConfig:\n    \"\"\"Config fields that are common to the Journalist and Source interfaces.\"\"\"\n\n    SESSION_COOKIE_NAME: str\n    SECRET_KEY: str\n\n    DEBUG: bool\n    TESTING: bool\n    WTF_CSRF_ENABLED: bool\n\n    # Use MAX_CONTENT_LENGTH to mimic the behavior of Apache's LimitRequestBody\n    # in the development environment. See #1714.\n    MAX_CONTENT_LENGTH: int\n\n    # This is recommended for performance, and also resolves #369\n    USE_X_SENDFILE: bool\n\n\n@dataclass(frozen=True)\nclass JournalistInterfaceConfig(_FlaskAppConfig):\n    # Additional config for JI Redis sessions\n    SESSION_SIGNER_SALT: str = \"js_session\"\n    SESSION_KEY_PREFIX: str = \"js_session:\"\n    SESSION_LIFETIME: int = 2 * 60 * 60\n    SESSION_RENEW_COUNT: int = 5\n\n\n@dataclass(frozen=True)\nclass SourceInterfaceConfig(_FlaskAppConfig):\n    pass\n\n\n@dataclass(frozen=True)\nclass SecureDropConfig:\n    JOURNALIST_APP_FLASK_CONFIG_CLS: JournalistInterfaceConfig\n    SOURCE_APP_FLASK_CONFIG_CLS: SourceInterfaceConfig\n\n    GPG_KEY_DIR: Path\n    JOURNALIST_KEY: str\n    SCRYPT_GPG_PEPPER: str\n    SCRYPT_ID_PEPPER: str\n    SCRYPT_PARAMS: Dict[str, int]\n\n    SECUREDROP_DATA_ROOT: Path\n\n    DATABASE_FILE: Path  # Path to the sqlite DB file\n\n    SECUREDROP_ROOT: Path\n    STATIC_DIR: Path\n    TRANSLATION_DIRS: Path\n    SOURCE_TEMPLATES_DIR: Path\n    JOURNALIST_TEMPLATES_DIR: Path\n    NOUNS: Path\n    ADJECTIVES: Path\n\n    DEFAULT_LOCALE: str\n    SUPPORTED_LOCALES: List[str]\n\n    SESSION_EXPIRATION_MINUTES: float\n\n    RQ_WORKER_NAME: str\n\n    @property\n    def TEMP_DIR(self) -> Path:\n        # We use a directory under the SECUREDROP_DATA_ROOT instead of `/tmp` because\n        # we need to expose this directory via X-Send-File, and want to minimize the\n        # potential for exposing unintended files.\n        return self.SECUREDROP_DATA_ROOT / \"tmp\"\n\n    @property\n    def STORE_DIR(self) -> Path:\n        return self.SECUREDROP_DATA_ROOT / \"store\"\n\n    @property\n    def DATABASE_URI(self) -> str:\n        return f\"sqlite:///{self.DATABASE_FILE}\"\n\n    @classmethod\n    def get_current(cls) -> \"SecureDropConfig\":\n        global _current_config\n        if _current_config is None:\n            # Retrieve the config by parsing it from ./config.py\n            _current_config = _parse_config_from_file(config_module_name=\"config\")\n        return _current_config\n\n\n_current_config: Optional[SecureDropConfig] = None\n\n\ndef _parse_config_from_file(config_module_name: str) -> SecureDropConfig:\n    \"\"\"Parse the config from a config.py file.\"\"\"\n    config_from_local_file = import_module(config_module_name)\n\n    # Parse the local config; as there are SD instances with very old config files\n    # the parsing logic here has to assume some values might be missing, and hence\n    # set default values for such config entries\n    final_default_locale = getattr(config_from_local_file, \"DEFAULT_LOCALE\", FALLBACK_LOCALE)\n    final_supp_locales = getattr(config_from_local_file, \"SUPPORTED_LOCALES\", [FALLBACK_LOCALE])\n    final_sess_expiration_mins = getattr(config_from_local_file, \"SESSION_EXPIRATION_MINUTES\", 120)\n\n    final_worker_name = getattr(config_from_local_file, \"RQ_WORKER_NAME\", \"default\")\n\n    final_scrypt_params = getattr(\n        config_from_local_file, \"SCRYPT_PARAMS\", dict(N=2**14, r=8, p=1)\n    )\n\n    try:\n        final_securedrop_root = Path(config_from_local_file.SECUREDROP_ROOT)\n    except AttributeError:\n        final_securedrop_root = DEFAULT_SECUREDROP_ROOT\n\n    try:\n        final_securedrop_data_root = Path(config_from_local_file.SECUREDROP_DATA_ROOT)\n    except AttributeError:\n        final_securedrop_data_root = Path(\"/var/lib/securedrop\")\n\n    try:\n        final_db_file = Path(config_from_local_file.DATABASE_FILE)\n    except AttributeError:\n        final_db_file = final_securedrop_data_root / \"db.sqlite\"\n\n    try:\n        final_gpg_key_dir = Path(config_from_local_file.GPG_KEY_DIR)\n    except AttributeError:\n        final_gpg_key_dir = final_securedrop_data_root / \"keys\"\n\n    try:\n        final_nouns = Path(config_from_local_file.NOUNS)\n    except AttributeError:\n        final_nouns = final_securedrop_root / \"dictionaries\" / \"nouns.txt\"\n\n    try:\n        final_adjectives = Path(config_from_local_file.ADJECTIVES)\n    except AttributeError:\n        final_adjectives = final_securedrop_root / \"dictionaries\" / \"adjectives.txt\"\n\n    try:\n        final_static_dir = Path(config_from_local_file.STATIC_DIR)  # type: ignore\n    except AttributeError:\n        final_static_dir = final_securedrop_root / \"static\"\n\n    try:\n        final_transl_dir = Path(config_from_local_file.TRANSLATION_DIRS)  # type: ignore\n    except AttributeError:\n        final_transl_dir = final_securedrop_root / \"translations\"\n\n    try:\n        final_source_tmpl_dir = Path(config_from_local_file.SOURCE_TEMPLATES_DIR)\n    except AttributeError:\n        final_source_tmpl_dir = final_securedrop_root / \"source_templates\"\n\n    try:\n        final_journ_tmpl_dir = Path(config_from_local_file.JOURNALIST_TEMPLATES_DIR)\n    except AttributeError:\n        final_journ_tmpl_dir = final_securedrop_root / \"journalist_templates\"\n\n    # Parse the Flask configurations\n    journ_flask_config = config_from_local_file.JournalistInterfaceFlaskConfig\n    parsed_journ_flask_config = JournalistInterfaceConfig(\n        SECRET_KEY=journ_flask_config.SECRET_KEY,\n        SESSION_COOKIE_NAME=getattr(journ_flask_config, \"SESSION_COOKIE_NAME\", \"js\"),\n        DEBUG=getattr(journ_flask_config, \"DEBUG\", False),\n        TESTING=getattr(journ_flask_config, \"TESTING\", False),\n        WTF_CSRF_ENABLED=getattr(journ_flask_config, \"WTF_CSRF_ENABLED\", True),\n        MAX_CONTENT_LENGTH=getattr(journ_flask_config, \"MAX_CONTENT_LENGTH\", 524288000),\n        USE_X_SENDFILE=getattr(journ_flask_config, \"USE_X_SENDFILE\", False),\n    )\n    source_flask_config = config_from_local_file.SourceInterfaceFlaskConfig\n    parsed_source_flask_config = SourceInterfaceConfig(\n        SECRET_KEY=source_flask_config.SECRET_KEY,\n        SESSION_COOKIE_NAME=getattr(journ_flask_config, \"SESSION_COOKIE_NAME\", \"ss\"),\n        DEBUG=getattr(journ_flask_config, \"DEBUG\", False),\n        TESTING=getattr(journ_flask_config, \"TESTING\", False),\n        WTF_CSRF_ENABLED=getattr(journ_flask_config, \"WTF_CSRF_ENABLED\", True),\n        MAX_CONTENT_LENGTH=getattr(journ_flask_config, \"MAX_CONTENT_LENGTH\", 524288000),\n        USE_X_SENDFILE=getattr(journ_flask_config, \"USE_X_SENDFILE\", False),\n    )\n\n    return SecureDropConfig(\n        JOURNALIST_APP_FLASK_CONFIG_CLS=parsed_journ_flask_config,\n        SOURCE_APP_FLASK_CONFIG_CLS=parsed_source_flask_config,\n        GPG_KEY_DIR=final_gpg_key_dir,\n        JOURNALIST_KEY=config_from_local_file.JOURNALIST_KEY,\n        SCRYPT_GPG_PEPPER=config_from_local_file.SCRYPT_GPG_PEPPER,\n        SCRYPT_ID_PEPPER=config_from_local_file.SCRYPT_ID_PEPPER,\n        SCRYPT_PARAMS=final_scrypt_params,\n        SECUREDROP_DATA_ROOT=final_securedrop_data_root,\n        SECUREDROP_ROOT=final_securedrop_root,\n        DATABASE_FILE=final_db_file,\n        STATIC_DIR=final_static_dir,\n        TRANSLATION_DIRS=final_transl_dir,\n        SOURCE_TEMPLATES_DIR=final_source_tmpl_dir,\n        JOURNALIST_TEMPLATES_DIR=final_journ_tmpl_dir,\n        NOUNS=final_nouns,\n        ADJECTIVES=final_adjectives,\n        DEFAULT_LOCALE=final_default_locale,\n        SUPPORTED_LOCALES=final_supp_locales,\n        SESSION_EXPIRATION_MINUTES=final_sess_expiration_mins,\n        RQ_WORKER_NAME=final_worker_name,\n    )\n", "path": "securedrop/sdconfig.py"}], "after_files": [{"content": "from dataclasses import dataclass\nfrom importlib import import_module\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nFALLBACK_LOCALE = \"en_US\"\n\nDEFAULT_SECUREDROP_ROOT = Path(__file__).absolute().parent\n\n\n@dataclass(frozen=True)\nclass _FlaskAppConfig:\n    \"\"\"Config fields that are common to the Journalist and Source interfaces.\"\"\"\n\n    SESSION_COOKIE_NAME: str\n    SECRET_KEY: str\n\n    DEBUG: bool\n    TESTING: bool\n    WTF_CSRF_ENABLED: bool\n\n    # Use MAX_CONTENT_LENGTH to mimic the behavior of Apache's LimitRequestBody\n    # in the development environment. See #1714.\n    MAX_CONTENT_LENGTH: int\n\n    # This is recommended for performance, and also resolves #369\n    USE_X_SENDFILE: bool\n\n\n@dataclass(frozen=True)\nclass JournalistInterfaceConfig(_FlaskAppConfig):\n    # Additional config for JI Redis sessions\n    SESSION_SIGNER_SALT: str = \"js_session\"\n    SESSION_KEY_PREFIX: str = \"js_session:\"\n    SESSION_LIFETIME: int = 2 * 60 * 60\n    SESSION_RENEW_COUNT: int = 5\n\n\n@dataclass(frozen=True)\nclass SourceInterfaceConfig(_FlaskAppConfig):\n    pass\n\n\n@dataclass(frozen=True)\nclass SecureDropConfig:\n    JOURNALIST_APP_FLASK_CONFIG_CLS: JournalistInterfaceConfig\n    SOURCE_APP_FLASK_CONFIG_CLS: SourceInterfaceConfig\n\n    GPG_KEY_DIR: Path\n    JOURNALIST_KEY: str\n    SCRYPT_GPG_PEPPER: str\n    SCRYPT_ID_PEPPER: str\n    SCRYPT_PARAMS: Dict[str, int]\n\n    SECUREDROP_DATA_ROOT: Path\n\n    DATABASE_FILE: Path  # Path to the sqlite DB file\n\n    SECUREDROP_ROOT: Path\n    STATIC_DIR: Path\n    TRANSLATION_DIRS: Path\n    SOURCE_TEMPLATES_DIR: Path\n    JOURNALIST_TEMPLATES_DIR: Path\n    NOUNS: Path\n    ADJECTIVES: Path\n\n    DEFAULT_LOCALE: str\n    SUPPORTED_LOCALES: List[str]\n\n    SESSION_EXPIRATION_MINUTES: float\n\n    RQ_WORKER_NAME: str\n\n    env: str = \"prod\"\n\n    @property\n    def TEMP_DIR(self) -> Path:\n        # We use a directory under the SECUREDROP_DATA_ROOT instead of `/tmp` because\n        # we need to expose this directory via X-Send-File, and want to minimize the\n        # potential for exposing unintended files.\n        return self.SECUREDROP_DATA_ROOT / \"tmp\"\n\n    @property\n    def STORE_DIR(self) -> Path:\n        return self.SECUREDROP_DATA_ROOT / \"store\"\n\n    @property\n    def DATABASE_URI(self) -> str:\n        return f\"sqlite:///{self.DATABASE_FILE}\"\n\n    @classmethod\n    def get_current(cls) -> \"SecureDropConfig\":\n        global _current_config\n        if _current_config is None:\n            # Retrieve the config by parsing it from ./config.py\n            _current_config = _parse_config_from_file(config_module_name=\"config\")\n        return _current_config\n\n\n_current_config: Optional[SecureDropConfig] = None\n\n\ndef _parse_config_from_file(config_module_name: str) -> SecureDropConfig:\n    \"\"\"Parse the config from a config.py file.\"\"\"\n    config_from_local_file = import_module(config_module_name)\n\n    # Parse the local config; as there are SD instances with very old config files\n    # the parsing logic here has to assume some values might be missing, and hence\n    # set default values for such config entries\n    final_default_locale = getattr(config_from_local_file, \"DEFAULT_LOCALE\", FALLBACK_LOCALE)\n    final_supp_locales = getattr(config_from_local_file, \"SUPPORTED_LOCALES\", [FALLBACK_LOCALE])\n    final_sess_expiration_mins = getattr(config_from_local_file, \"SESSION_EXPIRATION_MINUTES\", 120)\n\n    final_worker_name = getattr(config_from_local_file, \"RQ_WORKER_NAME\", \"default\")\n\n    final_scrypt_params = getattr(\n        config_from_local_file, \"SCRYPT_PARAMS\", dict(N=2**14, r=8, p=1)\n    )\n\n    env = getattr(config_from_local_file, \"env\", \"prod\")\n\n    try:\n        final_securedrop_root = Path(config_from_local_file.SECUREDROP_ROOT)\n    except AttributeError:\n        final_securedrop_root = DEFAULT_SECUREDROP_ROOT\n\n    try:\n        final_securedrop_data_root = Path(config_from_local_file.SECUREDROP_DATA_ROOT)\n    except AttributeError:\n        final_securedrop_data_root = Path(\"/var/lib/securedrop\")\n\n    try:\n        final_db_file = Path(config_from_local_file.DATABASE_FILE)\n    except AttributeError:\n        final_db_file = final_securedrop_data_root / \"db.sqlite\"\n\n    try:\n        final_gpg_key_dir = Path(config_from_local_file.GPG_KEY_DIR)\n    except AttributeError:\n        final_gpg_key_dir = final_securedrop_data_root / \"keys\"\n\n    try:\n        final_nouns = Path(config_from_local_file.NOUNS)\n    except AttributeError:\n        final_nouns = final_securedrop_root / \"dictionaries\" / \"nouns.txt\"\n\n    try:\n        final_adjectives = Path(config_from_local_file.ADJECTIVES)\n    except AttributeError:\n        final_adjectives = final_securedrop_root / \"dictionaries\" / \"adjectives.txt\"\n\n    try:\n        final_static_dir = Path(config_from_local_file.STATIC_DIR)  # type: ignore\n    except AttributeError:\n        final_static_dir = final_securedrop_root / \"static\"\n\n    try:\n        final_transl_dir = Path(config_from_local_file.TRANSLATION_DIRS)  # type: ignore\n    except AttributeError:\n        final_transl_dir = final_securedrop_root / \"translations\"\n\n    try:\n        final_source_tmpl_dir = Path(config_from_local_file.SOURCE_TEMPLATES_DIR)\n    except AttributeError:\n        final_source_tmpl_dir = final_securedrop_root / \"source_templates\"\n\n    try:\n        final_journ_tmpl_dir = Path(config_from_local_file.JOURNALIST_TEMPLATES_DIR)\n    except AttributeError:\n        final_journ_tmpl_dir = final_securedrop_root / \"journalist_templates\"\n\n    # Parse the Flask configurations\n    journ_flask_config = config_from_local_file.JournalistInterfaceFlaskConfig\n    parsed_journ_flask_config = JournalistInterfaceConfig(\n        SECRET_KEY=journ_flask_config.SECRET_KEY,\n        SESSION_COOKIE_NAME=getattr(journ_flask_config, \"SESSION_COOKIE_NAME\", \"js\"),\n        DEBUG=getattr(journ_flask_config, \"DEBUG\", False),\n        TESTING=getattr(journ_flask_config, \"TESTING\", False),\n        WTF_CSRF_ENABLED=getattr(journ_flask_config, \"WTF_CSRF_ENABLED\", True),\n        MAX_CONTENT_LENGTH=getattr(journ_flask_config, \"MAX_CONTENT_LENGTH\", 524288000),\n        USE_X_SENDFILE=getattr(journ_flask_config, \"USE_X_SENDFILE\", False),\n    )\n    source_flask_config = config_from_local_file.SourceInterfaceFlaskConfig\n    parsed_source_flask_config = SourceInterfaceConfig(\n        SECRET_KEY=source_flask_config.SECRET_KEY,\n        SESSION_COOKIE_NAME=getattr(journ_flask_config, \"SESSION_COOKIE_NAME\", \"ss\"),\n        DEBUG=getattr(journ_flask_config, \"DEBUG\", False),\n        TESTING=getattr(journ_flask_config, \"TESTING\", False),\n        WTF_CSRF_ENABLED=getattr(journ_flask_config, \"WTF_CSRF_ENABLED\", True),\n        MAX_CONTENT_LENGTH=getattr(journ_flask_config, \"MAX_CONTENT_LENGTH\", 524288000),\n        USE_X_SENDFILE=getattr(journ_flask_config, \"USE_X_SENDFILE\", False),\n    )\n\n    return SecureDropConfig(\n        env=env,\n        JOURNALIST_APP_FLASK_CONFIG_CLS=parsed_journ_flask_config,\n        SOURCE_APP_FLASK_CONFIG_CLS=parsed_source_flask_config,\n        GPG_KEY_DIR=final_gpg_key_dir,\n        JOURNALIST_KEY=config_from_local_file.JOURNALIST_KEY,\n        SCRYPT_GPG_PEPPER=config_from_local_file.SCRYPT_GPG_PEPPER,\n        SCRYPT_ID_PEPPER=config_from_local_file.SCRYPT_ID_PEPPER,\n        SCRYPT_PARAMS=final_scrypt_params,\n        SECUREDROP_DATA_ROOT=final_securedrop_data_root,\n        SECUREDROP_ROOT=final_securedrop_root,\n        DATABASE_FILE=final_db_file,\n        STATIC_DIR=final_static_dir,\n        TRANSLATION_DIRS=final_transl_dir,\n        SOURCE_TEMPLATES_DIR=final_source_tmpl_dir,\n        JOURNALIST_TEMPLATES_DIR=final_journ_tmpl_dir,\n        NOUNS=final_nouns,\n        ADJECTIVES=final_adjectives,\n        DEFAULT_LOCALE=final_default_locale,\n        SUPPORTED_LOCALES=final_supp_locales,\n        SESSION_EXPIRATION_MINUTES=final_sess_expiration_mins,\n        RQ_WORKER_NAME=final_worker_name,\n    )\n", "path": "securedrop/sdconfig.py"}]}