Coverage for whole_app/settings.py: 100%

66 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-21 23:45 +0000

1import enum 

2import pathlib 

3import typing 

4 

5import pydantic 

6import structlog 

7import toml 

8import typing_extensions 

9from pydantic import computed_field 

10from pydantic_settings import BaseSettings 

11 

12 

13LOGGER_OBJ: typing.Final = structlog.get_logger() 

14PATH_TO_PYPROJECT: typing.Final = pathlib.Path(__file__).parent.parent / "pyproject.toml" 

15AvailableLanguagesType = typing.Literal[ 

16 "ru_RU", 

17 "en_US", 

18 "es_ES", 

19 "fr_FR", 

20 "de_DE", 

21 "pt_PT", 

22] 

23AvailableLanguages: tuple[str, ...] = typing.get_args(AvailableLanguagesType) 

24 

25 

26def _warn_about_poor_lru_cache_size( 

27 possible_value: int, 

28) -> int: 

29 if int(possible_value) < 1: 

30 LOGGER_OBJ.warning( 

31 ("You set cache size less then 1. In this case, the cache size will be unlimited and polute your memory."), 

32 ) 

33 return 0 

34 return possible_value 

35 

36 

37def _warn_about_empty_api_key( 

38 possible_value: str, 

39) -> str: 

40 if not possible_value: 

41 LOGGER_OBJ.warning("You set empty API key. This is not recommended.") 

42 return possible_value 

43 

44 

45def _parse_version_from_local_file( 

46 default_value: str, 

47) -> str: 

48 try: 

49 pyproject_obj: typing.Final[dict[str, dict[str, object]]] = toml.loads( 

50 PATH_TO_PYPROJECT.read_text(), 

51 ) 

52 return typing.cast("str", pyproject_obj["project"]["version"]) 

53 except (toml.TomlDecodeError, KeyError, FileNotFoundError) as exc: 

54 LOGGER_OBJ.warning("Cant parse version from pyproject. Trouble %s", exc) 

55 return default_value 

56 

57 

58class StorageProviders(enum.Enum): 

59 FILE = "file" 

60 DUMMY = "dummy" 

61 

62 

63class SettingsOfMicroservice(BaseSettings): 

64 app_title: str = "Spellcheck API" 

65 service_name: str = "spellcheck-microservice" 

66 sentry_dsn: typing.Annotated[ 

67 str, 

68 pydantic.Field( 

69 description="Sentry DSN for integration. Empty field disables integration", 

70 ), 

71 pydantic.StringConstraints( 

72 strip_whitespace=True, 

73 ), 

74 ] = "" 

75 api_key: typing.Annotated[ 

76 str, 

77 pydantic.BeforeValidator(_warn_about_empty_api_key), 

78 pydantic.Field( 

79 description=( 

80 "define api key for users dictionaries mostly. " 

81 "Please, provide, if you want to enable user dictionaries API" 

82 ), 

83 ), 

84 ] = "" 

85 api_key_header_name: typing.Annotated[ 

86 str, 

87 pydantic.StringConstraints( 

88 strip_whitespace=True, 

89 ), 

90 ] = "Api-Key" 

91 enable_cors: typing.Annotated[ 

92 bool, 

93 pydantic.Field( 

94 description="enable CORS for all endpoints. In docker container this option is disabled", 

95 ), 

96 ] = True 

97 structured_logging: typing.Annotated[ 

98 bool, 

99 pydantic.Field( 

100 description="enables structured (json) logging", 

101 ), 

102 ] = True 

103 workers: typing.Annotated[ 

104 int, 

105 pydantic.Field( 

106 gt=0, 

107 lt=301, 

108 description=( 

109 "define application server workers count. " 

110 "If you plan to use k8s and only scale with replica sets, you might want to reduce this value to `1`" 

111 ), 

112 ), 

113 ] = 8 

114 server_address: typing.Annotated[ 

115 str, 

116 pydantic.StringConstraints( 

117 strip_whitespace=True, 

118 ), 

119 pydantic.Field( 

120 description="binding address, default value suitable for docker", 

121 ), 

122 ] = "0.0.0.0" # noqa: S104 

123 port: typing.Annotated[ 

124 int, 

125 pydantic.Field( 

126 gt=1_023, 

127 lt=65_536, 

128 description="binding port", 

129 ), 

130 ] = 10_113 

131 cache_size: typing.Annotated[ 

132 int, 

133 pydantic.BeforeValidator(_warn_about_poor_lru_cache_size), 

134 pydantic.Field( 

135 description=( 

136 "define LRU cache size for misspelled word/suggestions cache. " 

137 "Any value less than `1` makes the cache size unlimited, so be careful with this option" 

138 ), 

139 ), 

140 ] = 10_000 

141 api_prefix: typing.Annotated[ 

142 str, 

143 pydantic.StringConstraints( 

144 strip_whitespace=True, 

145 ), 

146 pydantic.BeforeValidator( 

147 lambda possible_value: f"/{possible_value.strip('/')}", 

148 ), 

149 pydantic.Field(description="define all API's URL prefix"), 

150 ] = "/api/" 

151 docs_url: typing.Annotated[ 

152 str, 

153 pydantic.StringConstraints( 

154 strip_whitespace=True, 

155 ), 

156 pydantic.Field( 

157 description="define documentation (swagger) URL prefix", 

158 ), 

159 ] = "/docs/" 

160 max_suggestions: typing.Annotated[ 

161 int, 

162 pydantic.Field( 

163 ge=0, 

164 description="defines how many maximum suggestions for each word will be available. 0 means unlimitied", 

165 ), 

166 ] = 0 

167 dictionaries_path: typing.Annotated[ 

168 pathlib.Path, 

169 pydantic.Field( 

170 description=( 

171 "define directory where user dicts is stored. " 

172 "This is inner directory in the docker image, please map it to volume as it " 

173 "shown in the quickstart part of this readme" 

174 ), 

175 ), 

176 ] = pathlib.Path("/data/") 

177 dictionaries_storage_provider: typing.Annotated[ 

178 StorageProviders, 

179 pydantic.Field( 

180 description="define wich engine will store user dictionaries", 

181 ), 

182 ] = StorageProviders.FILE 

183 dictionaries_disabled: typing.Annotated[ 

184 bool, 

185 pydantic.Field( 

186 description="switches off user dictionaries API no matter what", 

187 ), 

188 ] = False 

189 current_version: typing.Annotated[ 

190 str, 

191 pydantic.BeforeValidator(_parse_version_from_local_file), 

192 ] = "" 

193 username_min_length: typing.Annotated[ 

194 int, 

195 pydantic.Field( 

196 description="minimum length of username", 

197 ), 

198 ] = 3 

199 username_max_length: typing.Annotated[ 

200 int, 

201 pydantic.Field( 

202 description="maximum length of username", 

203 ), 

204 ] = 60 

205 username_regex: str = r"^[a-zA-Z0-9-_]*$" 

206 exclusion_words_str: typing.Annotated[ 

207 str, 

208 pydantic.Field( 

209 description="String with list of words which will be ignored in /api/check endpoint each request. " 

210 "Example: `'foo, bar'`" 

211 ), 

212 ] = "" 

213 _exclusion_words_set: typing.Annotated[ 

214 set[str], 

215 pydantic.Field( 

216 description="""set of words which will ignored by default(filled from exclusion_words_str). 

217 Example: `'["foo", "bar"]'` """, 

218 ), 

219 ] = set() 

220 

221 @computed_field 

222 def exclusion_words_set(self) -> set[str]: 

223 return self._exclusion_words_set 

224 

225 @pydantic.model_validator(mode="after") 

226 def _assemble_exclusion_words_set(self) -> typing_extensions.Self: 

227 self._exclusion_words_set = { 

228 one_word.strip().lower() for one_word in self.exclusion_words_str.split(",") if one_word 

229 } 

230 return self 

231 

232 class Config: 

233 env_prefix: str = "spellcheck_" 

234 

235 

236SETTINGS: SettingsOfMicroservice = SettingsOfMicroservice()