no-timeout-csv
v0.1.6
Published
Parameterized SQL reports with background export to CSV, for reports that can take hours or days to build. Server in pure Python (no third-party dependencies except the DB connector), client in pure JS with no frameworks.
Downloads
1,013
Maintainers
Readme
NoTimeoutCSV
Простые CSV-отчёты — параметризованные SQL-отчёты с выгрузкой в CSV в фоне, без ограничений по объёму и без риска словить таймаут веб-сервера. Каждый запуск — отдельный процесс: генерация может идти часами или сутками, не блокируя сервер. История запусков не подчищается автоматически — всегда можно сравнить с тем, что было вчера.
Живое демо (со статичными примерами, без реальной БД): https://windowrepino.ru/notimeoutcsv/index.html Видео: https://youtu.be/6V1Tl244W3Q
Скриншоты
| Список отчётов | Параметры запуска |
|---|---|
|
|
|
| История выгрузок | Конструктор отчёта |
|---|---|
|
|
|
Быстрый старт
Для корректной работы приложения нужно создать и заполнить файл .env. Можно просто переименовать .env.example и прописать свои значения.
cd server
pip install -r requirements.txt
cp .env.example .env # впишите свои настройки БД
python server.pyОткройте http://127.0.0.1:8000 — клиент отдаётся тем же процессом, отдельно поднимать ничего не нужно.
Через npm
npm install no-timeout-csv
python node_modules/no-timeout-csv/server/server.py(Node здесь используется только как способ доставки файлов на диск — сам сервер на Python, node для его работы не нужен.)
При установке через npm стоит сразу задать CONFIGS_DIR/RESULTS_DIR в .env на путь снаружи node_modules.
Как держать конфиги вне node_modules
npm install кладёт и server/.env, и client/js/config.js внутрь node_modules, а она слетает при каждой переустановке. Держите конфиги отдельно — через два необязательных флага:
python node_modules/no-timeout-csv/server/server.py \
--server-config /путь/до/своего/server.env \
--client-config /путь/до/своего/config.jsОба флага необязательные и независимые друг от друга — можно передать один, оба или ни одного (тогда используются встроенные файлы).
Важно: переменная окружения ОС всегда побеждает значение из .env (так же по умолчанию ведёт себя, например, python-dotenv). Если настройка как будто игнорируется, хотя --server-config указан верно — проверьте, не осталась ли CONFIGS_DIR/RESULTS_DIR (или любой другой ключ) уже выставленной в текущей сессии терминала с более раннего теста — забытый set CONFIGS_DIR=... будет молча перебивать файл каждый раз.
Настройки — два отдельных файла
Сервер и клиент — не один процесс, а два независимых куска, которые не читают настройки друг друга. Поэтому и файлов с настройками два:
server/.env— всё, что нужно серверу: подключение к БД,MAX_WORKERS,PORT,HOST,CONFIGS_DIR/RESULTS_DIR,CSV_DELIMITER,HELPER_TIMEOUT_SECONDS, язык серверных сообщений. Полный и всегда актуальный список — вserver/.env.example.client/js/config.js— то, что нужно браузеру: язык интерфейса, интервалы поллинга истории. Это обычный статический JS-файл, отдаётся браузеру как есть, безо всякой обработки сервером.
Оба правятся вручную, простым текстовым редактором, без пересборки — так же, как и всё остальное в проекте.
Структура
server/— Python, стандартная библиотека (кроме коннектора к БД)server.py— HTTP API + оркестратор воркеров, с БД не работаетworker.py— отдельный процесс на каждый запуск отчёта, пишет CSVparam_options.py— короткоживущий процесс: варианты для списка/мультивыбора, подсказки мин/макс для датreport_columns.py— короткоживущий процесс: список колонок для конструктора отчётаconnectors/— плагины подключения к БД (образец:postgresql.py)configs/— конфиги отчётов (SQL-запрос, параметры, маппинг колонок)results/— сюда копятся готовые файлы, по одному на запуск
client/— чистый JS, без сборки и фреймворковindex.html— список отчётовreport.html— запуск отчёта и история его выгрузокsettings.html— создание/редактирование отчёта (SQL, колонки, параметры)
Язык интерфейса
По умолчанию — русский. Переключается в двух местах отдельно (клиент и сервер не читают настройки друг друга):
- Клиент —
LANGUAGE: "ru"/"en"вclient/js/config.js. - Сервер —
LANGUAGE=ru/enвserver/.env.
Как добавить/настроить отчёт
В server/configs/ уже есть готовый пример — sales_data. Он показывает на практике всё сразу: обычные параметры (region), даты с подсказками мин/макс (date_from/date_to), мультивыбор (products), и column_mapping для заголовков CSV. Удобно открыть его в "Настройках" просто чтобы посмотреть, как всё это собирается вместе, прежде чем писать свой. Учтите: сам пример ссылается на таблицу sales_data, которой в вашей базе, скорее всего, нет — это образец структуры конфига, не готовый к запуску демо-набор данных.
Проще всего — через интерфейс: "+ Новый отчёт" на главной странице, или "Настройки" на странице уже существующего отчёта. Форма позволяет задать название, SQL-запрос, колонки и параметры, ничего руками в файлах редактировать не нужно.
При желании конфиг можно и написать/поправить руками — это просто .json в server/configs/, никакой магии:
{
"id": "my_report",
"name": "Название",
"connector": "postgresql",
"sql": "SELECT ... WHERE col = %(param)s",
"columns_query": "SELECT * FROM my_table LIMIT 0",
"column_mapping": {"col": "Колонка"},
"params": [{"name": "param", "view_name": "Параметр", "type": "string"}]
}Удаление отчёта
Кнопка "Удалить" рядом с каждым отчётом в общем списке (index.html) — сначала спрашивает подтверждение (на случай случайного клика), а после него удаляет всё сразу и необратимо: и сам конфиг (configs/<id>.json), и всю папку с накопленной историей выгрузок (results/<id>/). Частичного удаления (например, только конфиг, но не файлы) нет.
Как параметры на самом деле попадают в запрос
%(имя)s — это плейсхолдер psycopg2 (именованный, безопасный от SQL-инъекций — значение передаётся драйверу отдельно от текста запроса, а не подставляется как строка). Имя внутри скобок должно точно совпадать с полем "Имя переменной" параметра в настройках — с учётом регистра.
Важно: параметр из списка "Параметры" — это только описание для формы (какое поле показать пользователю, какого типа, откуда брать варианты для списка). Сам он ни на что не влияет, пока вы не написали %(имя)s где-то в тексте SQL-запроса. Добавили параметр region в настройках, но в SQL нигде нет %(region)s — пользователь увидит поле "Регион" на экране, что-то там выберет, но запрос выполнится так, будто этого поля не существует: psycopg2 просто не находит в тексте запроса место для этого значения и молча его игнорирует, никакой ошибки не будет. Иными словами: список параметров описывает форму, а %(имя)s в SQL — это то, что реально что-то делает.
Параметр не заполнен ("Все")
Если пользователь оставил параметр как "Все" (не ввёл значение/ничего не выбрал), сервер передаёт в запрос настоящий SQL NULL. Стандартный способ сделать фильтр необязательным — писать условие в паре с проверкой на NULL, оборачивая весь блок в скобки:
WHERE (%(region)s IS NULL OR region = %(region)s)
AND (%(revenue)s IS NULL OR revenue >= %(revenue)s)Скобки вокруг каждого блока обязательны — AND в SQL связывает крепче, чем OR, и без скобок ... AND %(x)s IS NULL OR revenue >= %(x)s разберётся не так, как ожидается (второе условие "вывалится" из-под остальных фильтров).
Для мультивыбора (type: "multilist") параметр приходит списком — используйте = ANY(%(имя)s::text[]) (тип массива подберите под свою колонку, для не-текстовых — например ::int[]).
Типы параметров
string,number,date— обычное поле ввода.dateможет дополнительно иметьmin_query/max_query— SQL (одна колонка, одно значение), результат которого показывается как подсказка рядом с названием поля (например "Дата с (мин. 20.05.2022)"). Это только подсказка, не ограничение — выбрать более раннюю дату всё равно можно.multilist— мультивыбор с поиском, требуетlist_query— SQL, отдающий варианты (1-я колонка — значение, 2-я опционально — подпись).
Если операция в SQL требует явно знать тип NULL (например, %(date)s + interval '1 day' — сложение с интервалом неоднозначно для нетипизированного NULL) — добавьте явный каст: %(date)s::timestamp. Для простых сравнений (=, >=, <=) это обычно не нужно — тип и так однозначно выводится из типа колонки.
Колонки
columns_query — отдельный SQL (например, тот же основной запрос с LIMIT 0, или вызов хранимой процедуры) — выполняется в настройках по кнопке "Загрузить колонки", возвращает список имён без данных. Дальше каждой колонке назначается своё название для заголовка CSV — это и есть column_mapping. Оба поля опциональны: без них CSV просто использует сырые имена колонок из БД как есть.
Свой коннектор к другой БД
Можно создать свой коннектор к любой БД, а не только к PostgreSQL. Достаточно написать свой файл в server/connectors/, с NAME и функцией stream_query(query, params) -> (columns, rows) — см. postgresql.py как образец.
Лицензия
Бесплатно для личного, образовательного и некоммерческого использования — см. LICENSE.
Для коммерческого использования нужна отдельная лицензия — см. LICENSE.commercial или напишите на [email protected].
