From 6480f31e8f996a651084da9b9769c50b0b1c8820 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 28 Apr 2026 16:18:07 +0300 Subject: [PATCH] initial commit --- .env.example | 20 +++++++++ .gitignore | 9 ++++ Dockerfile | 17 ++++++++ README.md | 103 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 53 +++++++++++++++++++++++ pyproject.toml | 28 ++++++++++++ requirements.txt | 13 ++++++ 7 files changed, 243 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 pyproject.toml create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..66a29d7 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +POSTGRES_DB=miem_workers +POSTGRES_USER=miem +POSTGRES_PASSWORD=change-me +POSTGRES_PORT=5432 + +DATABASE_URL=postgresql+psycopg://miem:change-me@postgres:5432/miem_workers +SOURCE_URL=https://miem.hse.ru/persons +CRAWL_CRON=0 3 * * 1 +CRAWL_LIMIT= +REQUEST_TIMEOUT=30 +REQUEST_DELAY_SECONDS=1 +PARSER_USE_PLAYWRIGHT=false + +ADMIN_USERNAME=admin +ADMIN_PASSWORD=change-me +SESSION_SECRET=change-me-session-secret +MCP_TOKEN=change-me-mcp-token + +API_PORT=8000 +MCP_PORT=8001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42a31c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.env +.venv/ +__pycache__/ +*.py[cod] +*.db +.pytest_cache/ +.coverage +htmlcov/ +postgres_data/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f512f81 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..df285c8 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# MIEM Employees Server + +Сервис собирает сотрудников МИЭМ с сайта ВШЭ, хранит карточки и историю обновлений в Postgres, показывает минимальную админку и отдает read-only MCP endpoint для ИИ-агентов. + +## Архитектура + +- `api`: FastAPI, REST API, HTML-админка, healthcheck. +- `worker`: weekly scheduler, который запускает парсинг по `CRAWL_CRON`. +- `mcp`: HTTP MCP endpoint с bearer token. +- `postgres`: основная БД. + +Парсер использует фиксированный источник сотрудников, по умолчанию `https://miem.hse.ru/persons`. Для каждой карточки сохраняются ФИО, должности, год начала работы, контакты, идентификаторы, вкладки профиля, секции, публикации, курсы, ВКР, JSON-снапшот и сжатый HTML-снапшот. Ссылки обходятся только из меню профиля самого сотрудника (`person-menu`), например `#sci`, `#teaching`, `#main`. + +## Переменные окружения + +Скопируйте `.env.example` в `.env` и поменяйте секреты: + +```bash +cp .env.example .env +``` + +Основные настройки: + +- `DATABASE_URL`: строка подключения SQLAlchemy. +- `SOURCE_URL`: список сотрудников МИЭМ. +- `CRAWL_CRON`: расписание в формате crontab, по умолчанию `0 3 * * 1`. +- `CRAWL_LIMIT`: опциональный лимит профилей для тестового запуска. +- `ADMIN_USERNAME`, `ADMIN_PASSWORD`: логин и пароль админки. +- `SESSION_SECRET`: секрет подписи cookie. +- `MCP_TOKEN`: bearer token для `/mcp`. +- `PARSER_USE_PLAYWRIGHT`: включение Playwright-рендера динамических вкладок. + +## Локальный запуск + +```bash +python -m venv .venv +.venv\Scripts\activate +pip install -r requirements.txt +uvicorn app.main:app --reload +``` + +Админка: `http://localhost:8000/admin`. + +## Docker Compose + +```bash +docker compose up --build +``` + +По умолчанию: + +- API и админка: `http://localhost:8000` +- MCP: `http://localhost:8001/mcp` +- Postgres: `localhost:5432` + +Таблицы создаются приложением при старте. SQL-миграция для ручного применения лежит в `migrations/001_init.sql`. + +## Парсинг + +Weekly worker запускается по `CRAWL_CRON`. Ручной запуск доступен в админке на странице `Runs` или через REST: + +```bash +curl -X POST http://localhost:8000/api/crawl-runs --cookie "miem_admin_session=..." +``` + +Алгоритм обновления: + +- найденные сотрудники получают статус `active` и обновленный `last_seen_at`; +- новые сотрудники добавляются в `employees`; +- активные сотрудники, исчезнувшие из текущего списка источника, получают статус `dismissed` и `dismissed_at`; +- каждый успешный разбор сохраняет запись в `employee_snapshots`. + +## MCP + +Endpoint: `POST /mcp`, авторизация `Authorization: Bearer `. + +Поддерживаемые tools: + +- `search_employees(query, status?, limit?)` +- `get_employee(profile_id_or_url)` +- `list_employee_publications(profile_id_or_url)` +- `list_employee_courses(profile_id_or_url)` +- `get_crawl_status()` + +Пример: + +```bash +curl http://localhost:8001/mcp \ + -H "Authorization: Bearer change-me-mcp-token" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' +``` + +## Обслуживание + +```bash +docker compose logs -f api +docker compose logs -f worker +docker compose exec postgres pg_dump -U miem miem_workers > backup.sql +docker compose down +``` + +Версия сервиса: `0.1.0`. Админка всегда показывает версии backend и frontend в footer. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9500db6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-miem_workers} + POSTGRES_USER: ${POSTGRES_USER:-miem} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-miem_password} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "${POSTGRES_PORT:-5432}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-miem} -d ${POSTGRES_DB:-miem_workers}"] + interval: 10s + timeout: 5s + retries: 5 + + api: + build: . + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + env_file: .env + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-miem}:${POSTGRES_PASSWORD:-miem_password}@postgres:5432/${POSTGRES_DB:-miem_workers} + ports: + - "${API_PORT:-8000}:8000" + depends_on: + postgres: + condition: service_healthy + + worker: + build: . + command: python -m app.worker + env_file: .env + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-miem}:${POSTGRES_PASSWORD:-miem_password}@postgres:5432/${POSTGRES_DB:-miem_workers} + depends_on: + postgres: + condition: service_healthy + + mcp: + build: . + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + env_file: .env + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-miem}:${POSTGRES_PASSWORD:-miem_password}@postgres:5432/${POSTGRES_DB:-miem_workers} + ports: + - "${MCP_PORT:-8001}:8000" + depends_on: + postgres: + condition: service_healthy + +volumes: + postgres_data: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6886bdb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "miem-workers" +version = "0.1.0" +description = "MIEM employees parser, admin API, and MCP server" +requires-python = ">=3.11" +dependencies = [ + "apscheduler>=3.10.4", + "beautifulsoup4>=4.12.3", + "fastapi>=0.115.0", + "httpx>=0.27.0", + "jinja2>=3.1.4", + "lxml>=5.2.0", + "psycopg[binary]>=3.2.0", + "pydantic-settings>=2.4.0", + "python-multipart>=0.0.9", + "requests>=2.32.0", + "sqlalchemy>=2.0.32", + "uvicorn[standard]>=0.30.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3.0", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e9226e7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +apscheduler>=3.10.4 +beautifulsoup4>=4.12.3 +fastapi>=0.115.0 +httpx>=0.27.0 +jinja2>=3.1.4 +lxml>=5.2.0 +psycopg[binary]>=3.2.0 +pydantic-settings>=2.4.0 +python-multipart>=0.0.9 +requests>=2.32.0 +sqlalchemy>=2.0.32 +uvicorn[standard]>=0.30.0 +pytest>=8.3.0