Coverage for functions \ flipdare \ app_env.py: 93%
131 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13from __future__ import annotations
15import os
16from enum import StrEnum
17from typing import Final, Literal, get_args
19__all__ = [
20 "get_env_type",
21 "APP_ENVIRONMENT_KEY",
22 "APP_USE_TEST_UID_KEY",
23 "AppEnv",
24 "EnvironmentType",
25 "get_app_environment",
26]
28# note, this 2 environment variables are controlled outside AppConfig / AppEnvLoader
29# note, primarily because they are used in testing.
31CoreEnvKeys = Literal["APP_ENVIRONMENT", "APP_USE_TEST_UID"]
33_VALID_KEYS = get_args(CoreEnvKeys)
35# needs to match the CoreEnvKey order..
36APP_ENVIRONMENT_KEY: Final = _VALID_KEYS[0]
37APP_USE_TEST_UID_KEY: Final = _VALID_KEYS[1]
40def get_app_environment() -> AppEnv:
41 return AppEnv.instance()
44def get_env_type() -> EnvironmentType:
45 try:
46 env_str = os.environ.get(APP_ENVIRONMENT_KEY, "prod").lower()
47 return EnvironmentType.from_string(env_str)
48 except Exception as e:
49 msg = (
50 "--------------------------------------------------------------------\n"
51 f"ERROR: Failed to load environment variable '{APP_ENVIRONMENT_KEY}': {e}\n"
52 "ERROR: Defaulting to PROD environment\n"
53 "--------------------------------------------------------------------"
54 )
55 print(msg) # noqa: T201
56 return EnvironmentType.PROD
59## NOTE: these should be used sparingly..
62class AppEnv:
63 _instance: AppEnv | None = None
65 __slots__ = (
66 "_env_type",
67 "_send_email_enabled",
68 "_uid_override",
69 "_use_emulator",
70 )
72 _env_type: EnvironmentType
74 # for testing
75 _send_email_enabled: bool
76 _use_emulator: bool
77 _uid_override: str | None
79 def __new__(cls) -> AppEnv: # noqa: PYI034
80 if cls._instance is not None:
81 return cls._instance
83 instance = super().__new__(cls)
84 # set immediately to prevent possible recursion issues.
85 cls._instance = instance
87 # set prod defaults
88 instance._env_type = EnvironmentType.PROD
89 instance._send_email_enabled = True
90 instance._use_emulator = False
91 instance._uid_override = None
93 instance.refresh()
94 return cls._instance
96 def refresh(self) -> None:
97 # NOTE: we cant use LOG() here since it calls get_app_environment,
98 # NOTE: which would cause infinite recursion if _instance is not set yet.
99 self._env_type = get_env_type()
100 env = self._env_type
101 if not env.is_prod:
102 self._use_emulator = True
103 self._send_email_enabled = env.is_integration_external
104 if APP_USE_TEST_UID_KEY in os.environ:
105 self._uid_override = os.environ[APP_USE_TEST_UID_KEY]
106 else:
107 self._uid_override = None
109 if env.is_prod:
110 # this is too verbose in test..
111 msg = (
112 "--------------------------------------------------------------------\n"
113 "Environment Configuration\n"
114 f"{self.debug_str()}"
115 "--------------------------------------------------------------------\n"
116 )
117 print(msg) # noqa: T201
119 @classmethod
120 def instance(cls) -> AppEnv:
121 # Now this just calls the constructor, which handles the logic
122 return cls()
124 @property
125 def env_type(self) -> EnvironmentType:
126 return self._env_type
128 @property
129 def in_cloud(self) -> bool:
130 return self.is_prod
132 @property
133 def use_emulator(self) -> bool:
134 if self.is_prod:
135 # safety check to prevent accidental use of emulator in prod,
136 # even if env variable is set.
137 return False
139 # for external integration tests, we use the cloud ..
140 return self._env_type == EnvironmentType.TEST_INTEGRATION
142 @property
143 def use_uid_override(self) -> bool:
144 if self.is_prod:
145 # safety check to prevent accidental use of UID override in prod,
146 # even if env variable is set.
147 return False
149 return self._uid_override is not None
151 @property
152 def uid_override(self) -> str | None:
153 if self.is_prod:
154 # safety check to prevent accidental use of UID override in prod,
155 # even if env variable is set.
156 return None
158 return self._uid_override
160 # environment helpers
161 @property
162 def is_dev(self) -> bool:
163 return self._env_type.is_dev
165 @property
166 def is_test(self) -> bool:
167 return self._env_type.is_test
169 @property
170 def is_prod(self) -> bool:
171 return self._env_type.is_prod
173 @property
174 def is_dev_test(self) -> bool:
175 return self._env_type.is_dev_test
177 def debug_str(self) -> str:
178 return (
179 "App Environment:\n"
180 f" Environment Type: {self._env_type}\n"
181 f" Is Cloud: {self.in_cloud}\n"
182 f" Use Emulator: {self.use_emulator}\n"
183 f" Use UID Override: {self.use_uid_override}\n"
184 f" UID Override: {self.uid_override}\n"
185 )
188class EnvironmentType(StrEnum):
189 DEV = "dev"
190 TEST = "test_unit"
191 TEST_INTEGRATION = "test_integration"
192 TEST_INTEGRATION_EXTERNAL = "test_integration_external"
193 PROD = "prod"
195 @property
196 def is_dev(self) -> bool:
197 return self == EnvironmentType.DEV
199 @property
200 def is_test(self) -> bool:
201 return self in {
202 EnvironmentType.TEST,
203 EnvironmentType.TEST_INTEGRATION,
204 EnvironmentType.TEST_INTEGRATION_EXTERNAL,
205 }
207 @property
208 def is_integration(self) -> bool:
209 return self == EnvironmentType.TEST_INTEGRATION
211 @property
212 def is_integration_external(self) -> bool:
213 return self == EnvironmentType.TEST_INTEGRATION_EXTERNAL
215 @property
216 def is_prod(self) -> bool:
217 return self == EnvironmentType.PROD
219 @property
220 def is_dev_test(self) -> bool:
221 return self.is_dev or self.is_test
223 def set_environ(self) -> None:
224 os.environ[APP_ENVIRONMENT_KEY] = self.value
226 @staticmethod
227 def from_string(env_str: str) -> EnvironmentType:
228 env_str = env_str.lower()
229 match env_str:
230 case "dev":
231 return EnvironmentType.DEV
232 case "test_unit":
233 return EnvironmentType.TEST
234 case "test_integration":
235 return EnvironmentType.TEST_INTEGRATION
236 case "test_integration_external":
237 return EnvironmentType.TEST_INTEGRATION_EXTERNAL
238 case "prod":
239 return EnvironmentType.PROD
240 case _:
241 msg = (
242 "--------------------------------------------------------------------"
243 f"WARNING: Unknown FLIPDARE_ENV value '{env_str}', defaulting to PROD"
244 "--------------------------------------------------------------------"
245 )
246 # we don't know if the logging system is available at this point,
247 # so we print directly to console.
248 print(msg) # noqa: T201
249 return EnvironmentType.PROD