regiments.core.Regiment.__getattr__()   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 20
rs 9.2333
c 0
b 0
f 0
cc 5
nop 2
1
import sys
2
3
if sys.version_info >= (3, 9):
4
  from collections.abc import Callable
5
6
  Dict = dict
7
  List = list
8
9
else:
10
  from typing import Callable
11
  from typing import Dict
12
  from typing import List
13
14
from pathlib import Path
15
16
from typing import Any
17
from typing import Optional
18
19
from verboselib import get_language
20
21
from il2fb.commons.air_forces import AirForceConstant
22
from il2fb.commons.air_forces import AIR_FORCES
23
24
from il2fb.commons.supported_languages import SUPPORTED_LANGUAGES
25
26
from .exceptions import IL2FBRegimentAttributeError
27
from .exceptions import IL2FBRegimentDataSourceNotFound
28
from .exceptions import IL2FBRegimentLookupError
29
30
from ._utils import export
31
32
33
__here__ = Path(__file__).absolute().parent
34
35
36
DEFAULT_LANGUAGE_NAME = SUPPORTED_LANGUAGES.get_default().name
37
38
DEFAULT_DATA_DIR_PATH = __here__ / "data"
39
DEFAULT_DATA_MISSING_VALUE = None
40
DEFAULT_DATA_FILE_ENCODING = "cp1251"
41
DEFAULT_VALUE_ENCODING = "unicode-escape"
42
43
DEFAULT_CATALOG_FILE_NAME = "regiments.ini"
44
45
DEFAULT_NAMES_FILE_NAME_FORMAT        = "regShort_{language}.properties"
46
DEFAULT_DESCRIPTIONS_FILE_NAME_FORMAT = "regInfo_{language}.properties"
47
48
DEFAULT_REGIMENTS_IDS = frozenset(AIR_FORCES.get_default_regiment_ids())
49
50
51
@export
52
class RegimentInfoLoader:
53
54
  def __init__(
55
    self,
56
    data_dir_path: Path=DEFAULT_DATA_DIR_PATH,
57
    data_file_encoding: str=DEFAULT_DATA_FILE_ENCODING,
58
    data_value_encoding: str=DEFAULT_VALUE_ENCODING,
59
    data_missing_value: str=DEFAULT_DATA_MISSING_VALUE,
60
    names_file_name_format: str=DEFAULT_NAMES_FILE_NAME_FORMAT,
61
    descriptions_file_name_format: str=DEFAULT_DESCRIPTIONS_FILE_NAME_FORMAT,
62
  ):
63
    self._data_dir_path = data_dir_path
64
    self._data_file_encoding = data_file_encoding
65
    self._data_value_encoding = data_value_encoding
66
    self._data_missing_value = data_missing_value
67
    self._names_file_name_format = names_file_name_format
68
    self._descriptions_file_name_format = descriptions_file_name_format
69
70
  def get_name(self, regiment_id: str, language: Any) -> str:
71
    return self._get_value(regiment_id, self._names_file_name_format, language)
72
73
  def get_description(self, regiment_id: str, language: Any) -> str:
74
    return self._get_value(regiment_id, self._descriptions_file_name_format, language)
75
76
  def _get_value(self, regiment_id: str, file_name_format: str, language: Any) -> str:
77
    language  = language and language.lower()
78
    file_name = file_name_format.format(language=language)
79
    file_path = self._data_dir_path / file_name
80
    try:
81
      return self._load_value_or_raise(regiment_id, file_path)
82
    except IL2FBRegimentLookupError:
83
      return self._data_missing_value
84
85
  def _load_value_or_raise(self, regiment_id: str, file_path: Path) -> str:
86
    if not file_path.exists():
87
      raise IL2FBRegimentDataSourceNotFound
88
89
    with file_path.open(mode="rb") as f:
90
      regiment_id = regiment_id.encode(self._data_file_encoding)
91
      for line in f:
92
        if line.startswith(regiment_id):
93
          key, value = line.split(maxsplit=1)
94
          return value.decode(self._data_value_encoding).strip()
95
      else:
96
        raise IL2FBRegimentLookupError
97
98
99
@export
100
class Regiment:
101
102
  def __init__(
103
    self,
104
    id:          str,
105
    air_force:   AirForceConstant,
106
    info_loader: Optional[RegimentInfoLoader]=None,
107
  ):
108
    self.id        = id
109
    self.air_force = air_force
110
111
    self._info_loader = info_loader or RegimentInfoLoader()
112
    self._text_attribute_loaders = {
113
      'verbose_name': self._info_loader.get_name,
114
      'help_text':    self._info_loader.get_description,
115
    }
116
117
  def __getattr__(self, name: str) -> str:
118
    loader = self._text_attribute_loaders.get(name)
119
    if not loader:
120
      raise IL2FBRegimentAttributeError(
121
        f"'{self.__class__}' object has no attribute '{name}'"
122
      )
123
124
    language = get_language()
125
    if language and language.upper() not in SUPPORTED_LANGUAGES:
126
      language = DEFAULT_LANGUAGE_NAME
127
128
    full_name = f"{name}_{language}"
129
130
    value = getattr(self, full_name, None)
131
132
    if not value:
133
      value = self._load_value(loader, language)
134
      setattr(self, full_name, value)
135
136
    return value
137
138
  def _load_value(
139
    self,
140
    loader:   Callable[[str, Any], str],
141
    language: Any,
142
  ) -> str:
143
144
    value = loader(regiment_id=self.id, language=language)
145
146
    if not value and language != DEFAULT_LANGUAGE_NAME:
147
      value = loader(regiment_id=self.id, language=DEFAULT_LANGUAGE_NAME)
148
149
    return value
150
151
  def to_primitive(self, context: Any=None) -> Dict[str, Any]:
152
    return {
153
      'id':           self.id,
154
      'air_force':    self.air_force.to_primitive(context),
155
      'verbose_name': self.verbose_name,
156
      'help_text':    self.help_text,
157
    }
158
159
  def __repr__(self) -> str:
160
    return f"<{self.__class__.__name__} '{self.id}'>"
161
162
163
@export
164
class Regiments:
165
166
  def __init__(
167
    self,
168
    data_dir_path:      Path=DEFAULT_DATA_DIR_PATH,
169
    data_file_name:     str=DEFAULT_CATALOG_FILE_NAME,
170
    data_file_encoding: str=DEFAULT_DATA_FILE_ENCODING,
171
    info_loader:        RegimentInfoLoader=None,
172
  ):
173
    self._data_file_path = data_dir_path / data_file_name
174
    if not self._data_file_path.exists():
175
      raise IL2FBRegimentDataSourceNotFound(
176
        f"data source file '{str(self._data_file_path)}' does not exist"
177
      )
178
179
    self._data_file_encoding = data_file_encoding
180
181
    self._info_loader = info_loader or RegimentInfoLoader(
182
      data_dir_path=data_dir_path,
183
      data_file_encoding=data_file_encoding,
184
    )
185
186
    self._cache = dict()
187
188
  def get_by_id(self, id: str) -> Regiment:
189
    regiment = self._cache.get(id)
190
191
    if not regiment:
192
      regiment = self._load_by_id_or_raise(id)
193
      self._cache[id] = regiment
194
195
    return regiment
196
197
  def _load_by_id_or_raise(self, id: str) -> Regiment:
198
    default_regiment_id = self._get_default_regiment_id_for_regiment(id)
199
    if not default_regiment_id:
200
      raise IL2FBRegimentLookupError(
201
        f"regiment with id '{id}' not found"
202
      )
203
204
    air_force = AIR_FORCES.get_by_default_regiment_id(default_regiment_id)
205
206
    return Regiment(
207
      id=id,
208
      air_force=air_force,
209
      info_loader=self._info_loader,
210
    )
211
212
  def _get_default_regiment_id_for_regiment(self, id: str) -> Optional[str]:
213
    with self._data_file_path.open(
214
      mode="rt",
215
      encoding=self._data_file_encoding,
216
      buffering=1,
217
    ) as f:
218
219
      default_regiment_id = None
220
221
      for line in f:
222
        line = line.strip()
223
        if not line:
224
          continue
225
226
        if line in DEFAULT_REGIMENTS_IDS:
227
          default_regiment_id = line
228
229
        elif line == id:
230
          return default_regiment_id
231
232
  def filter_by_air_force(self, air_force: AirForceConstant) -> List[Regiment]:
233
    result = []
234
235
    with self._data_file_path.open(
236
      mode="rt",
237
      encoding=self._data_file_encoding,
238
      buffering=1,
239
    ) as f:
240
241
      default_regiment_id = air_force.default_regiment_id
242
      air_force_is_found = False
243
244
      for line in f:
245
        line = line.strip()
246
247
        if not line:
248
          continue
249
250
        if line == default_regiment_id:
251
          air_force_is_found = True
252
253
        elif air_force_is_found:
254
          if (
255
             line in DEFAULT_REGIMENTS_IDS or
256
            (line.startswith('[') and line.endswith(']'))
257
          ):
258
            # Next section was found. Fullstop.
259
            break
260
261
          regiment = self._cache.get(line)
262
          if not regiment:
263
            regiment = Regiment(
264
              id=line,
265
              air_force=air_force,
266
              info_loader=self._info_loader,
267
            )
268
            self._cache[line] = regiment
269
270
          result.append(regiment)
271
272
    return result
273