edu-rdm-integration 3.3.6__py3-none-any.whl → 3.4.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.
@@ -32,6 +32,9 @@ from educommon.audit_log.models import (
32
32
  from educommon.integration_entities.enums import (
33
33
  EntityLogOperation,
34
34
  )
35
+ from educommon.utils.phone_number.phone_number import (
36
+ PhoneNumber,
37
+ )
35
38
  from function_tools.models import (
36
39
  Entity,
37
40
  EntityType,
@@ -166,6 +169,8 @@ class BaseCollectingFunctionTestCase(TestCase):
166
169
  fields[key] = value.name
167
170
  elif isinstance(value, list):
168
171
  fields[key] = f'{{{",".join(map(str, value))}}}'
172
+ elif isinstance(value, PhoneNumber):
173
+ fields[key] = value.cleaned
169
174
  else:
170
175
  fields[key] = value
171
176
 
@@ -1,7 +1,9 @@
1
1
  from typing import (
2
2
  Any,
3
3
  Dict,
4
+ NamedTuple,
4
5
  Optional,
6
+ Type,
5
7
  Tuple,
6
8
  )
7
9
 
@@ -23,6 +25,9 @@ from edu_rdm_integration.models import (
23
25
  RegionalDataMartEntityEnum,
24
26
  RegionalDataMartModelEnum,
25
27
  )
28
+ from edu_rdm_integration.typing import (
29
+ MODEL_TYPE_VAR,
30
+ )
26
31
  from edu_rdm_integration.utils import (
27
32
  camel_to_underscore,
28
33
  )
@@ -108,3 +113,17 @@ class ModelEnumRegisterMixin(BaseEnumRegisterMixin):
108
113
  register_params['loggable_models'] = cls.loggable_models
109
114
 
110
115
  return register_params
116
+
117
+
118
+ class FromNamedTupleMixin:
119
+ """Миксин получения экземпляра модели из получаемого кэша значений."""
120
+
121
+ @classmethod
122
+ def from_namedtuple(cls: Type[MODEL_TYPE_VAR], namedtuple: NamedTuple) -> MODEL_TYPE_VAR:
123
+ """Создает экземпляр класса из NamedTuple."""
124
+ return cls(
125
+ **{
126
+ field: getattr(namedtuple, field)
127
+ for field in [f.column for f in cls._meta.get_fields()]
128
+ }
129
+ )
@@ -15,6 +15,7 @@ from typing import (
15
15
  List,
16
16
  Set,
17
17
  Type,
18
+ Union,
18
19
  )
19
20
 
20
21
  from django.conf import (
@@ -69,6 +70,13 @@ from edu_rdm_integration.consts import (
69
70
  from edu_rdm_integration.enums import (
70
71
  FileUploadStatusEnum,
71
72
  )
73
+ from edu_rdm_integration.export_data.export_manger import (
74
+ ExportEntityQueueSender,
75
+ WorkerSender,
76
+ )
77
+ from edu_rdm_integration.export_data.queue import (
78
+ Queue,
79
+ )
72
80
  from edu_rdm_integration.helpers import (
73
81
  get_exporting_managers_max_period_ended_dates,
74
82
  )
@@ -77,6 +85,9 @@ from edu_rdm_integration.models import (
77
85
  ExportingDataSubStageStatus,
78
86
  RegionalDataMartEntityEnum,
79
87
  )
88
+ from edu_rdm_integration.redis_cache import (
89
+ AbstractCache,
90
+ )
80
91
  from edu_rdm_integration.signals import (
81
92
  manager_created,
82
93
  )
@@ -121,8 +132,6 @@ class BaseExportEntitiesData(BaseOperationData):
121
132
  # Карта соответствия manager_id сущности и его основной модели
122
133
  self.manager_main_model_map: Dict[str, ModelBase] = {}
123
134
 
124
- self._configure_agent_client()
125
-
126
135
  @property
127
136
  def _log_file_path(self) -> str:
128
137
  """
@@ -130,57 +139,6 @@ class BaseExportEntitiesData(BaseOperationData):
130
139
  """
131
140
  return os.path.join(settings.MEDIA_ROOT, settings.RDM_EXPORT_LOG_DIR, f'{self.command_id}.log')
132
141
 
133
- def _configure_agent_client(self):
134
- """
135
- Конфигурирование клиента загрузчика данных в Витрину.
136
-
137
- #TODO Вынужденная мера, т.к. при запуске команды не производится проверка готовности конфигов приложений.
138
- # Нужно переработать механизм конфигурирования клиента загрузчика.
139
- """
140
- import uploader_client
141
- from django.core.cache import (
142
- DEFAULT_CACHE_ALIAS,
143
- caches,
144
- )
145
- from uploader_client.contrib.rdm.interfaces.configurations import (
146
- RegionalDataMartUploaderConfig,
147
- )
148
- if settings.RDM_UPLOADER_CLIENT_ENABLE_REQUEST_EMULATION:
149
- uploader_client.set_config(
150
- RegionalDataMartUploaderConfig(
151
- interface='uploader_client.contrib.rdm.interfaces.rest.OpenAPIInterfaceEmulation',
152
- url=settings.RDM_UPLOADER_CLIENT_URL,
153
- datamart_name=settings.RDM_UPLOADER_CLIENT_DATAMART_NAME,
154
- timeout=1,
155
- request_retries=1,
156
- )
157
- )
158
- elif settings.RDM_UPLOADER_CLIENT_USE_PROXY_API:
159
- uploader_client.set_config(
160
- RegionalDataMartUploaderConfig(
161
- interface='uploader_client.contrib.rdm.interfaces.rest.ProxyAPIInterface',
162
- cache=caches[DEFAULT_CACHE_ALIAS],
163
- url=settings.RDM_UPLOADER_CLIENT_URL,
164
- datamart_name=settings.RDM_UPLOADER_CLIENT_DATAMART_NAME,
165
- timeout=settings.RDM_UPLOADER_CLIENT_REQUEST_TIMEOUT,
166
- request_retries=settings.RDM_UPLOADER_CLIENT_REQUEST_RETRIES,
167
- organization_ogrn=settings.RDM_UPLOADER_CLIENT_ORGANIZATION_OGRN,
168
- installation_name=settings.RDM_UPLOADER_CLIENT_INSTALLATION_NAME,
169
- installation_id=settings.RDM_UPLOADER_CLIENT_INSTALLATION_ID,
170
- username=settings.RDM_UPLOADER_CLIENT_USERNAME,
171
- password=settings.RDM_UPLOADER_CLIENT_PASSWORD,
172
- )
173
- )
174
- else:
175
- uploader_client.set_config(
176
- RegionalDataMartUploaderConfig(
177
- url=settings.RDM_UPLOADER_CLIENT_URL,
178
- datamart_name=settings.RDM_UPLOADER_CLIENT_DATAMART_NAME,
179
- timeout=settings.RDM_UPLOADER_CLIENT_REQUEST_TIMEOUT,
180
- request_retries=settings.RDM_UPLOADER_CLIENT_REQUEST_RETRIES,
181
- )
182
- )
183
-
184
142
  def _has_stage_created_or_in_progress(self, manager_id: str, entity: str) -> bool:
185
143
  """Проверяет есть ли готовый к работе stage или в работе для данной сущности."""
186
144
  stage_created_or_in_progress = ExportingDataStage.objects.filter(
@@ -414,3 +372,105 @@ class ExportLatestEntitiesData(BaseExportLatestEntitiesData):
414
372
  def _get_async_task(self) -> Model:
415
373
  """Возвращает модель асинхронной задачи."""
416
374
  return RunningTask
375
+
376
+
377
+ class UploadEntitiesData(BaseOperationData):
378
+ """Класс отправки файлов с сущностями в витрину."""
379
+
380
+ def __init__(
381
+ self,
382
+ entities: Iterable[str],
383
+ data_cache: AbstractCache,
384
+ queue: Queue,
385
+ **kwargs,
386
+ ):
387
+ super().__init__(**kwargs)
388
+
389
+ # Если сущности не указаны, берется значение по умолчанию - все сущности:
390
+ self.entities = entities if entities else RegionalDataMartEntityEnum.get_enum_data().keys()
391
+
392
+ self.data_cache = data_cache
393
+ self.queue = queue
394
+
395
+ self.task_id = kwargs.get('task_id')
396
+
397
+ self._configure_agent_client()
398
+
399
+ @property
400
+ def _log_file_path(self) -> Union[str, bytes]:
401
+ """
402
+ Путь до лог файла.
403
+ """
404
+ if self.command_id:
405
+ log_file_path = os.path.join(settings.MEDIA_ROOT, settings.RDM_UPLOAD_LOG_DIR, f'{self.command_id}.log')
406
+ else:
407
+ log_file_path = os.path.join(settings.MEDIA_ROOT, settings.RDM_UPLOAD_LOG_DIR, 'upload_entity.log')
408
+
409
+ return log_file_path
410
+
411
+ # TODO https://jira.bars.group/browse/EDUSCHL-22492. Вынужденная мера, т.к. при запуске команды не производится
412
+ # проверка готовности конфигов приложений. Нужно переработать механизм конфигурирования клиента загрузчика.
413
+ def _configure_agent_client(self):
414
+ """
415
+ Конфигурирование клиента загрузчика данных в Витрину.
416
+ """
417
+ import uploader_client
418
+ from django.core.cache import (
419
+ DEFAULT_CACHE_ALIAS,
420
+ caches,
421
+ )
422
+ from uploader_client.contrib.rdm.interfaces.configurations import (
423
+ RegionalDataMartUploaderConfig,
424
+ )
425
+ if settings.RDM_UPLOADER_CLIENT_ENABLE_REQUEST_EMULATION:
426
+ uploader_client.set_config(
427
+ RegionalDataMartUploaderConfig(
428
+ interface='uploader_client.contrib.rdm.interfaces.rest.OpenAPIInterfaceEmulation',
429
+ url=settings.RDM_UPLOADER_CLIENT_URL,
430
+ datamart_name=settings.RDM_UPLOADER_CLIENT_DATAMART_NAME,
431
+ timeout=1,
432
+ request_retries=1,
433
+ )
434
+ )
435
+ elif settings.RDM_UPLOADER_CLIENT_USE_PROXY_API:
436
+ uploader_client.set_config(
437
+ RegionalDataMartUploaderConfig(
438
+ interface='uploader_client.contrib.rdm.interfaces.rest.ProxyAPIInterface',
439
+ cache=caches[DEFAULT_CACHE_ALIAS],
440
+ url=settings.RDM_UPLOADER_CLIENT_URL,
441
+ datamart_name=settings.RDM_UPLOADER_CLIENT_DATAMART_NAME,
442
+ timeout=settings.RDM_UPLOADER_CLIENT_REQUEST_TIMEOUT,
443
+ request_retries=settings.RDM_UPLOADER_CLIENT_REQUEST_RETRIES,
444
+ organization_ogrn=settings.RDM_UPLOADER_CLIENT_ORGANIZATION_OGRN,
445
+ installation_name=settings.RDM_UPLOADER_CLIENT_INSTALLATION_NAME,
446
+ installation_id=settings.RDM_UPLOADER_CLIENT_INSTALLATION_ID,
447
+ username=settings.RDM_UPLOADER_CLIENT_USERNAME,
448
+ password=settings.RDM_UPLOADER_CLIENT_PASSWORD,
449
+ )
450
+ )
451
+ else:
452
+ uploader_client.set_config(
453
+ RegionalDataMartUploaderConfig(
454
+ url=settings.RDM_UPLOADER_CLIENT_URL,
455
+ datamart_name=settings.RDM_UPLOADER_CLIENT_DATAMART_NAME,
456
+ timeout=settings.RDM_UPLOADER_CLIENT_REQUEST_TIMEOUT,
457
+ request_retries=settings.RDM_UPLOADER_CLIENT_REQUEST_RETRIES,
458
+ )
459
+ )
460
+
461
+ def upload_data(self, *args, **kwargs):
462
+ """
463
+ Запускает отправку данных в витрину.
464
+ """
465
+ try:
466
+ exporter = ExportEntityQueueSender(self.data_cache, self.queue, self.entities)
467
+ exporter.run()
468
+
469
+ sender = WorkerSender(self.queue)
470
+ sender.run()
471
+
472
+ except Exception as err:
473
+ logger.exception(err)
474
+ raise err
475
+ finally:
476
+ self._remove_file_handler()
@@ -1,10 +1,8 @@
1
- from datetime import (
2
- datetime,
3
- )
4
1
  from pathlib import (
5
2
  Path,
6
3
  )
7
4
  from typing import (
5
+ Iterable,
8
6
  List,
9
7
  )
10
8
 
@@ -13,6 +11,7 @@ from django.conf import (
13
11
  )
14
12
  from django.db.models import (
15
13
  F,
14
+ Q,
16
15
  Sum,
17
16
  )
18
17
  from django.db.transaction import (
@@ -83,10 +82,14 @@ class ExportQueueSender:
83
82
  return file_size or 0
84
83
 
85
84
  @staticmethod
86
- def get_sub_stages_attachments_to_export():
85
+ def _make_stage_filter(self) -> Q:
86
+ """Формирование фильтра для выборки подэтапов."""
87
+ return Q(status_id=ExportingDataSubStageStatus.READY_FOR_EXPORT.key)
88
+
89
+ def get_sub_stages_attachments_to_export(self):
87
90
  """Выборка готовых к экспорту подэтапов."""
88
91
  return ExportingDataSubStage.objects.filter(
89
- status_id=ExportingDataSubStageStatus.READY_FOR_EXPORT.key
92
+ self._make_stage_filter()
90
93
  ).annotate(
91
94
  attachment_id=F('exportingdatasubstageattachment__id'),
92
95
  attachment_name=F('exportingdatasubstageattachment__attachment'),
@@ -117,10 +120,14 @@ class ExportQueueSender:
117
120
  timeout=settings.RDM_REDIS_CACHE_TIMEOUT_SECONDS
118
121
  )
119
122
 
123
+ logger.info(
124
+ f'ExportedDataSubStage {sub_stage_id} {entity_name} added to the queue'
125
+ )
126
+
120
127
  return True
121
128
 
122
129
  def run(self):
123
- """Запуск работы менджера."""
130
+ """Запуск работы очереди."""
124
131
  if not self.queue_total_file_size:
125
132
  self.queue_total_file_size = self.get_exported_file_size()
126
133
 
@@ -133,9 +140,7 @@ class ExportQueueSender:
133
140
  if prev_sub_stage != stage_attachment['id']:
134
141
  if stage_files:
135
142
  to_cache = self.set_sub_stage_to_cache(prev_sub_stage, entity, stage_files)
136
- logger.info(
137
- f'ExportedDataSubStage {prev_sub_stage} {entity} added to the queue'
138
- )
143
+
139
144
  stage_files = []
140
145
 
141
146
  if not to_cache:
@@ -162,6 +167,25 @@ class ExportQueueSender:
162
167
  self.queue_total_file_size,
163
168
  timeout=settings.RDM_REDIS_CACHE_TIMEOUT_SECONDS
164
169
  )
170
+ logger.warning(
171
+ f'Total exported file size: {self.queue_total_file_size} - queue is full!!!'
172
+ )
173
+
174
+
175
+ class ExportEntityQueueSender(ExportQueueSender):
176
+ """Класс отправки данных по конкретной сущности в очередь РВД."""
177
+
178
+ def __init__(self, data_cache: AbstractCache, queue: Queue, entity: Iterable[str]):
179
+ super().__init__(data_cache, queue)
180
+
181
+ self.entity = entity
182
+
183
+ def _make_stage_filter(self) -> Q:
184
+ """Формирование фильтра для выборки подэтапов."""
185
+ return Q(
186
+ status_id=ExportingDataSubStageStatus.READY_FOR_EXPORT.key,
187
+ exportingdatasubstageentity__entity_id__in=self.entity
188
+ )
165
189
 
166
190
 
167
191
  class WorkerSender:
@@ -240,6 +264,9 @@ class WorkerSender:
240
264
  sub_stage.save()
241
265
 
242
266
  self.queue.delete_from_queue(sub_stage_id=sub_stage_id, entity_name=entity_key)
267
+ logger.info(
268
+ f'ExportedDataSubStage {sub_stage_id} {entity_key} sended from the queue'
269
+ )
243
270
 
244
271
  def run(self):
245
272
  """Запуск воркера отправки."""
@@ -10,8 +10,8 @@ from typing import (
10
10
  TYPE_CHECKING,
11
11
  Any,
12
12
  Dict,
13
- List,
14
13
  Iterable,
14
+ List,
15
15
  Optional,
16
16
  Tuple,
17
17
  Union,
@@ -33,16 +33,16 @@ from django.db.models.functions import (
33
33
  Cast,
34
34
  Least,
35
35
  )
36
+ from uploader_client.adapters import (
37
+ adapter,
38
+ )
36
39
 
37
40
  from educommon import (
38
41
  logger,
39
42
  )
40
- from uploader_client.adapters import (
41
- adapter,
42
- )
43
+
43
44
  from edu_rdm_integration.collect_and_export_data.models import (
44
- EduRdmCollectDataCommandProgress,
45
- EduRdmExportDataCommandProgress,
45
+ AbstractExportDataCommandProgress,
46
46
  )
47
47
  from edu_rdm_integration.enums import (
48
48
  FileUploadStatusEnum,
@@ -54,32 +54,35 @@ from edu_rdm_integration.export_data.consts import (
54
54
  TOTAL_ATTACHMENTS_SIZE_KEY,
55
55
  )
56
56
  from edu_rdm_integration.models import (
57
- DataMartRequestStatus,
58
- ExportingDataStage,
59
57
  CollectingDataStageStatus,
60
58
  CollectingExportedDataStage,
59
+ DataMartRequestStatus,
60
+ ExportingDataStage,
61
+ ExportingDataStageStatus,
61
62
  ExportingDataSubStageUploaderClientLog,
63
+ UploadDataCommand,
62
64
  UploadStatusRequestLog,
63
- ExportingDataStageStatus,
64
65
  )
65
66
  from edu_rdm_integration.redis_cache import (
66
67
  AbstractCache,
67
68
  )
68
69
 
70
+
69
71
  if TYPE_CHECKING:
70
72
  from datetime import (
71
73
  datetime,
72
74
  )
73
75
 
76
+ from uploader_client.models import (
77
+ Entry,
78
+ )
79
+
74
80
  from edu_rdm_integration.collect_data.non_calculated.base.managers import (
75
81
  BaseCollectingExportedDataRunnerManager,
76
82
  )
77
83
  from edu_rdm_integration.export_data.base.managers import (
78
84
  BaseExportDataRunnerManager,
79
85
  )
80
- from uploader_client.models import (
81
- Entry,
82
- )
83
86
 
84
87
 
85
88
  class UploadStatusHelper:
@@ -311,7 +314,7 @@ class Graph:
311
314
 
312
315
 
313
316
  def save_command_log_link(
314
- command: Union[EduRdmCollectDataCommandProgress, EduRdmExportDataCommandProgress],
317
+ command: Union[AbstractExportDataCommandProgress, UploadDataCommand],
315
318
  log_dir: str
316
319
  ) -> None:
317
320
  """Сохраняет ссылку на лог команды."""
@@ -14,3 +14,6 @@ MODEL_FIELDS_LOG_FILTER: Dict[EntityLogOperation, Dict[str, Tuple]] = {
14
14
  EntityLogOperation.UPDATE: {},
15
15
  EntityLogOperation.DELETE: {}
16
16
  }
17
+
18
+ # Маппинг операций логов моделей и сущностей по умолчанию
19
+ DEFAULT_ENTITY_LOG_OPERATION_MAP = {op: op for op in EntityLogOperation.values}
@@ -0,0 +1,43 @@
1
+ # Generated by Django 3.1.14 on 2024-09-05 20:35
2
+
3
+ import uuid
4
+
5
+ import django.db.models.deletion
6
+ import django.utils.timezone
7
+ from django.db import (
8
+ migrations,
9
+ models,
10
+ )
11
+
12
+ import educommon.django.db.mixins
13
+
14
+ import edu_rdm_integration.utils
15
+
16
+
17
+ class Migration(migrations.Migration):
18
+
19
+ dependencies = [
20
+ ('async_task', '0002_task_type_and_status_data'),
21
+ ('edu_rdm_integration', '0013_set_attachment_size'),
22
+ ]
23
+
24
+ operations = [
25
+ migrations.CreateModel(
26
+ name='UploadDataCommand',
27
+ fields=[
28
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29
+ ('logs_link', models.FileField(max_length=255, upload_to=edu_rdm_integration.utils.get_data_command_progress_attachment_path, verbose_name='Ссылка на файл логов')),
30
+ ('type', models.PositiveSmallIntegerField(choices=[(1, 'Автоматический'), (2, 'Ручной')], verbose_name='Тип команды')),
31
+ ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания')),
32
+ ('generation_id', models.UUIDField(default=uuid.uuid4, verbose_name='Идентификатор генерации')),
33
+ ('entity', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='edu_rdm_integration.regionaldatamartentityenum', verbose_name='Сущность РВД')),
34
+ ('task', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='async_task.runningtask', verbose_name='Асинхронная задача')),
35
+ ],
36
+ options={
37
+ 'verbose_name': 'Команда отправки данных в витрину',
38
+ 'verbose_name_plural': 'Команды отправки данных в витрину',
39
+ 'db_table': 'rdm_upload_data_command',
40
+ },
41
+ bases=(educommon.django.db.mixins.ReprStrPreModelMixin, models.Model),
42
+ ),
43
+ ]
@@ -26,6 +26,7 @@ from django.db.models import (
26
26
  Manager,
27
27
  OneToOneField,
28
28
  PositiveIntegerField,
29
+ PositiveSmallIntegerField,
29
30
  SmallIntegerField,
30
31
  UUIDField,
31
32
  )
@@ -65,12 +66,14 @@ from m3_db_utils.models import (
65
66
  )
66
67
 
67
68
  from edu_rdm_integration.enums import (
69
+ CommandType,
68
70
  FileUploadStatusEnum,
69
71
  )
70
72
  from edu_rdm_integration.uploader_log.managers import (
71
73
  UploaderClientLogManager,
72
74
  )
73
75
  from edu_rdm_integration.utils import (
76
+ get_data_command_progress_attachment_path,
74
77
  get_exporting_data_stage_attachment_path,
75
78
  )
76
79
 
@@ -150,6 +153,7 @@ class CollectingExportedDataStage(ReprStrPreModelMixin, BaseObjectModel):
150
153
 
151
154
  @property
152
155
  def attrs_for_repr_str(self):
156
+ """Список атрибутов для отображения экземпляра модели."""
153
157
  return ['manager_id', 'logs_period_started_at', 'logs_period_ended_at', 'started_at', 'ended_at', 'status_id']
154
158
 
155
159
  def save(self, *args, **kwargs):
@@ -245,6 +249,7 @@ class CollectingExportedDataSubStage(ReprStrPreModelMixin, BaseObjectModel):
245
249
 
246
250
  @property
247
251
  def attrs_for_repr_str(self):
252
+ """Список атрибутов для отображения экземпляра модели."""
248
253
  return ['stage_id', 'function_id', 'started_at', 'ended_at', 'previous_id', 'status_id']
249
254
 
250
255
  def save(self, *args, **kwargs):
@@ -335,6 +340,7 @@ class ExportingDataStage(ReprStrPreModelMixin, BaseObjectModel):
335
340
 
336
341
  @property
337
342
  def attrs_for_repr_str(self):
343
+ """Список атрибутов для отображения экземпляра модели."""
338
344
  return ['manager_id', 'started_at', 'ended_at', 'status_id']
339
345
 
340
346
  def save(self, *args, **kwargs):
@@ -421,9 +427,11 @@ class ExportingDataSubStage(ReprStrPreModelMixin, BaseObjectModel):
421
427
 
422
428
  @property
423
429
  def attrs_for_repr_str(self):
430
+ """Список атрибутов для отображения экземпляра модели."""
424
431
  return ['function_id', 'collecting_data_sub_stage_id', 'stage_id', 'started_at', 'ended_at', 'status_id']
425
432
 
426
433
  def save(self, *args, **kwargs):
434
+ """Сохранение экземпляра модели."""
427
435
  if (
428
436
  self.status_id in {
429
437
  ExportingDataSubStageStatus.FAILED.key,
@@ -487,6 +495,7 @@ class ExportingDataSubStageAttachment(ReprStrPreModelMixin, BaseObjectModel):
487
495
 
488
496
  @property
489
497
  def attrs_for_repr_str(self):
498
+ """Список атрибутов для отображения экземпляра модели."""
490
499
  return ['exporting_data_sub_stage_id', 'attachment', 'operation', 'created', 'modified']
491
500
 
492
501
 
@@ -675,6 +684,7 @@ class BaseEntityModel(ReprStrPreModelMixin, BaseObjectModel):
675
684
 
676
685
  @property
677
686
  def attrs_for_repr_str(self):
687
+ """Список атрибутов для отображения экземпляра модели."""
678
688
  return ['collecting_sub_stage', 'exporting_sub_stage', 'operation', 'created', 'modified']
679
689
 
680
690
  class Meta:
@@ -931,6 +941,7 @@ class TransferredEntity(BaseObjectModel):
931
941
 
932
942
  @json_encode
933
943
  def no_export(self):
944
+ """Формирует отображение признака отключения экспорта."""
934
945
  return 'Нет' if self.export_enabled else 'Да'
935
946
 
936
947
 
@@ -953,3 +964,42 @@ class ExportingDataSubStageEntity(BaseObjectModel):
953
964
  db_table = 'rdm_exporting_data_sub_stage_entity'
954
965
  verbose_name = 'Связь сущности и подэтапа выгрузки'
955
966
  verbose_name_plural = 'Связи сущности и подэтапа выгрузки'
967
+
968
+
969
+ class UploadDataCommand(ReprStrPreModelMixin, BaseObjectModel):
970
+ """Модель, хранящая данные для формирования и отслеживания асинхронной задачи по отправке данных в витрину."""
971
+
972
+ task = ForeignKey(
973
+ to='async_task.RunningTask',
974
+ verbose_name='Асинхронная задача',
975
+ blank=True,
976
+ null=True,
977
+ on_delete=SET_NULL,
978
+ )
979
+ logs_link = FileField(
980
+ upload_to=get_data_command_progress_attachment_path,
981
+ max_length=255,
982
+ verbose_name='Ссылка на файл логов',
983
+ )
984
+ type = PositiveSmallIntegerField( # noqa: A003
985
+ verbose_name='Тип команды',
986
+ choices=CommandType.get_choices(),
987
+ )
988
+ entity = ForeignKey(
989
+ to=RegionalDataMartEntityEnum,
990
+ verbose_name='Сущность РВД',
991
+ on_delete=PROTECT,
992
+ )
993
+ created = DateTimeField(
994
+ verbose_name='Дата создания',
995
+ default=timezone.now,
996
+ )
997
+ generation_id = UUIDField(
998
+ 'Идентификатор генерации',
999
+ default=uuid.uuid4,
1000
+ )
1001
+
1002
+ class Meta:
1003
+ db_table = 'rdm_upload_data_command'
1004
+ verbose_name = 'Команда отправки данных в витрину'
1005
+ verbose_name_plural = 'Команды отправки данных в витрину'
@@ -9,9 +9,6 @@ from typing import (
9
9
  )
10
10
 
11
11
  import celery
12
- from celery.exceptions import (
13
- Ignore,
14
- )
15
12
  from celery.schedules import (
16
13
  crontab,
17
14
  )
@@ -25,9 +22,6 @@ from django.utils import (
25
22
  timezone,
26
23
  )
27
24
 
28
- from educommon.async_task.locker import (
29
- TaskLocker,
30
- )
31
25
  from educommon.async_task.models import (
32
26
  AsyncTaskType,
33
27
  RunningTask,
@@ -64,10 +58,7 @@ from edu_rdm_integration.enums import (
64
58
  )
65
59
  from edu_rdm_integration.export_data.export import (
66
60
  ExportLatestEntitiesData,
67
- )
68
- from edu_rdm_integration.export_data.export_manger import (
69
- ExportQueueSender,
70
- WorkerSender,
61
+ UploadEntitiesData,
71
62
  )
72
63
  from edu_rdm_integration.export_data.helpers import (
73
64
  set_failed_status_suspended_exporting_data_stages,
@@ -85,6 +76,7 @@ from edu_rdm_integration.models import (
85
76
  ExportingDataSubStageUploaderClientLog,
86
77
  RegionalDataMartEntityEnum,
87
78
  TransferredEntity,
79
+ UploadDataCommand,
88
80
  )
89
81
  from edu_rdm_integration.storages import (
90
82
  RegionalDataMartEntityStorage,
@@ -354,6 +346,7 @@ class TransferLatestEntitiesDataPeriodicTask(UniquePeriodicAsyncTask):
354
346
 
355
347
  class UploadDataAsyncTask(PeriodicAsyncTask):
356
348
  """Формирование очереди файлов и их отправка."""
349
+
357
350
  queue = TASK_QUEUE_NAME
358
351
  routing_key = TASK_QUEUE_NAME
359
352
  description = 'Отправка данных в витрину "Региональная витрина данных"'
@@ -369,11 +362,31 @@ class UploadDataAsyncTask(PeriodicAsyncTask):
369
362
  """Выполнение."""
370
363
  super().process(*args, **kwargs)
371
364
 
365
+ entity_ids_for_export = list(
366
+ TransferredEntity.objects.filter(export_enabled=True).values_list('entity_id', flat=True)
367
+ )
372
368
  queue = RdmRedisSubStageAttachmentQueue()
373
- exporter = ExportQueueSender(cache, queue)
374
- exporter.run()
375
- sender = WorkerSender(queue)
376
- sender.run()
369
+ task_id = RunningTask.objects.filter(
370
+ pk=self.request.id,
371
+ ).values_list('pk', flat=True).first()
372
+
373
+ if task_id:
374
+ for entity_id in entity_ids_for_export:
375
+ upload_data_command = UploadDataCommand.objects.create(
376
+ entity_id=entity_id,
377
+ task_id=task_id,
378
+ type=CommandType.AUTO,
379
+ )
380
+ if upload_data_command:
381
+ upload_data = UploadEntitiesData(
382
+ entities=[upload_data_command.entity_id],
383
+ data_cache=cache,
384
+ queue=queue,
385
+ task_id=task_id,
386
+ command_id=upload_data_command.id,
387
+ )
388
+ upload_data.upload_data()
389
+ save_command_log_link(upload_data_command, settings.RDM_UPLOAD_LOG_DIR)
377
390
 
378
391
 
379
392
  celery_app = celery.app.app_or_default()
@@ -381,4 +394,3 @@ celery_app.register_task(RDMCheckUploadStatus)
381
394
  celery_app.register_task(CheckSuspendedExportedStagePeriodicTask)
382
395
  celery_app.register_task(TransferLatestEntitiesDataPeriodicTask)
383
396
  celery_app.register_task(UploadDataAsyncTask)
384
-
@@ -0,0 +1,28 @@
1
+ from typing import (
2
+ Sequence,
3
+ TypeVar,
4
+ Union,
5
+ )
6
+
7
+ from django.db.models import (
8
+ Expression,
9
+ Model,
10
+ )
11
+
12
+
13
+ # Тип, обозначающий любую модель. При указании в качестве type annotation
14
+ # можно указать, что аргументом может быть любая модель или тип модели (через
15
+ # Type[MODEL_TYPE_VAR]), а сама функция возвращает инстанс этой
16
+ # конкретной модели
17
+ MODEL_TYPE_VAR = TypeVar('MODEL_TYPE_VAR', bound=Model)
18
+
19
+ # Аннотация типов для id записи в БД
20
+ RECORD_IDS = Union[
21
+ tuple[int, ...],
22
+ tuple[str, ...],
23
+ list[int],
24
+ list[str],
25
+ Sequence[int],
26
+ Sequence[str],
27
+ Expression,
28
+ ]
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from datetime import (
3
+ date,
3
4
  datetime,
4
5
  time,
5
6
  timedelta,
@@ -11,12 +12,30 @@ from typing import (
11
12
  List,
12
13
  Optional,
13
14
  Set,
15
+ Type,
16
+ TypeVar,
14
17
  Union,
15
18
  )
16
19
 
17
20
  from django.conf import (
18
21
  settings,
19
22
  )
23
+ from django.db.models import (
24
+ DateField,
25
+ DateTimeField,
26
+ Expression,
27
+ F,
28
+ )
29
+ from django.db.models.expressions import (
30
+ Func,
31
+ )
32
+ from django.db.models.functions import (
33
+ Cast,
34
+ Now,
35
+ )
36
+ from educommon.integration_entities.enums import (
37
+ EntityLogOperation,
38
+ )
20
39
 
21
40
  from edu_rdm_integration.apps import (
22
41
  EduRDMIntegrationConfig,
@@ -285,3 +304,59 @@ def get_data_command_progress_attachment_path(
285
304
  str(instance.type),
286
305
  filename
287
306
  )
307
+
308
+
309
+ class MakeInterval(Func):
310
+ """Функция обработки даты/времени."""
311
+
312
+ template = 'make_interval(%(expressions)s)'
313
+
314
+ def __init__(
315
+ self, *,
316
+ years: Union[int, F] = 0,
317
+ months: Union[int, F] = 0,
318
+ weeks: Union[int, F] = 0,
319
+ days: Union[int, F] = 0,
320
+ hours: Union[int, F] = 0,
321
+ minutes: Union[int, F] = 0,
322
+ seconds: Union[float, F] = 0.0,
323
+ output_field=None,
324
+ ) -> None:
325
+ self.years = years
326
+ self.months = months
327
+ self.weeks = weeks
328
+ self.days = days
329
+ self.hours = hours
330
+ self.minutes = minutes
331
+ self.seconds = seconds
332
+
333
+ super().__init__(
334
+ self.years,
335
+ self.months,
336
+ self.weeks,
337
+ self.days,
338
+ self.hours,
339
+ self.minutes,
340
+ self.seconds,
341
+ output_field=output_field or DateTimeField(),
342
+ )
343
+
344
+
345
+ def make_passed_datetime_from_today(
346
+ *,
347
+ years: int = 0,
348
+ months: int = 0,
349
+ days: int = 0,
350
+ ) -> Union[datetime, Expression, F]:
351
+ """Возвращает выражение на лет/месяцев/дней меньше относительно сегодня."""
352
+ now_expr = Now()
353
+
354
+ return now_expr - MakeInterval(years=years, months=months, days=days)
355
+
356
+
357
+ TODAY_EXPR: Union[date, Expression, F] = Cast(make_passed_datetime_from_today(), output_field=DateField())
358
+
359
+ THREE_YEARS_AGO_DATE_EXPR: Union[date, Expression, F] = Cast(
360
+ make_passed_datetime_from_today(years=3),
361
+ output_field=DateField()
362
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edu-rdm-integration
3
- Version: 3.3.6
3
+ Version: 3.4.0
4
4
  Summary: Интеграция с Региональной витриной данных
5
5
  Home-page:
6
6
  Download-URL:
@@ -28,7 +28,7 @@ Requires-Dist: wheel <0.42,>=0.37.1
28
28
  Requires-Dist: transliterate <2
29
29
  Requires-Dist: Django <3.3,>=2.2
30
30
  Requires-Dist: celery <5.3,>=4.4.7
31
- Requires-Dist: educommon <4,>=3.10.0
31
+ Requires-Dist: educommon <4,>=3.11.0
32
32
  Requires-Dist: function-tools <1,>=0.9.0
33
33
  Requires-Dist: m3-db-utils <1,>=0.3.10
34
34
  Requires-Dist: m3-django-compat <2,>=1.10.2
@@ -301,6 +301,29 @@ Requires-Dist: uploader-client <1,>=0.2.1
301
301
 
302
302
  ### Удалено
303
303
 
304
+
305
+ ## 3.4.0 - 2024-09-25
306
+ В базовый класс тестирования функций сбора добавлена поддержка формата PhoneNumber для формирования
307
+ экземпляра модели AuditLog.
308
+ Повышена версия зависимости educommon >= 3.11.0.
309
+ Вынесены утилиты и базовый класс для логирования и сбора по модели в рамках сущности
310
+
311
+ ## Добавлено
312
+ - [EDUSCHL-22378](https://jira.bars.group/browse/EDUSCHL-22378)
313
+ Реализация поля моделей Django содержащего номер телефона
314
+
315
+ ### Изменено
316
+ - [EDUKNDG-14516](https://jira.bars.group/browse/EDUKNDG-14516)
317
+ PATCH Вынесены утилиты и базовый класс для логирования и сбора по модели в рамках сущности
318
+
319
+
320
+ ## 3.3.7 - 2024-09-06
321
+ Добавлены команда (UploadEntitiesData) и модель (UploadDataCommand) для логирования и запуска выгрузки данных в витрину
322
+ - [EDUSCHL-22042](https://jira.bars.group/browse/EDUSCHL-22042)
323
+ PATCH Добавлены команда (UploadEntitiesData) и модель (UploadDataCommand) для логирования и запуска выгрузки данных
324
+ в витрину
325
+
326
+
304
327
  ## 3.3.6 - 2024-09-02
305
328
  Добавлена фильтрация по дате выгрузки при обновлении данных на экспорт в BaseExportLatestEntitiesData
306
329
 
@@ -309,7 +332,6 @@ Requires-Dist: uploader-client <1,>=0.2.1
309
332
  PATCH Добавлена фильтрация по дате выгрузки при обновлении данных на экспорт в BaseExportLatestEntitiesData
310
333
 
311
334
 
312
-
313
335
  ## 3.3.5 - 2024-08-26
314
336
  Классы UniquePeriodicAsyncTask, PeriodicTaskLocker перенесены в educommon.
315
337
 
@@ -6,14 +6,15 @@ edu_rdm_integration/base.py,sha256=_G0qPTAXe6bXfgDHNiZMSsYt3sMuUhLKnHuQCWSFttU,1
6
6
  edu_rdm_integration/consts.py,sha256=Qt52SOCQ-3wOet-_6inJih_W9nToORKXbkxb3jVSjEo,1079
7
7
  edu_rdm_integration/entities.py,sha256=qNVWUhjwvX298Ak86_AKmqBZioP0czGwBcAz_4dtUUE,14552
8
8
  edu_rdm_integration/enums.py,sha256=T3Mu5D-CbKO3BSg16MPPnIPlcc_YGLYR-ThS8dzl9gg,4246
9
- edu_rdm_integration/helpers.py,sha256=m98k8nichw5AIoPuXNPsI-ZFN9OYX5Wt2MU6ml_pg88,14615
10
- edu_rdm_integration/mapping.py,sha256=bwa2fJCbV4YjQcAgRrgT3hgM6dJhr_uBtQgx3L3F2Ck,473
11
- edu_rdm_integration/models.py,sha256=Fwt2O2j_aPI9owRZprreGQ2t7op-YmIYpZoq5HHjL3M,30692
9
+ edu_rdm_integration/helpers.py,sha256=_zQb3vuMrWClpUErshYTI1aVgdKjXf819pHfBjaIty4,14592
10
+ edu_rdm_integration/mapping.py,sha256=PGy6oH3Jzg4uuSIjlI0A-gWWpJPozqZHN4wGzp-ldFY,660
11
+ edu_rdm_integration/models.py,sha256=YP_vot6PbQ3jWv3NNW1wa0gScjOsT2lgdrj7z5V6NHw,33053
12
12
  edu_rdm_integration/redis_cache.py,sha256=GZhtM1d0cVr5TEqxh15K7dS371Msit6wRemIiYb2rzk,1548
13
13
  edu_rdm_integration/signals.py,sha256=3eRlpkDcFCF6TN80-QM8yBYLcyozzcmoPjz6r4_ApWg,73
14
14
  edu_rdm_integration/storages.py,sha256=o5WqUG7SnkeuMt-z8spUi-IraivST-7KHzfY-M3v7FA,6807
15
- edu_rdm_integration/tasks.py,sha256=WolNe2t3_NCOisZeg24HyPI3Y0ciOJbwaFprhWgt41Q,15282
16
- edu_rdm_integration/utils.py,sha256=NIOxlH4JiTOOxYgqcwtfqT5UAtNG24GLq_tsnBUtp8E,10370
15
+ edu_rdm_integration/tasks.py,sha256=QAOytSCEoWtFWJqmTkj3N6NRl_Hds9EXyRfngFHrbPk,16046
16
+ edu_rdm_integration/typing.py,sha256=6GOZfRDqOjN-o5NR86-f2o4uXQm_8AUCRRdQTITtrcs,785
17
+ edu_rdm_integration/utils.py,sha256=QMiohtOaId3Zab91IZPazGzhce5pa5e-ilRgXpISkHk,12249
17
18
  edu_rdm_integration/adapters/__init__.py,sha256=cU0swn4Ny5ZQz5buWRcWsT1mpWuUFJaUlHf2l7TtEBo,83
18
19
  edu_rdm_integration/adapters/apps.py,sha256=TyJTkSPs2qAHJ11fqbwLGk3Ea7ujtqWwbxqmvYNQxG8,363
19
20
  edu_rdm_integration/adapters/caches.py,sha256=OxSqeXySUN42LxEeHBLtC1ZBt-7aicbRbmP1EJYTvV4,1505
@@ -41,7 +42,7 @@ edu_rdm_integration/collect_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
41
42
  edu_rdm_integration/collect_data/collect.py,sha256=HIKO8Kvw9NY9Nt5M5w9XfcZ9E5IVue_4mT7ESYJ6n4I,11909
42
43
  edu_rdm_integration/collect_data/generators.py,sha256=f34AAwdEcQNIokX0ypqYgjRD1XolwBVLER_HYv9ibNw,9075
43
44
  edu_rdm_integration/collect_data/helpers.py,sha256=gAFZAm9YhMtQhvlwSF3LMthPPa8LsqG_zbVe7vnW_Ag,2995
44
- edu_rdm_integration/collect_data/tests.py,sha256=-UNX3-GL0H9i89GXRfICT0ABltn_9aN_I_cmn1gQcDA,5367
45
+ edu_rdm_integration/collect_data/tests.py,sha256=hlf2eXUQonYeLY2enm-ND712NrsGcYa03FxjCUOj7EE,5535
45
46
  edu_rdm_integration/collect_data/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
47
  edu_rdm_integration/collect_data/base/caches.py,sha256=XPMVZSgR8vRhCHmHsnUKy86rEIPUQXHz0wJabovPyfU,2170
47
48
  edu_rdm_integration/collect_data/base/functions.py,sha256=E1vYmC8F8NM5cBB5SGQEpluHrMynpjf3Ek3s0HsbxwY,2998
@@ -82,13 +83,13 @@ edu_rdm_integration/collect_data/non_calculated/base/strings.py,sha256=-k9dex8A7
82
83
  edu_rdm_integration/collect_data/non_calculated/base/tests.py,sha256=MoRY-a75Ow-7EjeQYxkXWunwqTGuBMaUyEkEV2oy05I,59
83
84
  edu_rdm_integration/collect_data/non_calculated/base/validators.py,sha256=0YvnfrfK1iFcZVSB-M-Xv82tIjYxEU_BwLofAEuGVW4,973
84
85
  edu_rdm_integration/enum_register/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
- edu_rdm_integration/enum_register/mixins.py,sha256=nmHue9-9bqKZn9_MFiL0E-5wBUBIZPZwk3DWVir5pKk,4021
86
+ edu_rdm_integration/enum_register/mixins.py,sha256=YYCvSQEf6RMaMnGS2vA2v-P1ojkXA5MgAuBOcFgLIRE,4642
86
87
  edu_rdm_integration/enum_register/register.py,sha256=5OWOjK-M0Erd_5CENpBaXhVtfL0pEaDl3Bev5QKNDJc,2218
87
88
  edu_rdm_integration/export_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
89
  edu_rdm_integration/export_data/consts.py,sha256=rmJ_18wHCE54j0VABxfo7Zu01EPFYSjYrj5L79hVW2Y,333
89
90
  edu_rdm_integration/export_data/dataclasses.py,sha256=IhftRopP4lS-m3ygdBU5Bz0HF71VSBP4JQ6-8VIVgtY,260
90
- edu_rdm_integration/export_data/export.py,sha256=JZAgT8K49buRb57Pvt7iRkKW_fm7P4luZ4_umbsoY_E,16609
91
- edu_rdm_integration/export_data/export_manger.py,sha256=_8gkFH380zZVJ9LVxqeZOXphS-RjeIoSbq0eUbXnIMA,9657
91
+ edu_rdm_integration/export_data/export.py,sha256=xR2QRRL_HLD9MUIoe98Krc4KN0itWCpEl3uDM2tcqeE,18513
92
+ edu_rdm_integration/export_data/export_manger.py,sha256=lbXK3aV0Te0ZfJHylvCBZ0bJ7vWwD-pMQBX4Du8gPro,10681
92
93
  edu_rdm_integration/export_data/generators.py,sha256=yLDOcHB1PoilJwXtKGxZQhDjpeKBzEWoosahbJJ4Ba4,4020
93
94
  edu_rdm_integration/export_data/helpers.py,sha256=hU346RmQ17Ra2etFvxXI7JQlLyp_0KxH1jm-eeCqejc,2933
94
95
  edu_rdm_integration/export_data/queue.py,sha256=NiWm7e59zOcGT8s87BxiyU4-nA02fH94oVKnhc8htQ4,6159
@@ -162,6 +163,7 @@ edu_rdm_integration/migrations/0010_transferredentity_export_enabled.py,sha256=L
162
163
  edu_rdm_integration/migrations/0011_exportingdatasubstageentity.py,sha256=2BfIif_hkFv1h6VEfe0Ys4J_uk6LR9YtO711ocDYPso,1263
163
164
  edu_rdm_integration/migrations/0012_exportingdatasubstageattachment_attachment_size.py,sha256=y_JQO69k9pEfrJyimaRiAOBmhaJmssIyepCGd-Sy9hs,511
164
165
  edu_rdm_integration/migrations/0013_set_attachment_size.py,sha256=Pj_n-ytsC0lhyU67qvH8UHHQ-c-TH5MZFfy-UF6y6M4,1809
166
+ edu_rdm_integration/migrations/0014_uploaddatacommand.py,sha256=Hh0vKKiGgKOvY1kBAcmway4dSYUXwVArHAc9YrsjCIU,2079
165
167
  edu_rdm_integration/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
168
  edu_rdm_integration/registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
169
  edu_rdm_integration/registry/actions.py,sha256=YUtCkpcc3DedM_x8rwJ6Aos-8HKkDCrOUdKnGhztfUU,5223
@@ -174,9 +176,9 @@ edu_rdm_integration/uploader_log/enums.py,sha256=rgSO3BL2rh2xpfm0Pt4waQW8fB1VMJL
174
176
  edu_rdm_integration/uploader_log/managers.py,sha256=y5wTSMzF9hpOpIU_A7nIafL_LBU3QEie6LAYWoB-pBQ,3203
175
177
  edu_rdm_integration/uploader_log/ui.py,sha256=YM9Buqp2wxE95Wf5gvAATBzuYzDOossK1sEmvFk07cI,2110
176
178
  edu_rdm_integration/uploader_log/templates/ui-js/object-grid-buttons.js,sha256=2xyGe0wdVokM0RhpzRzcRvJPBkBmPe3SlZry4oP4Nzs,6201
177
- edu_rdm_integration-3.3.6.dist-info/LICENSE,sha256=uw43Gjjj-1vXWCItfSrNDpbejnOwZMrNerUh8oWbq8Q,3458
178
- edu_rdm_integration-3.3.6.dist-info/METADATA,sha256=GF0r7I7EfNUKBqQ4USLovvZRvmn4gAJzDxG8KPeN81I,72243
179
- edu_rdm_integration-3.3.6.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
180
- edu_rdm_integration-3.3.6.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
181
- edu_rdm_integration-3.3.6.dist-info/top_level.txt,sha256=nRJV0O14UtNE-jGIYG03sohgFnZClvf57H5m6VBXe9Y,20
182
- edu_rdm_integration-3.3.6.dist-info/RECORD,,
179
+ edu_rdm_integration-3.4.0.dist-info/LICENSE,sha256=uw43Gjjj-1vXWCItfSrNDpbejnOwZMrNerUh8oWbq8Q,3458
180
+ edu_rdm_integration-3.4.0.dist-info/METADATA,sha256=yyTvi9MApMnWTUi9cv5G3MRQ8CKcEb48KFVSvcGQXzk,73632
181
+ edu_rdm_integration-3.4.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
182
+ edu_rdm_integration-3.4.0.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
183
+ edu_rdm_integration-3.4.0.dist-info/top_level.txt,sha256=nRJV0O14UtNE-jGIYG03sohgFnZClvf57H5m6VBXe9Y,20
184
+ edu_rdm_integration-3.4.0.dist-info/RECORD,,