edu-rdm-integration 3.5.8__py3-none-any.whl → 3.6.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.
@@ -41,6 +41,19 @@ RDM_TRANSFER_TASK_HOUR = '*/4'
41
41
  RDM_TRANSFER_TASK_DAY_OF_WEEK = '*'
42
42
  RDM_TRANSFER_TASK_LOCK_EXPIRE_SECONDS = 60 * 60 * 6
43
43
 
44
+ # Настройка запуска периодической задачи выгрузки данных - очередь быстрого уровня
45
+ RDM_FAST_TRANSFER_TASK_MINUTE = '*/5'
46
+ RDM_FAST_TRANSFER_TASK_HOUR = '*'
47
+ RDM_FAST_TRANSFER_TASK_DAY_OF_WEEK = '*'
48
+ RDM_FAST_TRANSFER_TASK_LOCK_EXPIRE_SECONDS = 60 * 30
49
+
50
+ # Настройка запуска периодической задачи выгрузки данных - очередь медленного уровня
51
+ RDM_LONG_TRANSFER_TASK_MINUTE = '0'
52
+ RDM_LONG_TRANSFER_TASK_HOUR = '*/6'
53
+ RDM_LONG_TRANSFER_TASK_DAY_OF_WEEK = '*'
54
+ RDM_LONG_TRANSFER_TASK_LOCK_EXPIRE_SECONDS = 60 * 60 * 6
55
+
56
+
44
57
  # Настройка запуска периодической задачи статуса загрузки данных в витрину:
45
58
  RDM_UPLOAD_STATUS_TASK_MINUTE = '*/30'
46
59
  RDM_UPLOAD_STATUS_TASK_HOUR = '*'
@@ -3,10 +3,40 @@ from abc import (
3
3
  ABC,
4
4
  abstractmethod,
5
5
  )
6
+ from collections import (
7
+ defaultdict,
8
+ )
6
9
  from typing import (
7
10
  Optional,
8
11
  )
9
12
 
13
+ from django.db.models import (
14
+ Q,
15
+ )
16
+
17
+ from m3_db_utils.consts import (
18
+ DEFAULT_ORDER_NUMBER,
19
+ )
20
+
21
+ from edu_rdm_integration.consts import (
22
+ REGIONAL_DATA_MART_INTEGRATION_COLLECTING_DATA,
23
+ REGIONAL_DATA_MART_INTEGRATION_EXPORTING_DATA,
24
+ )
25
+ from edu_rdm_integration.helpers import (
26
+ UploadStatusHelper,
27
+ get_collecting_managers_max_period_ended_dates,
28
+ get_exporting_managers_max_period_ended_dates,
29
+ save_command_log_link,
30
+ )
31
+ from edu_rdm_integration.models import (
32
+ ExportingDataSubStageUploaderClientLog,
33
+ RegionalDataMartEntityEnum,
34
+ TransferredEntity,
35
+ )
36
+ from edu_rdm_integration.storages import (
37
+ RegionalDataMartEntityStorage,
38
+ )
39
+
10
40
 
11
41
  class BaseOperationData(ABC):
12
42
  """Базовый класс операций с данными."""
@@ -39,3 +69,79 @@ class BaseOperationData(ABC):
39
69
  logging.getLogger('exception_logger').removeHandler(self._file_handler)
40
70
 
41
71
  self._file_handler.close()
72
+
73
+
74
+ class BaseTransferLatestEntitiesDataMixin:
75
+ """Миксин сбора и выгрузки данных."""
76
+
77
+ def __init__(self) -> None:
78
+ super().__init__()
79
+
80
+ self._collecting_data_managers: dict[str, type['RunnerManager']] = {}
81
+ self._collecting_data_manager_to_logs_period_end: dict[str, 'datetime'] = {}
82
+
83
+ self._exporting_data_managers: dict[str, type['RunnerManager']] = {}
84
+ self._exporting_data_manager_to_period_end: dict[str, 'datetime'] = {}
85
+
86
+ self._transferred_entities = []
87
+ self._entites_models_map = defaultdict(list)
88
+
89
+ def get_entity_qs(self) -> 'QuerySet[TransferredEntity]':
90
+ """Возвращает сущностей сбора и выгрузки."""
91
+ raise NotImplementedError
92
+
93
+ def _collect_transferred_entities(self) -> None:
94
+ """Собирает сущности РВД, по которым будет произведен сбор и экспорт данных."""
95
+
96
+ self._transferred_entities = [
97
+ (RegionalDataMartEntityEnum.get_model_enum_value(key=entity), export_enabled)
98
+ for entity, export_enabled in self.get_entity_qs().values_list('entity', 'export_enabled')
99
+ ]
100
+
101
+ # Собираем словарь по сущностям с моделями для сборки
102
+ for entity, _ in self._transferred_entities:
103
+ self._entites_models_map[entity.key].extend(
104
+ (model_enum for model_enum in (*entity.additional_model_enums, entity.main_model_enum)
105
+ if model_enum.order_number != DEFAULT_ORDER_NUMBER)
106
+ )
107
+
108
+ def _collect_managers(self) -> None:
109
+ """Собирает менеджеры Функций для сбора и выгрузки данных."""
110
+ entity_storage = RegionalDataMartEntityStorage()
111
+ entity_storage.prepare()
112
+
113
+ collecting_models_data_managers_map = entity_storage.prepare_entities_manager_map(
114
+ tags={REGIONAL_DATA_MART_INTEGRATION_COLLECTING_DATA},
115
+ )
116
+ exporting_entities_data_managers_map = entity_storage.prepare_entities_manager_map(
117
+ tags={REGIONAL_DATA_MART_INTEGRATION_EXPORTING_DATA},
118
+ )
119
+
120
+ for entity_key, entity_models in self._entites_models_map.items():
121
+ for entity_model in entity_models:
122
+ collect_manager_class = collecting_models_data_managers_map.get(entity_model.key)
123
+ if collect_manager_class:
124
+ self._collecting_data_managers[entity_model.key] = collect_manager_class
125
+
126
+ export_manager_class = exporting_entities_data_managers_map.get(entity_key)
127
+ if export_manager_class:
128
+ self._exporting_data_managers[entity_key] = export_manager_class
129
+
130
+ def _calculate_collecting_managers_logs_period_ended_at(self) -> None:
131
+ """Определяет дату последнего успешного этапа сбора у менеджеров Функций сбора."""
132
+ self._collecting_data_manager_to_logs_period_end = get_collecting_managers_max_period_ended_dates(
133
+ self._collecting_data_managers.values()
134
+ )
135
+
136
+ def _calculate_exporting_managers_ended_at(self) -> None:
137
+ """Определяет дату последнего успешного подэтапа экспорта у менеджеров Функций экспорта."""
138
+ self._exporting_data_manager_to_period_end = get_exporting_managers_max_period_ended_dates(
139
+ self._exporting_data_managers.values()
140
+ )
141
+
142
+ def prepare_collect_export_managers(self) -> None:
143
+ """Подготовка менджеров сбора и экспорта."""
144
+ self._collect_transferred_entities()
145
+ self._collect_managers()
146
+ self._calculate_collecting_managers_logs_period_ended_at()
147
+ self._calculate_exporting_managers_ended_at()
@@ -29,6 +29,8 @@ ACADEMIC_YEAR = {
29
29
  }
30
30
 
31
31
  TASK_QUEUE_NAME = 'RDM'
32
+ FAST_TRANSFER_TASK_QUEUE_NAME = 'RDM_FAST'
33
+ LONG_TRANSFER_TASK_QUEUE_NAME = 'RDM_LONG'
32
34
 
33
35
 
34
36
  # Кортеж операций для обновления данных
@@ -6,6 +6,12 @@ from m3.db import (
6
6
  BaseEnumerate,
7
7
  )
8
8
 
9
+ from edu_rdm_integration.consts import (
10
+ FAST_TRANSFER_TASK_QUEUE_NAME,
11
+ LONG_TRANSFER_TASK_QUEUE_NAME,
12
+ TASK_QUEUE_NAME,
13
+ )
14
+
9
15
 
10
16
  class ValueCodeEnumerate(BaseEnumerate):
11
17
  """
@@ -166,3 +172,28 @@ class CommandType(BaseEnumerate):
166
172
  AUTO: 'Автоматический',
167
173
  MANUAL: 'Ручной',
168
174
  }
175
+
176
+
177
+ class EntityLevelQueueTypeEnum(BaseEnumerate):
178
+ """Тип уровня очереди сущности."""
179
+
180
+ FAST = 1
181
+ BASE = 2
182
+ LONG = 3
183
+
184
+ values = {
185
+ FAST: 'Быстрая',
186
+ BASE: 'Основная',
187
+ LONG: 'Долгая'
188
+ }
189
+
190
+ celery_queue_maps = {
191
+ FAST: FAST_TRANSFER_TASK_QUEUE_NAME,
192
+ BASE: TASK_QUEUE_NAME,
193
+ LONG: LONG_TRANSFER_TASK_QUEUE_NAME
194
+ }
195
+
196
+ @classmethod
197
+ def get_queue_name(cls, level: int) -> Optional[str]:
198
+ """Возвращает очередь."""
199
+ return cls.celery_queue_maps.get(level)
@@ -0,0 +1,22 @@
1
+ # Generated by Django 3.2.24 on 2025-04-17 12:43
2
+
3
+ from django.db import (
4
+ migrations,
5
+ models,
6
+ )
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ('edu_rdm_integration', '0015_set_exporting_sub_stage_status'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name='transferredentity',
18
+ name='queue_level',
19
+ field=models.PositiveIntegerField(
20
+ choices=[(1, 'Быстрая'), (2, 'Основная'), (3, 'Долгая')], default=2, verbose_name='Уровень очереди'),
21
+ ),
22
+ ]
@@ -63,6 +63,7 @@ from m3_db_utils.models import (
63
63
 
64
64
  from edu_rdm_integration.enums import (
65
65
  CommandType,
66
+ EntityLevelQueueTypeEnum,
66
67
  FileUploadStatusEnum,
67
68
  )
68
69
  from edu_rdm_integration.uploader_log.managers import (
@@ -932,6 +933,11 @@ class TransferredEntity(BaseObjectModel):
932
933
  verbose_name='Включение экспорта для сущности',
933
934
  default=True,
934
935
  )
936
+ queue_level = PositiveIntegerField(
937
+ choices=EntityLevelQueueTypeEnum.get_choices(),
938
+ default=EntityLevelQueueTypeEnum.BASE,
939
+ verbose_name='Уровень очереди',
940
+ )
935
941
 
936
942
  class Meta:
937
943
  db_table = 'rdm_transferred_entity'
@@ -21,6 +21,7 @@ from edu_rdm_integration.models import (
21
21
  TransferredEntity,
22
22
  )
23
23
  from edu_rdm_integration.registry.ui import (
24
+ TransferredEntityEditWindow,
24
25
  TransferredEntityListWindow,
25
26
  )
26
27
 
@@ -97,14 +98,27 @@ class TransferredEntitySaveAction(BaseAction):
97
98
  """Объявляет контекст экшна."""
98
99
  return {
99
100
  'id': {'type': 'str_list', 'default': []},
101
+ self.parent.id_param_name: {'type': 'int'}
100
102
  }
101
103
 
102
104
  def run(self, request, context):
103
105
  """Обеспечивает выполнение запроса."""
104
- self.parent.model.objects.bulk_create([
105
- self.parent.model(entity_id=key)
106
- for key in context.id
107
- ])
106
+ obj_id = getattr(context, self.parent.id_param_name)
107
+ if obj_id:
108
+ queue_level = getattr(context, 'queue_level', None)
109
+ if queue_level:
110
+ self.parent.model.objects.filter(
111
+ id=obj_id
112
+ ).exclude(
113
+ queue_level=queue_level
114
+ ).update(
115
+ queue_level=queue_level
116
+ )
117
+ else:
118
+ self.parent.model.objects.bulk_create([
119
+ self.parent.model(entity_id=key)
120
+ for key in context.id
121
+ ])
108
122
 
109
123
  return OperationResult(success=True)
110
124
 
@@ -121,6 +135,7 @@ class TransferredEntityPack(ObjectPack):
121
135
 
122
136
  need_check_permission = True
123
137
  list_window = TransferredEntityListWindow
138
+ edit_window = TransferredEntityEditWindow
124
139
 
125
140
  columns = [
126
141
  {
@@ -134,6 +149,10 @@ class TransferredEntityPack(ObjectPack):
134
149
  {
135
150
  'data_index': 'no_export',
136
151
  'header': 'Отключение экспорта',
152
+ },
153
+ {
154
+ 'data_index': 'queue_level',
155
+ 'header': 'Уровень очереди',
137
156
  }
138
157
  ]
139
158
 
@@ -1,14 +1,22 @@
1
1
  from m3_ext.ui.all_components import (
2
2
  ExtButton,
3
+ ExtDisplayField,
3
4
  )
4
5
  from objectpack.ui import (
6
+ BaseEditWindow,
5
7
  BaseListWindow,
8
+ anchor100,
9
+ model_fields_to_controls,
6
10
  )
7
11
 
8
12
  from educommon.utils.ui import (
9
13
  append_template_globals,
10
14
  )
11
15
 
16
+ from edu_rdm_integration.models import (
17
+ TransferredEntity,
18
+ )
19
+
12
20
 
13
21
  class TransferredEntityListWindow(BaseListWindow):
14
22
  """Окно реестра сущностей для сбора и экспорта данных."""
@@ -39,3 +47,39 @@ class TransferredEntityListWindow(BaseListWindow):
39
47
  params['pack'].export_change_action.get_absolute_url()
40
48
  )
41
49
  self.pack = params['pack']
50
+
51
+
52
+ class TransferredEntityEditWindow(BaseEditWindow):
53
+ """Окно редактирования сущностей."""
54
+
55
+ def _init_components(self):
56
+ """Инициализация компонентов."""
57
+ super()._init_components()
58
+
59
+ self._controls = model_fields_to_controls(
60
+ TransferredEntity,
61
+ self,
62
+ field_list=[
63
+ 'queue_level',
64
+ ],
65
+ )
66
+ self.entity_name_field = ExtDisplayField(
67
+ read_only=True,
68
+ label='Сущность',
69
+ name='entity_name',
70
+ )
71
+ self._controls.insert(0, self.entity_name_field)
72
+
73
+ def _do_layout(self):
74
+ """Расположение компонентов."""
75
+ super()._do_layout()
76
+
77
+ self.form.items.extend(list(map(anchor100, self._controls)))
78
+
79
+ def set_params(self, params, *args, **kwargs):
80
+ """Простановка парметров."""
81
+ super().set_params(params, *args, **kwargs)
82
+
83
+ obj = params.get('object')
84
+ if obj:
85
+ self.entity_name_field.value = obj.entity_id
@@ -25,16 +25,15 @@ from educommon.async_task.models import (
25
25
  RunningTask,
26
26
  )
27
27
  from educommon.async_task.tasks import (
28
- PeriodicAsyncTask,
29
28
  UniquePeriodicAsyncTask,
30
29
  )
31
30
  from educommon.utils.date import (
32
31
  get_today_min_datetime,
33
32
  )
34
- from m3_db_utils.consts import (
35
- DEFAULT_ORDER_NUMBER,
36
- )
37
33
 
34
+ from edu_rdm_integration.base import (
35
+ BaseTransferLatestEntitiesDataMixin,
36
+ )
38
37
  from edu_rdm_integration.collect_and_export_data.models import (
39
38
  EduRdmCollectDataCommandProgress,
40
39
  EduRdmExportDataCommandProgress,
@@ -46,12 +45,13 @@ from edu_rdm_integration.collect_data.helpers import (
46
45
  set_failed_status_suspended_collecting_data_stages,
47
46
  )
48
47
  from edu_rdm_integration.consts import (
49
- REGIONAL_DATA_MART_INTEGRATION_COLLECTING_DATA,
50
- REGIONAL_DATA_MART_INTEGRATION_EXPORTING_DATA,
48
+ FAST_TRANSFER_TASK_QUEUE_NAME,
49
+ LONG_TRANSFER_TASK_QUEUE_NAME,
51
50
  TASK_QUEUE_NAME,
52
51
  )
53
52
  from edu_rdm_integration.enums import (
54
53
  CommandType,
54
+ EntityLevelQueueTypeEnum,
55
55
  FileUploadStatusEnum,
56
56
  )
57
57
  from edu_rdm_integration.export_data.export import (
@@ -66,18 +66,12 @@ from edu_rdm_integration.export_data.queue import (
66
66
  )
67
67
  from edu_rdm_integration.helpers import (
68
68
  UploadStatusHelper,
69
- get_collecting_managers_max_period_ended_dates,
70
- get_exporting_managers_max_period_ended_dates,
71
69
  save_command_log_link,
72
70
  )
73
71
  from edu_rdm_integration.models import (
74
72
  ExportingDataSubStageUploaderClientLog,
75
- RegionalDataMartEntityEnum,
76
73
  TransferredEntity,
77
74
  )
78
- from edu_rdm_integration.storages import (
79
- RegionalDataMartEntityStorage,
80
- )
81
75
 
82
76
 
83
77
  if TYPE_CHECKING:
@@ -85,10 +79,6 @@ if TYPE_CHECKING:
85
79
  datetime,
86
80
  )
87
81
 
88
- from function_tools.managers import (
89
- RunnerManager,
90
- )
91
-
92
82
 
93
83
  class RDMCheckUploadStatus(UniquePeriodicAsyncTask):
94
84
  """Периодическая задача для сбора статусов по загрузке файла в витрину."""
@@ -154,61 +144,24 @@ class CheckSuspendedExportedStagePeriodicTask(UniquePeriodicAsyncTask):
154
144
  )
155
145
 
156
146
 
157
- class TransferLatestEntitiesDataPeriodicTask(UniquePeriodicAsyncTask):
158
- """Периодическая задача сбора и выгрузки данных."""
159
-
160
- queue = TASK_QUEUE_NAME
161
- routing_key = TASK_QUEUE_NAME
162
- description = 'Периодическая задача сбора и экспорта данных РВД'
163
- lock_expire_seconds = settings.RDM_TRANSFER_TASK_LOCK_EXPIRE_SECONDS
164
- task_type = AsyncTaskType.UNKNOWN
165
- run_every = crontab(
166
- minute=settings.RDM_TRANSFER_TASK_MINUTE,
167
- hour=settings.RDM_TRANSFER_TASK_HOUR,
168
- day_of_week=settings.RDM_TRANSFER_TASK_DAY_OF_WEEK,
169
- )
147
+ class BaseTransferLatestEntitiesDataPeriodicTask(BaseTransferLatestEntitiesDataMixin, UniquePeriodicAsyncTask):
148
+ """Базовая периодическая задача сбора и выгрузки данных для переиспользования в разных очередях."""
170
149
 
171
150
  def __init__(self) -> None:
172
151
  super().__init__()
173
152
 
174
- self._collecting_data_managers: dict[str, type['RunnerManager']] = {}
175
- self._collecting_data_manager_to_logs_period_end: dict[str, 'datetime'] = {}
176
-
177
- self._exporting_data_managers: dict[str, type['RunnerManager']] = {}
178
- self._exporting_data_manager_to_period_end: dict[str, 'datetime'] = {}
179
-
180
- self._transferred_entities = []
181
- self._entites_models_map = defaultdict(list)
182
-
183
- def process(self, *args, **kwargs):
184
- """Выполняет задачу."""
185
- super().process(*args, **kwargs)
186
-
187
- self._collect_transferred_entities()
188
- self._collect_managers()
189
- self._calculate_collecting_managers_logs_period_ended_at()
190
- self._calculate_exporting_managers_ended_at()
191
-
192
- task_id = RunningTask.objects.filter(
193
- pk=self.request.id,
194
- ).values_list('pk', flat=True).first()
153
+ self._period_ended_at: Optional[datetime] = None
195
154
 
196
- collected_entity_models = set()
155
+ def _get_period_ended_at(self):
156
+ """Определяет единую дату окончания периода сбора.
197
157
 
198
- for entity_enum, export_enabled in sorted(
199
- self._transferred_entities, key=lambda entity: entity[0].order_number
200
- ):
201
- entity_models = self._entites_models_map.get(entity_enum.key, ())
202
- for model_enum_value in entity_models:
203
- if model_enum_value.key not in collected_entity_models:
204
- self._run_collect_model_data(model_enum_value.key, task_id)
205
- collected_entity_models.add(model_enum_value.key)
158
+ Это нужно для исключения разного времени окончания сбора при явном запуске функции - чтобы
159
+ избежать ошибки экспорта при недосборе данных.
160
+ """
161
+ if not self._period_ended_at:
162
+ self._period_ended_at = timezone.now()
206
163
 
207
- try:
208
- if export_enabled:
209
- self._run_export_entity_data(entity_enum.key, task_id)
210
- except Exception:
211
- continue
164
+ return self._period_ended_at
212
165
 
213
166
  def _run_collect_model_data(self, model: str, task_id: str) -> None:
214
167
  """Запускает сбор данных модели РВД."""
@@ -229,54 +182,6 @@ class TransferLatestEntitiesDataPeriodicTask(UniquePeriodicAsyncTask):
229
182
  command.refresh_from_db(fields=['stage_id'])
230
183
  save_command_log_link(command, settings.RDM_EXPORT_LOG_DIR)
231
184
 
232
- def _collect_transferred_entities(self) -> None:
233
- """Собирает сущности РВД, по которым будет произведен сбор и экспорт данных."""
234
- self._transferred_entities = [
235
- (RegionalDataMartEntityEnum.get_model_enum_value(key=entity), export_enabled)
236
- for entity, export_enabled in TransferredEntity.objects.values_list('entity', 'export_enabled')
237
- ]
238
-
239
- # Собираем словарь по сущностям с моделями для сборки
240
- for entity, _ in self._transferred_entities:
241
- self._entites_models_map[entity.key].extend(
242
- (model_enum for model_enum in (*entity.additional_model_enums, entity.main_model_enum)
243
- if model_enum.order_number != DEFAULT_ORDER_NUMBER)
244
- )
245
-
246
- def _collect_managers(self) -> None:
247
- """Собирает менеджеры Функций для сбора и выгрузки данных."""
248
- entity_storage = RegionalDataMartEntityStorage()
249
- entity_storage.prepare()
250
-
251
- collecting_models_data_managers_map = entity_storage.prepare_entities_manager_map(
252
- tags={REGIONAL_DATA_MART_INTEGRATION_COLLECTING_DATA},
253
- )
254
- exporting_entities_data_managers_map = entity_storage.prepare_entities_manager_map(
255
- tags={REGIONAL_DATA_MART_INTEGRATION_EXPORTING_DATA},
256
- )
257
-
258
- for entity_key, entity_models in self._entites_models_map.items():
259
- for entity_model in entity_models:
260
- collect_manager_class = collecting_models_data_managers_map.get(entity_model.key)
261
- if collect_manager_class:
262
- self._collecting_data_managers[entity_model.key] = collect_manager_class
263
-
264
- export_manager_class = exporting_entities_data_managers_map.get(entity_key)
265
- if export_manager_class:
266
- self._exporting_data_managers[entity_key] = export_manager_class
267
-
268
- def _calculate_collecting_managers_logs_period_ended_at(self) -> None:
269
- """Определяет дату последнего успешного этапа сбора у менеджеров Функций сбора."""
270
- self._collecting_data_manager_to_logs_period_end = get_collecting_managers_max_period_ended_dates(
271
- self._collecting_data_managers.values()
272
- )
273
-
274
- def _calculate_exporting_managers_ended_at(self) -> None:
275
- """Определяет дату последнего успешного подэтапа экспорта у менеджеров Функций экспорта."""
276
- self._exporting_data_manager_to_period_end = get_exporting_managers_max_period_ended_dates(
277
- self._exporting_data_managers.values()
278
- )
279
-
280
185
  def _create_collect_command(self, model: str, task_id: str) -> EduRdmCollectDataCommandProgress:
281
186
  """Создает команду сбора данных моделей РВД."""
282
187
  manager = self._collecting_data_managers[model]
@@ -286,7 +191,7 @@ class TransferLatestEntitiesDataPeriodicTask(UniquePeriodicAsyncTask):
286
191
  )
287
192
 
288
193
  period_started_at = manager_last_collected
289
- period_ended_at = timezone.now()
194
+ period_ended_at = self._get_period_ended_at()
290
195
 
291
196
  return EduRdmCollectDataCommandProgress.objects.create(
292
197
  model_id=model,
@@ -340,6 +245,91 @@ class TransferLatestEntitiesDataPeriodicTask(UniquePeriodicAsyncTask):
340
245
  task_id=self.request.id,
341
246
  )
342
247
 
248
+ def process(self, *args, **kwargs):
249
+ """Выполняет задачу."""
250
+ super().process(*args, **kwargs)
251
+
252
+ self._period_ended_at = None
253
+ self.prepare_collect_export_managers()
254
+
255
+ task_id = RunningTask.objects.filter(
256
+ pk=self.request.id,
257
+ ).values_list('pk', flat=True).first()
258
+
259
+ collected_entity_models = set()
260
+
261
+ for entity_enum, export_enabled in sorted(
262
+ self._transferred_entities, key=lambda entity: entity[0].order_number
263
+ ):
264
+ entity_models = self._entites_models_map.get(entity_enum.key, ())
265
+ for model_enum_value in entity_models:
266
+ if model_enum_value.key not in collected_entity_models:
267
+ collected_entity_models.add(model_enum_value.key)
268
+ try:
269
+ self._run_collect_model_data(model_enum_value.key, task_id)
270
+ except Exception:
271
+ continue
272
+
273
+ try:
274
+ if export_enabled:
275
+ self._run_export_entity_data(entity_enum.key, task_id)
276
+ except Exception:
277
+ continue
278
+
279
+
280
+ class TransferLatestEntitiesDataPeriodicTask(BaseTransferLatestEntitiesDataPeriodicTask):
281
+ """Периодическая задача сбора и выгрузки данных."""
282
+
283
+ queue = TASK_QUEUE_NAME
284
+ routing_key = TASK_QUEUE_NAME
285
+ description = 'Периодическая задача сбора и экспорта данных РВД'
286
+ lock_expire_seconds = settings.RDM_TRANSFER_TASK_LOCK_EXPIRE_SECONDS
287
+ task_type = AsyncTaskType.UNKNOWN
288
+ run_every = crontab(
289
+ minute=settings.RDM_TRANSFER_TASK_MINUTE,
290
+ hour=settings.RDM_TRANSFER_TASK_HOUR,
291
+ day_of_week=settings.RDM_TRANSFER_TASK_DAY_OF_WEEK,
292
+ )
293
+
294
+ def get_entity_qs(self) -> 'QuerySet[TransferredEntity]':
295
+ return TransferredEntity.objects.filter(queue_level=EntityLevelQueueTypeEnum.BASE)
296
+
297
+
298
+ class TransferLatestEntitiesDataFastPeriodicTask(BaseTransferLatestEntitiesDataPeriodicTask):
299
+ """Периодическая задача сбора и выгрузки данных для быстрого уровня очереди."""
300
+
301
+ queue = FAST_TRANSFER_TASK_QUEUE_NAME
302
+ routing_key = FAST_TRANSFER_TASK_QUEUE_NAME
303
+ description = 'Периодическая задача сбора и экспорта данных РВД (быстрый уровень)'
304
+ lock_expire_seconds = settings.RDM_FAST_TRANSFER_TASK_LOCK_EXPIRE_SECONDS
305
+ task_type = AsyncTaskType.UNKNOWN
306
+ run_every = crontab(
307
+ minute=settings.RDM_FAST_TRANSFER_TASK_MINUTE,
308
+ hour=settings.RDM_FAST_TRANSFER_TASK_HOUR,
309
+ day_of_week=settings.RDM_FAST_TRANSFER_TASK_DAY_OF_WEEK,
310
+ )
311
+
312
+ def get_entity_qs(self) -> 'QuerySet[TransferredEntity]':
313
+ return TransferredEntity.objects.filter(queue_level=EntityLevelQueueTypeEnum.FAST)
314
+
315
+
316
+ class TransferLatestEntitiesDataLongPeriodicTask(BaseTransferLatestEntitiesDataPeriodicTask):
317
+ """Периодическая задача сбора и выгрузки данных для долгого уровня очереди."""
318
+
319
+ queue = LONG_TRANSFER_TASK_QUEUE_NAME
320
+ routing_key = LONG_TRANSFER_TASK_QUEUE_NAME
321
+ description = 'Периодическая задача сбора и экспорта данных РВД (долгий уровень)'
322
+ lock_expire_seconds = settings.RDM_LONG_TRANSFER_TASK_LOCK_EXPIRE_SECONDS
323
+ task_type = AsyncTaskType.UNKNOWN
324
+ run_every = crontab(
325
+ minute=settings.RDM_LONG_TRANSFER_TASK_MINUTE,
326
+ hour=settings.RDM_LONG_TRANSFER_TASK_HOUR,
327
+ day_of_week=settings.RDM_LONG_TRANSFER_TASK_DAY_OF_WEEK,
328
+ )
329
+
330
+ def get_entity_qs(self) -> 'QuerySet[TransferredEntity]':
331
+ return TransferredEntity.objects.filter(queue_level=EntityLevelQueueTypeEnum.LONG)
332
+
343
333
 
344
334
  class UploadDataAsyncTask(UniquePeriodicAsyncTask):
345
335
  """Формирование очереди файлов и их отправка."""
@@ -383,3 +373,5 @@ celery_app.register_task(RDMCheckUploadStatus)
383
373
  celery_app.register_task(CheckSuspendedExportedStagePeriodicTask)
384
374
  celery_app.register_task(TransferLatestEntitiesDataPeriodicTask)
385
375
  celery_app.register_task(UploadDataAsyncTask)
376
+ celery_app.register_task(TransferLatestEntitiesDataFastPeriodicTask)
377
+ celery_app.register_task(TransferLatestEntitiesDataLongPeriodicTask)
@@ -1,15 +1,46 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: edu-rdm-integration
3
- Version: 3.5.8
3
+ Version: 3.6.0
4
4
  Summary: Интеграция с Региональной витриной данных
5
- Home-page:
6
- Download-URL:
7
- Author: BARS Group
8
- Author-email: bars@bars.group
9
- Platform: Any
5
+ Author-email: BARS Group <education_dev@bars.group>
6
+ Project-URL: Homepage, https://stash.bars-open.ru/projects/EDUBASE/repos/edu_rdm_integration/browse
7
+ Project-URL: Repository, https://stash.bars-open.ru/scm/edubase/edu_rdm_integration.git
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Framework :: Django :: 2.2
15
+ Classifier: Framework :: Django :: 3.0
16
+ Classifier: Framework :: Django :: 3.1
17
+ Classifier: Framework :: Django :: 3.2
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Environment :: Web Environment
10
20
  Requires-Python: >=3.9
11
21
  Description-Content-Type: text/markdown
12
22
  License-File: LICENSE
23
+ Requires-Dist: transliterate<2
24
+ Requires-Dist: Django<3.3,>=3.1
25
+ Requires-Dist: celery<5.3,>=4.4.7
26
+ Requires-Dist: asyncpg==0.23.0
27
+ Requires-Dist: educommon<4,>=3.11.0
28
+ Requires-Dist: function-tools<1,>=0.9.0
29
+ Requires-Dist: m3-db-utils<1,>=0.3.10
30
+ Requires-Dist: m3-django-compat<2,>=1.10.2
31
+ Requires-Dist: uploader-client<1,>=0.2.3
32
+ Provides-Extra: dev
33
+ Requires-Dist: isort==5.12.0; extra == "dev"
34
+ Requires-Dist: ruff==0.6.9; extra == "dev"
35
+ Requires-Dist: flake8<7,>=4.0.1; extra == "dev"
36
+ Requires-Dist: pytest<8,>=3.2.5; extra == "dev"
37
+ Requires-Dist: pytest-cov<5; extra == "dev"
38
+ Provides-Extra: docs
39
+ Requires-Dist: sphinx<7.5,>=7; extra == "docs"
40
+ Requires-Dist: sphinx-autodoc-typehints<2.5,>=2; extra == "docs"
41
+ Requires-Dist: myst-parser<3.2,>=3; extra == "docs"
42
+ Requires-Dist: sphinx_design<0.7,>=0.6; extra == "docs"
43
+ Dynamic: license-file
13
44
 
14
45
  # Проект "Интеграция с Региональной витриной данных (РВД)"
15
46
 
@@ -47,6 +78,19 @@ License-File: LICENSE
47
78
 
48
79
  Стоит обратить внимание, что сущности РВД могут содержать в себе данные из нескольких моделей РВД.
49
80
 
81
+ Очереди для периодических задач сборки и выгрузки данных
82
+ : Важно учитывать, что с версии пакета 3.6 вводится две новые очереди и соответсвущие этим очередям периодические задачи
83
+ сбора и выгрузки данных. Очередь для сущности указывается в реестре "Сущности для сбора и экспорта данных" - по умолчанию все
84
+ сущности относятся к основной очереди.
85
+ Итого - нужно настроить три очереди для работы.
86
+
87
+ - "Быстрая" очередь - RDM_FAST - для сущностей, по которым данные должны отдаваться каждые 5/10/15 минут по требованиям
88
+ витрины. Периодическая задача - TransferLatestEntitiesDataFastPeriodicTask.
89
+ - Основная очередь (та которая была до версии 3.6) - RDM- ля всех сущностей по умолчанию.
90
+ Периодическая задача - TransferLatestEntitiesDataFastPeriodicTask.
91
+ - "Долгая" очередь - RDM_LONG - для сущностей по которым идет долгий сбор, например для расчетных сущностей.
92
+ Периодическая задача - TransferLatestEntitiesDataLongPeriodicTask.
93
+
50
94
  ## Требования к окружению
51
95
 
52
96
  Для работы требуется Python >=3.9. Так же в зависимостях есть внутренние пакеты:
@@ -98,6 +142,39 @@ License-File: LICENSE
98
142
  ('uploader_client', 'ENABLE_REQUEST_EMULATION'): False,
99
143
  })
100
144
  ```
145
+ С версии пакета 3.6 добавляются новые настройки для двух периодических задач разных очередей сбора и выгрузки данных
146
+ ```
147
+ PROJECT_DEFAULT_CONFIG.update({
148
+ # Настройки РВД
149
+ ('rdm_general', 'EXPORT_ENTITY_ID_PREFIX'): '', # Дефолтное значение нужно изменить на специфическое системе
150
+ ('rdm_general', 'COLLECT_CHUNK_SIZE'): 500,
151
+ ('rdm_general', 'EXPORT_CHUNK_SIZE'): 500,
152
+ ('rdm_transfer_task', 'MINUTE'): '0',
153
+ ('rdm_transfer_task', 'HOUR'): '*/4',
154
+ ('rdm_transfer_task', 'DAY_OF_WEEK'): '*',
155
+ ('rdm_transfer_task', 'LOCK_EXPIRE_SECONDS'): 21600,
156
+ ('rdm_transfer_task', 'TIMEDELTA'): 3600,
157
+ ('rdm_transfer_task', 'ENTITIES'): '',
158
+ ('rdm_transfer_task_fast', 'MINUTE'): '*/5',
159
+ ('rdm_transfer_task_fast', 'HOUR'): '*',
160
+ ('rdm_transfer_task_fast', 'DAY_OF_WEEK'): '*',
161
+ ('rdm_transfer_task_fast', 'LOCK_EXPIRE_SECONDS'): 1800,
162
+ ('rdm_transfer_task_long', 'MINUTE'): '0',
163
+ ('rdm_transfer_task_long', 'HOUR'): '*/6',
164
+ ('rdm_transfer_task_long', 'DAY_OF_WEEK'): '*',
165
+ ('rdm_transfer_task_long', 'LOCK_EXPIRE_SECONDS'): 28800,
166
+ ('rdm_upload_status_task', 'MINUTE'): '*/30',
167
+ ('rdm_upload_status_task', 'HOUR'): '*',
168
+ ('rdm_upload_status_task', 'DAY_OF_WEEK'): '*',
169
+ ('rdm_upload_status_task', 'LOCK_EXPIRE_SECONDS'): 7200,
170
+ ('uploader_client', 'URL'): 'http://localhost:8090',
171
+ ('uploader_client', 'DATAMART_NAME'): '',
172
+ ('uploader_client', 'REQUEST_RETRIES'): 10,
173
+ ('uploader_client', 'REQUEST_TIMEOUT'): 10,
174
+ ('uploader_client', 'ENABLE_REQUEST_EMULATION'): False,
175
+ })
176
+
177
+ ```
101
178
  - Получение значений настроек из конфигурационного файла в settings.py:
102
179
 
103
180
  ```
@@ -156,7 +233,21 @@ License-File: LICENSE
156
233
  RDM_UPLOADER_CLIENT_ENABLE_REQUEST_EMULATION = conf.get_bool('uploader_client', 'ENABLE_REQUEST_EMULATION')
157
234
 
158
235
  ```
159
-
236
+ С версии пакета 3.6 добавляются настройки для двух новых периодических задач
237
+ ```
238
+ # Настройка запуска периодической задачи выгрузки данных - быстрая очередь:
239
+ RDM_FAST_TRANSFER_TASK_MINUTE = conf.get('rdm_transfer_task_fast', 'MINUTE')
240
+ RDM_FAST_TRANSFER_TASK_HOUR = conf.get('rdm_transfer_task_fast', 'HOUR')
241
+ RDM_FAST_TRANSFER_TASK_DAY_OF_WEEK = conf.get('rdm_transfer_task_fast', 'DAY_OF_WEEK')
242
+ RDM_FAST_TRANSFER_TASK_LOCK_EXPIRE_SECONDS = conf.get_int('rdm_transfer_task_fast', 'LOCK_EXPIRE_SECONDS')
243
+
244
+ # Настройка запуска периодической задачи выгрузки данных - долгая очередь расчетных моделей:
245
+ RDM_LONG_TRANSFER_TASK_MINUTE = conf.get('rdm_transfer_task_long', 'MINUTE')
246
+ RDM_LONG_TRANSFER_TASK_HOUR = conf.get('rdm_transfer_task_long', 'HOUR')
247
+ RDM_LONG_TRANSFER_TASK_DAY_OF_WEEK = conf.get('rdm_transfer_task_long', 'DAY_OF_WEEK')
248
+ RDM_LONG_TRANSFER_TASK_LOCK_EXPIRE_SECONDS = conf.get_int('rdm_transfer_task_long', 'LOCK_EXPIRE_SECONDS')
249
+
250
+ ```
160
251
  Перечень настроек в settings.py указан в таблице ниже.
161
252
 
162
253
  | Название настройки в settings | Описание | Значение по умолчанию |
@@ -180,7 +271,18 @@ License-File: LICENSE
180
271
  | RDM_UPLOAD_STATUS_TASK_LOCK_EXPIRE_SECONDS | Время по истечении которого, блокировка может быть снята (в секунадх) | 3600 |
181
272
  | RDM_CHECK_SUSPEND_TASK_STAGE_TIMEOUT | Дельта для определения зависшего подэтапа. Минута | 120 |
182
273
 
183
-
274
+ С версии пакета 3.6 добавляются новые настройки
275
+
276
+ | Название настройки в settings | Описание | Значение по умолчанию |
277
+ |--------------------------------------------|----------------------------------------------------------------------------------------|-----------------------|
278
+ | RDM_FAST_TRANSFER_TASK_MINUTE | Настройка запуска периодической задачи (быстрая очередь) выгрузки данных. Минута | '*/5' |
279
+ | RDM_FAST_TRANSFER_TASK_HOUR | Настройка запуска периодической задачи (быстрая очередь) выгрузки данных. Час | '*' |
280
+ | RDM_FAST_TRANSFER_TASK_DAY_OF_WEEK | Настройка запуска периодической задачи (быстрая очередь) выгрузки данных. День недели | '*' |
281
+ | RDM_FAST_TRANSFER_TASK_LOCK_EXPIRE_SECONDS | Время по истечении которого, блокировка может быть снята (в секунадх) | 1800 |
282
+ | RDM_LONG_TRANSFER_TASK_MINUTE | Настройка запуска периодической задачи (долгая очередь) выгрузки данных. Минута | 0 |
283
+ | RDM_LONG_TRANSFER_TASK_HOUR | Настройка запуска периодической задачи (долгая очередь) выгрузки данных. Час | '*/6' |
284
+ | RDM_LONG_TRANSFER_TASK_DAY_OF_WEEK | Настройка запуска периодической задачи (долгая очередь) выгрузки данных. День недели | '*' |
285
+ | RDM_LONG_TRANSFER_TASK_LOCK_EXPIRE_SECONDS | Время по истечении которого, блокировка может быть снята (в секунадх) | 28800 |
184
286
 
185
287
  - В дефолтный конфиг проекта необходимо добавить:
186
288
 
@@ -236,7 +338,21 @@ License-File: LICENSE
236
338
  # Включить эмуляцию отправки запросов
237
339
  ENABLE_REQUEST_EMULATION = True
238
340
  ```
341
+ - С версии 3.6 в деволтный конфиг также нужно добавить два дополнительных раздела
342
+ ```
343
+
344
+ [rdm_transfer_task_fast]
345
+ MINUTE=*/2
346
+ HOUR=*
347
+ DAY_OF_WEEK=*
348
+ LOCK_EXPIRE_SECONDS = 1800
239
349
 
350
+ [rdm_transfer_task_long]
351
+ MINUTE=*/15
352
+ HOUR=*
353
+ DAY_OF_WEEK=*
354
+ LOCK_EXPIRE_SECONDS = 21600
355
+ ```
240
356
  На основе дефолтного конфига произвести конфигурирование приложений.
241
357
 
242
358
  ## Сборка и распространение
@@ -1,18 +1,18 @@
1
1
  edu_rdm_integration/__init__.py,sha256=fVCvQ7QGI_iCyAeE8dMapyY8gOM617ye5GQqAVGPlZI,72
2
2
  edu_rdm_integration/app_meta.py,sha256=v5IU69yaeLbyHF0Ln6iPN_IfizbtF3rCWrz2n71m8dU,337
3
- edu_rdm_integration/app_settings.py,sha256=f7Obftma3sQSXZmQRkfas93VFnJi3eTGVE_JixzxAnQ,3035
3
+ edu_rdm_integration/app_settings.py,sha256=9alLUaen3DRCD7wngNyNI2oXf98R-qW1rvKpTvLTMrQ,3684
4
4
  edu_rdm_integration/apps.py,sha256=Dl1og2yZcRyJmqrifUNIjLZ9Us2jw2mgAP1_q2fg29A,3699
5
- edu_rdm_integration/base.py,sha256=_G0qPTAXe6bXfgDHNiZMSsYt3sMuUhLKnHuQCWSFttU,1341
6
- edu_rdm_integration/consts.py,sha256=nL8o-w73KjGM4r_--Nasffmo_DJx9HsgrPHSNtq-y-g,1080
5
+ edu_rdm_integration/base.py,sha256=1NgLZd0KZRRgUcVfufVIpDJU2coT2zksFGDaX4fWqps,5967
6
+ edu_rdm_integration/consts.py,sha256=QnL9TqTKuJ0MrysqtqHvtCwOFbS1T7iwcfUZGjVsZTU,1166
7
7
  edu_rdm_integration/entities.py,sha256=mhVeB88A-VD5IAzZCNeI1qnkvNoZ8LPiLBdqk1yA3Jc,14541
8
- edu_rdm_integration/enums.py,sha256=T3Mu5D-CbKO3BSg16MPPnIPlcc_YGLYR-ThS8dzl9gg,4246
8
+ edu_rdm_integration/enums.py,sha256=RpQIZM1iSgFbYDHfwrbT-BwgRf-N9ZZnJgx8UyRFQ2o,4978
9
9
  edu_rdm_integration/helpers.py,sha256=wr4ddI9LNsmcwdZMEUYT070YnM49ixbAQ0-tBh4gp08,14808
10
10
  edu_rdm_integration/mapping.py,sha256=1B6TsC4Os9wiM8L8BChnCNv_iWqjeWu3bdDsqKVsId0,616
11
- edu_rdm_integration/models.py,sha256=feAbpH1ji4MOHpYtzDfrumMYmXlz1ye-IDUFJSI7Nq0,33092
11
+ edu_rdm_integration/models.py,sha256=pimS5MIQzRl4b9RIUUCSxsAA54zB_aoiEY0evrywpHM,33325
12
12
  edu_rdm_integration/redis_cache.py,sha256=SP_rcL5t6PTVLOnEYn_NTX0Z666VdZT4By2pyED24Z4,1537
13
13
  edu_rdm_integration/signals.py,sha256=3eRlpkDcFCF6TN80-QM8yBYLcyozzcmoPjz6r4_ApWg,73
14
14
  edu_rdm_integration/storages.py,sha256=G4Q4tIyJdEyb9ka551PADCFIm66bpsJe9VBRcvQhLMI,6745
15
- edu_rdm_integration/tasks.py,sha256=ie9fduL1QMR_ndFXIruY4VXdTnStqiV2vd6iNILFxQw,15540
15
+ edu_rdm_integration/tasks.py,sha256=LyFc91ntwlYZoLN9-gYEzS_vjBnvVpF3D5N7ubtp6FM,15136
16
16
  edu_rdm_integration/typing.py,sha256=2asD8biX0l_DVqJSU4y19-zzEBJMk967PUj3UhzICzs,785
17
17
  edu_rdm_integration/utils.py,sha256=NNRfblJ9QoX07qUa2kfKG0jM3fHJ3oaDvG2zwj0As70,12120
18
18
  edu_rdm_integration/adapters/__init__.py,sha256=cU0swn4Ny5ZQz5buWRcWsT1mpWuUFJaUlHf2l7TtEBo,83
@@ -167,10 +167,11 @@ edu_rdm_integration/migrations/0012_exportingdatasubstageattachment_attachment_s
167
167
  edu_rdm_integration/migrations/0013_set_attachment_size.py,sha256=Gol8T137gdaCTSkJ2e4as5x4gfqeouZmgWgkOl7zxCQ,2048
168
168
  edu_rdm_integration/migrations/0014_uploaddatacommand.py,sha256=Hh0vKKiGgKOvY1kBAcmway4dSYUXwVArHAc9YrsjCIU,2079
169
169
  edu_rdm_integration/migrations/0015_set_exporting_sub_stage_status.py,sha256=zVe2baNq8JYzMPRmtpAwplmgKHOP3lwMKHLo_yRz0QE,790
170
+ edu_rdm_integration/migrations/0016_transferredentity_queue_level.py,sha256=xMnDYE5_fK8Sdwq-1GwJlJZlVoz4e4yml0SDueKcbUA,587
170
171
  edu_rdm_integration/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
171
172
  edu_rdm_integration/registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
172
- edu_rdm_integration/registry/actions.py,sha256=jNEq1RhSGtxdYOIKcTtqT1LPUfKvOIFgAQjP3q0Lv1s,5382
173
- edu_rdm_integration/registry/ui.py,sha256=YU7I53ipABEy2oAC3G-we8iTqNtHrYZ11tl-981EZmU,1329
173
+ edu_rdm_integration/registry/actions.py,sha256=-CHe95jbxO9JAEaOx6nV6Avn75ynppvZRWN8YPc6c6s,6074
174
+ edu_rdm_integration/registry/ui.py,sha256=v4GqbQUcoeHxfUPUYORuj4DfzIp49diY-F1hyL3TCPo,2592
174
175
  edu_rdm_integration/templates/ui-js/transferred-entity-list.js,sha256=IWEZ9JoTxD5-CLic5v07XHWW0iGRWk-cjeGVSzU4yKg,1117
175
176
  edu_rdm_integration/uploader_log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
177
  edu_rdm_integration/uploader_log/actions.py,sha256=2ckgI1PKgVcLYkbhtbK_bfv_6lDxpiIRSk1uPCIL-gg,6755
@@ -179,9 +180,8 @@ edu_rdm_integration/uploader_log/enums.py,sha256=rgSO3BL2rh2xpfm0Pt4waQW8fB1VMJL
179
180
  edu_rdm_integration/uploader_log/managers.py,sha256=OFdToWV8qhdfeGNpd-UWAmSEISzixmVQ6LF75EW7gzA,3248
180
181
  edu_rdm_integration/uploader_log/ui.py,sha256=YM9Buqp2wxE95Wf5gvAATBzuYzDOossK1sEmvFk07cI,2110
181
182
  edu_rdm_integration/uploader_log/templates/ui-js/object-grid-buttons.js,sha256=2xyGe0wdVokM0RhpzRzcRvJPBkBmPe3SlZry4oP4Nzs,6201
182
- edu_rdm_integration-3.5.8.dist-info/LICENSE,sha256=uw43Gjjj-1vXWCItfSrNDpbejnOwZMrNerUh8oWbq8Q,3458
183
- edu_rdm_integration-3.5.8.dist-info/METADATA,sha256=gLpWbKbbV5NwdI4QjG2uS-VM0ZqS5RX9OsmowZpGlbU,18691
184
- edu_rdm_integration-3.5.8.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
185
- edu_rdm_integration-3.5.8.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
186
- edu_rdm_integration-3.5.8.dist-info/top_level.txt,sha256=nRJV0O14UtNE-jGIYG03sohgFnZClvf57H5m6VBXe9Y,20
187
- edu_rdm_integration-3.5.8.dist-info/RECORD,,
183
+ edu_rdm_integration-3.6.0.dist-info/licenses/LICENSE,sha256=uw43Gjjj-1vXWCItfSrNDpbejnOwZMrNerUh8oWbq8Q,3458
184
+ edu_rdm_integration-3.6.0.dist-info/METADATA,sha256=rV5ZHyItlhOmIl5k0jC3nfJTrv6k7Cmr_S-bXUsI480,27415
185
+ edu_rdm_integration-3.6.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
186
+ edu_rdm_integration-3.6.0.dist-info/top_level.txt,sha256=nRJV0O14UtNE-jGIYG03sohgFnZClvf57H5m6VBXe9Y,20
187
+ edu_rdm_integration-3.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: setuptools (77.0.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5