Compare commits

..

No commits in common. "main" and "master" have entirely different histories.
main ... master

35 changed files with 1624 additions and 2051 deletions

View File

@ -1,12 +0,0 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"]
],
"subject-case": [2, "always", "lower-case"],
"subject-full-stop": [2, "never", "."]
}
}

BIN
.coverage Normal file

Binary file not shown.

View File

@ -1,111 +0,0 @@
name: PR Checks
on:
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Use system Python
run: |
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies (with dev)
env:
UV_NO_PROGRESS: "1"
run: uv sync --group dev
- name: Run tests with coverage
run: |
uv run pytest tests/unit/ \
-v \
--tb=short \
--cov=src/kwork_api \
--cov-report=term-missing \
--cov-report=html:coverage-html \
--html=test-results/report.html \
--self-contained-html
- name: Check coverage threshold (90%)
run: |
COVERAGE=$(uv run coverage report | grep TOTAL | awk '{print $NF}' | tr -d '%')
echo "Coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 90" | bc -l) )); then
echo "❌ Coverage ${COVERAGE}% is below 90% threshold"
exit 1
fi
echo "✅ Coverage ${COVERAGE}% meets 90% threshold"
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: test-results/
retention-days: 7
- name: Upload coverage report
uses: actions/upload-artifact@v3
if: always()
with:
name: coverage-report
path: coverage-html/
retention-days: 7
- name: Run linting
run: uv run ruff check src/kwork_api tests/
- name: Run formatter check
run: uv run ruff format --check src/kwork_api tests/
security:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Use system Python
run: |
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: uv sync --group dev
- name: Run safety check
env:
UV_NO_PROGRESS: "1"
run: |
uv pip compile pyproject.toml --no-dev -o requirements-prod.txt && uv run pip-audit --format json --output audit-results.json -r requirements-prod.txt && test ! -s audit-results.json || test "$(cat audit-results.json)" = "[]"
- name: Upload audit log
uses: actions/upload-artifact@v3
if: failure()
with:
name: security-audit
path: audit-results.json
retention-days: 7
- name: Check for secrets
run: |
if grep -r "password\s*=" --include="*.py" src/; then
echo "❌ Found hardcoded passwords in src/"
exit 1
fi
if grep -r "token\s*=" --include="*.py" src/; then
echo "❌ Found hardcoded tokens in src/"
exit 1
fi
echo "✅ No hardcoded secrets found"

View File

@ -1,128 +0,0 @@
name: Release & Publish
env:
GITEA_TOKEN: ${{ secrets.CI_TOKEN }}
on:
push:
branches: [main]
tags:
- 'v*'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
semantic-release:
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
outputs:
new_release_version: ${{ steps.semantic.outputs['new_release_version'] }}
new_release_published: ${{ steps.semantic.outputs['new_release_published'] }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITEA_TOKEN }}
- name: Use system Python
run: |
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
env:
UV_NO_PROGRESS: "1"
run: uv sync --group dev
- name: Run semantic-release
id: semantic
env:
GH_TOKEN: ${{ secrets.GITEA_TOKEN }}
GIT_AUTHOR_NAME: claw-bot
GIT_AUTHOR_EMAIL: claw-bot@much-data.ru
GIT_COMMITTER_NAME: claw-bot
GIT_COMMITTER_EMAIL: claw-bot@much-data.ru
run: |
uv run semantic-release version --no-push
NEW_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
echo "new_release_version=$NEW_VERSION" >> $GITHUB_OUTPUT
# Check if version changed
if git diff --quiet pyproject.toml; then
echo "new_release_published=false" >> $GITHUB_OUTPUT
else
echo "new_release_published=true" >> $GITHUB_OUTPUT
fi
- name: Commit and push version bump
if: steps.semantic.outputs.new_release_published == 'true'
run: |
uv run semantic-release changelog
git add pyproject.toml CHANGELOG.md
git commit -m "chore(release): v${{ steps.semantic.outputs.new_release_version }} [skip ci]"
git tag v${{ steps.semantic.outputs.new_release_version }}
git push origin main --tags
build:
runs-on: ubuntu-latest
timeout-minutes: 15
if: github.ref == 'refs/tags/v*'
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use system Python
run: |
echo "Python $(python3 --version)"
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Install dependencies (production only)
env:
UV_NO_PROGRESS: "1"
run: uv sync --no-dev
- name: Build package
run: uv build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
retention-days: 7
publish-gitea:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Publish to Gitea Packages
run: |
uv publish \
--username ${{ github.actor }} \
--password ${{ env.GITEA_TOKEN }} \
https://git.much-data.ru/api/packages/${{ github.repository_owner }}/pypi

34
.gitea/workflows/test.yml Normal file
View File

@ -0,0 +1,34 @@
name: CI - Tests & Lint
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: uv sync --group dev
- name: Run tests with coverage
run: uv run pytest tests/unit/ -v --tb=short --cov=src/kwork_api --cov-report=term-missing
- name: Run linting
run: uv run ruff check src/kwork_api tests/

156
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,156 @@
name: CI/CD Pipeline
on:
push:
branches: [main, master]
tags: ['v*']
pull_request:
branches: [main, master]
env:
PYTHON_VERSION: '3.12'
jobs:
test:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install UV
uses: astral-sh/setup-uv@v3
with:
version: "latest"
enable-cache: true
- name: Set up Python
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: uv sync --frozen --dev
- name: Run linters
run: uv run ruff check src/ tests/
- name: Run tests
run: uv run pytest --cov=kwork_api --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
fail_ci_if_error: false
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Install UV
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Set up Python
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Build package
run: uv build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
release:
name: Semantic Release
runs-on: ubuntu-latest
needs: [test, build, docs]
if: github.ref == 'refs/heads/main'
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install UV
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Set up Python
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: uv sync --frozen --dev
- name: Run semantic-release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
uv run semantic-release version --push --no-mock
publish:
name: Publish to Gitea Registry
runs-on: ubuntu-latest
needs: release
if: startsWith(github.ref, 'refs/tags/v')
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Install UV
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Publish to Gitea
env:
UV_PUBLISH_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
uv publish \
--publish-url https://git.much-data.ru/api/packages/claw/pypi \
--token $UV_PUBLISH_TOKEN
docs:
name: Build & Deploy Documentation
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Install UV
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Set up Python
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: uv sync --frozen --dev
- name: Build docs
run: uv run mkdocs build
- name: Deploy to Gitea Pages
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
gitea_token: ${{ secrets.GITEA_TOKEN }}
gitea_server_url: https://git.much-data.ru
publish_dir: ./site
publish_branch: gh-pages
force_orphan: true

7
.gitignore vendored
View File

@ -1,14 +1,9 @@
# Build artifacts
# Build
site/
dist/
build/
*.egg-info/
# Virtual environments
.venv/
venv/
.env/
# Documentation
api_reference.md

View File

@ -1,26 +0,0 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: local
hooks:
- id: commitlint
name: commitlint
entry: commitlint
language: node
additional_dependencies:
- @commitlint/cli
- @commitlint/config-conventional
stages: [commit-msg]
- id: pytest
name: pytest
entry: uv run pytest tests/unit/ -v
language: system
pass_filenames: false
always_run: true
stages: [pre-push]

View File

@ -5,15 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
**This changelog is automatically generated by [python-semantic-release](https://python-semantic-release.readthedocs.io/).**
---
## [Unreleased]
See [commits](https://git.much-data.ru/much-data/kwork-api/compare/v0.1.0...main) since last release.
---
### Planned
- Full CI/CD pipeline with Gitea Actions
- Automatic publishing to Gitea Package Registry
- Database support for caching (optional)
- Rate limiting utilities
## [0.1.0] - 2026-03-23
@ -21,10 +19,52 @@ See [commits](https://git.much-data.ru/much-data/kwork-api/compare/v0.1.0...main
- Initial release
- Complete Kwork.ru API client with 45+ endpoints
- Pydantic models for all API responses
- Comprehensive error handling
- Comprehensive error handling (7 exception types)
- 100% docstring coverage (Russian language)
- MkDocs documentation with mkdocstrings
- Unit tests with 92% coverage
- CI/CD pipeline with Gitea Actions
- UV package manager support
- Gitea Actions CI/CD pipeline
---
### Models
- KworkUser, KworkCategory, Kwork, KworkDetails
- PaginationInfo, CatalogResponse
- Project, ProjectsResponse
- Review, ReviewsResponse
- Notification, NotificationsResponse, Dialog
- AuthResponse, ErrorDetail, APIErrorResponse
- City, Country, TimeZone, Feature, Badge
- DataResponse
*For older versions, see the Git history.*
### API Groups
- CatalogAPI — каталог кворков
- ProjectsAPI — биржа проектов
- UserAPI — пользовательские данные
- ReferenceAPI — справочные данные
- NotificationsAPI — уведомления
- OtherAPI — дополнительные эндпоинты
### Security
- Two-step authentication (cookies + web_auth_token)
- Session management
- Token-based authentication
### Documentation
- Full API reference (MkDocs + mkdocstrings)
- Usage examples in all docstrings
- RELEASE.md guide
- ARCHITECTURE.md
### Technical
- Python 3.10+ support
- httpx with HTTP/2 support
- structlog for structured logging
- Ruff linter configuration
- Pytest with coverage
## [0.0.1] - 2026-03-22
### Added
- Project initialization
- Basic project structure
- First API endpoints implementation

View File

@ -1,146 +0,0 @@
# Contributing to kwork-api
## Development Setup
```bash
# Clone repository
git clone https://git.much-data.ru/much-data/kwork-api.git
cd kwork-api
# Install UV (if not installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install dependencies
uv sync --group dev
# Install pre-commit hooks
uv run pre-commit install
```
## Documentation
Generate documentation locally:
```bash
# Install docs dependencies
uv sync --group docs
# Build HTML docs
uv run mkdocs build
# Serve locally (optional)
uv run mkdocs serve
```
Documentation is built automatically by CI.
---
## End-to-End Testing
E2E тесты требуют реальных credentials Kwork.ru:
```bash
# 1. Скопируй шаблон
cp tests/e2e/.env.example tests/e2e/.env
# 2. Заполни credentials
nano tests/e2e/.env # KWORK_USERNAME, KWORK_PASSWORD
# 3. Запусти тесты
uv run pytest tests/e2e/ -v
# С задержками (rate limiting)
uv run pytest tests/e2e/ -v --slowmo=1
```
⚠️ **Не запускай E2E в CI** — только локально с тестовым аккаунтом!
См. [tests/e2e/README.md](tests/e2e/README.md) для деталей.
## Branch Naming
- `feature/description` — новые фичи
- `fix/description` — багфиксы
- `docs/description` — документация
- `refactor/description` — рефакторинг
## Commit Messages
Используем [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
**Types:**
- `feat` — новая фича
- `fix` — исправление бага
- `docs` — документация
- `style` — форматирование
- `refactor` — рефакторинг
- `test` — тесты
- `chore` — обслуживание
- `ci` — CI/CD
**Примеры:**
```
feat(validation): add /api/validation/checktext endpoint
fix(auth): handle expired token error
docs(api): update client examples
```
## Pre-commit Hooks
Автоматически запускаются перед коммитом:
1. **ruff check** — линтинг с авто-исправлением
2. **ruff format** — форматирование кода
3. **commitlint** — проверка формата коммита
Перед push:
- **pytest** — запуск тестов
## Pull Requests
1. Создай ветку от `main`
2. Вноси изменения с правильными коммитами
3. Запушь ветку
4. Создай PR в `main`
5. Дождись прохождения CI
6. После review — merge
## CI/CD
**PR Checks:**
- ✅ Тесты с coverage (90% threshold)
- ✅ Линтинг (ruff)
- ✅ Форматирование (ruff format)
- ✅ Безопасность (pip-audit + secrets scan)
- ✅ Commitlint (PR title)
**Release (merge в main):**
- 📦 Semantic release (auto versioning)
- 📝 CHANGELOG generation
- 🏷️ Git tag creation
**Tag (v*):**
- 📦 Сборка пакета
- 🚀 Публикация в Gitea Packages
## Versioning
Используем [Semantic Versioning](https://semver.org/):
- `MAJOR.MINOR.PATCH` (например, `1.2.3`)
- Теги: `v1.2.3`
Для создания релиза:
```bash
git tag v1.2.3
git push origin v1.2.3
```

View File

@ -265,8 +265,3 @@ MIT License
This is an unofficial client. Kwork.ru is not affiliated with this project.
Use at your own risk and respect Kwork's terms of service.
## CI Test
## CI Test
Testing Gitea Actions workflow.

239
WIP.md
View File

@ -5,158 +5,121 @@
| Параметр | Значение |
|----------|----------|
| **Проект** | kwork-api |
| **Начало** | 2026-03-23 |
| **Начало** | 2026-03-23 02:16 UTC |
| **Прогресс** | 100% |
| **Статус** | ✅ Готово к v1.0 |
| **Статус** | ✅ Готово |
---
## 📋 План
- [x] Структура проекта (pyproject.toml, UV)
- [x] Модели Pydantic (20+ моделей)
- [x] API клиент (45+ эндпоинтов)
- [x] Обработка ошибок (7 типов исключений)
- [x] Тесты unit (46 тестов, 92% coverage)
- [x] Документация (MkDocs + mkdocstrings)
- [x] CI/CD pipeline (Gitea Actions)
- [x] Semantic release (авто-версионирование)
- [ ] `/kworks` endpoint (требует исследования модели)
- [x] Структура проекта (pyproject.toml, зависимости)
- [x] Модели Pydantic (20+ моделей для всех ответов API)
- [x] API клиент (KworkClient с 45 эндпоинтами)
- [x] Обработка ошибок (KworkAuthError, KworkApiError, etc.)
- [x] Тесты unit (49 тестов, 92% coverage)
- [x] Документация (README + docs/)
- [x] **Аудит эндпоинтов** — все 33 endpoint протестированы ✅
- [x] **Автогенерация документации** — pydoc-markdown ✅
- [x] **Docstrings** — 100% покрытие ✅
- [x] **Добавить `/api/validation/checktext` (валидация текста)**
- [ ] Добавить `/kworks` endpoint (альтернатива каталогу)
- [ ] Тесты integration (шаблон готов, нужны реальные credentials)
- [ ] CI/CD pipeline (Gitea Actions)
- [ ] Публикация на internal PyPI
- [ ] Интеграция с kwork-parser
---
## 🔨 Сейчас в работе
**Проект готов!** Ожидает настройки GITEA_TOKEN для автоматических релизов.
**Проект завершён!** ✅
**Что можно сделать потом:**
- Добавить `/kworks` endpoint (когда будет модель ответа)
- Настроить публикацию на internal PyPI
- Интеграция с kwork-parser
---
## 📝 Заметки
### 🏗️ Архитектура
**Стек:**
- Python 3.10+ (тесты на 3.12)
- UV (package manager)
- httpx[http2] (async client)
- pydantic v2 (модели)
- pytest + respx (тесты)
- ruff (lint + format)
**Структура:**
```
kwork-api/
├── src/kwork_api/
│ ├── __init__.py # __version__
│ ├── client.py # KworkClient (45+ методов)
│ ├── models.py # 20+ Pydantic моделей
│ └── errors.py # 7 типов исключений
├── tests/
│ ├── unit/ # Unit тесты (mock)
│ └── integration/ # Integration тесты (требуют credentials)
├── docs/ # Исходники документации
├── .gitea/workflows/ # CI/CD pipeline
└── pyproject.toml # Конфигурация проекта
```
### 📚 Документация
### Автогенерация документации (2026-03-23 04:35)
**Инструмент:** MkDocs + mkdocstrings + Material theme
**Структура:**
```
docs/
├── index.md # Quick start guide
├── api-reference.md # API overview
├── api/
│ ├── client.md # KworkClient documentation
│ ├── models.md # Pydantic models
│ └── errors.md # Exception classes
└── examples.md # Usage examples
site/ # Generated HTML (не коммитим)
├── index.html
├── api-reference/
└── ...
```
**Конфигурация:**
- `mkdocs.yml` — MkDocs конфигурация
- Pre-commit hook — автогенерация HTML при коммите
**Покрытие документацией:**
- `KworkClient` — класс, аутентификация, все методы
- `CatalogAPI` — каталог кворков
- `ProjectsAPI` — биржа проектов
- `UserAPI` — пользовательские данные
- `ReferenceAPI` — справочники
- `NotificationsAPI` — уведомления
- `client.get_*()` — настройки и предпочтения (бывший OtherAPI)
- `models.py` — все модели
- `errors.py` — все исключения
**Команды:**
```bash
# Локальная разработка
mkdocs serve
# Сборка HTML (для CI)
# Сборка HTML документации
mkdocs build
# Локальный просмотр
mkdocs serve
```
**Деплой:** Автоматически через Gitea Actions на Gitea Pages
### Аудит эндпоинтов (2026-03-23 03:08)
**Покрытие:** 100% docstrings (Russian language)
**Из HAR дампа:** 44 эндпоинта
- **Пропущено (internal/analytics):** 9
- **Реализовано:** 33/33 (100%) ✅
- **Протестировано:** 33/33 (100%) ✅
### 🧪 Тестирование
**Пропущенные эндпоинты (анализ):**
**Coverage:** 92% (порог 90% для PR)
| Эндпоинт | Размер | Описание | Решение |
|----------|--------|----------|---------|
| `/signIn` | - | Авторизация | ✅ Реализовано в `login()` |
| `/getWebAuthToken` | - | Получение токена | ✅ Реализовано в `login()` |
| `/kworks` | 22KB | Список кворков | 🔴 Добавить |
| `/quick-faq/init` | 3.7MB | FAQ данные | ⏪ Опционально |
| `/api/validation/checktext` | - | Валидация текста | 🔴 Добавить |
| Остальные | - | Analytics/UI | ⏪ Пропустить |
**Запуск:**
```bash
# Все тесты
uv run pytest tests/unit/ -v
**Тесты:**
- Unit тесты: 46 passed
- Покрытие: 92%
- Файлы: `test_client.py` (13 тестов), `test_all_endpoints.py` (33 теста)
# С coverage
uv run pytest --cov=src/kwork_api --cov-report=html
```
**Аутентификация:** cookies + web_auth_token (2 этапа)
**Стек:** UV + httpx(http2) + pydantic v2 + structlog + mkdocstrings
**HAR дамп:** 45 эндпоинтов проанализировано
**Артефакты:**
- HTML отчёт pytest (test-results/report.html)
- HTML отчёт coverage (coverage-html/index.html)
### 🚀 CI/CD (Gitea Actions)
**Workflow 1: PR Checks** (`pr-check.yml`)
- Триггер: `pull_request` в `main`
- Тесты с coverage (90% threshold)
- Linting (ruff)
- Formatter check
- Security scan (pip-audit)
- Артефакты: test results + coverage report
**Workflow 2: Release & Publish** (`release.yml`)
- Триггер: `push` в `main` ИЛИ теги `v*`
- **main:** Build (production deps only) → Deploy docs
- **tags:** Build → Publish to Gitea Packages
**Semantic Release:**
- Анализирует Conventional Commits
- Auto-bump версии (MAJOR/MINOR/PATCH)
- Auto-generate CHANGELOG
- Auto-create git tags
- Обновляет `pyproject.toml` и `__init__.py`
**Типы коммитов:**
- `feat:` → MINOR (0.1.0 → 0.2.0)
- `fix:` → PATCH (0.1.0 → 0.1.1)
- `feat: + BREAKING CHANGE` → MAJOR (0.1.0 → 1.0.0)
- `docs:`, `style:`, `refactor:`, `test:`, `chore:` → без изменений версии
### 📦 Публикация
**Gitea Packages:** PyPI-compatible registry
```bash
uv publish \
--username <user> \
--password <token> \
https://git.much-data.ru/api/packages/much-data/pypi
```
**Установка:**
```bash
uv pip install kwork-api --index-url https://git.much-data.ru/api/packages/much-data/pypi
```
---
## 🎯 Endpoints
**Реализовано:** 33/33 (100%)
| Группа | Эндпоинты | Статус |
|--------|-----------|--------|
| CatalogAPI | `/catalogMainv2`, `/getKworkDetails` | ✅ |
| ProjectsAPI | `/projects`, `/getPayerOrders` | ✅ |
| UserAPI | `/getUserInfo`, `/getReviews` | ✅ |
| ReferenceAPI | `/cities`, `/countries`, `/categories` | ✅ |
| NotificationsAPI | `/notifications`, `/dialogs` | ✅ |
| OtherAPI | 25+ дополнительных | ✅ |
| ValidationAPI | `/api/validation/checktext` | ✅ |
**Не реализовано:**
- `/kworks` — требует исследования модели ответа
- Analytics эндпоинты — не нужны для библиотеки
**Решения:**
- Rate limiting на стороне пользователя (не в библиотеке)
- Только библиотека (без CLI)
- Pydantic модели для всех ответов
- Автогенерация документации через mkdocstrings+griffe
---
@ -168,21 +131,19 @@ uv pip install kwork-api --index-url https://git.much-data.ru/api/packages/much-
## 📅 История
- **2026-03-29** — Репозиторий очищен от артефактов (site/, .coverage)
- **2026-03-29** — Настроен semantic-release для авто-версионирования
- **2026-03-29** — CI/CD pipeline готов (PR checks + release)
- **2026-03-29** — История переписана (1 чистый коммит)
- **2026-03-23** — Initial release (45+ endpoints, 92% coverage)
---
## 🔐 Секреты (Gitea)
**Требуется:** `GITEA_TOKEN` в repository secrets
**Права:**
- `write:repository` (push tags, create releases)
- `write:package` (publish to Gitea Packages)
**Где настроить:**
https://git.much-data.ru/much-data/kwork-api/settings/secrets
- **23:24****v1.0 выпущен!**Все изменения запушены в Gitea
- **23:12** — Добавлен `/api/validation/checktext` endpoint ✅
- Модели: `ValidationResponse`, `ValidationIssue`
- Метод: `client.other.validate_text()`
- Тесты: 3 новых теста (16 total)
- **03:44** — mkdocstrings+griffe настроен, документация генерируется
- **03:38** — Выбран mkdocstrings+griffe вместо pydoc-markdown
- **03:26** — Автогенерация документации настроена (pre-commit hook)
- **03:20** — Создана docs/ структура
- **03:17** — WIP.md восстановлен после rebase
- **03:14** — Анализ пропущенных эндпоинтов
- **03:08** — Аудит завершён: 33/33 endpoint протестированы (92% coverage)
- **02:48**Все unit тесты пройдены (13/13)
- **02:35** — Завершён этап "API клиент"
- **02:20** — Завершён этап "Структура проекта"
- **02:16** — Начат проект

261
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,261 @@
# Architecture — kwork-api
## 📐 Обзор
**kwork-api** — асинхронный Python клиент для Kwork.ru API с полной типизацией и документацией.
---
## 🏗️ Архитектура
```
┌─────────────────────────────────────────────────────────┐
│ User Application │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ KworkClient │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Authentication Layer │ │
│ │ - login() / token auth │ │
│ │ - Session management │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ API Groups │ │
│ │ - catalog, projects, user, reference, ... │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ HTTP Layer (httpx) │ │
│ │ - HTTP/2 support │ │
│ │ - Timeout handling │ │
│ │ - Error handling │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Kwork.ru API (HTTPS) │
└─────────────────────────────────────────────────────────┘
```
---
## 📁 Структура проекта
```
kwork-api/
├── src/kwork_api/
│ ├── __init__.py # Public API
│ ├── client.py # KworkClient + API groups
│ ├── models.py # Pydantic models
│ └── errors.py # Exception classes
├── tests/
│ ├── test_client.py # Client tests
│ ├── test_models.py # Model tests
│ └── test_all_endpoints.py # Endpoint tests
├── docs/
│ ├── index.md # Quick start
│ ├── api-reference.md # API docs
│ ├── RELEASE.md # Release guide
│ └── ARCHITECTURE.md # This file
├── .github/workflows/
│ └── ci.yml # CI/CD pipeline
├── pyproject.toml # Project config
├── uv.lock # Dependencies lock
└── README.md # Main documentation
```
---
## 🔑 Компоненты
### 1. KworkClient
**Ответственность:** Основное взаимодействие с API
**Функции:**
- Аутентификация (login / token)
- Управление сессией
- Делегирование API группам
**API Groups:**
```python
client.catalog # CatalogAPI
client.projects # ProjectsAPI
client.user # UserAPI
client.reference # ReferenceAPI
client.notifications # NotificationsAPI
client.other # OtherAPI
```
---
### 2. Models (Pydantic)
**Ответственность:** Валидация и типизация ответов API
**Категории:**
- **User models:** KworkUser, AuthResponse
- **Kwork models:** Kwork, KworkDetails, KworkCategory
- **Project models:** Project, ProjectsResponse
- **Review models:** Review, ReviewsResponse
- **Notification models:** Notification, Dialog
- **Reference models:** City, Country, TimeZone, Feature, Badge
- **Error models:** ErrorDetail, APIErrorResponse
---
### 3. Errors
**Ответственность:** Обработка ошибок API
**Иерархия:**
```
KworkError (base)
├── KworkAuthError # 401, 403
├── KworkApiError # 4xx, 5xx
│ ├── KworkNotFoundError # 404
│ ├── KworkRateLimitError # 429
│ └── KworkValidationError # 400
└── KworkNetworkError # Network issues
```
---
### 4. HTTP Layer (httpx)
**Ответственность:** HTTP запросы
**Функции:**
- HTTP/2 поддержка
- Таймауты
- Cookies management
- Token authentication
- Error handling
---
## 🔄 Flow: Аутентификация
```
User → KworkClient.login(username, password)
POST /signIn (cookies)
POST /getWebAuthToken (token)
Store token + cookies
Return authenticated client
```
---
## 🔄 Flow: API Request
```
User → client.catalog.get_list(page=1)
CatalogAPI.get_list()
KworkClient._request()
httpx.AsyncClient.post()
KworkClient._handle_response()
CatalogResponse.model_validate()
Return typed response
```
---
## 🚀 CI/CD Pipeline
```
Push/Tag → GitHub Actions
├── Test Job
│ ├── Install UV + Python
│ ├── Run linters (ruff)
│ ├── Run tests (pytest)
│ └── Upload coverage
├── Build Job
│ ├── Build wheel + sdist
│ └── Upload artifacts
├── Publish Job (on tag)
│ ├── Download artifacts
│ └── Publish to Gitea Registry
└── Docs Job
├── Build MkDocs
└── Upload site
```
---
## 📊 Зависимости
### Runtime
- `httpx[http2]>=0.26.0` — HTTP client
- `pydantic>=2.0.0` — Data validation
- `structlog>=24.0.0` — Logging
### Development
- `pytest>=8.0.0` — Testing
- `pytest-cov>=4.0.0` — Coverage
- `pytest-asyncio>=0.23.0` — Async tests
- `respx>=0.20.0` — HTTP mocking
- `ruff>=0.3.0` — Linting
- `mkdocs + mkdocstrings` — Documentation
---
## 🔒 Безопасность
- **Токены:** Не сохраняются в логах
- **Пароли:** Передаются только при login()
- **Сессии:** Token + cookies хранятся в клиенте
- **HTTPS:** Все запросы через HTTPS
---
## 📈 Производительность
- **Async/Await:** Неблокирующие запросы
- **HTTP/2:** Multiplexing запросов
- **Connection pooling:** Переиспользование соединений
- **Timeouts:** Защита от зависаний
---
## 🧪 Тестирование
- **Unit тесты:** 92% coverage
- **Mock HTTP:** respx для изоляции
- **Async tests:** pytest-asyncio
- **CI:** Автоматический прогон при каждом коммите
---
## 📝 Лицензия
MIT License — свободное использование с указанием авторства.

211
docs/GITEA_PAGES.md Normal file
View File

@ -0,0 +1,211 @@
# Gitea Pages — Хостинг документации
## 📋 Обзор
**Gitea Pages** — аналог GitHub Pages для хостинга статических сайтов напрямую из Gitea.
**URL документации:** `https://git.much-data.ru/claw/kwork-api/`
---
## 🔧 НАСТРОЙКА
### **Шаг 1: Включить Pages в репозитории**
1. Зайди в https://git.much-data.ru/claw/kwork-api
2. **Settings** → **Pages**
3. Включить **Enable Pages**
4. Выбрать источник:
- **Source:** `gh-pages` branch
- **Folder:** `/ (root)`
5. **Save**
---
### **Шаг 2: Настроить Gitea Token**
1. https://git.much-data.ru → Settings → Applications
2. Создать токен с правами:
- `write:repository`
- `write:package`
3. Скопировать токен
4. В репозитории: **Settings** → **Secrets**
5. Добавить секрет: `GITEA_TOKEN` = твой токен
---
### **Шаг 3: Первый деплой**
```bash
cd /root/kwork-api
# Собрать документацию
uv run mkdocs build
# Проверить что site/ создан
ls -la site/
# Закоммитить и запушить
git add .
git commit -m "docs: initial documentation"
git push origin main
# CI/CD автоматически задеплоит на gh-pages
```
---
### **Шаг 4: Проверить**
После успешного CI/CD:
**Документация доступна:**
```
https://git.much-data.ru/claw/kwork-api/
```
Или если включён custom domain:
```
https://kwork-api.much-data.ru/
```
---
## 🔄 АВТОМАТИЧЕСКИЙ ДЕПЛОЙ
**Workflow срабатывает при:**
- ✅ Push в `main` ветку
- ✅ Создании тега релиза
**Что делает:**
1. Собирает MkDocs документацию
2. Пушит в `gh-pages` ветку
3. Gitea Pages автоматически обновляет сайт
---
## 🌐 CUSTOM DOMAIN (опционально)
### **DNS настройка:**
```
# В панели управления доменом much-data.ru
# CNAME запись:
kwork-api CNAME git.much-data.ru
# Или A запись:
kwork-api A 5.188.26.192
```
### **В Gitea Pages:**
1. **Settings** → **Pages**
2. **Custom Domain:** `kwork-api.much-data.ru`
3. **Save**
### **Создать CNAME файл:**
```bash
# В корне проекта (не в site/)
echo "kwork-api.much-data.ru" > static/CNAME
git add static/CNAME
git commit -m "docs: add custom domain"
git push
```
---
## 📊 СТРУКТУРА ВЕТКИ
```
main (исходный код)
├── src/
├── docs/
├── mkdocs.yml
└── .github/workflows/ci.yml
gh-pages (автоматически, только сайт)
├── index.html
├── api-reference/
├── search/
└── ...
```
---
## 🔍 TROUBLESHOOTING
### **Pages не включаются:**
```bash
# Проверить что Gitea версия >= 1.19
# Админ должен включить Pages в настройках сервера
```
### **CI/CD ошибка деплоя:**
```bash
# Проверить токен
# Проверить права токена (write:repository)
# Проверить что gh-pages ветка существует
```
### **404 на странице:**
```bash
# Подождать 1-2 минуты (Gitea обрабатывает)
# Проверить что site/ не пустой
# Проверить workflow логи
```
---
## 📋 ЧЕКЛИСТ
- [ ] Включить Pages в настройках репозитория
- [ ] Создать Gitea Token
- [ ] Добавить токен в Secrets (`GITEA_TOKEN`)
- [ ] Запушить изменения в main
- [ ] Дождаться CI/CD
- [ ] Проверить https://git.much-data.ru/claw/kwork-api/
- [ ] (Опционально) Настроить custom domain
---
## 🎯 АЛЬТЕРНАТИВЫ
Если Gitea Pages не работает:
### **1. Netlify (бесплатно)**
```bash
# Подключить репозиторий
# Build command: uv run mkdocs build
# Publish directory: site/
```
### **2. Vercel (бесплатно)**
```bash
# Аналогично Netlify
# Автоматический деплой из Git
```
### **3. Cloudflare Pages (бесплатно)**
```bash
# Быстрый CDN
# Автоматический HTTPS
```
### **4. Своё сервер (nginx)**
```bash
# Скопировать site/ на сервер
# Настроить nginx на /var/www/kwork-api-docs/
```
---
## 📞 ССЫЛКИ
- [Gitea Pages Documentation](https://docs.gitea.com/usage/pages)
- [MkDocs Documentation](https://www.mkdocs.org/)
- [Gitea Actions](https://docs.gitea.com/usage/actions)

131
docs/RELEASE.md Normal file
View File

@ -0,0 +1,131 @@
# Release Guide — kwork-api
## 📋 Стратегия версионирования
Используем **SemVer** (Semantic Versioning): `MAJOR.MINOR.PATCH`
- **MAJOR** — ломающие изменения API
- **MINOR** — новая функциональность (обратно совместимая)
- **PATCH** — багфиксы (обратно совместимые)
---
## 🚀 Процесс релиза
### 1. Подготовка
```bash
# Убедись что все тесты проходят
uv run pytest
# Проверь линтеры
uv run ruff check src/ tests/
# Проверь сборку
uv build
```
### 2. Обновление версии
```bash
# Обновить версию в pyproject.toml
# Например: 0.1.0 → 0.1.1
# Создать тег
git tag -a v0.1.1 -m "Release v0.1.1"
# Отпушить тег
git push origin v0.1.1
```
### 3. Автоматическая публикация
После пуша тега:
1. ✅ Запускается CI/CD pipeline
2. ✅ Прогоняются тесты
3. ✅ Собирается пакет
4. ✅ Публикуется в Gitea Registry
---
## 📦 Gitea Package Registry
**URL:** `https://git.much-data.ru/api/packages/claw/pypi`
**Установка:**
```bash
# Создать .pypirc в домашней директории
cat > ~/.pypirc << EOF
[pypi]
username = claw
password = YOUR_GITEA_TOKEN
[git.much-data.ru]
repository = https://git.much-data.ru/api/packages/claw/pypi
username = claw
password = YOUR_GITEA_TOKEN
EOF
# Установить из Gitea
uv pip install kwork-api --index-url https://git.much-data.ru/api/packages/claw/pypi
```
---
## 🔑 Получение Gitea Token
1. Зайди в https://git.much-data.ru
2. Профиль → Settings → Applications
3. Создать токен с правами `write:package`
4. Сохрани токен в секреты репозитория: `GITEA_TOKEN`
---
## 📊 Changelog
Ведётся в `CHANGELOG.md` по формату [Keep a Changelog](https://keepachangelog.com/).
### Пример:
```markdown
## [0.1.1] - 2026-03-23
### Fixed
- Исправлена ошибка аутентификации при истечении токена
### Changed
- Обновлены зависимости
## [0.1.0] - 2026-03-23
### Added
- Первый релиз
- Базовая функциональность клиента
- Документация 100%
```
---
## ✅ Чеклист перед релизом
- [ ] Все тесты проходят
- [ ] Линтеры без ошибок
- [ ] Документация обновлена
- [ ] CHANGELOG.md обновлён
- [ ] Версия в pyproject.toml обновлена
- [ ] Тег создан и отправлен
- [ ] CI/CD pipeline успешен
- [ ] Пакет опубликован
---
## 🔧 Ручная публикация (если нужно)
```bash
# Собрать
uv build
# Опубликовать
uv publish \
--publish-url https://git.much-data.ru/api/packages/claw/pypi \
--token YOUR_GITEA_TOKEN
```

281
docs/SEMANTIC_RELEASE.md Normal file
View File

@ -0,0 +1,281 @@
# Semantic Release — Автоматическое версионирование
## 📋 Обзор
**python-semantic-release** автоматически определяет версию на основе Conventional Commits.
**Как работает:**
```
Commit → Анализ сообщения → Определение типа → Bump версии → Тег → Релиз
```
---
## 🎯 CONVENTIONAL COMMITS
### **Формат коммита:**
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
### **Типы коммитов и влияние на версию:**
| Тип | Влияние | Пример | Версия |
|-----|---------|--------|--------|
| `feat` | **MINOR** | `feat: add new API endpoint` | 0.1.0 → 0.2.0 |
| `fix` | **PATCH** | `fix: handle timeout errors` | 0.1.0 → 0.1.1 |
| `perf` | **PATCH** | `perf: optimize HTTP requests` | 0.1.0 → 0.1.1 |
| `feat` + BREAKING | **MAJOR** | `feat: change auth method` | 0.1.0 → 1.0.0 |
| `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `build` | Нет | `docs: update README` | Без изменений |
---
## 📝 ПРИМЕРЫ КОММИТОВ
### **PATCH (багфиксы):**
```bash
git commit -m "fix: handle 404 error in catalog API"
git commit -m "fix(auth): restore session from token correctly"
git commit -m "perf: reduce HTTP connection overhead"
```
### **MINOR (новая функциональность):**
```bash
git commit -m "feat: add batch kwork details endpoint"
git commit -m "feat(projects): add get_payer_orders method"
git commit -m "feat: support HTTP/2 for faster requests"
```
### **MAJOR (ломающие изменения):**
```bash
git commit -m "feat: redesign authentication flow
BREAKING CHANGE: login() now returns KworkClient instead of tuple
Migration:
Before: token, cookies = await login(user, pass)
After: client = await KworkClient.login(user, pass)
"
```
### **Без влияния на версию:**
```bash
git commit -m "docs: add usage examples to README"
git commit -m "test: increase coverage to 95%"
git commit -m "style: fix formatting with ruff"
git commit -m "refactor: simplify error handling"
git commit -m "chore: update dependencies"
git commit -m "ci: add Gitea Actions workflow"
git commit -m "build: configure UV build system"
```
---
## 🔄 WORKFLOW
### **Разработка:**
```bash
# Делай коммиты по Conventional Commits
git commit -m "feat: add new endpoint"
git commit -m "fix: handle edge case"
git commit -m "docs: update documentation"
# Пуш в main
git push origin main
```
### **CI/CD (автоматически):**
```
1. Тесты запускаются
2. Сборка пакета
3. Semantic Release анализирует коммиты
4. Определяет тип версии (MAJOR/MINOR/PATCH)
5. Обновляет версию в pyproject.toml и __init__.py
6. Создаёт Git тег
7. Генерирует CHANGELOG
8. Создаёт релиз в Gitea
9. Публикует пакет в Gitea Registry
```
### **Результат:**
```
✅ v0.1.1 создан
✅ CHANGELOG.md обновлён
✅ Пакет опубликован
✅ Релиз в Gitea создан
```
---
## 🔧 РУЧНОЕ УПРАВЛЕНИЕ
### **Проверить следующую версию:**
```bash
cd /root/kwork-api
uv run semantic-release version --print
```
### **Сгенерировать CHANGELOG:**
```bash
uv run semantic-release changelog
```
### **Создать релиз вручную:**
```bash
# Bump версии
uv run semantic-release version --no-push
# Проверить что изменилось
git diff
# Запушить
git push origin main --tags
```
---
## 📊 ПРИМЕР ИСТОРИИ ВЕРСИЙ
```
v0.1.0 (2026-03-23)
- Initial release
- Complete API client
- 100% documentation
v0.1.1 (2026-03-24)
- fix: handle timeout errors
- fix: restore session correctly
v0.2.0 (2026-03-25)
- feat: add batch endpoint
- feat: support HTTP/2
v0.2.1 (2026-03-26)
- perf: optimize requests
v1.0.0 (2026-03-27)
- feat: new authentication
- BREAKING CHANGE: API changed
```
---
## ⚙️ КОНФИГУРАЦИЯ
**Файл:** `pyproject.toml`
```toml
[tool.semantic_release]
version_toml = ["pyproject.toml:project.version"]
version_variables = ["src/kwork_api/__init__.py:__version__"]
branch = "main"
build_command = "uv build"
commit_parser = "angular"
tag_format = "v{version}"
[tool.semantic_release.commit_parser_options]
minor_tags = ["feat"]
patch_tags = ["fix", "perf"]
breaking_change_tags = ["feat"]
[tool.semantic_release.remote]
type = "gitea"
domain = "https://git.much-data.ru"
owner = "claw"
repo_name = "kwork-api"
```
---
## 🚨 TROUBLESHOOTING
### **Ошибка: "No commits to release"**
```bash
# Значит не было коммитов с типами feat/fix/perf
# Сделай коммит с правильным форматом
git commit -m "feat: add something new"
```
### **Ошибка: "Gitea token invalid"**
```bash
# Проверь токен
# Settings → Applications → Create new token
# Права: write:repository, write:package
# Обнови секрет в Gitea Actions
```
### **Версия не обновляется**
```bash
# Проверь конфигурацию
uv run semantic-release --version
# Проверь что __version__ есть в __init__.py
grep "__version__" src/kwork_api/__init__.py
```
---
## 📋 ЧЕКЛИСТ ПЕРЕД ПУШЕМ
- [ ] Коммиты по Conventional Commits
- [ ] Тесты проходят
- [ ] CHANGELOG обновлён (автоматически)
- [ ] Версия в __init__.py совпадает
- [ ] GITEA_TOKEN настроен в секретах
---
## 🎯 BEST PRACTICES
### **✅ Делай:**
```bash
# Атомарные коммиты
git commit -m "feat: add user endpoint"
git commit -m "fix: handle 404 error"
# Понятные описания
git commit -m "fix: restore session from saved token"
# Scope для больших изменений
git commit -m "feat(auth): add OAuth2 support"
```
### **Не делай:**
```bash
# Слишком общие
git commit -m "fix stuff"
# Несколько изменений в одном
git commit -m "feat: add user endpoint and fix auth and update docs"
# Не по формату
git commit -m "added new feature"
```
---
## 📞 ССЫЛКИ
- [python-semantic-release](https://python-semantic-release.readthedocs.io/)
- [Conventional Commits](https://www.conventionalcommits.org/)
- [Angular Commit Guidelines](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit)

View File

@ -1,9 +1,9 @@
# API Reference
Auto-generated API documentation is available below.
Complete API documentation for Kwork API client.
::: kwork_api.client.KworkClient
handler: python
options:
show_root_heading: true
show_source: true
## Modules
- [Client](api/client.md) Main client class and API groups
- [Models](api/models.md) — Pydantic models for API responses
- [Errors](api/errors.md) — Exception classes

3
docs/api/client.md Normal file
View File

@ -0,0 +1,3 @@
# Client API
::: kwork_api.client.KworkClient

5
docs/api/errors.md Normal file
View File

@ -0,0 +1,5 @@
# Errors
Exception classes for API errors.
::: kwork_api.errors

5
docs/api/models.md Normal file
View File

@ -0,0 +1,5 @@
# Models
Pydantic models for API responses.
::: kwork_api.models

0
docs/api_reference.md Normal file
View File

View File

@ -1,47 +1,109 @@
# Kwork API
# Kwork API — Python Client
Unofficial Python client for Kwork.ru API.
## Features
- Complete async API client (45+ endpoints)
- Pydantic models for all responses
- Two-step authentication (cookies + web_auth_token)
- Comprehensive error handling
- HTTP/2 support
## Installation
```bash
uv add kwork-api
# or
pip install kwork-api
```
Or with UV:
```bash
uv add kwork-api
```
## Quick Start
### Login with credentials
```python
from kwork_api import KworkClient
# Login with credentials
async with await KworkClient.login("username", "password") as client:
# Get catalog
catalog = await client.catalog.get_list(page=1)
# Get projects
projects = await client.projects.get_list()
# Get user info
user = await client.user.get_info()
# Authenticate
client = await KworkClient.login("username", "password")
# Get catalog
catalog = await client.catalog.get_list(page=1)
# Get projects
projects = await client.projects.get_list(page=1)
# Close when done
await client.close()
```
## Documentation
### Using context manager
- [Usage Guide](usage.md) — Examples and best practices
- [API Reference](api-reference.md) — Complete API documentation
```python
async with await KworkClient.login("username", "password") as client:
catalog = await client.catalog.get_list(page=1)
# Client automatically closes
```
## Links
### Save and restore session
- [Source Code](https://git.much-data.ru/much-data/kwork-api)
- [Issues](https://git.much-data.ru/much-data/kwork-api/issues)
- [Gitea Packages](https://git.much-data.ru/much-data/kwork-api/packages)
```python
# Save credentials after login
client = await KworkClient.login("username", "password")
# Option 1: Save token only
token = client.token
# Option 2: Save full credentials (token + cookies)
creds = client.credentials
import json
with open("session.json", "w") as f:
json.dump(creds, f)
# Later, restore session
client = KworkClient(token=token)
# or
client = KworkClient(**creds)
user_info = await client.user.get_info()
```
## API Overview
### Catalog API
- `client.catalog.get_list()` — Get kworks catalog
- `client.catalog.get_details(kwork_id)` — Get kwork details
### Projects API
- `client.projects.get_list()` — Get freelance projects
- `client.projects.get_payer_orders()` — Your orders as customer
- `client.projects.get_worker_orders()` — Your orders as performer
### User API
- `client.user.get_info()` — Get user profile
- `client.user.get_reviews()` — Get user reviews
- `client.user.get_favorite_kworks()` — Get favorite kworks
### Settings & Preferences
- `client.get_wants()` — User preferences
- `client.get_kworks_status()` — Kworks status
- `client.update_settings()` — Update settings
- `client.go_offline()` — Set offline status
See [API Reference](api-reference.md) for full documentation.
## Error Handling
```python
from kwork_api import KworkError, KworkAuthError, KworkApiError
try:
await client.catalog.get_list()
except KworkAuthError:
print("Invalid credentials")
except KworkApiError as e:
print(f"API error: {e.status_code}")
except KworkError as e:
print(f"General error: {e.message}")
```

View File

@ -1,132 +0,0 @@
# Usage Guide
## Authentication
### Login with credentials
```python
from kwork_api import KworkClient
async with await KworkClient.login("username", "password") as client:
# Your code here
pass
```
### Restore session from token
```python
from kwork_api import KworkClient
# Save token after first login
client = await KworkClient.login("username", "password")
token = client.token # Save this
# Later, restore session
client = KworkClient(token="saved_token")
user = await client.user.get_info()
```
## API Groups
### Catalog API
```python
# Get catalog list
catalog = await client.catalog.get_list(page=1, category_id=1)
# Get kwork details
kwork = await client.catalog.get_details(kwork_id=123)
```
### Projects API (Freelance)
```python
# Get projects list
projects = await client.projects.get_list(page=1)
# Get your orders
my_orders = await client.projects.get_payer_orders()
```
### User API
```python
# Get user info
user = await client.user.get_info()
# Get reviews
reviews = await client.user.get_reviews(user_id=123)
```
### Reference API
```python
# Get cities
cities = await client.reference.get_cities()
# Get countries
countries = await client.reference.get_countries()
# Get categories
categories = await client.reference.get_categories()
```
### Notifications API
```python
# Get notifications
notifications = await client.notifications.get_list()
# Get dialogs
dialogs = await client.notifications.get_dialogs()
```
## Error Handling
```python
from kwork_api import KworkApiError, KworkAuthError
try:
catalog = await client.catalog.get_list()
except KworkAuthError as e:
print(f"Authentication failed: {e}")
except KworkApiError as e:
print(f"API error: {e.status_code} - {e.message}")
```
## Best Practices
### Rate Limiting
Kwork doesn't have official rate limits, but be respectful:
```python
import asyncio
for page in range(1, 11):
catalog = await client.catalog.get_list(page=page)
await asyncio.sleep(0.5) # 500ms delay
```
### Session Management
Always use context manager for automatic cleanup:
```python
async with await KworkClient.login("user", "pass") as client:
# Client automatically closed
pass
```
### Save Token
Save token after first login to avoid repeated authentication:
```python
# First login
client = await KworkClient.login("user", "pass")
save_token(client.token) # Save to secure storage
# Later
client = KworkClient(token=load_token())
```

View File

@ -1,9 +1,9 @@
site_name: Kwork API
site_description: Unofficial Python client for Kwork.ru API
site_url: https://git.much-data.ru/much-data/kwork-api
site_url: https://github.com/claw/kwork-api
repo_name: much-data/kwork-api
repo_url: https://git.much-data.ru/much-data/kwork-api
repo_name: claw/kwork-api
repo_url: https://github.com/claw/kwork-api
theme:
name: material
@ -58,7 +58,7 @@ markdown_extensions:
- pymdownx.keys
- pymdownx.magiclink:
repo_url_shorthand: true
user: much-data
user: claw
repo: kwork-api
- pymdownx.mark
- pymdownx.smartsymbols
@ -71,5 +71,9 @@ markdown_extensions:
nav:
- Home: index.md
- API Reference: api-reference.md
- Usage: usage.md
- API Reference:
- Overview: api-reference.md
- Client: api/client.md
- Models: api/models.md
- Errors: api/errors.md
- Examples: examples.md

22
pydoc-markdown.yml Normal file
View File

@ -0,0 +1,22 @@
loaders:
- type: python
search_path: [src]
packages: [kwork_api]
processors:
- type: filter
skip_empty_modules: true
documented_only: true
do_not_filter_modules: true
expression: "not (name.startswith('_') and not name.startswith('__'))"
- type: smart
- type: crossref
renderer:
type: markdown
filename: api_reference.md
render_toc: true
descriptive_class_title: false
descriptive_module_title: true
add_method_class_prefix: true
add_member_class_prefix: false

View File

@ -27,9 +27,18 @@ dependencies = [
"structlog>=24.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-cov>=4.0.0",
"pytest-asyncio>=0.23.0",
"respx>=0.20.0",
"ruff>=0.3.0",
]
[project.urls]
Homepage = "https://git.much-data.ru/much-data/kwork-api"
Repository = "https://git.much-data.ru/much-data/kwork-api.git"
Homepage = "http://5.188.26.192:3000/claw/kwork-api"
Repository = "http://5.188.26.192:3000/claw/kwork-api.git"
[build-system]
requires = ["hatchling"]
@ -40,59 +49,50 @@ packages = ["src/kwork_api"]
[dependency-groups]
dev = [
# Testing
"pytest>=8.0.0",
"pytest-cov>=4.0.0",
"pytest-asyncio>=0.23.0",
"pytest-html>=4.0.0",
"respx>=0.20.0",
# Linting & formatting
"ruff>=0.3.0",
# CI tools
"python-semantic-release>=9.0.0",
"pip-audit>=2.7.0",
]
docs = [
# Documentation (optional, for local development)
"pydoc-markdown>=4.8.2",
"mkdocs>=1.6.1",
"mkdocs-material>=9.7.6",
"mkdocstrings[python]>=1.0.3",
"mkdocstrings>=1.0.3",
"mkdocstrings-python>=2.0.3",
"python-semantic-release>=10.5.3",
]
# ========== Tool Configurations ==========
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
asyncio_mode = "auto"
markers = [
"unit: Unit tests with mocks",
"integration: Integration tests with real API",
]
addopts = "-v --cov=kwork_api --cov-report=term-missing"
[tool.ruff]
target-version = "py310"
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long
]
ignore = ["E501"]
[tool.ruff.lint.isort]
known-first-party = ["kwork_api"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "-v --tb=short"
[tool.coverage.run]
source = ["src/kwork_api"]
source = ["kwork_api"]
branch = true
[tool.coverage.report]
@ -100,22 +100,30 @@ exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
# ========== Semantic Release ==========
# ============================================
# Python Semantic Release Configuration
# ============================================
[tool.semantic_release]
version_toml = ["pyproject.toml:project.version"]
version_variables = ["src/kwork_api/__init__.py:__version__"]
version_variables = [
"src/kwork_api/__init__.py:__version__",
]
branch = "main"
build_command = "uv build"
commit_parser = "angular"
upload_to_vcs_release = true
tag_format = "v{version}"
major_on_zero = true
allow_zero_version = true
[tool.semantic_release.branches.main]
match = "main"
prerelease = false
[tool.semantic_release.commit_parser_options]
allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"]
minor_tags = ["feat"]
patch_tags = ["fix", "perf"]
breaking_change_tags = ["feat"]
@ -123,10 +131,24 @@ breaking_change_tags = ["feat"]
[tool.semantic_release.remote]
type = "gitea"
domain = "https://git.much-data.ru"
owner = "much-data"
owner = "claw"
repo_name = "kwork-api"
token = { env = "GH_TOKEN" }
token = { env = "GITEA_TOKEN" }
[tool.semantic_release.publish]
dist_glob_patterns = ["dist/*"]
upload_to_vcs_release = true
[tool.semantic_release.changelog]
template_dir = "templates"
changelog_file = "CHANGELOG.md"
exclude_commit_patterns = [
"chore\\(release\\):.*",
"ci\\(release\\):.*",
]
[tool.semantic_release.changelog.environment]
trim_blocks = true
lstrip_blocks = true

View File

@ -28,7 +28,7 @@ from .models import (
ProjectsResponse,
)
__version__ = "0.1.0" # Updated by semantic-release
__version__ = "0.1.0"
__all__ = [
"KworkClient",
"KworkError",

View File

@ -15,16 +15,6 @@
from typing import Any, Optional
__all__ = [
"KworkError",
"KworkAuthError",
"KworkApiError",
"KworkNotFoundError",
"KworkRateLimitError",
"KworkValidationError",
"KworkNetworkError",
]
class KworkError(Exception):
"""

View File

@ -1,5 +0,0 @@
# Kwork.ru credentials for E2E testing
# Copy this file to .env and fill in your test credentials
KWORK_USERNAME=your_test_username
KWORK_PASSWORD=your_test_password

View File

@ -1,136 +0,0 @@
# End-to-End (E2E) Testing
E2E тесты требуют реальных credentials Kwork.ru и запускаются **только локально** (не в CI).
## ⚠️ Предупреждение
- **Не запускай в CI** — требуются реальные credentials
- **Используй тестовый аккаунт** — не основной аккаунт Kwork
- **Rate limiting** — добавляй задержки между запросами
---
## 🔧 Настройка
### 1. Создай файл окружения
```bash
cd /root/kwork-api
cp tests/e2e/.env.example tests/e2e/.env
```
### 2. Заполни credentials
```bash
# tests/e2e/.env
KWORK_USERNAME=your_test_username
KWORK_PASSWORD=your_test_password
```
### 3. Установи зависимости
```bash
uv sync --group dev
```
---
## 🚀 Запуск тестов
### Все E2E тесты
```bash
uv run pytest tests/e2e/ -v
```
### Конкретный тест
```bash
uv run pytest tests/e2e/test_auth.py -v
uv run pytest tests/e2e/test_catalog.py::test_get_catalog_list -v
```
### С задержками (rate limiting)
```bash
uv run pytest tests/e2e/ -v --slowmo=1
```
---
## 📁 Структура тестов
```
tests/e2e/
├── README.md # Этот файл
├── .env.example # Шаблон для credentials
├── conftest.py # Фикстуры и setup
├── test_auth.py # Аутентификация
├── test_catalog.py # Каталог кворков
├── test_projects.py # Биржа проектов
└── test_user.py # Пользовательские данные
```
---
## 🧪 Пример теста
```python
import pytest
from kwork_api import KworkClient
@pytest.mark.e2e
async def test_get_user_info():
"""E2E тест: получение информации о пользователе."""
async with await KworkClient.login(
username="test_user",
password="test_pass"
) as client:
user = await client.user.get_info()
assert user.username == "test_user"
assert user.balance >= 0
```
---
## 🏷️ Маркировка тестов
E2E тесты маркируются `@pytest.mark.e2e` для изоляции:
```bash
# Запустить только unit тесты (исключить e2e)
uv run pytest tests/ -v -m "not e2e"
# Запустить только e2e тесты
uv run pytest tests/ -v -m e2e
```
---
## 🔒 Безопасность
1. **Никогда не коммить `.env`** — добавлен в `.gitignore`
2. **Используй тестовый аккаунт** — не основной
3. **Не сохраняй токены в коде** — только через env vars
---
## 🐛 Troubleshooting
### Ошибка аутентификации
```
KworkAuthError: Invalid credentials
```
**Решение:** Проверь credentials в `.env`
### Rate limit
```
KworkApiError: Too many requests
```
**Решение:** Запусти с задержкой: `pytest --slowmo=2`
### Session expired
```
KworkAuthError: Session expired
```
**Решение:** Перезапусти тесты (session создаётся заново)

View File

@ -1,59 +0,0 @@
"""
E2E тесты для Kwork API.
Требуют реальных credentials и запускаются только локально.
"""
import os
import pytest
from pathlib import Path
from dotenv import load_dotenv
# Загружаем .env
load_dotenv(Path(__file__).parent / ".env")
@pytest.fixture(scope="session")
def kwork_credentials():
"""Credentials для тестового аккаунта."""
return {
"username": os.getenv("KWORK_USERNAME"),
"password": os.getenv("KWORK_PASSWORD"),
}
@pytest.fixture(scope="session")
def require_credentials(kwork_credentials):
"""Пропускает тест если нет credentials."""
if not kwork_credentials["username"] or not kwork_credentials["password"]:
pytest.skip(
"E2E credentials not set. "
"Copy tests/e2e/.env.example to tests/e2e/.env and fill in credentials."
)
return kwork_credentials
@pytest.fixture(scope="function")
def slowmo(request):
"""Задержка между тестами для rate limiting."""
slowmo = request.config.getoption("--slowmo", default=0)
if slowmo > 0:
import time
time.sleep(slowmo)
def pytest_configure(config):
"""Регистрация маркера e2e."""
config.addinivalue_line(
"markers", "e2e: mark test as end-to-end (requires credentials)"
)
def pytest_addoption(parser):
"""Добавляет опцию --slowmo."""
parser.addoption(
"--slowmo",
type=float,
default=0,
help="Delay between tests in seconds (for rate limiting)"
)

View File

@ -1,52 +0,0 @@
"""
E2E тесты аутентификации.
"""
import pytest
from kwork_api import KworkClient
from kwork_api.errors import KworkAuthError
@pytest.mark.e2e
async def test_login_success(require_credentials):
"""E2E: Успешная аутентификация."""
client = await KworkClient.login(
username=require_credentials["username"],
password=require_credentials["password"]
)
try:
assert client.token is not None
assert len(client.token) > 0
finally:
await client.aclose()
@pytest.mark.e2e
async def test_login_invalid_credentials():
"""E2E: Неверные credentials."""
with pytest.raises(KworkAuthError):
await KworkClient.login(
username="invalid_user_12345",
password="invalid_pass_12345"
)
@pytest.mark.e2e
async def test_restore_session(require_credentials):
"""E2E: Восстановление сессии из токена."""
# First login
client1 = await KworkClient.login(
username=require_credentials["username"],
password=require_credentials["password"]
)
token = client1.token
await client1.aclose()
# Restore from token
client2 = KworkClient(token=token)
try:
user = await client2.user.get_info()
assert user.username == require_credentials["username"]
finally:
await client2.aclose()

View File

@ -1,743 +0,0 @@
"""
Extended unit tests for KworkClient to improve coverage.
These tests cover additional API endpoints and edge cases.
"""
import pytest
import respx
from httpx import Response
from kwork_api import KworkClient, KworkAuthError, KworkApiError
from kwork_api.models import (
NotificationsResponse,
Kwork,
Dialog,
City,
Country,
TimeZone,
Feature,
Badge,
Project,
)
BASE_URL = "https://api.kwork.ru"
class TestClientProperties:
"""Test client properties and initialization."""
def test_token_property(self):
"""Test token property getter."""
client = KworkClient(token="test_token_123")
assert client.token == "test_token_123"
def test_token_property_none(self):
"""Test token property when no token."""
client = KworkClient()
assert client.token is None
def test_cookies_property_empty(self):
"""Test cookies property with no cookies."""
client = KworkClient(token="test")
assert client.cookies == {}
def test_cookies_property_with_cookies(self):
"""Test cookies property returns copy."""
client = KworkClient(token="test", cookies={"userId": "123", "session": "abc"})
cookies = client.cookies
assert cookies == {"userId": "123", "session": "abc"}
cookies["modified"] = "value"
assert "modified" not in client.cookies
def test_credentials_property(self):
"""Test credentials property returns token and cookies."""
client = KworkClient(token="test_token", cookies={"userId": "123"})
creds = client.credentials
assert creds["token"] == "test_token"
assert creds["cookies"] == {"userId": "123"}
def test_credentials_property_no_cookies(self):
"""Test credentials with no cookies."""
client = KworkClient(token="test_token")
creds = client.credentials
assert creds["token"] == "test_token"
assert creds["cookies"] is None
class TestCatalogAPIExtended:
"""Extended tests for CatalogAPI."""
@respx.mock
async def test_get_details_extra(self):
"""Test get_details_extra endpoint."""
client = KworkClient(token="test")
mock_data = {
"id": 456,
"title": "Extra Details Kwork",
"extra_field": "extra_value",
}
respx.post(f"{BASE_URL}/getKworkDetailsExtra").mock(
return_value=Response(200, json=mock_data)
)
result = await client.catalog.get_details_extra(456)
assert result["id"] == 456
assert result["extra_field"] == "extra_value"
class TestProjectsAPIExtended:
"""Extended tests for ProjectsAPI."""
@respx.mock
async def test_get_payer_orders(self):
"""Test getting payer orders."""
client = KworkClient(token="test")
mock_data = {
"orders": [
{"id": 101, "title": "Order 1", "amount": 5000.0, "status": "active"},
]
}
respx.post(f"{BASE_URL}/payerOrders").mock(
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_payer_orders()
assert isinstance(result, list)
assert len(result) == 1
assert isinstance(result[0], Project)
@respx.mock
async def test_get_worker_orders(self):
"""Test getting worker orders."""
client = KworkClient(token="test")
mock_data = {
"orders": [
{"id": 202, "title": "Worker Order", "amount": 3000.0, "status": "completed"},
]
}
respx.post(f"{BASE_URL}/workerOrders").mock(
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_worker_orders()
assert isinstance(result, list)
assert len(result) == 1
class TestUserAPI:
"""Tests for UserAPI."""
@respx.mock
async def test_get_info(self):
"""Test getting user info."""
client = KworkClient(token="test")
mock_data = {
"userId": 12345,
"username": "testuser",
"email": "test@example.com",
"balance": 50000.0,
}
respx.post(f"{BASE_URL}/user").mock(
return_value=Response(200, json=mock_data)
)
result = await client.user.get_info()
assert result["userId"] == 12345
assert result["username"] == "testuser"
@respx.mock
async def test_get_reviews(self):
"""Test getting user reviews."""
client = KworkClient(token="test")
mock_data = {
"reviews": [
{"id": 1, "rating": 5, "comment": "Great work!", "author": {"id": 999, "username": "client1"}},
],
"pagination": {"current_page": 1, "total_pages": 5},
}
respx.post(f"{BASE_URL}/userReviews").mock(
return_value=Response(200, json=mock_data)
)
result = await client.user.get_reviews(user_id=12345, page=1)
assert len(result.reviews) == 1
assert result.reviews[0].rating == 5
@respx.mock
async def test_get_favorite_kworks(self):
"""Test getting favorite kworks."""
client = KworkClient(token="test")
mock_data = {
"kworks": [
{"id": 100, "title": "Favorite Kwork 1", "price": 2000.0},
{"id": 101, "title": "Favorite Kwork 2", "price": 3000.0},
]
}
respx.post(f"{BASE_URL}/favoriteKworks").mock(
return_value=Response(200, json=mock_data)
)
result = await client.user.get_favorite_kworks()
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], Kwork)
class TestReferenceAPI:
"""Tests for ReferenceAPI."""
@respx.mock
async def test_get_cities(self):
"""Test getting cities list."""
client = KworkClient(token="test")
mock_data = {
"cities": [
{"id": 1, "name": "Москва", "country_id": 1},
{"id": 2, "name": "Санкт-Петербург", "country_id": 1},
]
}
respx.post(f"{BASE_URL}/cities").mock(
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_cities()
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], City)
@respx.mock
async def test_get_countries(self):
"""Test getting countries list."""
client = KworkClient(token="test")
mock_data = {
"countries": [
{"id": 1, "name": "Россия", "code": "RU"},
{"id": 2, "name": "Беларусь", "code": "BY"},
]
}
respx.post(f"{BASE_URL}/countries").mock(
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_countries()
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], Country)
@respx.mock
async def test_get_timezones(self):
"""Test getting timezones list."""
client = KworkClient(token="test")
mock_data = {
"timezones": [
{"id": 1, "name": "Europe/Moscow", "offset": "+03:00"},
{"id": 2, "name": "Europe/Kaliningrad", "offset": "+02:00"},
]
}
respx.post(f"{BASE_URL}/timezones").mock(
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_timezones()
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], TimeZone)
@respx.mock
async def test_get_features(self):
"""Test getting features list."""
client = KworkClient(token="test")
mock_data = {
"features": [
{"id": 1, "name": "Feature 1", "category_id": 5, "price": 1000, "type": "extra"},
{"id": 2, "name": "Feature 2", "category_id": 5, "price": 2000, "type": "extra"},
]
}
respx.post(f"{BASE_URL}/getAvailableFeatures").mock(
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_features()
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], Feature)
@respx.mock
async def test_get_public_features(self):
"""Test getting public features list."""
client = KworkClient(token="test")
mock_data = {
"features": [
{"id": 10, "name": "Public Feature", "is_public": True, "price": 500, "type": "extra"},
]
}
respx.post(f"{BASE_URL}/getPublicFeatures").mock(
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_public_features()
assert isinstance(result, list)
assert len(result) == 1
@respx.mock
async def test_get_badges_info(self):
"""Test getting badges info."""
client = KworkClient(token="test")
mock_data = {
"badges": [
{"id": 1, "name": "Pro Seller", "icon_url": "https://example.com/badge1.png"},
{"id": 2, "name": "Verified", "icon_url": "https://example.com/badge2.png"},
]
}
respx.post(f"{BASE_URL}/getBadgesInfo").mock(
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_badges_info()
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], Badge)
class TestNotificationsAPI:
"""Tests for NotificationsAPI."""
@respx.mock
async def test_get_list(self):
"""Test getting notifications list."""
client = KworkClient(token="test")
mock_data = {
"notifications": [
{"id": 1, "type": "order", "title": "New Order", "message": "New order received", "is_read": False},
{"id": 2, "type": "message", "title": "New Message", "message": "You have a new message", "is_read": True},
],
"unread_count": 5,
}
respx.post(f"{BASE_URL}/notifications").mock(
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_list()
assert isinstance(result, NotificationsResponse)
assert result.unread_count == 5
@respx.mock
async def test_fetch(self):
"""Test fetching notifications."""
client = KworkClient(token="test")
mock_data = {
"notifications": [
{"id": 3, "type": "system", "title": "System Update", "message": "System update available", "is_read": False},
],
"unread_count": 1,
}
respx.post(f"{BASE_URL}/notificationsFetch").mock(
return_value=Response(200, json=mock_data)
)
result = await client.notifications.fetch()
assert isinstance(result, NotificationsResponse)
assert len(result.notifications) == 1
@respx.mock
async def test_get_dialogs(self):
"""Test getting dialogs."""
client = KworkClient(token="test")
mock_data = {
"dialogs": [
{"id": 1, "user_id": 100, "last_message": "Hello", "unread": 2},
{"id": 2, "user_id": 200, "last_message": "Hi", "unread": 0},
]
}
respx.post(f"{BASE_URL}/dialogs").mock(
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_dialogs()
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], Dialog)
@respx.mock
async def test_get_blocked_dialogs(self):
"""Test getting blocked dialogs."""
client = KworkClient(token="test")
mock_data = {
"dialogs": [
{"id": 99, "user_id": 999, "last_message": "Spam", "blocked": True},
]
}
respx.post(f"{BASE_URL}/blockedDialogList").mock(
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_blocked_dialogs()
assert isinstance(result, list)
assert len(result) == 1
class TestOtherAPI:
"""Tests for OtherAPI."""
@respx.mock
async def test_get_wants(self):
"""Test getting wants."""
client = KworkClient(token="test")
mock_data = {
"wants": [{"id": 1, "title": "I need a logo"}],
"count": 1,
}
respx.post(f"{BASE_URL}/myWants").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_wants()
assert "wants" in result
assert result["count"] == 1
@respx.mock
async def test_get_wants_status(self):
"""Test getting wants status."""
client = KworkClient(token="test")
mock_data = {
"active_wants": 5,
"completed_wants": 10,
}
respx.post(f"{BASE_URL}/wantsStatusList").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_wants_status()
assert result["active_wants"] == 5
@respx.mock
async def test_get_kworks_status(self):
"""Test getting kworks status."""
client = KworkClient(token="test")
mock_data = {
"active_kworks": 3,
"total_sales": 50,
}
respx.post(f"{BASE_URL}/kworksStatusList").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_kworks_status()
assert result["active_kworks"] == 3
@respx.mock
async def test_get_offers(self):
"""Test getting offers."""
client = KworkClient(token="test")
mock_data = {
"offers": [{"id": 1, "title": "Special offer"}],
}
respx.post(f"{BASE_URL}/offers").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_offers()
assert "offers" in result
@respx.mock
async def test_get_exchange_info(self):
"""Test getting exchange info."""
client = KworkClient(token="test")
mock_data = {
"usd_rate": 90.5,
"eur_rate": 98.2,
}
respx.post(f"{BASE_URL}/exchangeInfo").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_exchange_info()
assert result["usd_rate"] == 90.5
@respx.mock
async def test_get_channel(self):
"""Test getting channel info."""
client = KworkClient(token="test")
mock_data = {
"channel_id": "main",
"name": "Main Channel",
}
respx.post(f"{BASE_URL}/getChannel").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_channel()
assert result["channel_id"] == "main"
@respx.mock
async def test_get_in_app_notification(self):
"""Test getting in-app notifications."""
client = KworkClient(token="test")
mock_data = {
"notifications": [{"id": 1, "message": "App update"}],
}
respx.post(f"{BASE_URL}/getInAppNotification").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_in_app_notification()
assert "notifications" in result
@respx.mock
async def test_get_security_user_data(self):
"""Test getting security user data."""
client = KworkClient(token="test")
mock_data = {
"two_factor_enabled": True,
"last_login": "2024-01-01T00:00:00Z",
}
respx.post(f"{BASE_URL}/getSecurityUserData").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_security_user_data()
assert result["two_factor_enabled"] is True
@respx.mock
async def test_is_dialog_allow_true(self):
"""Test is_dialog_allow returns True."""
client = KworkClient(token="test")
respx.post(f"{BASE_URL}/isDialogAllow").mock(
return_value=Response(200, json={"allowed": True})
)
result = await client.other.is_dialog_allow(12345)
assert result is True
@respx.mock
async def test_is_dialog_allow_false(self):
"""Test is_dialog_allow returns False."""
client = KworkClient(token="test")
respx.post(f"{BASE_URL}/isDialogAllow").mock(
return_value=Response(200, json={"allowed": False})
)
result = await client.other.is_dialog_allow(67890)
assert result is False
@respx.mock
async def test_get_viewed_kworks(self):
"""Test getting viewed kworks."""
client = KworkClient(token="test")
mock_data = {
"kworks": [
{"id": 500, "title": "Viewed Kwork", "price": 1500.0},
]
}
respx.post(f"{BASE_URL}/viewedCatalogKworks").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_viewed_kworks()
assert isinstance(result, list)
assert len(result) == 1
assert isinstance(result[0], Kwork)
@respx.mock
async def test_get_favorite_categories(self):
"""Test getting favorite categories."""
client = KworkClient(token="test")
mock_data = {
"categories": [1, 5, 10, 15],
}
respx.post(f"{BASE_URL}/favoriteCategories").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_favorite_categories()
assert isinstance(result, list)
assert 1 in result
assert 5 in result
@respx.mock
async def test_update_settings(self):
"""Test updating settings."""
client = KworkClient(token="test")
mock_data = {
"success": True,
"updated": {"notifications_enabled": False},
}
respx.post(f"{BASE_URL}/updateSettings").mock(
return_value=Response(200, json=mock_data)
)
settings = {"notifications_enabled": False, "theme": "dark"}
result = await client.other.update_settings(settings)
assert result["success"] is True
@respx.mock
async def test_go_offline(self):
"""Test going offline."""
client = KworkClient(token="test")
mock_data = {
"success": True,
"status": "offline",
}
respx.post(f"{BASE_URL}/offline").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.go_offline()
assert result["success"] is True
assert result["status"] == "offline"
@respx.mock
async def test_get_actor(self):
"""Test getting actor info."""
client = KworkClient(token="test")
mock_data = {
"actor_id": 123,
"name": "Test Actor",
}
respx.post(f"{BASE_URL}/actor").mock(
return_value=Response(200, json=mock_data)
)
result = await client.other.get_actor()
assert result["actor_id"] == 123
class TestClientInternals:
"""Tests for internal client methods."""
def test_handle_response_success(self):
"""Test _handle_response with successful response."""
import httpx
client = KworkClient(token="test")
response = httpx.Response(200, json={"success": True, "data": "test"})
result = client._handle_response(response)
assert result["success"] is True
assert result["data"] == "test"
def test_handle_response_error(self):
"""Test _handle_response with error response."""
import httpx
client = KworkClient(token="test")
response = httpx.Response(400, json={"message": "Bad request"})
with pytest.raises(KworkApiError) as exc_info:
client._handle_response(response)
assert exc_info.value.status_code == 400
@respx.mock
async def test_request_method(self):
"""Test _request method directly."""
client = KworkClient(token="test")
mock_data = {"result": "success"}
respx.post(f"{BASE_URL}/test-endpoint").mock(
return_value=Response(200, json=mock_data)
)
result = await client._request("POST", "/test-endpoint", json={"param": "value"})
assert result["result"] == "success"
async def test_context_manager_creates_client(self):
"""Test that context manager creates httpx client."""
async with KworkClient(token="test") as client:
assert client.token == "test"
assert client._client is None or client._client.is_closed

435
uv.lock generated
View File

@ -58,30 +58,33 @@ wheels = [
]
[[package]]
name = "boolean-py"
version = "5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
]
[[package]]
name = "cachecontrol"
version = "0.14.4"
name = "black"
version = "23.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "msgpack" },
{ name = "requests" },
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/f4/a57cde4b60da0e249073009f4a9087e9e0a955deae78d3c2a493208d0c5c/black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", size = 620809, upload-time = "2023-12-22T23:06:17.382Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
]
[package.optional-dependencies]
filecache = [
{ name = "filelock" },
{ url = "https://files.pythonhosted.org/packages/fb/58/677da52d845b59505a8a787ff22eff9cfd9046b5789aa2bd387b236db5c5/black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2", size = 1560531, upload-time = "2023-12-22T23:18:20.555Z" },
{ url = "https://files.pythonhosted.org/packages/11/92/522a4f1e4b2b8da62e4ec0cb8acf2d257e6d39b31f4214f0fd94d2eeb5bd/black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", size = 1404644, upload-time = "2023-12-22T23:17:46.425Z" },
{ url = "https://files.pythonhosted.org/packages/a4/dc/af67d8281e9a24f73d24b060f3f03f6d9ad6be259b3c6acef2845e17d09c/black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", size = 1711153, upload-time = "2023-12-22T23:08:34.4Z" },
{ url = "https://files.pythonhosted.org/packages/7e/0f/94d7c36b421ea187359c413be7b9fc66dc105620c3a30b1c94310265830a/black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", size = 1332918, upload-time = "2023-12-22T23:10:28.188Z" },
{ url = "https://files.pythonhosted.org/packages/ed/2c/d9b1a77101e6e5f294f6553d76c39322122bfea2a438aeea4eb6d4b22749/black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", size = 1541926, upload-time = "2023-12-22T23:23:17.72Z" },
{ url = "https://files.pythonhosted.org/packages/72/e2/d981a3ff05ba9abe3cfa33e70c986facb0614fd57c4f802ef435f4dd1697/black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", size = 1388465, upload-time = "2023-12-22T23:19:00.611Z" },
{ url = "https://files.pythonhosted.org/packages/eb/59/1f5c8eb7bba8a8b1bb5c87f097d16410c93a48a6655be3773db5d2783deb/black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", size = 1691993, upload-time = "2023-12-22T23:08:32.018Z" },
{ url = "https://files.pythonhosted.org/packages/37/bf/a80abc6fcdb00f0d4d3d74184b172adbf2197f6b002913fa0fb6af4dc6db/black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", size = 1340929, upload-time = "2023-12-22T23:09:37.088Z" },
{ url = "https://files.pythonhosted.org/packages/66/16/8726cedc83be841dfa854bbeef1288ee82272282a71048d7935292182b0b/black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", size = 1569989, upload-time = "2023-12-22T23:20:22.158Z" },
{ url = "https://files.pythonhosted.org/packages/d2/1e/30f5eafcc41b8378890ba39b693fa111f7dca8a2620ba5162075d95ffe46/black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", size = 1398647, upload-time = "2023-12-22T23:19:57.225Z" },
{ url = "https://files.pythonhosted.org/packages/99/de/ddb45cc044256431d96d846ce03164d149d81ca606b5172224d1872e0b58/black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", size = 1720450, upload-time = "2023-12-22T23:08:52.675Z" },
{ url = "https://files.pythonhosted.org/packages/98/2b/54e5dbe9be5a10cbea2259517206ff7b6a452bb34e07508c7e1395950833/black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", size = 1351070, upload-time = "2023-12-22T23:09:32.762Z" },
{ url = "https://files.pythonhosted.org/packages/7b/14/4da7b12a9abc43a601c215cb5a3d176734578da109f0dbf0a832ed78be09/black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", size = 194363, upload-time = "2023-12-22T23:06:14.278Z" },
]
[[package]]
@ -350,28 +353,43 @@ toml = [
]
[[package]]
name = "cyclonedx-python-lib"
version = "11.7.0"
name = "databind"
version = "4.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "license-expression" },
{ name = "packageurl-python" },
{ name = "py-serializable" },
{ name = "sortedcontainers" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "deprecated" },
{ name = "nr-date" },
{ name = "nr-stream" },
{ name = "typeapi" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/21/0d/64f02d3fd9c116d6f50a540d04d1e4f2e3c487f5062d2db53733ddb25917/cyclonedx_python_lib-11.7.0.tar.gz", hash = "sha256:fb1bc3dedfa31208444dbd743007f478ab6984010a184e5bd466bffd969e936e", size = 1411174, upload-time = "2026-03-17T15:19:16.606Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0c/b8/a6beffa3dd3d7898003d32b3ff5dc0be422c54efed5e0e3f85e92c65c2b2/databind-4.5.2.tar.gz", hash = "sha256:0a8aa0ff130a0306581c559388f5ef65e0fae7ef4b86412eacb1f4a0420006c4", size = 43001, upload-time = "2024-05-31T15:29:07.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl", hash = "sha256:02fa4f15ddbba21ac9093039f8137c0d1813af7fe88b760c5dcd3311a8da2178", size = 513041, upload-time = "2026-03-17T15:19:14.369Z" },
{ url = "https://files.pythonhosted.org/packages/0b/5b/39577d7629da11765786f45a37dccdf7f420038f6040325fe1ca40f52a93/databind-4.5.2-py3-none-any.whl", hash = "sha256:b9c3a03c0414aa4567f095d7218ac904bd2b267b58e3763dac28e83d64b69770", size = 49283, upload-time = "2024-05-31T15:29:00.026Z" },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
name = "databind-core"
version = "4.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
dependencies = [
{ name = "databind" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/78/d05b13cc6aee2e84a3253c193e8dd2487c89ca80b9ecf63721e41cce4b78/databind.core-4.5.2.tar.gz", hash = "sha256:b8ac8127bc5d6b239a2a81aeddb268b0c4cadd53fbce7e8b2c7a9ef6413bccb3", size = 1485, upload-time = "2024-05-31T15:29:09.625Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
{ url = "https://files.pythonhosted.org/packages/32/54/eed2d15f7e1465a7a5a00958c0c926d153201c6cf37a5012d9012005bd8b/databind.core-4.5.2-py3-none-any.whl", hash = "sha256:a1dd1c6bd8ca9907d1292d8df9ec763ce91543e27f7eda4268e4a1a84fcd1c42", size = 1477, upload-time = "2024-05-31T15:29:02.264Z" },
]
[[package]]
name = "databind-json"
version = "4.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "databind" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/15/77a84f4b552365119dcc03076daeb0e1e0167b337ec7fbdfabe722f2d5e8/databind.json-4.5.2.tar.gz", hash = "sha256:6cc9b5c6fddaebd49b2433932948eb3be8a41633b90aa37998d7922504b8f165", size = 1466, upload-time = "2024-05-31T15:29:11.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/0f/a2f53f5e7be49bfa98dcb4e552382a6dc8c74ea74e755723654b85062316/databind.json-4.5.2-py3-none-any.whl", hash = "sha256:a803bf440634685984361cb2a5a975887e487c854ed48d81ff7aaf3a1ed1e94c", size = 1473, upload-time = "2024-05-31T15:29:05.857Z" },
]
[[package]]
@ -386,6 +404,40 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
[[package]]
name = "docspec"
version = "2.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "databind-core" },
{ name = "databind-json" },
{ name = "deprecated" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/fe/1ad244d0ca186b5386050ec30dfd59bd3dbeea5baec33ca861dd43b922e6/docspec-2.2.2.tar.gz", hash = "sha256:c772c6facfce839176b647701082c7a22b3d22d872d392552cf5d65e0348c919", size = 14086, upload-time = "2025-05-06T12:39:59.466Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/57/1011f2e88743a818cced9a95d54200ba6a05decaf43fd91d8c6ed9f6470d/docspec-2.2.2-py3-none-any.whl", hash = "sha256:854d25401e7ec2d155b0c1e001e25819d16b6df3a7575212a7f340ae8b00122e", size = 9726, upload-time = "2025-05-06T12:39:58.047Z" },
]
[[package]]
name = "docspec-python"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "black" },
{ name = "docspec" },
{ name = "nr-util" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/88/99c5e27a894f01290364563c84838cf68f1a8629474b5bbfc3bf35a8d923/docspec_python-2.2.1.tar.gz", hash = "sha256:c41b850b4d6f4de30999ea6f82c9cdb9183d9bcba45559ee9173d3dab7281559", size = 13838, upload-time = "2023-05-28T11:24:19.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/49/b8d1a2fa01b6f7a1a9daa1d485efc7684489028d6a356fc2bc5b40131061/docspec_python-2.2.1-py3-none-any.whl", hash = "sha256:76ac41d35a8face35b2d766c2e8a416fb8832359785d396f0d53bcb00f178e54", size = 16093, upload-time = "2023-05-28T11:24:17.261Z" },
]
[[package]]
name = "docstring-parser"
version = "0.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/ce/5d6a3782b9f88097ce3e579265015db3372ae78d12f67629b863a9208c96/docstring_parser-0.11.tar.gz", hash = "sha256:93b3f8f481c7d24e37c5d9f30293c89e2933fa209421c8abd731dd3ef0715ecb", size = 22775, upload-time = "2021-09-30T07:44:10.288Z" }
[[package]]
name = "dotty-dict"
version = "1.3.1"
@ -407,15 +459,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "filelock"
version = "3.25.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" },
]
[[package]]
name = "ghp-import"
version = "2.1.0"
@ -456,7 +499,6 @@ wheels = [
name = "griffelib"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
]
@ -583,58 +625,57 @@ dependencies = [
{ name = "structlog" },
]
[package.dev-dependencies]
[package.optional-dependencies]
dev = [
{ name = "pip-audit" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "pytest-html" },
{ name = "python-semantic-release" },
{ name = "respx" },
{ name = "ruff" },
]
docs = [
[package.dev-dependencies]
dev = [
{ name = "mkdocs" },
{ name = "mkdocs-material" },
{ name = "mkdocstrings", extra = ["python"] },
{ name = "mkdocstrings" },
{ name = "mkdocstrings-python" },
{ name = "pydoc-markdown" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "python-semantic-release" },
{ name = "respx" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", extras = ["http2"], specifier = ">=0.26.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },
{ name = "respx", marker = "extra == 'dev'", specifier = ">=0.20.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" },
{ name = "structlog", specifier = ">=24.0.0" },
]
provides-extras = ["dev"]
[package.metadata.requires-dev]
dev = [
{ name = "pip-audit", specifier = ">=2.7.0" },
{ name = "mkdocs", specifier = ">=1.6.1" },
{ name = "mkdocs-material", specifier = ">=9.7.6" },
{ name = "mkdocstrings", specifier = ">=1.0.3" },
{ name = "mkdocstrings-python", specifier = ">=2.0.3" },
{ name = "pydoc-markdown", specifier = ">=4.8.2" },
{ name = "pytest", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
{ name = "pytest-cov", specifier = ">=4.0.0" },
{ name = "pytest-html", specifier = ">=4.0.0" },
{ name = "python-semantic-release", specifier = ">=9.0.0" },
{ name = "python-semantic-release", specifier = ">=10.5.3" },
{ name = "respx", specifier = ">=0.20.0" },
{ name = "ruff", specifier = ">=0.3.0" },
]
docs = [
{ name = "mkdocs", specifier = ">=1.6.1" },
{ name = "mkdocs-material", specifier = ">=9.7.6" },
{ name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.3" },
]
[[package]]
name = "license-expression"
version = "30.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "boolean-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" },
]
[[package]]
name = "markdown"
@ -860,11 +901,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" },
]
[package.optional-dependencies]
python = [
{ name = "mkdocstrings-python" },
]
[[package]]
name = "mkdocstrings-python"
version = "2.0.3"
@ -881,73 +917,43 @@ wheels = [
]
[[package]]
name = "msgpack"
version = "1.1.2"
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
{ url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
{ url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
{ url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
{ url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
{ url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
{ url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
{ url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
{ url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
{ url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
{ url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
{ url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
{ url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
{ url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
{ url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
{ url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
{ url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
{ url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
{ url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
{ url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
{ url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
{ url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
{ url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
{ url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
{ url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
{ url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
{ url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
{ url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
{ url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
{ url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
{ url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
{ url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
{ url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
{ url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
{ url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
{ url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
{ url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
{ url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
{ url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
{ url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
{ url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
{ url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "packageurl-python"
version = "0.17.6"
name = "nr-date"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a0/92/08110dd3d7ff5e2b852a220752eb6c40183839f5b7cc91f9f38dd2298e7d/nr_date-2.1.0.tar.gz", hash = "sha256:0643aea13bcdc2a8bc56af9d5e6a89ef244c9744a1ef00cdc735902ba7f7d2e6", size = 8789, upload-time = "2023-08-16T13:46:04.114Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" },
{ url = "https://files.pythonhosted.org/packages/f9/10/1d2b00172537c1522fe64bbc6fb16b015632a02f7b3864e788ccbcb4dd85/nr_date-2.1.0-py3-none-any.whl", hash = "sha256:bd672a9dfbdcf7c4b9289fea6750c42490eaee08036a72059dcc78cb236ed568", size = 10496, upload-time = "2023-08-16T13:46:02.627Z" },
]
[[package]]
name = "nr-stream"
version = "1.1.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/37/e4d36d852c441233c306c5fbd98147685dce3ac9b0a8bbf4a587d0ea29ea/nr_stream-1.1.5.tar.gz", hash = "sha256:eb0216c6bfc61a46d4568dba3b588502c610ec8ddef4ac98f3932a2bd7264f65", size = 10053, upload-time = "2023-02-14T22:44:09.074Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/e1/f93485fe09aa36c0e1a3b76363efa1791241f7f863a010f725c95e8a74fe/nr_stream-1.1.5-py3-none-any.whl", hash = "sha256:47e12150b331ad2cb729cfd9d2abd281c9949809729ba461c6aa87dd9927b2d4", size = 10448, upload-time = "2023-02-14T22:44:07.72Z" },
]
[[package]]
name = "nr-util"
version = "0.8.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/0c/078c567d95e25564bc1ede3c2cf6ce1c91f50648c83786354b47224326da/nr.util-0.8.12.tar.gz", hash = "sha256:a4549c2033d99d2f0379b3f3d233fd2a8ade286bbf0b3ad0cc7cea16022214f4", size = 63707, upload-time = "2022-06-20T13:29:29.192Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/58/eab08df9dbd69d9e21fc5e7be6f67454f386336ec71e6b64e378a2dddea4/nr.util-0.8.12-py3-none-any.whl", hash = "sha256:91da02ac9795eb8e015372275c1efe54bac9051231ee9b0e7e6f96b0b4e7d2bb", size = 90319, upload-time = "2022-06-20T13:29:27.312Z" },
]
[[package]]
@ -977,61 +983,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
[[package]]
name = "pip"
version = "26.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" },
]
[[package]]
name = "pip-api"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pip" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" },
]
[[package]]
name = "pip-audit"
version = "2.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachecontrol", extra = ["filecache"] },
{ name = "cyclonedx-python-lib" },
{ name = "packaging" },
{ name = "pip-api" },
{ name = "pip-requirements-parser" },
{ name = "platformdirs" },
{ name = "requests" },
{ name = "rich" },
{ name = "tomli" },
{ name = "tomli-w" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bd/89/0e999b413facab81c33d118f3ac3739fd02c0622ccf7c4e82e37cebd8447/pip_audit-2.10.0.tar.gz", hash = "sha256:427ea5bf61d1d06b98b1ae29b7feacc00288a2eced52c9c58ceed5253ef6c2a4", size = 53776, upload-time = "2025-12-01T23:42:40.612Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/f3/4888f895c02afa085630a3a3329d1b18b998874642ad4c530e9a4d7851fe/pip_audit-2.10.0-py3-none-any.whl", hash = "sha256:16e02093872fac97580303f0848fa3ad64f7ecf600736ea7835a2b24de49613f", size = 61518, upload-time = "2025-12-01T23:42:39.193Z" },
]
[[package]]
name = "pip-requirements-parser"
version = "32.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "pyparsing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" },
]
[[package]]
name = "platformdirs"
version = "4.9.4"
@ -1050,18 +1001,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "py-serializable"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "defusedxml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
@ -1195,6 +1134,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
]
[[package]]
name = "pydoc-markdown"
version = "4.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "databind-core" },
{ name = "databind-json" },
{ name = "docspec" },
{ name = "docspec-python" },
{ name = "docstring-parser" },
{ name = "jinja2" },
{ name = "nr-util" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "tomli" },
{ name = "tomli-w" },
{ name = "watchdog" },
{ name = "yapf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/8a/2c7f7ad656d22371a596d232fc140327b958d7f1d491b889632ea0cb7e87/pydoc_markdown-4.8.2.tar.gz", hash = "sha256:fb6c927e31386de17472d42f9bd3d3be2905977d026f6216881c65145aa67f0b", size = 44506, upload-time = "2023-06-26T12:37:01.152Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/5a/ce0b056d9a95fd0c06a6cfa5972477d79353392d19230c748a7ba5a9df04/pydoc_markdown-4.8.2-py3-none-any.whl", hash = "sha256:203f74119e6bb2f9deba43d452422de7c8ec31955b61e0620fa4dd8c2611715f", size = 67830, upload-time = "2023-06-26T12:36:59.502Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
@ -1217,15 +1181,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" },
]
[[package]]
name = "pyparsing"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
@ -1272,32 +1227,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
]
[[package]]
name = "pytest-html"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "pytest" },
{ name = "pytest-metadata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/08/2076aa09507e51c1119d16a84c6307354d16270558f1a44fc9a2c99fdf1d/pytest_html-4.2.0.tar.gz", hash = "sha256:b6a88cba507500d8709959201e2e757d3941e859fd17cfd4ed87b16fc0c67912", size = 108634, upload-time = "2026-01-19T11:25:26.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/47/07046e0acedc12fe2bae79cf6c73ad67f51ae9d67df64d06b0f3eac73d36/pytest_html-4.2.0-py3-none-any.whl", hash = "sha256:ff5caf3e17a974008e5816edda61168e6c3da442b078a44f8744865862a85636", size = 23801, upload-time = "2026-01-19T11:25:25.008Z" },
]
[[package]]
name = "pytest-metadata"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@ -1527,15 +1456,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "structlog"
version = "25.5.0"
@ -1620,6 +1540,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
]
[[package]]
name = "typeapi"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d5/92/5a23ad34aa877edf00906166e339bfdc571543ea183ea7ab727bb01516c7/typeapi-2.3.0.tar.gz", hash = "sha256:a60d11f72c5ec27338cfd1c807f035b0b16ed2e3b798fb1c1d34fc5589f544be", size = 122687, upload-time = "2025-10-23T13:44:11.26Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/84/021bbeb7edb990dd6875cb6ab08d32faaa49fec63453d863730260a01f9e/typeapi-2.3.0-py3-none-any.whl", hash = "sha256:576b7dcb94412e91c5cae107a393674f8f99c10a24beb8be2302e3fed21d5cc2", size = 26858, upload-time = "2025-10-23T13:44:09.833Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
@ -1767,3 +1699,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" },
{ url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
]
[[package]]
name = "yapf"
version = "0.43.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "platformdirs" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907, upload-time = "2024-11-14T00:11:41.584Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/81/6acd6601f61e31cfb8729d3da6d5df966f80f374b78eff83760714487338/yapf-0.43.0-py3-none-any.whl", hash = "sha256:224faffbc39c428cb095818cf6ef5511fdab6f7430a10783fdfb292ccf2852ca", size = 256158, upload-time = "2024-11-14T00:11:39.37Z" },
]