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
}
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"
sleep 45
wait_for_migrations
call_webhook "dev worker" "${DOKPLOY_DEV_WORKER_WEBHOOK_URL}" "worker"
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(
_("дата записи"),
max_length=30,
max_length=255,
blank=True,
db_index=True,
help_text=_("Дата записи в формате источника"),

View File

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

View File

@@ -1,6 +1,7 @@
"""Tests for parsers models."""
from apps.parsers.models import (
GenericParserRecord,
IndustrialCertificateRecord,
IndustrialProductRecord,
ManufacturerRecord,
@@ -97,6 +98,15 @@ class ParserLoadLogModelTest(TestCase):
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):
"""Tests for IndustrialCertificateRecord model."""

View File

@@ -896,6 +896,34 @@ class ParseInspectionsTaskTestCase(TestCase):
self.assertEqual(result["status"], "success")
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):
last_year = 2024
last_month = 12

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import io
import os
import tempfile
from unittest.mock import Mock, patch
from apps.parsers.models import FinancialReport, FinancialReportLine, ProcurementRecord
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.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)