Files
Web-Parser-Constructor/docs/implementation-plan.md
T
2026-10-01 00:28:16 +10:00

35 KiB
Raw Blame History

Архив первоначального проекта. С 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 выгрузки.

Особое внимание при проверке

  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-критерий выполненным.

Контроль перед завершением

  • Сопоставить реализованные функции со всеми критериями спецификации, включая отсутствие утечек полей и сохранность версий.
  • Проверить весь проект независимым обзором после реализации; устранить подтверждённые дефекты и повторить затронутые проверки.
  • Удалить только собственные временные тестовые данные из поставки, остановить временные процессы, сохранить пользовательские данные.
  • Передать путь к проекту, инструкцию запуска, проверенные команды и известные ограничения. Публикацию на внешний сервер выполнять только по отдельному запросу.

Порядок исполнения

Задачи выполняются последовательно в этой беседе. Тесно связанные интерфейсы импорта, представлений и публикаций делают этот вариант предпочтительным для первой реализации. Делегирование отдельным агентам возможно по выбору пользователя; оно не требуется для корректного выполнения плана.

План подготовлен для проверки перед началом реализации. Во время исполнения чекбоксы и результаты обновляются по фактическим проверкам, а не по наличию файлов.