educommon 3.10.1__py3-none-any.whl → 3.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,9 @@ from educommon.django.db.validators import (
24
24
  from educommon.utils.misc import (
25
25
  cached_property,
26
26
  )
27
+ from educommon.utils.phone_number.modelfields import (
28
+ PhoneField,
29
+ )
27
30
 
28
31
 
29
32
  __all__ = [
@@ -41,7 +44,8 @@ __all__ = [
41
44
  'PassportNumberField',
42
45
  'INNField',
43
46
  'KPPField',
44
- 'OGRNField'
47
+ 'OGRNField',
48
+ 'PhoneField',
45
49
  ]
46
50
 
47
51
 
File without changes
@@ -0,0 +1,47 @@
1
+ from enum import (
2
+ Enum,
3
+ )
4
+
5
+
6
+ class PhoneFieldType(Enum):
7
+ """Тип номера телефона для поля модели."""
8
+
9
+ COMMON = 'COMMON'
10
+ E164 = 'E164'
11
+ RU = 'RU'
12
+ RU_E164 = 'RU_E164'
13
+ RU_LOCAL = 'RU_LOCAL'
14
+ RU_MOBILE = 'RU_MOBILE'
15
+
16
+ values = {
17
+ COMMON: 'Общий формат телефона',
18
+ E164: 'Международный формат телефона',
19
+ RU: 'Общий формат российского номера',
20
+ RU_E164: 'Российский номер в международном формате',
21
+ RU_LOCAL: 'Российский городской номер',
22
+ RU_MOBILE: 'Российский мобильный номер',
23
+ }
24
+
25
+ @classmethod
26
+ def get_choices(cls):
27
+ return list(cls.values.items())
28
+
29
+
30
+ class PhoneNumberType(Enum):
31
+ """Тип формата номера телефона."""
32
+
33
+ E164 = 'E164'
34
+ RU_E164 = 'RU_E164'
35
+ RU_LOCAL = 'RU_LOCAL'
36
+ UNKNOWN = 'UNKNOWN'
37
+
38
+ values = {
39
+ E164: 'Международный формат телефона',
40
+ RU_E164: 'Российский номер в международном формате',
41
+ RU_LOCAL: 'Российский городской номер',
42
+ UNKNOWN: 'Неизвестный формат',
43
+ }
44
+
45
+ @classmethod
46
+ def get_choices(cls):
47
+ return list(cls.values.items())
@@ -0,0 +1,91 @@
1
+ from django.db.models import (
2
+ CharField,
3
+ )
4
+
5
+ from educommon.utils.phone_number.enums import (
6
+ PhoneFieldType,
7
+ )
8
+ from educommon.utils.phone_number.phone_number import (
9
+ PhoneNumber,
10
+ to_python,
11
+ )
12
+ from educommon.utils.phone_number.validators import (
13
+ validate_common_phone_number,
14
+ validate_e164_phone_number,
15
+ validate_ru_e164_phone_number,
16
+ validate_ru_mobile_phone_number,
17
+ validate_ru_phone_number,
18
+ )
19
+
20
+
21
+ PHONE_FIELD_TYPE_TO_VALIDATOR = {
22
+ PhoneFieldType.COMMON: validate_common_phone_number,
23
+ PhoneFieldType.E164: validate_e164_phone_number,
24
+ PhoneFieldType.RU: validate_ru_phone_number,
25
+ PhoneFieldType.RU_E164: validate_ru_e164_phone_number,
26
+ PhoneFieldType.RU_MOBILE: validate_ru_mobile_phone_number,
27
+ }
28
+
29
+
30
+ class PhoneNumberDescriptor:
31
+ """Дескриптор для поля с номером телефона инстанса модели.
32
+
33
+ Возвращает PhoneNumber при доступе к полю телефона.
34
+
35
+ Позволяет для поля задавать номер телефона как:
36
+ instance.phone = PhoneNumber(...)
37
+ либо
38
+ instance.phone = '+7 (900) 555-55-55'
39
+ """
40
+
41
+ def __init__(self, field):
42
+ self.field = field
43
+
44
+ def __get__(self, instance, owner):
45
+ if instance is None:
46
+ return self
47
+
48
+ if self.field.name in instance.__dict__:
49
+ value = instance.__dict__[self.field.name]
50
+ else:
51
+ instance.refresh_from_db(fields=[self.field.name])
52
+ value = getattr(instance, self.field.name)
53
+
54
+ return value
55
+
56
+ def __set__(self, instance, value):
57
+ instance.__dict__[self.field.name] = to_python(value)
58
+
59
+
60
+ class PhoneField(CharField):
61
+ """Поле с номером телефона."""
62
+
63
+ attr_class = PhoneNumber
64
+ descriptor_class = PhoneNumberDescriptor
65
+
66
+ empty_values = [*CharField.empty_values, PhoneNumber('')]
67
+
68
+ def __init__(self, *args, phone_type: PhoneFieldType = PhoneFieldType.COMMON, **kwargs):
69
+ kwargs.setdefault('max_length', 31)
70
+ super().__init__(*args, **kwargs)
71
+
72
+ self.validators.append(
73
+ PHONE_FIELD_TYPE_TO_VALIDATOR.get(phone_type, validate_common_phone_number)
74
+ )
75
+
76
+ def to_python(self, value):
77
+ """Преобразование входящего значения в ожидаемый тип Python."""
78
+ return to_python(value)
79
+
80
+ def from_db_value(self, value, expression, connection):
81
+ """Преобразование значения полученного из БД."""
82
+ return self.to_python(value)
83
+
84
+ def get_prep_value(self, value):
85
+ """Подготовка значения перед сохранением в БД."""
86
+ if isinstance(value, PhoneNumber):
87
+ value = value.cleaned
88
+ elif value:
89
+ value = to_python(value).cleaned
90
+
91
+ return value or super().get_prep_value(value)
@@ -0,0 +1,273 @@
1
+ import re
2
+ from typing import (
3
+ Any,
4
+ Optional,
5
+ Union,
6
+ )
7
+
8
+ from educommon.utils.phone_number.enums import (
9
+ PhoneNumberType,
10
+ )
11
+
12
+
13
+ PHONE_REGEX = re.compile(
14
+ r'^(?:' # группа с кодом страны и кодом региона
15
+ r'(\+[1-6,90]{1,3}|\+?7|\+?8)?-?\s*' # опциональный код страны (+XXX, для России может быть +7, 7, 8)
16
+ r'\(?(\d{3}(?:[-\s]?\d{0,2}(?=[)-.\s]))?)\)?[-.\s]*' # оцпиональный код региона/города в необязательных скобках
17
+ r')?' # группа с кодом страны и кодом региона может отсутствовать
18
+ r'(\d{1,2}[-.\s]*\d?)[-.\s]*(\d[-.\s]*\d)[-.\s]*(\d[-.\s]*\d)$' # абонентский номер в формате XXX-XX-XX
19
+ # первая группа может состоять от 1 до 3 цифр
20
+ )
21
+
22
+ # российский мобильный номер вида +7 (9XX) XXX-XX-XX
23
+ RU_MOBILE_PHONE_REGEX = re.compile(r'^(\+?7|8)?-?\s*\(?(9\d{2})\)?[-.\s]*(\d{3})[-.\s]*(\d{2})[-.\s]*(\d{2})$')
24
+
25
+ # местный номер в формате: XXX-XX-XX, XX-XX-XX, X-XX-XX
26
+ LOCAL_PHONE_REGEX = re.compile(r'^(\d{1,3})[-.\s]*(\d{2})[-.\s]*(\d{2})$')
27
+
28
+ RU_PHONE_CODE = '+7'
29
+
30
+
31
+ class PhoneNumber:
32
+ """Телефонный номер.
33
+
34
+ В качестве параметра принимает строку с номером телефона в различных форматах,
35
+ производит разбор и валидацию номера, приводит его к единому формату.
36
+
37
+ Поддерживаемые форматы:
38
+ +XXX (XXX) XXX-XX-XX - в общем виде
39
+ +7 (XXX) XXX-XX-XX - российский формат
40
+ 8 (XXX) XXX-XX-XX, (XXX-XX) X-XX-XX - российский формат городских номеров
41
+ XXX-XX-XX, XX-XX-XX, X-XX-X - местные российские номера
42
+
43
+ Свойство cleaned возвращает номер в очищенном виде для возможности сохранения в БД.
44
+ Примеры входной строки и её преобразования в cleaned:
45
+ +12 (555) 888-77-66 -> +12(555)8887766
46
+ +7 (900) 555-44-33 -> +79005554433
47
+ 8 800 555-44-33 -> +78005554433
48
+ (818) 222-11-00 -> +78182221100
49
+ (818-53) 2-11-00 -> +7(81853)21100
50
+ 222 44 55 -> 2224455
51
+ 2-44-55 -> 24455
52
+
53
+ Возвращаемое текстовое представление номера в формате:
54
+ +XXX (XXX) XXX-XX-XX
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ phone: Optional[str],
60
+ *,
61
+ need_sanitize: bool = False,
62
+ ) -> None:
63
+ """Инициализация.
64
+
65
+ Args:
66
+ need_sanitize: Признак необходимости дополнительной очистки номера перед выполнением парсинга
67
+ """
68
+ self.raw_phone = phone or ''
69
+ self._need_sanitize = need_sanitize
70
+
71
+ self._cleaned = ''
72
+
73
+ self.country_code = ''
74
+ self.region_code = ''
75
+ self.subscriber_part_1 = ''
76
+ self.subscriber_part_2 = ''
77
+ self.subscriber_part_3 = ''
78
+
79
+ self._is_parsed = False
80
+ self._is_e164 = False
81
+
82
+ self.parse()
83
+
84
+ @staticmethod
85
+ def _sanitize(value: str) -> str:
86
+ """Очистка значения от незначащих символов."""
87
+ return value.replace(' ', '').replace('-', '')
88
+
89
+ @staticmethod
90
+ def _only_digits(value: str) -> str:
91
+ """Возвращает из строки только цифры."""
92
+ return ''.join(ch for ch in value if ch.isdigit())
93
+
94
+ @property
95
+ def _sanitized(self) -> str:
96
+ """Возвращает введенный номер в очищенном виде."""
97
+ return self._sanitize(self.raw_phone)
98
+
99
+ def parse(self):
100
+ """Разбор номера."""
101
+ if self._is_parsed or not self.raw_phone:
102
+ return
103
+
104
+ raw_phone = self._sanitized if self._need_sanitize else self.raw_phone.strip()
105
+ regex_match = PHONE_REGEX.search(raw_phone)
106
+
107
+ self._is_parsed = True
108
+
109
+ if regex_match:
110
+ self.subscriber_part_1 = self._only_digits(regex_match.group(3))
111
+ self.subscriber_part_2 = self._only_digits(regex_match.group(4))
112
+ self.subscriber_part_3 = self._only_digits(regex_match.group(5))
113
+
114
+ self.region_code = regex_match.group(2)
115
+ if self.region_code:
116
+ self.region_code = self._sanitize(self.region_code)
117
+
118
+ country_code = regex_match.group(1)
119
+
120
+ if country_code in ('8', '7') or (not country_code and self.region_code):
121
+ self.country_code = RU_PHONE_CODE
122
+ else:
123
+ self.country_code = country_code
124
+
125
+ if self.country_code == RU_PHONE_CODE and (len(self.region_code) + len(self.subscriber_part_1)) != 6:
126
+ # у российских номеров код региона и первый блок абонентского номера в сумме должны быть 6 цифр
127
+ return
128
+
129
+ if self.region_code:
130
+ self._is_e164 = True
131
+
132
+ if len(self.region_code) != 3 or len(self.country_code) > 2:
133
+ cleaned_template = '{0}({1}){2}{3}{4}'
134
+ else:
135
+ cleaned_template = '{0}{1}{2}{3}{4}'
136
+
137
+ self._cleaned = cleaned_template.format(
138
+ self.country_code,
139
+ self.region_code,
140
+ self.subscriber_part_1,
141
+ self.subscriber_part_2,
142
+ self.subscriber_part_3,
143
+ )
144
+
145
+ else:
146
+ self._cleaned = '{0}{1}{2}'.format(
147
+ self.subscriber_part_1,
148
+ self.subscriber_part_2,
149
+ self.subscriber_part_3,
150
+ )
151
+
152
+ @property
153
+ def type(self) -> PhoneNumberType:
154
+ """Тип номера."""
155
+ if self.is_valid:
156
+ if self.country_code == RU_PHONE_CODE:
157
+ return PhoneNumberType.RU_E164
158
+
159
+ elif self.country_code:
160
+ return PhoneNumberType.E164
161
+
162
+ elif not self.country_code:
163
+ return PhoneNumberType.RU_LOCAL
164
+
165
+ return PhoneNumberType.UNKNOWN
166
+
167
+ @property
168
+ def is_valid(self) -> bool:
169
+ """Признак корректности введенного номера."""
170
+ return True if self._is_parsed and self._cleaned else False
171
+
172
+ @property
173
+ def is_e164(self):
174
+ """Признак, что номер в международном формате."""
175
+ return self._is_e164
176
+
177
+ @property
178
+ def is_russia(self) -> bool:
179
+ """Признак, что номер российский."""
180
+ return (
181
+ self._cleaned.startswith(RU_PHONE_CODE)
182
+ or (self.is_valid and not self.region_code)
183
+ )
184
+
185
+ @property
186
+ def cleaned(self) -> str:
187
+ """Очищенный номер.
188
+
189
+ Используется для сохранения в БД.
190
+ """
191
+ return self._cleaned
192
+
193
+ @property
194
+ def formatted(self) -> str:
195
+ """Отформатированный номер для отображения пользователю."""
196
+ if not self.is_valid:
197
+ return ''
198
+
199
+ formatted_number = '{0}-{1}-{2}'.format(
200
+ self.subscriber_part_1,
201
+ self.subscriber_part_2,
202
+ self.subscriber_part_3,
203
+ )
204
+
205
+ if self._is_e164:
206
+ formatted_number = '{0} ({1}) {2}'.format(
207
+ self.country_code,
208
+ self.region_code,
209
+ formatted_number,
210
+ )
211
+
212
+ return formatted_number
213
+
214
+ def __str__(self) -> str:
215
+ """Текстовое представление."""
216
+ if self.is_valid:
217
+ return self.formatted
218
+ else:
219
+ return self.raw_phone
220
+
221
+ def __repr__(self) -> str:
222
+ """Представление объекта."""
223
+ if self.is_valid or not self.raw_phone:
224
+ return '<{0}: {1}>'.format(type(self).__name__, str(self) or "''")
225
+
226
+ else:
227
+ return '<Invalid {0}: raw_phone="{1}">'.format(type(self).__name__, self.raw_phone)
228
+
229
+ def __len__(self):
230
+ return len(self.cleaned)
231
+
232
+ def __bool__(self):
233
+ return bool(self.cleaned)
234
+
235
+ def __hash__(self):
236
+ return hash(self.cleaned)
237
+
238
+ def __eq__(self, other_phone) -> bool:
239
+ if isinstance(other_phone, str):
240
+ other_phone = PhoneNumber(other_phone)
241
+
242
+ elif not isinstance(other_phone, PhoneNumber):
243
+ return False
244
+
245
+ self_str = self.formatted if self.is_valid else self.raw_phone
246
+ other_str = other_phone.formatted if other_phone.is_valid else other_phone.raw_phone
247
+
248
+ return self_str == other_str
249
+
250
+ def __add__(self, other: str) -> str:
251
+ """Конкатенация со строкой."""
252
+ return str(self) + other
253
+
254
+ def __radd__(self, other: str) -> str:
255
+ """Конкатенация со строкой."""
256
+ return other + str(self)
257
+
258
+
259
+ def to_python(value: Any) -> Union[PhoneNumber, str, None]:
260
+ """Преобразование значения к объекту номера телефона."""
261
+ if value in (None, ''):
262
+ phone_number = value
263
+
264
+ elif isinstance(value, str):
265
+ phone_number = PhoneNumber(value)
266
+
267
+ elif isinstance(value, PhoneNumber):
268
+ phone_number = value
269
+
270
+ else:
271
+ raise TypeError(f'Невозможно преобразовать {type(value).__name__} к PhoneNumber.')
272
+
273
+ return phone_number
@@ -0,0 +1,92 @@
1
+ from typing import (
2
+ Union,
3
+ )
4
+
5
+ from django.core.exceptions import (
6
+ ValidationError,
7
+ )
8
+
9
+ from educommon.utils.phone_number.phone_number import (
10
+ PhoneNumber,
11
+ to_python,
12
+ )
13
+
14
+
15
+ def validate_common_phone_number(phone_number: Union[str, PhoneNumber]):
16
+ """Валидация номера телефона в общем формате."""
17
+ phone_number = to_python(phone_number)
18
+
19
+ if isinstance(phone_number, PhoneNumber) and not phone_number.is_valid:
20
+ raise ValidationError(
21
+ 'Неверный формат! Примеры допустимых форматов: '
22
+ '+XXX (XXX) XXX XX XX, 8 (XXXXX) X-XX-XX, XXX-XX-XX',
23
+ code='invalid',
24
+ )
25
+
26
+
27
+ def validate_e164_phone_number(phone_number: Union[str, PhoneNumber]):
28
+ """Валидация номера телефона в международном формате."""
29
+ phone_number = to_python(phone_number)
30
+
31
+ if (
32
+ isinstance(phone_number, PhoneNumber)
33
+ and not phone_number.is_valid
34
+ or not phone_number.is_e164
35
+ ):
36
+ raise ValidationError(
37
+ 'Неверный формат! Примеры допустимых форматов: '
38
+ '+XXX (XXX) XXX XX XX, +7 (XXX) XXX-XX-XX, 8 (XXXXX) X-XX-XX',
39
+ code='invalid',
40
+ )
41
+
42
+
43
+ def validate_ru_phone_number(phone_number: Union[str, PhoneNumber]):
44
+ """Валидация российского номера телефона в общем формате."""
45
+ phone_number = to_python(phone_number)
46
+
47
+ if (
48
+ isinstance(phone_number, PhoneNumber)
49
+ and not phone_number.is_valid
50
+ or not phone_number.is_russia
51
+ ):
52
+ raise ValidationError(
53
+ 'Неверный формат! Примеры допустимых форматов: '
54
+ '+7 (XXX) XXX-XX-XX, 8 (XXXXX) X-XX-XX, XXX-XX-XX',
55
+ code='invalid',
56
+ )
57
+
58
+
59
+ def validate_ru_e164_phone_number(phone_number: Union[str, PhoneNumber]):
60
+ """Валидация российского номера телефона в международном формате."""
61
+ phone_number = to_python(phone_number)
62
+
63
+ if (
64
+ isinstance(phone_number, PhoneNumber)
65
+ and not phone_number.is_valid
66
+ or not (phone_number.is_russia and phone_number.is_e164)
67
+ ):
68
+ raise ValidationError(
69
+ 'Неверный формат! Примеры допустимых форматов: '
70
+ '+7 (XXX) XXX-XX-XX, 8 (XXXXX) X-XX-XX, (XXX) XXX-XX-XX',
71
+ code='invalid',
72
+ )
73
+
74
+
75
+ def validate_ru_mobile_phone_number(phone_number: Union[str, PhoneNumber]):
76
+ """Валидация российского мобильного номера телефона."""
77
+ phone_number = to_python(phone_number)
78
+
79
+ if (
80
+ isinstance(phone_number, PhoneNumber)
81
+ and not phone_number.is_valid
82
+ or not (
83
+ phone_number.region_code
84
+ and len(phone_number.region_code) == 3
85
+ and phone_number.region_code.startswith('9')
86
+ )
87
+ ):
88
+ raise ValidationError(
89
+ 'Неверный формат! Примеры допустимых форматов: '
90
+ '+7 (9XX) XXX-XX-XX, 7 (9XX) XXX XX XX, 89XXXXXXXXX',
91
+ code='invalid',
92
+ )
educommon/version.conf CHANGED
@@ -4,8 +4,8 @@
4
4
  # нормальной установки обновлений.
5
5
 
6
6
  [version]
7
- BRANCH = tags/3.10.1
8
- VERSION = 3.10.1
9
- REVISION = 00d0a35e160f94820482deca72a676cb2d334a8b
10
- VERSION_DATE = 13.09.2024
11
- REVISION_DATE = 13.09.2024
7
+ BRANCH = tags/3.11.0
8
+ VERSION = 3.11.0
9
+ REVISION = f55e33a2f910df4cd73e441a843b681a481919bc
10
+ VERSION_DATE = 25.09.2024
11
+ REVISION_DATE = 25.09.2024
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: educommon
3
- Version: 3.10.1
3
+ Version: 3.11.0
4
4
  Summary: Общая кодовая база для проектов БЦ Образование
5
5
  Home-page: https://stash.bars-open.ru/projects/EDUBASE/repos/educommon
6
6
  Author: BARS Group
@@ -1,6 +1,6 @@
1
1
  educommon/__init__.py,sha256=fvsBDL7g8HgOTd-JHOh7TSvMcnUauvGVgPuyA2Z9hUI,419
2
2
  educommon/thread_data.py,sha256=n0XtdesP9H92O3rJ8K6fVnJLiHqyJEfh2xpuT36wzxs,61
3
- educommon/version.conf,sha256=r6d2hYszhvj6Gj4LKxIxT-XQb_dpgltGIthLz5qjlPU,450
3
+ educommon/version.conf,sha256=wgh9j5tHQoztDcRaVnISzOv7b8mQS40XZNuU8O4qFR8,450
4
4
  educommon/about/README.rst,sha256=U48UW5jv-8qHyaV56atzzkNMvzHKXVcWSb_NR06PnMo,2685
5
5
  educommon/about/__init__.py,sha256=H1W0IgW-qX9LCZ49GOJzHdmQGHhh-MA6U1xmNx7WnfM,132
6
6
  educommon/about/apps.py,sha256=GrpJAOE2sF0ukWsqugP_WJS88DO4aL-T3kTLprrJrcA,259
@@ -137,7 +137,7 @@ educommon/contingent/json_data/okogu.json,sha256=1yK8VWxivX5VeFQzkNSUWqHabu6RBy4
137
137
  educommon/contingent/json_data/oksm.json,sha256=kZIQPcdWhwtHKJGgfO79gCYtk6dEe2iAz3Lj5L_AZrU,41859
138
138
  educommon/django/__init__.py,sha256=0Fe7dZFIVdw18JIpvl9P7q0GoqKN7ySr5D1rA9YaAzk,69
139
139
  educommon/django/db/__init__.py,sha256=bkN8tB5vTllcTXYt8ERHn1wgUQCFXNUmZi068u1jBfU,73
140
- educommon/django/db/fields.py,sha256=to1sa79s-9WaFwYXUQeDjW4J7TFie5SPr6sNQxV6ZbA,7049
140
+ educommon/django/db/fields.py,sha256=a_CCuDthg9Gv_T3Ngm93Iz6kTFNo1LJMnk2-PFKohsM,7141
141
141
  educommon/django/db/models.py,sha256=mNtrj3MZYCXJJ2alw5TaIiN_fFjsCUrm8OCeuZ9isWI,2107
142
142
  educommon/django/db/observer.py,sha256=oAh0Uu0fwlGZ4pj1ytKZyAhosbWKs4THGBTVyR4NYmA,10990
143
143
  educommon/django/db/routers.py,sha256=Zx8cIf7ZQpQLVhxutNj31_IQqYS6J3MnuBTOA5m43SQ,3268
@@ -305,6 +305,11 @@ educommon/utils/fonts/Tahoma.ttf,sha256=bGmcQotghnnHrebZwP92ZkobWSdMOUzwq6DuLDP4
305
305
  educommon/utils/fonts/__init__.py,sha256=KfGNPOWk7up_p62UKt0lZIuMpMryXYo0qoqI5_Yp7Zc,4815
306
306
  educommon/utils/licence/__init__.py,sha256=qr2XwwmrewMNM2y_bHaLq2Q20I8I66wfxtvrBABnpFU,5095
307
307
  educommon/utils/licence/converters.py,sha256=BLXjqy4kgBj78qqfiXtuz0Nwu5rVb_us3omNJkObTtA,1174
308
+ educommon/utils/phone_number/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
309
+ educommon/utils/phone_number/enums.py,sha256=EqTCTeCxYioKco07fkG25hq6lae63o0WZIuo-6fdNRg,1420
310
+ educommon/utils/phone_number/modelfields.py,sha256=gIdDYSf7a30m85JTdmXVxOv0j-IYMUntweZngS7sV6I,2938
311
+ educommon/utils/phone_number/phone_number.py,sha256=80dzAMiJ1rDWFRx2GsC1PTtg9GgXxdvty2-7XLhZTik,9969
312
+ educommon/utils/phone_number/validators.py,sha256=QFMWGbatinNBjdMNYsMfNSO7VPARMdW4BFtIM_kOoxI,3290
308
313
  educommon/utils/system_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
309
314
  educommon/utils/system_app/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
310
315
  educommon/utils/system_app/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -344,8 +349,8 @@ educommon/ws_log/smev/exceptions.py,sha256=lmy7o2T3dJkqgIhG07qyh5yPqO3qZAYABuT4J
344
349
  educommon/ws_log/templates/report/smev_logs.xlsx,sha256=nnYgB0Z_ix8HoxsRICjsZfFRQBdra-5Gd8nWhCxTjYg,10439
345
350
  educommon/ws_log/templates/ui-js/smev-logs-list-window.js,sha256=AGup3D8GTJSY9WdDPj0zBJeYQBFOmGgcbxPOJbKK-nY,513
346
351
  educommon/ws_log/templates/ui-js/smev-logs-report-setting-window.js,sha256=nQ7QYK9frJcE7g7kIt6INg9TlEEJAPPayBJgRaoTePA,1103
347
- educommon-3.10.1.dist-info/METADATA,sha256=YZqvwSEOzTu0Kzwx-hcGe_GLqA4qvC8FV3g8T2w-VHs,1563
348
- educommon-3.10.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
349
- educommon-3.10.1.dist-info/dependency_links.txt,sha256=RNlr4t-BxZRm7e_IfVo1ikr5ln-7viimzLHvQMO1C_Q,43
350
- educommon-3.10.1.dist-info/top_level.txt,sha256=z5fbW7bz_0V1foUm_FGcZ9_MTpW3N1dBN7-kEmMowl4,10
351
- educommon-3.10.1.dist-info/RECORD,,
352
+ educommon-3.11.0.dist-info/METADATA,sha256=B8t2OQ9Hzeu07wf-yJbwyDJKcwUpLAVW-ktXa9tXOz8,1563
353
+ educommon-3.11.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
354
+ educommon-3.11.0.dist-info/dependency_links.txt,sha256=RNlr4t-BxZRm7e_IfVo1ikr5ln-7viimzLHvQMO1C_Q,43
355
+ educommon-3.11.0.dist-info/top_level.txt,sha256=z5fbW7bz_0V1foUm_FGcZ9_MTpW3N1dBN7-kEmMowl4,10
356
+ educommon-3.11.0.dist-info/RECORD,,