From 0824733d1a04a764ef8b6b6c05223548a34134ce Mon Sep 17 00:00:00 2001 From: Aleksandr Meshchriakov Date: Fri, 20 Mar 2026 10:42:17 +0100 Subject: [PATCH 1/2] feat(registers): add command to generate test data from factories --- src/apps/registers/management/__init__.py | 1 + .../registers/management/commands/__init__.py | 1 + .../management/commands/generate_test_data.py | 205 ++++++++++++++++++ .../test_generate_test_data_command.py | 73 +++++++ 4 files changed, 280 insertions(+) create mode 100644 src/apps/registers/management/__init__.py create mode 100644 src/apps/registers/management/commands/__init__.py create mode 100644 src/apps/registers/management/commands/generate_test_data.py create mode 100644 tests/apps/registers/test_generate_test_data_command.py diff --git a/src/apps/registers/management/__init__.py b/src/apps/registers/management/__init__.py new file mode 100644 index 0000000..7c40610 --- /dev/null +++ b/src/apps/registers/management/__init__.py @@ -0,0 +1 @@ +"""Management package for registers app.""" diff --git a/src/apps/registers/management/commands/__init__.py b/src/apps/registers/management/commands/__init__.py new file mode 100644 index 0000000..4dec1e9 --- /dev/null +++ b/src/apps/registers/management/commands/__init__.py @@ -0,0 +1 @@ +"""Management commands for registers app.""" diff --git a/src/apps/registers/management/commands/generate_test_data.py b/src/apps/registers/management/commands/generate_test_data.py new file mode 100644 index 0000000..6ded7af --- /dev/null +++ b/src/apps/registers/management/commands/generate_test_data.py @@ -0,0 +1,205 @@ +"""Команда генерации тестовых данных по реестрам через test factories.""" + +from __future__ import annotations + +import sys +from importlib import import_module +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from apps.core.management.commands.base import BaseAppCommand +from apps.parsers.models import ParserLoadLog +from django.conf import settings +from django.core.management.base import CommandError +from django.db.models import Max + + +class Command(BaseAppCommand): + """Генерирует тестовые данные для всех реестров на основе фабрик тестов.""" + + help = ( + "Генерация тестовых данных для registers/parsers на основе фабрик из tests" + ) + use_transaction = True + + def add_arguments(self, parser) -> None: + """Добавление аргументов команды.""" + super().add_arguments(parser) + parser.add_argument( + "--count", + type=int, + default=20, + help="Количество записей для каждого парсерного реестра", + ) + parser.add_argument( + "--registers-count", + type=int, + default=5, + help="Количество записей в справочнике реестров (apps.registers)", + ) + + def execute_command(self, *args: Any, **options: Any) -> str: + """Основная логика генерации тестовых данных.""" + count = options["count"] + registers_count = options["registers_count"] + + self._validate_positive("count", count) + self._validate_positive("registers_count", registers_count) + + factories = self._load_factories() + + register_factory = factories["register_factory"] + organization_factory = factories["organization_factory"] + register_upload_factory = factories["register_upload_factory"] + registry_membership_period_factory = factories[ + "registry_membership_period_factory" + ] + parser_load_log_factory = factories["parser_load_log_factory"] + proxy_factory = factories["proxy_factory"] + industrial_certificate_factory = factories["industrial_certificate_factory"] + manufacturer_factory = factories["manufacturer_factory"] + industrial_product_factory = factories["industrial_product_factory"] + inspection_factory = factories["inspection_factory"] + procurement_factory = factories["procurement_factory"] + + run_token = uuid4().hex[:10] + run_seed = int(run_token[:6], 16) % 1_000_000 + + self.log_info("Создание тестовых реестров organizations/registers...") + registers = [ + register_factory.create(name=f"Тестовый реестр {run_token}-{index + 1}") + for index in range(registers_count) + ] + for register_index, register in enumerate( + self.progress_iter( + registers, + desc="Заполнение реестров организаций", + total=registers_count, + ) + ): + for item_index in range(count): + org_serial = register_index * count + item_index + 1 + organization = organization_factory.create( + mn_ogrn=80_000_000_000_000 + run_seed * 100_000 + org_serial, + mn_inn=1_000_000_000 + + (run_seed * 10_000 + org_serial) % 8_000_000_000, + ) + upload = register_upload_factory.create(registry=register) + registry_membership_period_factory.create( + registry=register, + organization=organization, + started_by_upload=upload, + ) + + self.log_info("Создание тестовых данных parser-реестров...") + for index in range(1, count + 1): + industrial_certificate_factory.create( + certificate_number=f"TST-CERT-{run_token}-{index:05d}" + ) + + for index in range(1, count + 1): + manufacturer_factory.create( + inn=f"{run_seed:06d}{index:09d}", + ) + + industrial_product_factory.create_batch(count) + + for index in range(1, count + 1): + inspection_factory.create( + registration_number=f"TST-INSP-{run_token}-{index:05d}" + ) + + for index in range(1, count + 1): + procurement_factory.create( + purchase_number=f"{run_seed:06d}{index:013d}" + ) + + created_by_registry = { + "industrial_certificates": count, + "manufacturers": count, + "industrial_products": count, + "inspections": count, + "procurements": count, + } + + self.log_info("Создание прокси и логов загрузок...") + for index in range(1, count + 1): + proxy_factory.create( + address=f"http://proxy-{run_token}-{index}.local:8080" + ) + + load_log_sources = [source for source, _ in ParserLoadLog.Source.choices] + for source in load_log_sources: + max_batch = ( + ParserLoadLog.objects.filter(source=source).aggregate( + max_batch=Max("batch_id") + )["max_batch"] + or 0 + ) + for index in range(1, count + 1): + parser_load_log_factory.create( + source=source, + batch_id=max_batch + index, + ) + + summary_lines = [ + "Тестовые данные успешно созданы.", + f"registers.Register: {registers_count}", + f"registers.RegisterUpload: {registers_count * count}", + f"registers.RegistryMembershipPeriod: {registers_count * count}", + f"parsers.Proxy: {count}", + f"parsers.ParserLoadLog: {len(load_log_sources) * count}", + ] + summary_lines.extend( + f"parsers.{registry_name}: {created_count}" + for registry_name, created_count in created_by_registry.items() + ) + return "\n".join(summary_lines) + + @staticmethod + def _validate_positive(option_name: str, value: int) -> None: + """Проверка, что числовой аргумент больше нуля.""" + if value < 1: + raise CommandError(f"--{option_name.replace('_', '-')} должен быть >= 1") + + @staticmethod + def _load_factories() -> dict[str, Any]: + """Импорт фабрик из тестов (tests/apps/*/factories.py).""" + Command._ensure_project_root_on_path() + + try: + registers_factories = import_module("tests.apps.registers.factories") + parsers_factories = import_module("tests.apps.parsers.factories") + except ModuleNotFoundError as exc: + raise CommandError( + "Не удалось импортировать тестовые фабрики из tests/apps/*/factories.py. " + "Проверьте, что каталог tests доступен в PYTHONPATH." + ) from exc + + return { + "register_factory": registers_factories.RegisterFactory, + "organization_factory": registers_factories.OrganizationFactory, + "register_upload_factory": registers_factories.RegisterUploadFactory, + "registry_membership_period_factory": ( + registers_factories.RegistryMembershipPeriodFactory + ), + "parser_load_log_factory": parsers_factories.ParserLoadLogFactory, + "proxy_factory": parsers_factories.ProxyFactory, + "industrial_certificate_factory": ( + parsers_factories.IndustrialCertificateRecordFactory + ), + "manufacturer_factory": parsers_factories.ManufacturerRecordFactory, + "industrial_product_factory": parsers_factories.IndustrialProductRecordFactory, + "inspection_factory": parsers_factories.InspectionRecordFactory, + "procurement_factory": parsers_factories.ProcurementRecordFactory, + } + + @staticmethod + def _ensure_project_root_on_path() -> None: + """Добавить корень проекта в PYTHONPATH для импорта tests.*.""" + base_dir = Path(settings.BASE_DIR) + project_root = Path(getattr(settings, "PROJECT_ROOT", base_dir.parent)) + project_root_str = str(project_root) + if project_root_str not in sys.path: + sys.path.append(project_root_str) diff --git a/tests/apps/registers/test_generate_test_data_command.py b/tests/apps/registers/test_generate_test_data_command.py new file mode 100644 index 0000000..36d8554 --- /dev/null +++ b/tests/apps/registers/test_generate_test_data_command.py @@ -0,0 +1,73 @@ +"""Тесты команды generate_test_data.""" + +from apps.parsers.models import ( + IndustrialCertificateRecord, + IndustrialProductRecord, + InspectionRecord, + ManufacturerRecord, + ParserLoadLog, + ProcurementRecord, + Proxy, +) +from apps.registers.models import ( + Organization, + Register, + RegisterUpload, + RegistryMembershipPeriod, +) +from django.core.management import call_command +from django.test import TestCase + + +class GenerateTestDataCommandTest(TestCase): + """Проверки management command generate_test_data.""" + + def test_command_creates_data_for_every_registry(self): + """Команда создаёт данные по всем целевым реестрам.""" + call_command( + "generate_test_data", + count=2, + registers_count=3, + silent=True, + verbosity=0, + ) + + self.assertEqual(Register.objects.count(), 3) + self.assertEqual(RegisterUpload.objects.count(), 6) + self.assertEqual(RegistryMembershipPeriod.objects.count(), 6) + self.assertEqual(Organization.objects.count(), 6) + + self.assertEqual(IndustrialCertificateRecord.objects.count(), 2) + self.assertEqual(ManufacturerRecord.objects.count(), 2) + self.assertEqual(IndustrialProductRecord.objects.count(), 2) + self.assertEqual(InspectionRecord.objects.count(), 2) + self.assertEqual(ProcurementRecord.objects.count(), 2) + + self.assertEqual(Proxy.objects.count(), 2) + self.assertEqual( + ParserLoadLog.objects.count(), + len(ParserLoadLog.Source.values) * 2, + ) + + def test_command_dry_run_rolls_back_everything(self): + """В dry-run режиме все изменения откатываются.""" + call_command( + "generate_test_data", + count=1, + registers_count=1, + dry_run=True, + silent=True, + verbosity=0, + ) + + self.assertEqual(Register.objects.count(), 0) + self.assertEqual(RegisterUpload.objects.count(), 0) + self.assertEqual(RegistryMembershipPeriod.objects.count(), 0) + self.assertEqual(Organization.objects.count(), 0) + self.assertEqual(IndustrialCertificateRecord.objects.count(), 0) + self.assertEqual(ManufacturerRecord.objects.count(), 0) + self.assertEqual(IndustrialProductRecord.objects.count(), 0) + self.assertEqual(InspectionRecord.objects.count(), 0) + self.assertEqual(ProcurementRecord.objects.count(), 0) + self.assertEqual(Proxy.objects.count(), 0) + self.assertEqual(ParserLoadLog.objects.count(), 0) -- 2.39.5 From 46e6e24934c9645b21d630988125411cd4060b96 Mon Sep 17 00:00:00 2001 From: Aleksandr Meshchriakov Date: Fri, 20 Mar 2026 10:56:33 +0100 Subject: [PATCH 2/2] fix pre-commit --- .../management/commands/generate_test_data.py | 12 +++--------- src/settings/dev.py | 7 +++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/apps/registers/management/commands/generate_test_data.py b/src/apps/registers/management/commands/generate_test_data.py index 6ded7af..895abda 100644 --- a/src/apps/registers/management/commands/generate_test_data.py +++ b/src/apps/registers/management/commands/generate_test_data.py @@ -18,9 +18,7 @@ from django.db.models import Max class Command(BaseAppCommand): """Генерирует тестовые данные для всех реестров на основе фабрик тестов.""" - help = ( - "Генерация тестовых данных для registers/parsers на основе фабрик из tests" - ) + help = "Генерация тестовых данных для registers/parsers на основе фабрик из tests" use_transaction = True def add_arguments(self, parser) -> None: @@ -111,9 +109,7 @@ class Command(BaseAppCommand): ) for index in range(1, count + 1): - procurement_factory.create( - purchase_number=f"{run_seed:06d}{index:013d}" - ) + procurement_factory.create(purchase_number=f"{run_seed:06d}{index:013d}") created_by_registry = { "industrial_certificates": count, @@ -125,9 +121,7 @@ class Command(BaseAppCommand): self.log_info("Создание прокси и логов загрузок...") for index in range(1, count + 1): - proxy_factory.create( - address=f"http://proxy-{run_token}-{index}.local:8080" - ) + proxy_factory.create(address=f"http://proxy-{run_token}-{index}.local:8080") load_log_sources = [source for source, _ in ParserLoadLog.Source.choices] for source in load_log_sources: diff --git a/src/settings/dev.py b/src/settings/dev.py index f09282c..f49306a 100644 --- a/src/settings/dev.py +++ b/src/settings/dev.py @@ -69,3 +69,10 @@ CACHES = { # Email EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +# DRF: отключаем throttling в dev, чтобы не мешать фронту тестировать API. +REST_FRAMEWORK = { + **globals().get("REST_FRAMEWORK", {}), + "DEFAULT_THROTTLE_CLASSES": [], + "DEFAULT_THROTTLE_RATES": {}, +} -- 2.39.5