fix parser schedule run issues
All checks were successful
CI/CD Pipeline / Manual Action Help (push) Has been skipped
CI/CD Pipeline / Start Dev Containers in Dokploy (push) Has been skipped
CI/CD Pipeline / Drop and Recreate Dev Database (push) Has been skipped
CI/CD Pipeline / Quality Gate (push) Successful in 1m53s
CI/CD Pipeline / Build and Push Images (push) Successful in 2m42s
CI/CD Pipeline / Internal Notify (push) Successful in 1s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s

This commit is contained in:
2026-04-28 13:58:55 +02:00
parent b373341fcd
commit c72343a375
7 changed files with 179 additions and 25 deletions

View File

@@ -764,8 +764,40 @@ jobs:
fi fi
} }
wait_for_migrations() {
export PGPASSWORD="${POSTGRES_PASSWORD}"
for attempt in $(seq 1 60); do
SCHEMA_STATE=$(psql \
--set ON_ERROR_STOP=1 \
--host="${POSTGRES_HOST}" \
--port="${POSTGRES_PORT}" \
--username="${POSTGRES_USER}" \
--dbname="${POSTGRES_DB}" \
--tuples-only \
--no-align \
<<'SQL'
SELECT CASE
WHEN to_regclass('public.django_migrations') IS NOT NULL
AND to_regclass('public.core_backgroundjob') IS NOT NULL
THEN 'ready'
ELSE 'waiting'
END;
SQL
)
if [ "${SCHEMA_STATE}" = "ready" ]; then
echo "Database schema is ready after web deploy"
return 0
fi
echo "Waiting for web migrations (${attempt}/60)"
sleep 5
done
echo "Database schema was not ready after web deploy" >&2
exit 1
}
call_webhook "dev web" "${DOKPLOY_DEV_WEB_WEBHOOK_URL}" "web" call_webhook "dev web" "${DOKPLOY_DEV_WEB_WEBHOOK_URL}" "web"
sleep 45 wait_for_migrations
call_webhook "dev worker" "${DOKPLOY_DEV_WORKER_WEBHOOK_URL}" "worker" call_webhook "dev worker" "${DOKPLOY_DEV_WORKER_WEBHOOK_URL}" "worker"
call_webhook "dev beat" "${DOKPLOY_DEV_BEAT_WEBHOOK_URL}" "beat" call_webhook "dev beat" "${DOKPLOY_DEV_BEAT_WEBHOOK_URL}" "beat"

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2026-04-28 11:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parsers', '0018_seed_weekly_parser_schedules'),
]
operations = [
migrations.AlterField(
model_name='genericparserrecord',
name='record_date',
field=models.CharField(blank=True, db_index=True, help_text='Дата записи в формате источника', max_length=255, verbose_name='дата записи'),
),
]

View File

@@ -405,7 +405,7 @@ class GenericParserRecord(TimestampMixin, models.Model):
) )
record_date = models.CharField( record_date = models.CharField(
_("дата записи"), _("дата записи"),
max_length=30, max_length=255,
blank=True, blank=True,
db_index=True, db_index=True,
help_text=_("Дата записи в формате источника"), help_text=_("Дата записи в формате источника"),

View File

@@ -914,6 +914,11 @@ def sync_inspections( # noqa: C901
proxies: list[str] | None = None, proxies: list[str] | None = None,
client_adapter: BaseAdapter | None = None, client_adapter: BaseAdapter | None = None,
use_playwright: bool | None = None, use_playwright: bool | None = None,
max_months_per_law: int | None = None,
start_year: int | None = None,
start_month: int | None = None,
include_fz294: bool | None = None,
include_fz248: bool | None = None,
current_year: int | None = None, current_year: int | None = None,
current_month: int | None = None, current_month: int | None = None,
requested_by_id: int | None = None, requested_by_id: int | None = None,
@@ -932,6 +937,11 @@ def sync_inspections( # noqa: C901
proxies: Список прокси-серверов (опционально) proxies: Список прокси-серверов (опционально)
client_adapter: HTTP-адаптер (опционально). client_adapter: HTTP-адаптер (опционально).
use_playwright: Использовать Playwright (опционально). use_playwright: Использовать Playwright (опционально).
max_months_per_law: Максимум месяцев для каждого закона.
start_year: Год стартового периода, если нужно переопределить resume.
start_month: Месяц стартового периода, если нужно переопределить resume.
include_fz294: Загружать проверки по ФЗ-294.
include_fz248: Загружать проверки по ФЗ-248.
current_year: Год (опционально) для ограничения периода. current_year: Год (опционально) для ограничения периода.
current_month: Месяц (опционально) для ограничения периода. current_month: Месяц (опционально) для ограничения периода.
@@ -967,6 +977,10 @@ def sync_inspections( # noqa: C901
now = datetime.now() now = datetime.now()
current_year = current_year or now.year current_year = current_year or now.year
current_month = current_month or now.month current_month = current_month or now.month
include_fz294 = True if include_fz294 is None else include_fz294
include_fz248 = True if include_fz248 is None else include_fz248
if max_months_per_law is not None:
max_months_per_law = max(1, int(max_months_per_law))
total_saved = 0 total_saved = 0
results = {"fz294": [], "fz248": []} results = {"fz294": [], "fz248": []}
@@ -978,43 +992,68 @@ def sync_inspections( # noqa: C901
client_kwargs["use_playwright"] = use_playwright client_kwargs["use_playwright"] = use_playwright
with ProverkiClient(**client_kwargs) as client: with ProverkiClient(**client_kwargs) as client:
# Обрабатываем оба типа проверок # Обрабатываем оба типа проверок
for is_fz248 in [False, True]: law_modes = []
if include_fz294:
law_modes.append(False)
if include_fz248:
law_modes.append(True)
for is_fz248 in law_modes:
fz_key = "fz248" if is_fz248 else "fz294" fz_key = "fz248" if is_fz248 else "fz294"
fz_name = "ФЗ-248" if is_fz248 else "ФЗ-294" fz_name = "ФЗ-248" if is_fz248 else "ФЗ-294"
# Определяем начальную точку # Определяем начальную точку
if start_year and start_month:
year, month = start_year, start_month
logger.info(
"%s: starting from explicit period %d/%d",
fz_name,
year,
month,
)
else:
last_year, last_month = InspectionService.get_last_loaded_period( last_year, last_month = InspectionService.get_last_loaded_period(
is_federal_law_248=is_fz248 is_federal_law_248=is_fz248
) )
if last_year and last_month: if last_year and last_month:
# Начинаем со следующего месяца после последнего загруженного # Начинаем со следующего месяца после последнего загруженного
start_year, start_month = _get_next_month(last_year, last_month) year, month = _get_next_month(last_year, last_month)
logger.info( logger.info(
"%s: continuing from %d/%d (last loaded: %d/%d)", "%s: continuing from %d/%d (last loaded: %d/%d)",
fz_name, fz_name,
start_year, year,
start_month, month,
last_year, last_year,
last_month, last_month,
) )
else: else:
# Начинаем с дефолтной даты # Начинаем с дефолтной даты
start_year, start_month = DEFAULT_START_YEAR, DEFAULT_START_MONTH year, month = DEFAULT_START_YEAR, DEFAULT_START_MONTH
logger.info( logger.info(
"%s: no data in DB, starting from %d/%d", "%s: no data in DB, starting from %d/%d",
fz_name, fz_name,
start_year, year,
start_month, month,
) )
# Загружаем месяц за месяцем
year, month = start_year, start_month
empty_months_count = 0 empty_months_count = 0
processed_months = 0
while year < current_year or ( while year < current_year or (
year == current_year and month <= current_month year == current_year and month <= current_month
): ):
if (
max_months_per_law is not None
and processed_months >= max_months_per_law
):
logger.info(
"%s: stopping after %d processed months by request limit",
fz_name,
processed_months,
)
break
# Прекращаем если 2 месяца подряд нет данных # Прекращаем если 2 месяца подряд нет данных
if empty_months_count >= 2: if empty_months_count >= 2:
logger.info( logger.info(
@@ -1082,7 +1121,8 @@ def sync_inspections( # noqa: C901
) )
empty_months_count += 1 empty_months_count += 1
# Переходим к следующему месяцу processed_months += 1
# Переходим к следующему месяцу.
year, month = _get_next_month(year, month) year, month = _get_next_month(year, month)
# Обновляем лог # Обновляем лог

View File

@@ -1,6 +1,7 @@
"""Tests for parsers models.""" """Tests for parsers models."""
from apps.parsers.models import ( from apps.parsers.models import (
GenericParserRecord,
IndustrialCertificateRecord, IndustrialCertificateRecord,
IndustrialProductRecord, IndustrialProductRecord,
ManufacturerRecord, ManufacturerRecord,
@@ -97,6 +98,15 @@ class ParserLoadLogModelTest(TestCase):
self.assertIsNotNone(log.updated_at) self.assertIsNotNone(log.updated_at)
class GenericParserRecordModelTest(TestCase):
"""Tests for generic parser records."""
def test_record_date_allows_source_specific_long_values(self):
field = GenericParserRecord._meta.get_field("record_date")
self.assertEqual(field.max_length, 255)
class IndustrialCertificateRecordModelTest(TestCase): class IndustrialCertificateRecordModelTest(TestCase):
"""Tests for IndustrialCertificateRecord model.""" """Tests for IndustrialCertificateRecord model."""

View File

@@ -896,6 +896,34 @@ class ParseInspectionsTaskTestCase(TestCase):
self.assertEqual(result["status"], "success") self.assertEqual(result["status"], "success")
self.assertEqual(result["total_saved"], 0) self.assertEqual(result["total_saved"], 0)
def test_sync_inspections_honors_limited_params(self):
xml_content, rows = build_proverki_xml(count=1)
archive = build_zip([("inspections.xml", xml_content)])
with TestHTTPServer() as server:
server.add_bytes(
_portal_path(2026, 4),
archive,
content_type="application/zip",
)
result = sync_inspections(
proxies=[],
client_adapter=server.adapter,
use_playwright=False,
max_months_per_law=1,
start_year=2026,
start_month=4,
include_fz294=True,
include_fz248=False,
current_year=2026,
current_month=5,
)
self.assertEqual(result["status"], "success")
self.assertEqual(len(result["results"]["fz294"]), 1)
self.assertEqual(result["results"]["fz248"], [])
self.assertGreaterEqual(result["total_saved"], len(rows))
def test_sync_inspections_resumes_from_last_loaded(self): def test_sync_inspections_resumes_from_last_loaded(self):
last_year = 2024 last_year = 2024
last_month = 12 last_month = 12

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import io import io
import os import os
import tempfile import tempfile
from unittest.mock import Mock, patch
from apps.parsers.models import FinancialReport, FinancialReportLine, ProcurementRecord from apps.parsers.models import FinancialReport, FinancialReportLine, ProcurementRecord
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
@@ -341,3 +342,28 @@ class ParsersViewSetTest(APITestCase):
self.assertEqual(updated.status_code, status.HTTP_200_OK) self.assertEqual(updated.status_code, status.HTTP_200_OK)
self.assertEqual(updated.data["planned_inspections"], "weekly") self.assertEqual(updated.data["planned_inspections"], "weekly")
def test_run_sync_inspections_accepts_limited_sync_params(self):
self.client.force_authenticate(self.user)
url = reverse("api_v1:parsers:run-parser", args=["sync_inspections"])
payload = {
"max_months_per_law": 1,
"start_year": 2026,
"start_month": 4,
"include_fz294": True,
"include_fz248": False,
"current_year": 2026,
"current_month": 4,
}
with patch(
"apps.parsers.views.tasks.sync_inspections.apply_async",
return_value=Mock(id="task-123"),
) as apply_async_mock:
response = self.client.post(url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
task_kwargs = apply_async_mock.call_args.kwargs["kwargs"]
for key, value in payload.items():
self.assertEqual(task_kwargs[key], value)
self.assertEqual(task_kwargs["requested_by_id"], self.user.id)