35 KiB
Архив первоначального проекта. С 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: Утверждённая спецификация.
Рабочее место и ограничения
Новый проект создаётся в 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 выгрузки.
Особое внимание при проверке
- Заголовки JSON с точками, слешами и тильдами не должны путаться с вложенными путями — задачи 2 и 4.
- Значения null, false, 0 и пустая строка не должны схлопываться в отсутствие поля — задачи 2 и 5.
- Замена файла и авария публикации не должны повреждать доступный студентам снимок — задачи 3 и 7.
- Прямой запрос страницы деталей или callback Rio не должен обходить авторизацию либо раскрывать скрытое поле — задачи 6, 8 и 10.
- Две вкладки преподавателя не должны незаметно перезаписывать чужие изменения — задача 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-критерий выполненным.
Контроль перед завершением
- Сопоставить реализованные функции со всеми критериями спецификации, включая отсутствие утечек полей и сохранность версий.
- Проверить весь проект независимым обзором после реализации; устранить подтверждённые дефекты и повторить затронутые проверки.
- Удалить только собственные временные тестовые данные из поставки, остановить временные процессы, сохранить пользовательские данные.
- Передать путь к проекту, инструкцию запуска, проверенные команды и известные ограничения. Публикацию на внешний сервер выполнять только по отдельному запросу.
Порядок исполнения
Задачи выполняются последовательно в этой беседе. Тесно связанные интерфейсы импорта, представлений и публикаций делают этот вариант предпочтительным для первой реализации. Делегирование отдельным агентам возможно по выбору пользователя; оно не требуется для корректного выполнения плана.
План подготовлен для проверки перед началом реализации. Во время исполнения чекбоксы и результаты обновляются по фактическим проверкам, а не по наличию файлов.