Compare commits
No commits in common. "master" and "main" have entirely different histories.
12
.commitlintrc.json
Normal file
12
.commitlintrc.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"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", "."]
|
||||
}
|
||||
}
|
||||
111
.gitea/workflows/pr-check.yml
Normal file
111
.gitea/workflows/pr-check.yml
Normal file
@ -0,0 +1,111 @@
|
||||
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"
|
||||
128
.gitea/workflows/release.yml
Normal file
128
.gitea/workflows/release.yml
Normal file
@ -0,0 +1,128 @@
|
||||
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
|
||||
@ -1,34 +0,0 @@
|
||||
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
156
.github/workflows/ci.yml
vendored
@ -1,156 +0,0 @@
|
||||
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
7
.gitignore
vendored
@ -1,9 +1,14 @@
|
||||
# Build
|
||||
# Build artifacts
|
||||
site/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
.env/
|
||||
|
||||
# Documentation
|
||||
api_reference.md
|
||||
|
||||
|
||||
26
.pre-commit-config.yaml
Normal file
26
.pre-commit-config.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
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]
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@ -5,13 +5,15 @@ 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]
|
||||
|
||||
### Planned
|
||||
- Full CI/CD pipeline with Gitea Actions
|
||||
- Automatic publishing to Gitea Package Registry
|
||||
- Database support for caching (optional)
|
||||
- Rate limiting utilities
|
||||
See [commits](https://git.much-data.ru/much-data/kwork-api/compare/v0.1.0...main) since last release.
|
||||
|
||||
---
|
||||
|
||||
## [0.1.0] - 2026-03-23
|
||||
|
||||
@ -19,52 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Initial release
|
||||
- Complete Kwork.ru API client with 45+ endpoints
|
||||
- Pydantic models for all API responses
|
||||
- Comprehensive error handling (7 exception types)
|
||||
- 100% docstring coverage (Russian language)
|
||||
- MkDocs documentation with mkdocstrings
|
||||
- Comprehensive error handling
|
||||
- Unit tests with 92% coverage
|
||||
- UV package manager support
|
||||
- Gitea Actions CI/CD pipeline
|
||||
- CI/CD pipeline with Gitea Actions
|
||||
|
||||
### Models
|
||||
- KworkUser, KworkCategory, Kwork, KworkDetails
|
||||
- PaginationInfo, CatalogResponse
|
||||
- Project, ProjectsResponse
|
||||
- Review, ReviewsResponse
|
||||
- Notification, NotificationsResponse, Dialog
|
||||
- AuthResponse, ErrorDetail, APIErrorResponse
|
||||
- City, Country, TimeZone, Feature, Badge
|
||||
- DataResponse
|
||||
---
|
||||
|
||||
### 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
|
||||
*For older versions, see the Git history.*
|
||||
|
||||
146
CONTRIBUTING.md
Normal file
146
CONTRIBUTING.md
Normal file
@ -0,0 +1,146 @@
|
||||
# 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
|
||||
```
|
||||
@ -265,3 +265,8 @@ 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.
|
||||
|
||||
229
WIP.md
229
WIP.md
@ -5,121 +5,158 @@
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **Проект** | kwork-api |
|
||||
| **Начало** | 2026-03-23 02:16 UTC |
|
||||
| **Начало** | 2026-03-23 |
|
||||
| **Прогресс** | 100% |
|
||||
| **Статус** | ✅ Готово |
|
||||
| **Статус** | ✅ Готово к v1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 План
|
||||
|
||||
- [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)
|
||||
- [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 (требует исследования модели)
|
||||
- [ ] Публикация на internal PyPI
|
||||
- [ ] Интеграция с kwork-parser
|
||||
|
||||
---
|
||||
|
||||
## 🔨 Сейчас в работе
|
||||
|
||||
**Проект завершён!** ✅
|
||||
|
||||
**Что можно сделать потом:**
|
||||
- Добавить `/kworks` endpoint (когда будет модель ответа)
|
||||
- Настроить публикацию на internal PyPI
|
||||
- Интеграция с kwork-parser
|
||||
**Проект готов!** Ожидает настройки GITEA_TOKEN для автоматических релизов.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Заметки
|
||||
|
||||
### Автогенерация документации (2026-03-23 04:35)
|
||||
### 🏗️ Архитектура
|
||||
|
||||
**Инструмент:** MkDocs + mkdocstrings + Material theme
|
||||
**Стек:**
|
||||
- Python 3.10+ (тесты на 3.12)
|
||||
- UV (package manager)
|
||||
- httpx[http2] (async client)
|
||||
- pydantic v2 (модели)
|
||||
- pytest + respx (тесты)
|
||||
- ruff (lint + format)
|
||||
|
||||
**Структура:**
|
||||
```
|
||||
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/
|
||||
└── ...
|
||||
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 # Конфигурация проекта
|
||||
```
|
||||
|
||||
**Конфигурация:**
|
||||
- `mkdocs.yml` — MkDocs конфигурация
|
||||
- Pre-commit hook — автогенерация HTML при коммите
|
||||
### 📚 Документация
|
||||
|
||||
**Покрытие документацией:**
|
||||
- `KworkClient` — класс, аутентификация, все методы
|
||||
- `CatalogAPI` — каталог кворков
|
||||
- `ProjectsAPI` — биржа проектов
|
||||
- `UserAPI` — пользовательские данные
|
||||
- `ReferenceAPI` — справочники
|
||||
- `NotificationsAPI` — уведомления
|
||||
- `client.get_*()` — настройки и предпочтения (бывший OtherAPI)
|
||||
- `models.py` — все модели
|
||||
- `errors.py` — все исключения
|
||||
**Инструмент:** MkDocs + mkdocstrings + Material theme
|
||||
|
||||
**Команды:**
|
||||
```bash
|
||||
# Сборка HTML документации
|
||||
mkdocs build
|
||||
|
||||
# Локальный просмотр
|
||||
# Локальная разработка
|
||||
mkdocs serve
|
||||
|
||||
# Сборка HTML (для CI)
|
||||
mkdocs build
|
||||
```
|
||||
|
||||
### Аудит эндпоинтов (2026-03-23 03:08)
|
||||
**Деплой:** Автоматически через Gitea Actions на Gitea Pages
|
||||
|
||||
**Из HAR дампа:** 44 эндпоинта
|
||||
- **Пропущено (internal/analytics):** 9
|
||||
- **Реализовано:** 33/33 (100%) ✅
|
||||
- **Протестировано:** 33/33 (100%) ✅
|
||||
**Покрытие:** 100% docstrings (Russian language)
|
||||
|
||||
**Пропущенные эндпоинты (анализ):**
|
||||
### 🧪 Тестирование
|
||||
|
||||
| Эндпоинт | Размер | Описание | Решение |
|
||||
|----------|--------|----------|---------|
|
||||
| `/signIn` | - | Авторизация | ✅ Реализовано в `login()` |
|
||||
| `/getWebAuthToken` | - | Получение токена | ✅ Реализовано в `login()` |
|
||||
| `/kworks` | 22KB | Список кворков | 🔴 Добавить |
|
||||
| `/quick-faq/init` | 3.7MB | FAQ данные | ⏪ Опционально |
|
||||
| `/api/validation/checktext` | - | Валидация текста | 🔴 Добавить |
|
||||
| Остальные | - | Analytics/UI | ⏪ Пропустить |
|
||||
**Coverage:** 92% (порог 90% для PR)
|
||||
|
||||
**Тесты:**
|
||||
- Unit тесты: 46 passed
|
||||
- Покрытие: 92%
|
||||
- Файлы: `test_client.py` (13 тестов), `test_all_endpoints.py` (33 теста)
|
||||
**Запуск:**
|
||||
```bash
|
||||
# Все тесты
|
||||
uv run pytest tests/unit/ -v
|
||||
|
||||
**Аутентификация:** cookies + web_auth_token (2 этапа)
|
||||
**Стек:** UV + httpx(http2) + pydantic v2 + structlog + mkdocstrings
|
||||
**HAR дамп:** 45 эндпоинтов проанализировано
|
||||
# С coverage
|
||||
uv run pytest --cov=src/kwork_api --cov-report=html
|
||||
```
|
||||
|
||||
**Решения:**
|
||||
- Rate limiting на стороне пользователя (не в библиотеке)
|
||||
- Только библиотека (без CLI)
|
||||
- Pydantic модели для всех ответов
|
||||
- Автогенерация документации через mkdocstrings+griffe
|
||||
**Артефакты:**
|
||||
- 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 эндпоинты — не нужны для библиотеки
|
||||
|
||||
---
|
||||
|
||||
@ -131,19 +168,21 @@ mkdocs serve
|
||||
|
||||
## 📅 История
|
||||
|
||||
- **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** — Начат проект
|
||||
- **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
|
||||
|
||||
@ -1,261 +0,0 @@
|
||||
# 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 — свободное использование с указанием авторства.
|
||||
@ -1,211 +0,0 @@
|
||||
# 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
131
docs/RELEASE.md
@ -1,131 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@ -1,281 +0,0 @@
|
||||
# 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)
|
||||
@ -1,9 +1,9 @@
|
||||
# API Reference
|
||||
|
||||
Complete API documentation for Kwork API client.
|
||||
Auto-generated API documentation is available below.
|
||||
|
||||
## 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
|
||||
::: kwork_api.client.KworkClient
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_source: true
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
# Client API
|
||||
|
||||
::: kwork_api.client.KworkClient
|
||||
@ -1,5 +0,0 @@
|
||||
# Errors
|
||||
|
||||
Exception classes for API errors.
|
||||
|
||||
::: kwork_api.errors
|
||||
@ -1,5 +0,0 @@
|
||||
# Models
|
||||
|
||||
Pydantic models for API responses.
|
||||
|
||||
::: kwork_api.models
|
||||
114
docs/index.md
114
docs/index.md
@ -1,109 +1,47 @@
|
||||
# Kwork API — Python Client
|
||||
# Kwork API
|
||||
|
||||
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
|
||||
pip install kwork-api
|
||||
```
|
||||
|
||||
Or with UV:
|
||||
|
||||
```bash
|
||||
uv add kwork-api
|
||||
# or
|
||||
pip install kwork-api
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Login with credentials
|
||||
|
||||
```python
|
||||
from kwork_api import KworkClient
|
||||
|
||||
# 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()
|
||||
```
|
||||
|
||||
### Using context manager
|
||||
|
||||
```python
|
||||
# Login with credentials
|
||||
async with await KworkClient.login("username", "password") as client:
|
||||
# Get catalog
|
||||
catalog = await client.catalog.get_list(page=1)
|
||||
# Client automatically closes
|
||||
|
||||
# Get projects
|
||||
projects = await client.projects.get_list()
|
||||
|
||||
# Get user info
|
||||
user = await client.user.get_info()
|
||||
```
|
||||
|
||||
### Save and restore session
|
||||
## Documentation
|
||||
|
||||
```python
|
||||
# Save credentials after login
|
||||
client = await KworkClient.login("username", "password")
|
||||
- [Usage Guide](usage.md) — Examples and best practices
|
||||
- [API Reference](api-reference.md) — Complete API documentation
|
||||
|
||||
# Option 1: Save token only
|
||||
token = client.token
|
||||
## Links
|
||||
|
||||
# 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}")
|
||||
```
|
||||
- [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)
|
||||
|
||||
132
docs/usage.md
Normal file
132
docs/usage.md
Normal file
@ -0,0 +1,132 @@
|
||||
# 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())
|
||||
```
|
||||
16
mkdocs.yml
16
mkdocs.yml
@ -1,9 +1,9 @@
|
||||
site_name: Kwork API
|
||||
site_description: Unofficial Python client for Kwork.ru API
|
||||
site_url: https://github.com/claw/kwork-api
|
||||
site_url: https://git.much-data.ru/much-data/kwork-api
|
||||
|
||||
repo_name: claw/kwork-api
|
||||
repo_url: https://github.com/claw/kwork-api
|
||||
repo_name: much-data/kwork-api
|
||||
repo_url: https://git.much-data.ru/much-data/kwork-api
|
||||
|
||||
theme:
|
||||
name: material
|
||||
@ -58,7 +58,7 @@ markdown_extensions:
|
||||
- pymdownx.keys
|
||||
- pymdownx.magiclink:
|
||||
repo_url_shorthand: true
|
||||
user: claw
|
||||
user: much-data
|
||||
repo: kwork-api
|
||||
- pymdownx.mark
|
||||
- pymdownx.smartsymbols
|
||||
@ -71,9 +71,5 @@ markdown_extensions:
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- API Reference:
|
||||
- Overview: api-reference.md
|
||||
- Client: api/client.md
|
||||
- Models: api/models.md
|
||||
- Errors: api/errors.md
|
||||
- Examples: examples.md
|
||||
- API Reference: api-reference.md
|
||||
- Usage: usage.md
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
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
|
||||
110
pyproject.toml
110
pyproject.toml
@ -27,18 +27,9 @@ 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 = "http://5.188.26.192:3000/claw/kwork-api"
|
||||
Repository = "http://5.188.26.192:3000/claw/kwork-api.git"
|
||||
Homepage = "https://git.much-data.ru/much-data/kwork-api"
|
||||
Repository = "https://git.much-data.ru/much-data/kwork-api.git"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
@ -49,50 +40,59 @@ 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",
|
||||
"pydoc-markdown>=4.8.2",
|
||||
# CI tools
|
||||
"python-semantic-release>=9.0.0",
|
||||
"pip-audit>=2.7.0",
|
||||
]
|
||||
docs = [
|
||||
# Documentation (optional, for local development)
|
||||
"mkdocs>=1.6.1",
|
||||
"mkdocs-material>=9.7.6",
|
||||
"mkdocstrings>=1.0.3",
|
||||
"mkdocstrings-python>=2.0.3",
|
||||
"python-semantic-release>=10.5.3",
|
||||
"mkdocstrings[python]>=1.0.3",
|
||||
]
|
||||
|
||||
[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 Configurations ==========
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
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 = ["kwork_api"]
|
||||
source = ["src/kwork_api"]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
@ -100,30 +100,22 @@ exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"raise NotImplementedError",
|
||||
"if __name__ == .__main__.:",
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
|
||||
# ============================================
|
||||
# Python Semantic Release Configuration
|
||||
# ============================================
|
||||
# ========== Semantic Release ==========
|
||||
|
||||
[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}"
|
||||
|
||||
[tool.semantic_release.branches.main]
|
||||
match = "main"
|
||||
prerelease = false
|
||||
major_on_zero = true
|
||||
allow_zero_version = true
|
||||
|
||||
[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"]
|
||||
@ -131,24 +123,10 @@ breaking_change_tags = ["feat"]
|
||||
[tool.semantic_release.remote]
|
||||
type = "gitea"
|
||||
domain = "https://git.much-data.ru"
|
||||
owner = "claw"
|
||||
owner = "much-data"
|
||||
repo_name = "kwork-api"
|
||||
token = { env = "GITEA_TOKEN" }
|
||||
token = { env = "GH_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
|
||||
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ from .models import (
|
||||
ProjectsResponse,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.1.0" # Updated by semantic-release
|
||||
__all__ = [
|
||||
"KworkClient",
|
||||
"KworkError",
|
||||
|
||||
@ -15,6 +15,16 @@
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
__all__ = [
|
||||
"KworkError",
|
||||
"KworkAuthError",
|
||||
"KworkApiError",
|
||||
"KworkNotFoundError",
|
||||
"KworkRateLimitError",
|
||||
"KworkValidationError",
|
||||
"KworkNetworkError",
|
||||
]
|
||||
|
||||
|
||||
class KworkError(Exception):
|
||||
"""
|
||||
|
||||
5
tests/e2e/.env.example
Normal file
5
tests/e2e/.env.example
Normal file
@ -0,0 +1,5 @@
|
||||
# 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
|
||||
136
tests/e2e/README.md
Normal file
136
tests/e2e/README.md
Normal file
@ -0,0 +1,136 @@
|
||||
# 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 создаётся заново)
|
||||
59
tests/e2e/conftest.py
Normal file
59
tests/e2e/conftest.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""
|
||||
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)"
|
||||
)
|
||||
52
tests/e2e/test_auth.py
Normal file
52
tests/e2e/test_auth.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
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()
|
||||
Binary file not shown.
743
tests/unit/test_client_extended.py
Normal file
743
tests/unit/test_client_extended.py
Normal file
@ -0,0 +1,743 @@
|
||||
"""
|
||||
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
|
||||
437
uv.lock
generated
437
uv.lock
generated
@ -58,33 +58,30 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "23.12.1"
|
||||
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"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ name = "msgpack" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -353,43 +350,28 @@ toml = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "databind"
|
||||
version = "4.5.2"
|
||||
name = "cyclonedx-python-lib"
|
||||
version = "11.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "deprecated" },
|
||||
{ name = "nr-date" },
|
||||
{ name = "nr-stream" },
|
||||
{ name = "typeapi" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "license-expression" },
|
||||
{ name = "packageurl-python" },
|
||||
{ name = "py-serializable" },
|
||||
{ name = "sortedcontainers" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "databind-core"
|
||||
version = "4.5.2"
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -404,40 +386,6 @@ 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"
|
||||
@ -459,6 +407,15 @@ 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"
|
||||
@ -499,6 +456,7 @@ 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" },
|
||||
]
|
||||
@ -625,57 +583,58 @@ dependencies = [
|
||||
{ name = "structlog" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "respx" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "mkdocs" },
|
||||
{ name = "mkdocs-material" },
|
||||
{ name = "mkdocstrings" },
|
||||
{ name = "mkdocstrings-python" },
|
||||
{ name = "pydoc-markdown" },
|
||||
{ name = "pip-audit" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-html" },
|
||||
{ name = "python-semantic-release" },
|
||||
{ name = "respx" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "mkdocs" },
|
||||
{ name = "mkdocs-material" },
|
||||
{ name = "mkdocstrings", extra = ["python"] },
|
||||
]
|
||||
|
||||
[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 = "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 = "pip-audit", specifier = ">=2.7.0" },
|
||||
{ name = "pytest", specifier = ">=8.0.0" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
|
||||
{ name = "pytest-cov", specifier = ">=4.0.0" },
|
||||
{ name = "python-semantic-release", specifier = ">=10.5.3" },
|
||||
{ name = "pytest-html", specifier = ">=4.0.0" },
|
||||
{ name = "python-semantic-release", specifier = ">=9.0.0" },
|
||||
{ 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"
|
||||
@ -901,6 +860,11 @@ 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"
|
||||
@ -917,43 +881,73 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
name = "msgpack"
|
||||
version = "1.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nr-date"
|
||||
version = "2.1.0"
|
||||
name = "packageurl-python"
|
||||
version = "0.17.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -983,6 +977,61 @@ 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"
|
||||
@ -1001,6 +1050,18 @@ 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"
|
||||
@ -1134,31 +1195,6 @@ 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"
|
||||
@ -1181,6 +1217,15 @@ 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"
|
||||
@ -1227,6 +1272,32 @@ 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"
|
||||
@ -1456,6 +1527,15 @@ 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"
|
||||
@ -1540,18 +1620,6 @@ 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"
|
||||
@ -1699,16 +1767,3 @@ 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" },
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user