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
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-21 23:45 +0000
1import enum
2import pathlib
3import typing
5import pydantic
6import structlog
7import toml
8import typing_extensions
9from pydantic import computed_field
10from pydantic_settings import BaseSettings
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)
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
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
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
58class StorageProviders(enum.Enum):
59 FILE = "file"
60 DUMMY = "dummy"
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()
221 @computed_field
222 def exclusion_words_set(self) -> set[str]:
223 return self._exclusion_words_set
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
232 class Config:
233 env_prefix: str = "spellcheck_"
236SETTINGS: SettingsOfMicroservice = SettingsOfMicroservice()