MVP сайта
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
> Архив первоначального проекта. С 2026-10-01 Rio удалён: приложение использует FastAPI, Jinja2 и HTML/CSS/JavaScript. Актуальное описание — в README.md.
|
||||
|
||||
# Спецификация учебного сайта для парсинга данных
|
||||
|
||||
Документ описывает первую версию сайта для занятий по основам ML, в том числе для психологов. Преподаватель загружает данные неизвестной заранее структуры и публикует их в разных представлениях. Студенты собирают опубликованные записи обычными HTTP-парсерами или браузерной автоматизацией. Реализация начнётся после согласования спецификации и плана.
|
||||
|
||||
## Согласованные требования
|
||||
|
||||
- Один преподаватель, закрытый кабинет на Python Rio, вход по логину и паролю без регистрации.
|
||||
- Публичные учебные страницы без аккаунтов студентов.
|
||||
- Импорт CSV, JSON и JSONL через кабинет.
|
||||
- Таблицы, карточки, сообщения и заполненные анкеты; сбор новых ответов не входит в первую версию.
|
||||
- Два режима доступа к одному набору: HTML для requests, BeautifulSoup и Scrapy; динамический Rio для Playwright и Selenium.
|
||||
- Предпросмотр, черновики, публикация и снятие с публикации.
|
||||
- Docker Compose, постоянное хранение данных и возможность разместить приложение на выбранном сервере.
|
||||
- Современное адаптивное оформление и сохранение исходных пропусков и неоднородных значений.
|
||||
|
||||
Описанные ниже конкретные правила, пределы и адреса являются предлагаемыми решениями для первой версии.
|
||||
|
||||
## Архитектура
|
||||
|
||||
Один Python-процесс и контейнер обслуживают Rio и HTML-маршруты через FastAPI. Кабинет расположен под `/admin`, динамические страницы под `/interactive`, серверные HTML-страницы под `/html`. Конкретную интеграцию проверяем на закреплённой версии rio-ui до разработки остальных экранов.
|
||||
|
||||
Импорт, хранение, настройка полей и выборка записей образуют общее ядро. Оба механизма отображения получают одинаковые записи и конфигурацию; логика фильтрации и пагинации не дублируется. HTML генерируется сервером из шаблонов с экранированием текста. Полезное содержимое присутствует в исходном HTTP-ответе без запуска JavaScript.
|
||||
|
||||
SQLite хранит наборы, записи, настройки представлений, черновики и публикации. Исходные файлы находятся отдельно в постоянном каталоге. Данные пользователя не размещаются в публичном каталоге статических файлов. Схема базы имеет версию и последовательные миграции.
|
||||
|
||||
Модули разделяются по ответственности: импорт; модель данных и хранилище; публикации; авторизация; кабинет Rio; динамические представления Rio; HTML-маршруты и шаблоны. Запуск приложения не требует pandas для каждого запроса.
|
||||
|
||||
## Путь преподавателя и экраны
|
||||
|
||||
1. **Вход.** Логин и пароль, понятная ошибка без раскрытия существования пользователя, выход из кабинета.
|
||||
2. **Каталог.** Название набора, число записей, статус, время изменения и действия «Редактировать», «Предпросмотр», «Опубликовать», «Снять с публикации».
|
||||
3. **Импорт.** Выбор файла, параметры чтения, список ошибок и первые 20 записей. После подтверждения создаётся черновик; исходный файл сохраняется.
|
||||
4. **Настройка.** Название, описание, адрес набора, представления, поля, подписи и количество записей на странице.
|
||||
5. **Предпросмотр.** Проверка обоих режимов с реальными данными черновика. Предпросмотр доступен только преподавателю.
|
||||
6. **Публикация.** Итоговая проверка, создание версии и ссылки на HTML и Rio для каждого включённого представления.
|
||||
|
||||
Изменения сохраняются явной кнопкой. При уходе с несохранёнными изменениями показывается предупреждение. Замена файла создаёт новую редакцию черновика и требует повторной проверки соответствия полей.
|
||||
|
||||
## Правила импорта
|
||||
|
||||
Начальные настраиваемые ограничения: файл до 25 МиБ, до 50 000 записей, глубина JSON до 20 уровней. Превышение вызывает понятную ошибку до публикации. Это границы приёма, а не обещание конкретной скорости работы; производительность проверяется отдельно на целевом оборудовании.
|
||||
|
||||
### CSV
|
||||
|
||||
Поддерживаются UTF-8 с BOM и без него, а также выбираемая Windows-1251. Преподаватель может указать запятую, точку с запятой или табуляцию; автоматическое предположение видно в предпросмотре. Первая строка содержит заголовки. Пустые и повторяющиеся заголовки блокируют импорт с указанием колонок. Поддерживаются кавычки, разделители и переводы строк внутри значений.
|
||||
|
||||
Значения сохраняются строками, включая ведущие нули, даты и десятичные запятые. Пустая ячейка остаётся пустой строкой. Ошибка количества полей или синтаксиса сообщает позицию; повреждённые строки не пропускаются молча.
|
||||
|
||||
### JSON
|
||||
|
||||
Вход — массив объектов, одиночный объект как одна запись либо объект, внутри которого преподаватель выбирает массив объектов. Выбор вложенного массива не выполняется скрыто. Массив скалярных значений и смесь объектов со скалярами отклоняются с объяснением поддерживаемой структуры. Повторяющиеся ключи объектов и нестандартные NaN/Infinity отклоняются, чтобы исключить потерю значений.
|
||||
|
||||
Объекты и массивы сохраняются вложенными. Поля выбираются деревом структуры; внутри конфигурации используются однозначные пути JSON Pointer. Отсутствующее поле, null, пустая строка, false и 0 остаются различимыми.
|
||||
|
||||
### JSONL
|
||||
|
||||
Каждая непустая строка содержит один JSON-объект. Пустые строки игнорируются с отображением их количества. Некорректная строка блокирует сохранение импорта; сообщение содержит номер строки и причину. Используется UTF-8 с допустимым BOM в начале файла.
|
||||
|
||||
### Общие правила
|
||||
|
||||
Порядок записей сохраняется. Дубликаты и пропуски автоматически не удаляются. Каждая запись получает внутренний идентификатор, который не зависит от наличия уникального поля в файле и стабилен внутри опубликованной версии. Поля определяются по всему принятому набору, а не только по предпросмотру. Если необязательного поля нет в части записей, это отражается при настройке.
|
||||
|
||||
Некорректный импорт не заменяет существующие данные. Файлы хранятся под внутренними именами; пользовательское имя используется только для отображения. Загруженное содержимое считается данными и не исполняется.
|
||||
|
||||
## Представления
|
||||
|
||||
| Вид | Настройка | Отображение |
|
||||
|---|---|---|
|
||||
| Таблица | Произвольные выбранные поля, подписи и порядок колонок | Заголовки и строки, переходы на страницу записи |
|
||||
| Карточки | Заголовок, необязательное описание и дополнительные поля | Сетка карточек с переходом к деталям |
|
||||
| Сообщения | Текст, необязательные автор и дата, дополнительные поля | Линейная лента сообщений без веток обсуждений |
|
||||
| Анкеты | Идентификатор или заголовок участника, выбранные ответы | Заполненный бланк с вопросами и значениями |
|
||||
|
||||
Для анкет поддерживаются два явных способа настройки: колонки как ответы с подписями вопросов либо вложенный массив пар «вопрос — ответ», для которого задаются пути к обоим значениям. Нет расчёта шкал, интерпретации результатов или конструктора опросов.
|
||||
|
||||
Один набор может включать несколько представлений с независимыми настройками. Отсутствие необходимого поля, например текста сообщения, блокирует публикацию этого представления. Необязательные поля можно не назначать. Объекты и массивы в обычной ячейке выводятся как экранированный JSON; специальный способ вывода вложенных анкет применяется только при явной настройке.
|
||||
|
||||
Для отсутствующего значения и null используются разные видимые обозначения; пустая строка остаётся пустой. Их семантика также доступна через атрибут состояния значения в HTML. Исходный тип не определяется по внешнему виду текста.
|
||||
|
||||
## Учебные режимы
|
||||
|
||||
Пример адреса HTML: `/html/study-a/v/1/table?page=2`. Пример Rio: `/interactive/study-a/v/1/table`. У каждого представления есть собственный сегмент адреса. В ссылках содержится версия публикации.
|
||||
|
||||
### Обычный HTML
|
||||
|
||||
Таблицы используют table, thead, tbody, th и td. Остальные представления используют обычные семантические блоки, заголовки и ссылки. Записи и значения имеют документированные устойчивые CSS-классы и идентификаторы. Ни данные, ни переходы не требуют JavaScript, WebSocket или особого User-Agent.
|
||||
|
||||
Предлагаемые размеры страниц: 10, 25, 50, 100; по умолчанию 25. Порядок соответствует импорту. Страницы доступны через реальные ссылки. Нецелый, отрицательный или нулевой номер возвращает 400; номер за пределами набора — 404. Публикация пустого набора запрещена. Индивидуальные страницы записей дают упражнение на переходы по ссылкам.
|
||||
|
||||
### Динамический Rio
|
||||
|
||||
Первая порция содержит настроенное число записей. Кнопка «Загрузить ещё» добавляет следующую порцию, показывает состояние загрузки и исчезает после последней записи. Повторные нажатия во время запроса не создают дубликатов.
|
||||
|
||||
Преподаватель выбирает одно текстовое поле для поиска и, при необходимости, одно поле категориального фильтра. Изменение фильтра сбрасывает список и позицию загрузки. Доступны состояния ожидания, отсутствия результатов и ошибки. Динамические упражнения не включают CAPTCHA, искусственные задержки или противодействие парсерам.
|
||||
|
||||
В HTML те же фильтры передаются query-параметрами и применяются сервером. Для обоих режимов поиск — регистронезависимое вхождение в выбранное текстовое поле, категориальный фильтр — точное совпадение значения. Результат всегда сохраняет исходный порядок.
|
||||
|
||||
Публичная выдача исходного файла и отдельный JSON API не включаются в первую версию. Это учебная организация доступа, а не гарантия, что видимые данные невозможно получить другим способом. Сервер отдаёт только поля, включённые в соответствующее представление; скрытые поля не передаются клиенту внутри полного объекта записи.
|
||||
|
||||
## Публикации и сохранность
|
||||
|
||||
Публикация атомарно создаёт неизменяемый снимок записей и настроек всех включённых представлений. Неудачная операция оставляет предыдущую публикацию доступной. Изменения черновика не затрагивают опубликованные версии.
|
||||
|
||||
Повторная публикация создаёт новый номер и новые ссылки. Старые ссылки продолжают показывать прежний материал до снятия соответствующей версии с публикации. Это предотвращает изменение данных между запросами студенческого парсера. Снятая версия возвращает 404 в обоих режимах; черновик остаётся у преподавателя. В первой версии достаточно снятия с публикации, без интерфейса безвозвратного удаления истории.
|
||||
|
||||
## Внешний вид
|
||||
|
||||
Интерфейс на русском языке: светлый фон, тёмный читаемый текст, спокойный сине-зелёный акцент, единые размеры отступов и контрастные состояния элементов. Кабинет использует боковую навигацию на широком экране и компактную на узком. Основные действия визуально выделены, технические параметры появляются только там, где нужны для выбора.
|
||||
|
||||
Таблицы прокручиваются горизонтально, длинные сообщения переносятся, анкеты сохраняют связь вопроса и ответа. Карточки перестраиваются в одну колонку на телефоне. HTML и Rio имеют общий визуальный стиль, но не требуют идентичной внутренней разметки. Шрифты и необходимые статические ресурсы поставляются с приложением, чтобы интерфейс не зависел от внешнего CDN.
|
||||
|
||||
## Авторизация и Docker
|
||||
|
||||
Учётные данные задаются при развёртывании: имя пользователя и хеш пароля через переменные окружения или файл секрета. Открытая регистрация и стандартный встроенный пароль отсутствуют. Инструкция содержит способ создания хеша и смены пароля. Вход ограничивается по частоте; сессия защищает все операции кабинета, предпросмотр и доступ к исходным файлам. Изменяющие операции проверяют подлинность сессии и защищены от CSRF. Пароли и данные анкет не попадают в журналы запросов.
|
||||
|
||||
Контейнер запускается непривилегированным пользователем, слушает порт 8080 и использует `/data` как постоянный том. SQLite и исходные файлы находятся в этом томе. Порт хоста настраивается через Compose; доступны проверка работоспособности и корректное завершение процесса. При отсутствии настроек входа приложение сообщает ошибку запуска, а не открывает кабинет без защиты.
|
||||
|
||||
HTTPS завершается на внешнем reverse proxy, который передаёт HTTP и WebSocket. Поддерживаемый вариант первой версии — приложение в корне отдельного домена; размещение под произвольным префиксом URL не обещается. При HTTPS cookie сессии получает Secure; для локального HTTP есть явная настройка. Работа нескольких реплик с общей SQLite в первую версию не входит.
|
||||
|
||||
В комплект входят Dockerfile, compose.yaml, пример переменных окружения без секретов и инструкция запуска, обновления и резервного копирования. Простой согласованный способ резервного копирования — остановить контейнер и скопировать весь том; восстановление проверяется отдельно. Данные не встраиваются в образ как изменяемое хранилище.
|
||||
|
||||
## Демонстрационные данные
|
||||
|
||||
Старый хоккейный CSV сохраняется в качестве примера таблицы. Дополнительные небольшие синтетические наборы демонстрируют сообщения, карточки и оба формата заполненных анкет. Примеры включают Unicode, пустые значения, длинный текст и повторяющиеся значения. Демонстрационные наборы импортируются явно и не перезаписывают рабочие данные при перезапуске.
|
||||
|
||||
Старый NiceGUI-проект остаётся источником примера, а не основой нового интерфейса. Совместимость старого адреса `?pageNum=` не входит в первую версию.
|
||||
|
||||
## Проверка и критерии готовности
|
||||
|
||||
1. Преподаватель импортирует каждый поддерживаемый формат без редактирования кода; ошибки показывают причину и расположение.
|
||||
2. Назначение полей работает с произвольными заголовками и вложенными значениями. Несовместимые настройки нельзя опубликовать.
|
||||
3. Для одного набора включаются несколько представлений, и каждое доступно в обоих режимах.
|
||||
4. Парсер requests и BeautifulSoup собирает все видимые записи по ссылкам и страницам без пропусков и дублей. Проверяются значения, а не только количество.
|
||||
5. Парсер Playwright получает те же записи, используя динамическую загрузку и фильтры. При отсутствии фильтров порядок совпадает с HTML.
|
||||
6. Редактирование и повторная публикация не изменяют старые опубликованные ссылки. Черновики, исходные файлы и скрытые поля недоступны студентам.
|
||||
7. Замена контейнера сохраняет данные; резервная копия восстанавливается в чистом окружении.
|
||||
8. Через браузер проверяются все четыре представления, узкий экран, длинные тексты, пустые значения, вход и ошибки загрузки.
|
||||
9. Контейнер собирается из документированной команды; проверяются старт, HTTP, WebSocket, том и завершение.
|
||||
|
||||
Примеры парсеров передаются преподавателю отдельными файлами, без автоматической публикации решений на учебном сайте. Версии Python и библиотек закрепляются после проверки совместимости Rio, FastAPI и контейнера. До этого документация текущего Rio подтверждает направление интеграции, но не заменяет проверку установленной версии.
|
||||
|
||||
## Границы первой версии
|
||||
|
||||
Не входят: несколько преподавателей, аккаунты студентов, сбор ответов, оценивание работ, обучение моделей, произвольный редактор HTML, ветки комментариев, импорт по внешним URL, публичный API выгрузки, горизонтальное масштабирование и автоматическое размещение на конкретном сервере. Объём первой версии заканчивается работающим проверенным Docker-пакетом и инструкциями.
|
||||
|
||||
## Последовательность дальнейшей работы
|
||||
|
||||
После проверки этой спецификации готовится отдельный план реализации с файлами, зависимостями, этапами и проверками. Порядок: совместимость Rio и HTML в одном приложении; хранение и вход; импорт; настройка представлений; публикации и оба режима; оформление; парсеры и проверка Docker. Продуктовый код в рамках подготовки спецификации не изменяется.
|
||||
|
||||
## Основания технических решений
|
||||
|
||||
Изученный старый проект расположен в `P:\Coding\Python\NetologyWebParserSite\HockeyTeamParse`: NiceGUI, pandas, один CSV, страницы по 25 строк, Docker Compose. Окружение с установленным Rio в изученном проекте не найдено.
|
||||
|
||||
Официальные источники Rio, проверенные при обсуждении: [App и as_fastapi](https://github.com/rio-labs/rio/blob/main/rio/app.py), [сервер и динамическая загрузка интерфейса](https://github.com/rio-labs/rio/blob/main/rio/app_server/fastapi_server.py). Архитектура этого документа является проектным предложением, а не утверждением о готовности перечисленных функций в старом приложении.
|
||||
@@ -0,0 +1,210 @@
|
||||
> Архив первоначального проекта. С 2026-10-01 Rio удалён: приложение использует FastAPI, Jinja2 и HTML/CSS/JavaScript. Актуальное описание — в README.md.
|
||||
|
||||
# План реализации учебного сайта для парсинга
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans for implementation in this session, or superpowers:subagent-driven-development if the user chooses delegation. Steps use checkbox syntax for tracking.
|
||||
|
||||
**Goal:** Создать универсальный учебный сайт с кабинетом преподавателя на Rio, импортом данных, HTML и динамическими упражнениями, готовый к запуску через Docker Compose.
|
||||
|
||||
**Architecture:** Один Python-сервис объединяет Rio и FastAPI с Jinja2. Импорт, выборка, публикации и SQLite общие для обоих режимов; публичные запросы читают неизменяемые версии.
|
||||
|
||||
**Tech Stack:** Python, rio-ui, FastAPI, Jinja2, SQLite через sqlite3, Argon2 для пароля, pytest, requests, BeautifulSoup, Playwright, Docker Compose. Точные версии закрепляются после проверки совместимости в задаче 1.
|
||||
|
||||
**Spec:** [Утверждённая спецификация](design-spec.md).
|
||||
|
||||
## Рабочее место и ограничения
|
||||
|
||||
Новый проект создаётся в `outputs/parser-lab/` относительно текущего рабочего каталога. Все пути ниже относятся к этому корню. Старый проект `P:\Coding\Python\NetologyWebParserSite\HockeyTeamParse` служит источником хоккейного примера и не перезаписывается. Документы копируются в `docs/` нового проекта. Временные результаты проверок находятся в `work/` текущего рабочего каталога. При наличии Git этапы фиксируются отдельными коммитами; отсутствие Git не блокирует сборку и передачу проекта.
|
||||
|
||||
- Один аккаунт преподавателя; без регистрации и аккаунтов студентов.
|
||||
- Четыре представления; оба режима для каждого включённого представления.
|
||||
- Файл до 25 МиБ, до 50 000 записей, глубина JSON до 20 уровней; пределы настраиваемы.
|
||||
- Размеры страниц 10, 25, 50, 100; исходное значение 25. Предпросмотр первых 20 записей.
|
||||
- Unicode, пропуски, дубликаты, порядок и исходные типы сохраняются; CSV остаётся строковым.
|
||||
- Русский интерфейс, светлая тема, адаптивность, локальные ресурсы.
|
||||
- Неизменяемые публикации, отсутствие доступа к черновикам и скрытым полям извне.
|
||||
- Один контейнер, порт 8080, постоянный каталог `/data`, HTTP и WebSocket за reverse proxy.
|
||||
- Нет автоматического деплоя на внешний сервер, обучения моделей или публичного API выгрузки.
|
||||
|
||||
## Особое внимание при проверке
|
||||
|
||||
1. Заголовки JSON с точками, слешами и тильдами не должны путаться с вложенными путями — задачи 2 и 4.
|
||||
2. Значения null, false, 0 и пустая строка не должны схлопываться в отсутствие поля — задачи 2 и 5.
|
||||
3. Замена файла и авария публикации не должны повреждать доступный студентам снимок — задачи 3 и 7.
|
||||
4. Прямой запрос страницы деталей или callback Rio не должен обходить авторизацию либо раскрывать скрытое поле — задачи 6, 8 и 10.
|
||||
5. Две вкладки преподавателя не должны незаметно перезаписывать чужие изменения — задача 3; сохранять черновик с ожидаемой ревизией и показывать конфликт.
|
||||
|
||||
## Карта файлов
|
||||
|
||||
| Файлы | Ответственность |
|
||||
|---|---|
|
||||
| `pyproject.toml`, `requirements.lock`, `requirements-dev.lock` | Зависимости приложения и проверок |
|
||||
| `src/parser_lab/app.py`, `config.py`, `__main__.py` | Сборка сервиса, конфигурация, запуск |
|
||||
| `src/parser_lab/domain.py`, `paths.py` | Общие типы и JSON Pointer |
|
||||
| `src/parser_lab/importing/csv_reader.py`, `json_reader.py`, `service.py` | Разбор файлов и диагностика |
|
||||
| `src/parser_lab/storage/database.py`, `repository.py`, `migrations/001.sql` | База, миграции, транзакции |
|
||||
| `src/parser_lab/views.py`, `query.py`, `publishing.py` | Конфигурация представлений, выборка, публикация |
|
||||
| `src/parser_lab/auth.py`, `cli.py` | Сессии, вход, создание хеша пароля |
|
||||
| `src/parser_lab/web/routes.py`, `templates/`, `static/` | HTML, стили и ресурсы |
|
||||
| `src/parser_lab/rio_ui/admin/`, `rio_ui/public/`, `rio_ui/theme.py` | Кабинет, динамические страницы, тема |
|
||||
| `tests/unit/`, `tests/integration/`, `tests/browser/` | Проверки поведения |
|
||||
| `examples/data/`, `examples/parsers/` | Данные и решения для преподавателя |
|
||||
| `Dockerfile`, `compose.yaml`, `.env.example`, `.dockerignore`, `README.md` | Развёртывание и использование |
|
||||
|
||||
Каждая задача включает запуск своей проверки до реализации, подтверждение ожидаемого сбоя, реализацию, повторную проверку и фиксацию результата. Ошибки настройки тестового окружения не считать ожидаемым провалом поведения.
|
||||
|
||||
## Задача 1 Совместимый каркас сервиса
|
||||
|
||||
**Files:** создать `pyproject.toml`, оба lock-файла, `src/parser_lab/{__init__,app,config,__main__}.py`, `tests/integration/test_app.py`.
|
||||
|
||||
**Interfaces:** `Settings.from_env() -> Settings`; `create_app(settings: Settings) -> FastAPI`. Settings содержит каталог данных, лимиты импорта, имя администратора, хеш пароля, секрет сессий, secure-cookie и доверенные origin. В тестах настройки передаются явно.
|
||||
|
||||
- [ ] Проверить доступные Python и Docker, прочитать API установленной для проекта Rio, выбрать поддерживаемую Python и закрепить совместимые версии. Не обновлять глобальное окружение пользователя.
|
||||
- [ ] Добавить тест `test_html_route_coexists_with_rio`: `client.get('/healthz').status_code == 200`, HTML-проба содержит `'<table'`, запрос динамической страницы возвращает Rio-оболочку. Проверить lifespan при старте и завершении.
|
||||
- [ ] Запустить `python -m pytest tests/integration/test_app.py -q`, подтвердить отсутствие реализации.
|
||||
- [ ] Реализовать app factory, конфигурацию и совмещение пользовательских маршрутов с `rio.App.as_fastapi()` по подтверждённому API. Проба находится в тестовом приложении, а не в рабочей выдаче.
|
||||
- [ ] Повторить тест и открыть динамическую пробу в браузере: компонент отображается и кнопка обновляет значение через WebSocket. Зафиксировать версии, команду запуска и результат.
|
||||
|
||||
Результат: работающий общий сервер; архитектурные ограничения Rio выявлены до создания кабинета.
|
||||
|
||||
## Задача 2 Импорт без потери исходных значений
|
||||
|
||||
**Files:** создать `domain.py`, `paths.py`, `importing/{__init__,csv_reader,json_reader,service}.py`, `tests/unit/test_importing.py`, `tests/unit/test_paths.py`.
|
||||
|
||||
**Interfaces:** `JsonValue` — стандартное JSON-значение; `Record = dict[str, JsonValue]`; `ImportOptions(format, encoding, delimiter, record_pointer)`; `ImportResult(records, fields, warnings)`; `ImportIssue(message, line, column)`; `read_upload(data: bytes, filename: str, options: ImportOptions, settings: Settings) -> ImportResult`; `resolve_pointer(record: Record, pointer: str) -> ResolvedValue(present, value)`.
|
||||
|
||||
- [ ] Добавить проверки: CSV `'id;answer\n001;\n'` даёт `{'id': '001', 'answer': ''}`; JSON сохраняет `False`, `0`, `None`; `resolve_pointer({'a/b': {'~c': 0}}, '/a~1b/~0c').value == 0`; отсутствующий путь имеет `present == False`.
|
||||
- [ ] Добавить параметризованные случаи: UTF-8 BOM, Windows-1251, многострочные CSV-ячейки, плохая длина строки, повторные заголовки и JSON-ключи, JSONL с ошибкой на строке 3, пустые строки JSONL, одиночный объект, вложенный массив, NaN/Infinity, глубина 21, лимиты размера и количества, массив скаляров.
|
||||
- [ ] Выполнить `python -m pytest tests/unit/test_importing.py tests/unit/test_paths.py -q`, проверить ожидаемые провалы.
|
||||
- [ ] Реализовать чтение с `csv` и `json`, строгой обработкой повторяющихся ключей, ограничением размера до разбора и контролем глубины/числа записей. Не угадывать формат по пользовательскому имени без проверки содержимого. JSONL читает построчно; результаты не сохраняются частично.
|
||||
- [ ] Повторить тесты. Проверить объединение полей по всем записям, явный выбор вложенного массива и различение пустого набора от ошибки.
|
||||
|
||||
## Задача 3 Хранение черновиков и исходных файлов
|
||||
|
||||
**Files:** создать `storage/{__init__,database,repository}.py`, `storage/migrations/001.sql`, `tests/integration/test_repository.py`.
|
||||
|
||||
**Interfaces:** `Repository.create_draft(title: str, source: bytes, imported: ImportResult) -> Draft`; `get_draft(dataset_id: str) -> Draft`; `save_draft(draft: Draft, expected_revision: int) -> Draft`; `replace_import(dataset_id: str, source: bytes, imported: ImportResult, expected_revision: int) -> Draft`. `Draft` содержит id, slug, revision, записи, источник, представления, название и описание. Ошибки: `NotFound`, `RevisionConflict`.
|
||||
|
||||
- [ ] Добавить тесты сохранения и повторного открытия SQLite: `reopened.records == imported.records`; повторное сохранение со старой revision вызывает `RevisionConflict`; авария транзакции не меняет старую запись. Проверить, что исходное имя `'../../outside.json'` не влияет на фактический путь хранения.
|
||||
- [ ] Запустить `python -m pytest tests/integration/test_repository.py -q` до реализации.
|
||||
- [ ] Реализовать таблицы datasets, draft_records, sources, views, publications, published_records, sessions, schema_version. Записи хранятся JSON вместе с ordinal и внутренним id. Все SQL параметризованы; опубликованный снимок не зависит от изменяемого черновика.
|
||||
- [ ] Реализовать файлы под UUID, временную запись с последующим переносом в пределах `/data`, транзакционную привязку к базе и очистку несвязанных временных файлов. Замена импорта сбрасывает подтверждение настройки полей.
|
||||
- [ ] Повторить тесты и проверить идемпотентный запуск миграции и откат при ошибке записи файла.
|
||||
|
||||
## Задача 4 Настройки представлений
|
||||
|
||||
**Files:** создать `views.py`, `tests/unit/test_views.py`; дополнить общие модели в `domain.py`.
|
||||
|
||||
**Interfaces:** `ViewConfig(kind, columns, roles, questionnaire_mode, questions_pointer, page_size, search_pointer, category_pointer)`; `FieldConfig(pointer, label)`; `validate_view(config: ViewConfig, records: list[Record]) -> list[ImportIssue]`; `project_record(record: Record, config: ViewConfig) -> DisplayRecord`. `DisplayRecord` содержит только назначенные значения и сведения об их наличии/типе.
|
||||
|
||||
- [ ] Добавить проверки для всех четырёх видов: произвольные подписи колонок; текст сообщения обязателен в конфигурации, автор — нет; анкета из колонок и массива question/answer; отсутствующее значение в отдельной записи допустимо и помечается. Путь, которого нет ни в одной записи, не допускается как обязательная роль.
|
||||
- [ ] Проверить страницу 25 по умолчанию и отклонение 24; выбор только текстового поля для поиска; категориальное поле должно быть скалярным. Проверить, что `secret` отсутствует во всём сериализованном DisplayRecord, когда оно не выбрано.
|
||||
- [ ] Запустить `python -m pytest tests/unit/test_views.py -q` до реализации.
|
||||
- [ ] Реализовать явные конфигурации без эвристического назначения смысла полям. JSON Pointer отвечает за чтение и экранирование имён; структура анкеты не смешивается с универсальным табличным рендером.
|
||||
- [ ] Повторить тесты, включая слеши и тильды в именах полей, смешанные записи и многострочные ответы.
|
||||
|
||||
## Задача 5 Общая выборка и HTML
|
||||
|
||||
**Files:** создать `query.py`, `web/{__init__,routes}.py`, `web/templates/{base,list,detail,table,cards,messages,questionnaire}.html`, `web/static/site.css`, `tests/unit/test_query.py`, `tests/integration/test_html.py`.
|
||||
|
||||
**Interfaces:** `Query(page=1, search='', category=None)`; `select_records(records: list[StoredRecord], config: ViewConfig, query: Query) -> PageResult`; `PageResult(items, total, page, page_size, has_next)`. `StoredRecord(id, ordinal, data)` используется обоими режимами. `register_html_routes(app: FastAPI, repository: Repository) -> None` после задачи 7 читает Publication; до этого HTML тестируется на явной фикстуре снимка.
|
||||
|
||||
- [ ] Добавить тесты выборки: 51 запись и размер 25 дают страницы 25/25/1, ids не пересекаются; поиск регистронезависимый, фильтр точный и сохраняет различие числа 0 и строки `'0'`; порядок стабилен.
|
||||
- [ ] Добавить тест HTML: `BeautifulSoup(response.text, 'html.parser').select('tbody tr')` содержит нужные значения без JavaScript. Вредоносное значение `<script>alert(1)</script>` остаётся текстом, исполняемый тег не появляется. Проверить отсутствие скрытых полей на списке и деталях.
|
||||
- [ ] Запустить `python -m pytest tests/unit/test_query.py tests/integration/test_html.py -q` до реализации.
|
||||
- [ ] Реализовать общий отбор, Jinja2 с autoescape, реальные ссылки пагинации и деталей. CSS-контракт: `.record`, `.field`, `data-record-id`, `data-field`, `data-value-state`; состояния `missing`, `null`, `value`. Ссылки сохраняют фильтры и версию, значения URL кодируются.
|
||||
- [ ] Определить детали по `/html/{slug}/v/{version}/{view}/records/{id}`. Некорректная страница — 400, отсутствующая — 404. Для фильтра без совпадений страница 1 возвращает 200 с пустым состоянием; следующие — 404.
|
||||
- [ ] Повторить тесты и проверить ручным HTTP-запросом, что содержимое и ссылки входят в исходный ответ.
|
||||
|
||||
## Задача 6 Авторизация преподавателя
|
||||
|
||||
**Files:** создать `auth.py`, `cli.py`, `tests/integration/test_auth.py`; дополнить `app.py`, `config.py`.
|
||||
|
||||
**Interfaces:** `AuthService.login(username: str, password: str, client_key: str) -> SessionToken`; `require_admin(token: str) -> AdminSession`; `logout(token: str) -> None`; CLI `python -m parser_lab.cli hash-password` читает пароль скрытым вводом.
|
||||
|
||||
- [ ] Написать тесты: неверный пароль не создаёт сессию; приватный маршрут без сессии отклонён; выход отзывает сессию; изменяющий POST без CSRF отклонён; внешний Origin при WebSocket/callback отклонён; cookie HttpOnly/SameSite и Secure соответствуют настройке.
|
||||
- [ ] Зафиксировать предел входа: 5 неудачных попыток на client_key за 5 минут, далее 429 до истечения окна; срок сессии 12 часов. Добавить тест управляемым временем, без реального ожидания. Не доверять произвольному X-Forwarded-For.
|
||||
- [ ] Запустить `python -m pytest tests/integration/test_auth.py -q` до реализации.
|
||||
- [ ] Реализовать Argon2, случайный токен с хранением только его хеша, проверку срока сессии в каждом защищённом действии. HTML-формы и Rio-сессии используют одну авторизацию; редирект страницы сам по себе не считается защитой callback.
|
||||
- [ ] Повторить тесты, проверить отсутствие паролей в логах и безопасный отказ запуска без обязательных настроек. Изменение учётных данных отзывает прежние сессии.
|
||||
|
||||
## Задача 7 Неизменяемые публикации
|
||||
|
||||
**Files:** создать `publishing.py`, `tests/integration/test_publications.py`; дополнить repository и HTML routes.
|
||||
|
||||
**Interfaces:** `PublishingService.publish(dataset_id: str, expected_revision: int) -> Publication`; `get_publication(slug: str, version: int) -> Publication`; `unpublish(slug: str, version: int) -> None`. `Publication` содержит slug, version, title, description, views и снимок records. Неопубликованная версия вызывает `NotFound`.
|
||||
|
||||
- [ ] Добавить тест: после публикации A и изменения черновика на B `get_publication(slug, 1).records` остаются A; повторная публикация создаёт version 2. Проверить откат при сбое копирования, запрет пустого набора и конфликт revision.
|
||||
- [ ] Запустить `python -m pytest tests/integration/test_publications.py -q` до реализации.
|
||||
- [ ] Реализовать атомарную публикацию всех включённых валидных представлений и отдельное снятие версии. Если одно включённое представление невалидно, публикация целиком отклоняется с указанием причины.
|
||||
- [ ] Закрепить slug после первой публикации, чтобы переименование заголовка не ломало ссылки. Разрешить ASCII lowercase, цифры и дефисы; уникальность проверяет база.
|
||||
- [ ] Подключить HTML к реальным снимкам и повторить тесты: снятая версия и неизвестный id возвращают 404; прежние ссылки остаются прежними после редактирования.
|
||||
|
||||
## Задача 8 Кабинет Rio
|
||||
|
||||
**Files:** создать `rio_ui/admin/{__init__,login,catalog,import_wizard,view_editor,preview,publications}.py`, `rio_ui/theme.py`, `tests/browser/test_admin.py`.
|
||||
|
||||
**Interfaces:** компоненты `LoginPage`, `CatalogPage`, `ImportWizard`, `ViewEditor`, `PreviewPage`, `PublicationsPage` получают сервисы через подтверждённый механизм Rio attachment. Состояние несохранённых изменений принадлежит конкретной сессии, постоянные данные — Repository. Вызовы методов изменения требуют `require_admin` и expected_revision.
|
||||
|
||||
- [ ] Создать браузерные сценарии входа, импорта CSV, выбора полей, чернового предпросмотра, публикации и выхода; до реализации сценарии не находят соответствующие экраны.
|
||||
- [ ] Реализовать каталог и мастер с шагами «Файл», «Проверка», «Отображение», «Предпросмотр». Внешние названия не содержат JSON Pointer; выбор путей показан деревом. Ошибка импорта сохраняет выбранные настройки и объясняет причину.
|
||||
- [ ] Реализовать оба предпросмотра за авторизацией, смену представлений, предупреждение о несохранённых изменениях и сообщение о конфликте двух вкладок. Дублирующие операции отключаются до завершения запроса.
|
||||
- [ ] При работе FileInfo прочитать загруженные байты один раз и хранить до подтверждения/отмены; валидация повторяется на сервере, а не только фильтром расширений. Предпросмотр показывает 20 записей, число всех записей и предупреждения.
|
||||
- [ ] Выполнить `python -m pytest tests/browser/test_admin.py -q` с браузером Playwright и отдельной временной базой. Проверить ручной вход, восстановление после ошибки и невозможность действия после logout в другой вкладке.
|
||||
|
||||
## Задача 9 Динамические упражнения Rio
|
||||
|
||||
**Files:** создать `rio_ui/public/{__init__,listing,detail,renderers}.py`, `tests/browser/test_interactive.py`.
|
||||
|
||||
**Interfaces:** `InteractivePage` загружает Publication по slug/version и использует `select_records` и `project_record`; `RecordPage` показывает только выбранные значения. Состояние страницы хранит фильтры, next_page, generation и флаг loading.
|
||||
|
||||
- [ ] Создать сценарий: 51 запись, размер 25; после двух нажатий «Загрузить ещё» получены 51 уникальных id, кнопки больше нет. Проверить каждое из четырёх представлений.
|
||||
- [ ] Проверить быстрые повторные клики, смену фильтра во время загрузки и снятие публикации при открытой странице. Устаревший ответ фильтра не должен добавляться к новому списку; снятые данные не выдаются следующими запросами.
|
||||
- [ ] Запустить `python -m pytest tests/browser/test_interactive.py -q` до реализации.
|
||||
- [ ] Реализовать дозагрузку, состояния и детали, используя подтверждённые Rio API. Стабильные доступные подписи и роли обязательны; способы установки дополнительных DOM-атрибутов сначала проверить по версии Rio. Не строить тесты на случайных сгенерированных классах.
|
||||
- [ ] Повторить сценарии; проверить прямое открытие вложенного URL, обновление браузера и отсутствие скрытых значений в передаваемых данных.
|
||||
|
||||
## Задача 10 Дизайн и проверка пользовательского сценария
|
||||
|
||||
**Files:** доработать `rio_ui/theme.py`, Rio-компоненты, `web/static/site.css`, HTML-шаблоны; создать `tests/browser/test_layout.py`, `tests/integration/test_visibility.py`.
|
||||
|
||||
- [ ] Реализовать общие цветовые и типографические параметры, боковую навигацию, компактную навигацию, карточки статусов и локальные ресурсы. Не добавлять новые функции ради оформления.
|
||||
- [ ] Проверить 1440×900 и 390×844: основные действия доступны; длинный текст переносится; горизонтально прокручивается таблица, а не вся страница; заголовки и ответы не перекрываются. Сохранить скриншоты в рабочую папку и визуально просмотреть.
|
||||
- [ ] Проверить клавиатурный фокус, подписи полей, ошибки рядом с полями и состояния пустых результатов. Для простых косметических правок использовать визуальную проверку, не тестировать каждую константу стиля.
|
||||
- [ ] Добавить интеграционные тесты скрытых полей на списках, деталях, HTML предпросмотре без входа и прямых служебных запросах. Проверить пользовательские `<script>`, HTML и URL как неисполняемые значения.
|
||||
- [ ] Запустить `python -m pytest tests/integration/test_visibility.py tests/browser/test_layout.py -q`, исправить наблюдаемые проблемы и повторить только затронутые проверки.
|
||||
|
||||
## Задача 11 Примеры и контроль сбора данных
|
||||
|
||||
**Files:** создать `examples/data/{hockey_teams.csv,messages.jsonl,questionnaires.json,profiles.csv}`, `examples/parsers/{scrape_html.py,scrape_interactive.py}`, `tests/integration/test_scraping.py`.
|
||||
|
||||
**Interfaces:** оба CLI принимают URL опубликованного представления и путь результата; выводят JSONL с теми же видимыми полями, record_id и состоянием отсутствующих значений. Решения не монтируются как публичные assets.
|
||||
|
||||
- [ ] Скопировать хоккейный CSV из старого проекта без изменения источника; подготовить синтетические примеры других представлений.
|
||||
- [ ] Создать проверку ожидаемых данных: экспорт обоих парсеров совпадает с проекцией исходных записей, включая порядок, дубликаты значений, missing/null и последнюю неполную страницу.
|
||||
- [ ] Реализовать HTTP-парсер на requests и BeautifulSoup с переходом по ссылкам, динамический — на Playwright с ожиданием состояния/элементов, без фиксированных sleep. Проверить таймауты, HTTP-ошибку и прекращение при снятой версии.
|
||||
- [ ] Выполнить `python -m pytest tests/integration/test_scraping.py -q` на локальном тестовом сервере. Сверить каждый вид в двух режимах; автоматизировать повторяющиеся варианты параметризацией.
|
||||
|
||||
## Задача 12 Docker и передача проекта
|
||||
|
||||
**Files:** создать `Dockerfile`, `compose.yaml`, `.env.example`, `.dockerignore`, `README.md`, `docs/{design-spec,implementation-plan}.md`.
|
||||
|
||||
- [ ] Собрать образ на закреплённой совместимой Python, установить lock-зависимости, включить локальные ресурсы, запускать непривилегированным пользователем. Порт 8080, healthcheck `/healthz`, named volume в `/data`, без исходников и секретов рабочего окружения в образе.
|
||||
- [ ] Проверить `docker compose config` и `docker compose build`; затем `docker compose up -d`. Передавать тестовые секреты безопасным локальным файлом, исключённым из Git и образа.
|
||||
- [ ] Через контейнер пройти импорт и публикацию, HTTP-парсер и Playwright. Выполнить `docker compose down` без `-v`, поднять снова и проверить прежние ссылки и данные.
|
||||
- [ ] Проверить резервное копирование при остановленном контейнере и восстановление в отдельный пустой том, не удаляя рабочий. Зафиксировать права каталога и успешный healthcheck после восстановления.
|
||||
- [ ] Документировать запуск, генерацию хеша, переменные окружения, смену пароля, reverse proxy с WebSocket, сохранность тома, обновление, резервную копию, примеры файлов и парсеров. Не обещать проверку HTTPS, если proxy фактически не запускался.
|
||||
- [ ] Выполнить полный `python -m pytest -q`, сборку и контрольный пользовательский сценарий. Если Docker Engine недоступен, сохранить Docker-файлы, явно отметить непроверенную сборку и не считать Docker-критерий выполненным.
|
||||
|
||||
## Контроль перед завершением
|
||||
|
||||
- [ ] Сопоставить реализованные функции со всеми критериями спецификации, включая отсутствие утечек полей и сохранность версий.
|
||||
- [ ] Проверить весь проект независимым обзором после реализации; устранить подтверждённые дефекты и повторить затронутые проверки.
|
||||
- [ ] Удалить только собственные временные тестовые данные из поставки, остановить временные процессы, сохранить пользовательские данные.
|
||||
- [ ] Передать путь к проекту, инструкцию запуска, проверенные команды и известные ограничения. Публикацию на внешний сервер выполнять только по отдельному запросу.
|
||||
|
||||
## Порядок исполнения
|
||||
|
||||
Задачи выполняются последовательно в этой беседе. Тесно связанные интерфейсы импорта, представлений и публикаций делают этот вариант предпочтительным для первой реализации. Делегирование отдельным агентам возможно по выбору пользователя; оно не требуется для корректного выполнения плана.
|
||||
|
||||
План подготовлен для проверки перед началом реализации. Во время исполнения чекбоксы и результаты обновляются по фактическим проверкам, а не по наличию файлов.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Проверка FastAPI-версии — 2026-10-01
|
||||
|
||||
Рабочий проект: `P:\Coding\Python\NetologyWebParserSite`.
|
||||
|
||||
## Архитектура
|
||||
|
||||
Rio полностью удалён из кода приложения, pyproject.toml и uv.lock. Сервер — FastAPI; страницы — Jinja2, HTML и CSS; интерактивность — небольшие JavaScript-модули с HTTP-запросами. WebSocket не используется.
|
||||
|
||||
Кабинет поддерживает импорт CSV/JSON/JSONL, выбор вложенного массива JSON, настройку четырёх представлений, живой предпросмотр, сохранение черновика, публикацию и снятие версии. Обычные учебные страницы используют ссылочную пагинацию. Динамические страницы отдают первую порцию данных в HTML и поддерживают дозагрузку через JavaScript.
|
||||
|
||||
Хранилище SQLite, формат публикаций, учётные данные и адреса опубликованных страниц сохранены. Тесты работают с изолированными данными; рабочий `.env` не изменялся. Отдельно от тестов исправлен найденный черновик с точным содержимым примера questionnaires.json: две записи участников и автоматически настроенные анкеты. Прежний черновик сохранён в `data/backups/b2cce4ed61034aaab20242c364c21924-revision-3.json`; опубликованные версии и исходный файл не изменены.
|
||||
|
||||
## Результаты
|
||||
|
||||
- `uv sync --locked`: зависимости установлены, Rio отсутствует.
|
||||
- Полный pytest с `LAB_BROWSER_TESTS=1`, `PLAYWRIGHT_CHANNEL=chrome`: **49 passed**, в том числе 14 реальных браузерных сценариев; 27,97 секунды.
|
||||
- Один сторонний warning: Starlette предупреждает о будущем переходе TestClient с httpx на httpx2. Ошибок тестов нет.
|
||||
- Импорт, предпросмотр, публикация, конфликт ревизий, замена исходника без изменения опубликованной версии, авторизация и CSRF проверены HTTP-тестами.
|
||||
- Браузер проверяет импорт CSV и вложенного JSON, настройку заполненных анкет, блокировку сохранения после неуспешной проверки файла, изменение подписей в предпросмотре и предупреждение о несохранённых изменениях.
|
||||
- Все четыре представления проверены парсерами и Chrome: 51 запись, дозагрузка, поиск, фильтры, отсутствие скрытых полей в публичных данных.
|
||||
- Переход «Parser Lab / Коллекции» и выход из кабинета проверены.
|
||||
- Единый favicon проверен на главной, входе, в кабинете и обоих типах учебных страниц.
|
||||
- Одинаковые размеры, шрифты, цвета и положение элементов обычной/динамической страницы проверены на ширине 1440 и 390 пикселей.
|
||||
- Снимки реального интерфейса кабинета и четырёх форматов проверены на настольном и мобильном экранах. Горизонтальная прокрутка широких таблиц остаётся внутри таблицы.
|
||||
- `uv build`: собраны sdist и wheel с HTML/CSS/JS, шрифтами и favicon.
|
||||
- Отдельное окружение `uv sync --locked --no-dev --no-editable`: установлен 21 пакет. Запуск установленного пакета, healthz, главная, вход, перенаправление из закрытого кабинета и статические ресурсы проверены без Rio и pytest.
|
||||
|
||||
Все проверки выполнялись на временных данных. Тестовые серверы остановлены.
|
||||
|
||||
## Запуск
|
||||
|
||||
Из корня проекта:
|
||||
|
||||
```powershell
|
||||
uv sync --locked
|
||||
uv run --locked --env-file .env python -m parser_lab
|
||||
```
|
||||
|
||||
Если предыдущая версия ещё запущена, остановите её перед запуском новой. Повторно создавать `.env` и импортировать существующие данные не нужно.
|
||||
|
||||
Dockerfile и Compose сохранены. Сборка и запуск самого контейнера в этой среде **не проверены**, поскольку Docker недоступен. Проверена отдельная production-установка с теми же uv-зависимостями.
|
||||
|
||||
`design-spec.md` и `implementation-plan.md` помечены как архив первоначальной Rio-архитектуры. Актуальная инструкция — README.md.
|
||||
|
||||
|
||||
## Исправление импорта questionnaires.json
|
||||
|
||||
Браузерный сценарий загружает реальный `examples/data/questionnaires.json`, нажимает «Проверить файл» и «Сохранить и настроить», не выбирая вручную JSON-пути. Проверяются две анкеты A01/A02 и шесть исходных ответов: 8, Прогулка, true, 5, null, false. Импорт и редактор показывают отформатированные анкеты. Отдельный сценарий исправляет старый импорт целого документа кнопкой «Настроить анкеты автоматически», проверяет сохранение после перезагрузки и публикацию.
|
||||
|
||||
HTTP-проверки подтверждают независимость распознавания от имени файла, отказ от автоматического выбора при нескольких подходящих массивах, сохранение опубликованного снимка и исходника при исправлении, а также защиту от устаревшей ревизии. При неизвестной структуре остаётся ручной выбор полей.
|
||||
Reference in New Issue
Block a user