9.2 KiB
Матрица файловых выгрузок источников
Пользовательский контракт
Frontend отправляет администраторский
POST /api/v2/organization-source-records/export-ticket/ с массивом sources
и выбранным format. В ответ он получает короткоживущий одноразовый ticket и
передаёт его обычной HTML-формой в
POST /api/v2/organization-source-records/export-download/. Поэтому браузер
сохраняет потоковый ZIP напрямую на диск, не удерживая весь архив как Blob в
JavaScript. Ticket передаётся в теле формы, не попадает в URL и после первого
запроса становится недействительным.
Имя скачиваемого архива формируется по выбранным источникам и времени создания
запроса: <source-stem>_YYYYMMDD_HHMMSS.zip, а для нескольких источников их
имена соединяются через __. Это же имя возвращается в file_name при выдаче
ticket и затем используется в Content-Disposition.
Совместимый администраторский
POST /api/v2/organization-source-records/export/ по-прежнему сразу возвращает
тот же ZIP API-клиентам. Крупный XLSX может состоять из нескольких файлов
*-part-001.xlsx, *-part-002.xlsx и далее.
При скачивании endpoint не читает таблицы записей источников и не строит CSV,
XLSX или JSON заново. Он упаковывает файлы последнего полностью опубликованного
ночного поколения и сразу потоково отправляет ZIP без временной копии всего
архива. Поэтому Content-Length у ответа отсутствует. Если ни одного поколения
ещё нет, API отвечает 503 с кодом source_export_not_ready.
Каждое поколение содержит только текущий календарный год в timezone сервиса.
Для записей с предметной датой год определяется по ней, для записей без такой
даты — по created_at, для финансовых отчётов — по году financial_lines.
Вложенные финансовые строки других лет исключаются. После смены года поколение
прошлого года не раздаётся: до первой успешной сборки нового года API отвечает
503 source_export_not_ready.
Матрица
| Группа API | Файл | CSV | XLSX | JSON |
|---|---|---|---|---|
financial_indicators |
financial-indicators |
— | — | да |
government_procurements |
public-procurements |
да | да | да |
industrial_production |
manufacturers-and-products |
да | да | да |
planned_inspections |
planned-inspections |
да | да | да |
bankruptcy |
bankruptcy-procedures |
да | да | да |
defense_suppliers |
defense-unreliable-suppliers |
да | да | да |
arbitration |
arbitration-cases |
да | да | да |
security_registries |
information-security-registries |
да | да | да |
vacancies |
labor-vacancies |
да | да | да |
Итого формируется 25 логических артефактов: один JSON для финансовых показателей и по три формата для остальных восьми групп. Физических файлов может быть больше из-за разбиения крупных XLSX. Если финансовые показатели выбраны вместе с другим форматом, в ZIP для них всё равно включается JSON.
Все форматы начинают строку организации с полей Наименование, ИНН, ОГРН,
КПП, ОКПО, после которых следуют поля исходной записи и развёрнутого
payload. Все записи текущего года включаются в публичные файлы, а техническое
наименование внешнего поставщика нейтрализуется. Исходные значения в БД
сохраняются для работы интеграции и дедупликации.
Ночная генерация
Celery Beat запускает
organizations.tasks.refresh_source_record_export_artifacts ежедневно в
05:30 Europe/Moscow, после ежедневного обновления organization sources в
04:30.
Генератор:
- читает каждую группу из БД один раз без глобальной сортировки миллионов строк;
выборка текущего года использует функциональный индекс по году строковой
record_date, а записи без предметной даты — индексcreated_at; - пишет compact JSON-массив на диск и использует его как готовый JSON без второй копии;
- потоково создаёт CSV и XLSX без накопления всех строк в памяти;
- разбивает XLSX по умолчанию по 100 000 строк на отдельные файлы, ограничивая временный XML и не превышая лимит Excel;
- записывает размеры файлов, номера частей и календарный
export_yearв manifest; - атомарно переключает
current.jsonтолько после готовности всей матрицы; - сохраняет текущее и предыдущее поколения по умолчанию.
При ошибке незавершённое поколение удаляется, а download endpoint продолжает отдавать предыдущую успешную версию.
Расчёт диска
Атомарная публикация требует одновременно хранить уже опубликованные поколения
и одно новое поколение в staging. Минимальный запас под артефакты рассчитывается
как (SOURCE_RECORD_EXPORT_GENERATIONS_TO_KEEP + 1) * размер поколения, плюс
рабочий запас файловой системы. Исторический снимок dev от 2026-08-03 до
ограничения по году занимал 16,55 ГБ (54 физических файла); актуальный размер
годового поколения нужно брать из total_size результата команды сборки.
Временная копия целого ZIP при скачивании не создаётся.
На локальном снимке от 2026-08-04 поколение за 2026 год содержит 122 582 записи
и занимает 809 768 139 байт (772,3 MiB) для всей матрицы из 25 файлов. Архив со
всеми источниками оценивается в 410,1 MiB для JSON, 289,9 MiB для CSV и
72,3 MiB для XLSX; ZIP использует ZIP_STORED, поэтому к сумме файлов
добавляется только небольшой служебный overhead.
Хранение и первый запуск
Каталог задаётся через SOURCE_RECORD_EXPORT_DIRECTORY, по умолчанию —
media/source-record-exports. Он должен быть общим read-write volume для web и
Celery worker. В Docker Compose используется ./media:/app/media.
После первого развёртывания готовое поколение можно создать сразу, не ожидая ночного расписания:
PYTHONPATH=src uv run python src/manage.py build_source_record_exports
Доступные настройки:
| Настройка | Значение по умолчанию | Назначение |
|---|---|---|
SOURCE_RECORD_EXPORT_DIRECTORY |
media/source-record-exports |
Общий каталог артефактов |
SOURCE_RECORD_EXPORT_GENERATIONS_TO_KEEP |
2 |
Число сохраняемых успешных поколений |
SOURCE_RECORD_EXPORT_LOCK_TTL_SECONDS |
21600 |
TTL распределённой блокировки Celery |
SOURCE_RECORD_EXPORT_XLSX_ROWS_PER_FILE |
100000 |
Максимум строк данных в одной XLSX-части |
SOURCE_RECORD_EXPORT_DOWNLOAD_TICKET_TTL_SECONDS |
300 |
Срок действия одноразового browser-download ticket |