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

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# 

12 

13from __future__ import annotations 

14 

15import os 

16from enum import StrEnum 

17from typing import Final, Literal, get_args 

18 

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] 

27 

28# note, this 2 environment variables are controlled outside AppConfig / AppEnvLoader 

29# note, primarily because they are used in testing. 

30 

31CoreEnvKeys = Literal["APP_ENVIRONMENT", "APP_USE_TEST_UID"] 

32 

33_VALID_KEYS = get_args(CoreEnvKeys) 

34 

35# needs to match the CoreEnvKey order.. 

36APP_ENVIRONMENT_KEY: Final = _VALID_KEYS[0] 

37APP_USE_TEST_UID_KEY: Final = _VALID_KEYS[1] 

38 

39 

40def get_app_environment() -> AppEnv: 

41 return AppEnv.instance() 

42 

43 

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 

57 

58 

59## NOTE: these should be used sparingly.. 

60 

61 

62class AppEnv: 

63 _instance: AppEnv | None = None 

64 

65 __slots__ = ( 

66 "_env_type", 

67 "_send_email_enabled", 

68 "_uid_override", 

69 "_use_emulator", 

70 ) 

71 

72 _env_type: EnvironmentType 

73 

74 # for testing 

75 _send_email_enabled: bool 

76 _use_emulator: bool 

77 _uid_override: str | None 

78 

79 def __new__(cls) -> AppEnv: # noqa: PYI034 

80 if cls._instance is not None: 

81 return cls._instance 

82 

83 instance = super().__new__(cls) 

84 # set immediately to prevent possible recursion issues. 

85 cls._instance = instance 

86 

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 

92 

93 instance.refresh() 

94 return cls._instance 

95 

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 

108 

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 

118 

119 @classmethod 

120 def instance(cls) -> AppEnv: 

121 # Now this just calls the constructor, which handles the logic 

122 return cls() 

123 

124 @property 

125 def env_type(self) -> EnvironmentType: 

126 return self._env_type 

127 

128 @property 

129 def in_cloud(self) -> bool: 

130 return self.is_prod 

131 

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 

138 

139 # for external integration tests, we use the cloud .. 

140 return self._env_type == EnvironmentType.TEST_INTEGRATION 

141 

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 

148 

149 return self._uid_override is not None 

150 

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 

157 

158 return self._uid_override 

159 

160 # environment helpers 

161 @property 

162 def is_dev(self) -> bool: 

163 return self._env_type.is_dev 

164 

165 @property 

166 def is_test(self) -> bool: 

167 return self._env_type.is_test 

168 

169 @property 

170 def is_prod(self) -> bool: 

171 return self._env_type.is_prod 

172 

173 @property 

174 def is_dev_test(self) -> bool: 

175 return self._env_type.is_dev_test 

176 

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 ) 

186 

187 

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" 

194 

195 @property 

196 def is_dev(self) -> bool: 

197 return self == EnvironmentType.DEV 

198 

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 } 

206 

207 @property 

208 def is_integration(self) -> bool: 

209 return self == EnvironmentType.TEST_INTEGRATION 

210 

211 @property 

212 def is_integration_external(self) -> bool: 

213 return self == EnvironmentType.TEST_INTEGRATION_EXTERNAL 

214 

215 @property 

216 def is_prod(self) -> bool: 

217 return self == EnvironmentType.PROD 

218 

219 @property 

220 def is_dev_test(self) -> bool: 

221 return self.is_dev or self.is_test 

222 

223 def set_environ(self) -> None: 

224 os.environ[APP_ENVIRONMENT_KEY] = self.value 

225 

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