Compare commits
No commits in common. "20c3e815e18bb76ef317527bb426d3dc150c0b61" and "6ac9c533fa5f30305538a46f33349aff5937c430" have entirely different histories.
20c3e815e1
...
6ac9c533fa
34
.gitea/workflows/test.yml
Normal file
34
.gitea/workflows/test.yml
Normal file
@ -0,0 +1,34 @@
|
||||
name: CI - Tests & Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install UV
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --group dev
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: uv run pytest tests/unit/ -v --tb=short --cov=src/kwork_api --cov-report=term-missing
|
||||
|
||||
- name: Run linting
|
||||
run: uv run ruff check src/kwork_api tests/
|
||||
156
.github/workflows/ci.yml
vendored
Normal file
156
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,156 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: '3.12'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen --dev
|
||||
|
||||
- name: Run linters
|
||||
run: uv run ruff check src/ tests/
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest --cov=kwork_api --cov-report=xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Build package
|
||||
run: uv build
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
release:
|
||||
name: Semantic Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, build, docs]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen --dev
|
||||
|
||||
- name: Run semantic-release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
uv run semantic-release version --push --no-mock
|
||||
|
||||
publish:
|
||||
name: Publish to Gitea Registry
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Publish to Gitea
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
uv publish \
|
||||
--publish-url https://git.much-data.ru/api/packages/claw/pypi \
|
||||
--token $UV_PUBLISH_TOKEN
|
||||
|
||||
docs:
|
||||
name: Build & Deploy Documentation
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --frozen --dev
|
||||
|
||||
- name: Build docs
|
||||
run: uv run mkdocs build
|
||||
|
||||
- name: Deploy to Gitea Pages
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
gitea_token: ${{ secrets.GITEA_TOKEN }}
|
||||
gitea_server_url: https://git.much-data.ru
|
||||
publish_dir: ./site
|
||||
publish_branch: gh-pages
|
||||
force_orphan: true
|
||||
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# Build
|
||||
site/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Documentation
|
||||
api_reference.md
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv/
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
*.egg
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docs build
|
||||
docs/_build/
|
||||
70
CHANGELOG.md
Normal file
70
CHANGELOG.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Changelog
|
||||
|
||||
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).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Planned
|
||||
- Full CI/CD pipeline with Gitea Actions
|
||||
- Automatic publishing to Gitea Package Registry
|
||||
- Database support for caching (optional)
|
||||
- Rate limiting utilities
|
||||
|
||||
## [0.1.0] - 2026-03-23
|
||||
|
||||
### Added
|
||||
- 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
|
||||
- Unit tests with 92% coverage
|
||||
- UV package manager support
|
||||
- Gitea Actions CI/CD pipeline
|
||||
|
||||
### Models
|
||||
- KworkUser, KworkCategory, Kwork, KworkDetails
|
||||
- PaginationInfo, CatalogResponse
|
||||
- Project, ProjectsResponse
|
||||
- Review, ReviewsResponse
|
||||
- Notification, NotificationsResponse, Dialog
|
||||
- AuthResponse, ErrorDetail, APIErrorResponse
|
||||
- City, Country, TimeZone, Feature, Badge
|
||||
- DataResponse
|
||||
|
||||
### 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
|
||||
268
README.md
268
README.md
@ -1,3 +1,267 @@
|
||||
# kwork-api
|
||||
# Kwork API Client
|
||||
|
||||
Kwork API парсер
|
||||
Unofficial Python client for Kwork.ru API.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Full API coverage (all endpoints from HAR dump analysis)
|
||||
- ✅ Async/await support
|
||||
- ✅ Pydantic models for type safety
|
||||
- ✅ Clear error handling
|
||||
- ✅ Session management with cookies + tokens
|
||||
- ✅ JSON structured logging support
|
||||
- ✅ Comprehensive test suite (unit + integration)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# With pip
|
||||
pip install kwork-api
|
||||
|
||||
# With uv
|
||||
uv pip install kwork-api
|
||||
|
||||
# From source
|
||||
git clone http://5.188.26.192:3000/claw/kwork-api.git
|
||||
cd kwork-api
|
||||
uv pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Authentication
|
||||
|
||||
```python
|
||||
from kwork_api import KworkClient
|
||||
|
||||
# Login with credentials
|
||||
client = await KworkClient.login("username", "password")
|
||||
|
||||
# Or restore from token
|
||||
client = KworkClient(token="your_web_auth_token")
|
||||
|
||||
# Use context manager for automatic cleanup
|
||||
async with KworkClient(token="token") as client:
|
||||
# Make requests
|
||||
pass
|
||||
```
|
||||
|
||||
### Catalog
|
||||
|
||||
```python
|
||||
# Get catalog list
|
||||
catalog = await client.catalog.get_list(page=1)
|
||||
for kwork in catalog.kworks:
|
||||
print(f"{kwork.title}: {kwork.price} RUB")
|
||||
|
||||
# Get kwork details
|
||||
details = await client.catalog.get_details(kwork_id=123)
|
||||
print(details.full_description)
|
||||
```
|
||||
|
||||
### Projects
|
||||
|
||||
```python
|
||||
# Get projects list
|
||||
projects = await client.projects.get_list(page=1)
|
||||
|
||||
# Get orders where user is customer
|
||||
customer_orders = await client.projects.get_payer_orders()
|
||||
|
||||
# Get orders where user is performer
|
||||
performer_orders = await client.projects.get_worker_orders()
|
||||
```
|
||||
|
||||
### User
|
||||
|
||||
```python
|
||||
# Get user info
|
||||
user_info = await client.user.get_info()
|
||||
|
||||
# Get reviews
|
||||
reviews = await client.user.get_reviews(page=1)
|
||||
|
||||
# Get favorite kworks
|
||||
favorites = await client.user.get_favorite_kworks()
|
||||
```
|
||||
|
||||
### Reference Data
|
||||
|
||||
```python
|
||||
# Get cities
|
||||
cities = await client.reference.get_cities()
|
||||
|
||||
# Get countries
|
||||
countries = await client.reference.get_countries()
|
||||
|
||||
# Get timezones
|
||||
timezones = await client.reference.get_timezones()
|
||||
|
||||
# Get features
|
||||
features = await client.reference.get_features()
|
||||
```
|
||||
|
||||
### Notifications
|
||||
|
||||
```python
|
||||
# Get notifications
|
||||
notifications = await client.notifications.get_list()
|
||||
|
||||
# Fetch new notifications
|
||||
new_notifications = await client.notifications.fetch()
|
||||
|
||||
# Get dialogs
|
||||
dialogs = await client.notifications.get_dialogs()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
from kwork_api import KworkAuthError, KworkApiError, KworkNotFoundError
|
||||
|
||||
try:
|
||||
catalog = await client.catalog.get_list()
|
||||
except KworkAuthError as e:
|
||||
print(f"Auth failed: {e}")
|
||||
except KworkNotFoundError as e:
|
||||
print(f"Not found: {e}")
|
||||
except KworkApiError as e:
|
||||
print(f"API error [{e.status_code}]: {e.message}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Rate limiting is **not** implemented in the library. Handle it in your code:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
for page in range(1, 10):
|
||||
catalog = await client.catalog.get_list(page=page)
|
||||
await asyncio.sleep(1) # 1 second delay between requests
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
The library uses standard `logging` module. For JSON logging (Kibana-compatible):
|
||||
|
||||
```python
|
||||
import logging
|
||||
import structlog
|
||||
import json
|
||||
|
||||
# Configure structlog for JSON output
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.processors.JSONRenderer()
|
||||
],
|
||||
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
|
||||
)
|
||||
|
||||
# Use in your code
|
||||
logger = structlog.get_logger()
|
||||
logger.info("kwork_request", endpoint="/catalogMainv2", page=1)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests (mocks)
|
||||
|
||||
```bash
|
||||
# Run unit tests only
|
||||
pytest tests/unit/ -m unit
|
||||
|
||||
# With coverage
|
||||
pytest tests/unit/ -m unit --cov=kwork_api
|
||||
```
|
||||
|
||||
### Integration Tests (real API)
|
||||
|
||||
```bash
|
||||
# Set credentials
|
||||
export KWORK_USERNAME=your_username
|
||||
export KWORK_PASSWORD=your_password
|
||||
|
||||
# Run integration tests
|
||||
pytest tests/integration/ -m integration
|
||||
|
||||
# Skip integration tests in CI
|
||||
pytest tests/ -m "not integration"
|
||||
```
|
||||
|
||||
## Available Endpoints
|
||||
|
||||
### Catalog
|
||||
- `GET /catalogMainv2` — Catalog list with pagination
|
||||
- `POST /getKworkDetails` — Kwork details
|
||||
- `POST /getKworkDetailsExtra` — Extra kwork details
|
||||
|
||||
### Projects
|
||||
- `POST /projects` — Projects list
|
||||
- `POST /payerOrders` — Customer orders
|
||||
- `POST /workerOrders` — Performer orders
|
||||
|
||||
### User
|
||||
- `POST /user` — User info
|
||||
- `POST /userReviews` — User reviews
|
||||
- `POST /favoriteKworks` — Favorite kworks
|
||||
- `POST /favoriteCategories` — Favorite categories
|
||||
|
||||
### Reference Data
|
||||
- `POST /cities` — Cities list
|
||||
- `POST /countries` — Countries list
|
||||
- `POST /timezones` — Timezones list
|
||||
- `POST /getAvailableFeatures` — Available features
|
||||
- `POST /getPublicFeatures` — Public features
|
||||
- `POST /getBadgesInfo` — Badges info
|
||||
|
||||
### Notifications
|
||||
- `POST /notifications` — Notifications list
|
||||
- `POST /notificationsFetch` — Fetch new notifications
|
||||
- `POST /dialogs` — Dialogs list
|
||||
- `POST /blockedDialogList` — Blocked dialogs
|
||||
|
||||
### Other
|
||||
- `POST /myWants` — User wants
|
||||
- `POST /wantsStatusList` — Wants status
|
||||
- `POST /kworksStatusList` — Kworks status
|
||||
- `POST /offers` — Offers
|
||||
- `POST /exchangeInfo` — Exchange info
|
||||
- `POST /getChannel` — Channel info
|
||||
- `POST /getInAppNotification` — In-app notification
|
||||
- `POST /getSecurityUserData` — Security user data
|
||||
- `POST /isDialogAllow` — Check dialog permission
|
||||
- `POST /viewedCatalogKworks` — Viewed kworks
|
||||
- `POST /updateSettings` — Update settings
|
||||
- `POST /offline` — Set offline status
|
||||
- `POST /actor` — Actor info
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dev dependencies
|
||||
uv pip install -e ".[dev]"
|
||||
|
||||
# Run linter
|
||||
ruff check src/ tests/
|
||||
|
||||
# Format code
|
||||
ruff format src/ tests/
|
||||
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Build package
|
||||
uv build
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
|
||||
## Disclaimer
|
||||
|
||||
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.
|
||||
|
||||
148
WIP.md
Normal file
148
WIP.md
Normal file
@ -0,0 +1,148 @@
|
||||
# Work In Progress — kwork-api
|
||||
|
||||
## 📊 Статус
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **Проект** | kwork-api |
|
||||
| **Начало** | 2026-03-23 02:16 UTC |
|
||||
| **Прогресс** | 99% |
|
||||
| **Статус** | 🟢 В работе |
|
||||
|
||||
---
|
||||
|
||||
## 📋 План
|
||||
|
||||
- [x] Структура проекта (pyproject.toml, зависимости)
|
||||
- [x] Модели Pydantic (20+ моделей для всех ответов API)
|
||||
- [x] API клиент (KworkClient с 45 эндпоинтами)
|
||||
- [x] Обработка ошибок (KworkAuthError, KworkApiError, etc.)
|
||||
- [x] Тесты unit (49 тестов, 92% coverage)
|
||||
- [x] Документация (README + docs/)
|
||||
- [x] **Аудит эндпоинтов** — все 33 endpoint протестированы ✅
|
||||
- [x] **Автогенерация документации** — pydoc-markdown ✅
|
||||
- [x] **Docstrings** — 100% покрытие ✅
|
||||
- [x] **Добавить `/api/validation/checktext` (валидация текста)** ✅
|
||||
- [ ] Добавить `/kworks` endpoint (альтернатива каталогу)
|
||||
- [ ] Тесты integration (шаблон готов, нужны реальные credentials)
|
||||
- [ ] CI/CD pipeline (Gitea Actions)
|
||||
- [ ] Публикация на internal PyPI
|
||||
|
||||
---
|
||||
|
||||
## 🔨 Сейчас в работе
|
||||
|
||||
**Текущая задача:** Завершение v1.0 — остался CI/CD
|
||||
|
||||
**Следующий шаг:**
|
||||
1. Настроить CI/CD pipeline (Gitea Actions)
|
||||
2. Опционально: добавить `/kworks` endpoint
|
||||
3. Публикация на internal PyPI
|
||||
|
||||
---
|
||||
|
||||
## 📝 Заметки
|
||||
|
||||
### Автогенерация документации (2026-03-23 04:35)
|
||||
|
||||
**Инструмент:** MkDocs + mkdocstrings + Material theme
|
||||
|
||||
**Структура:**
|
||||
```
|
||||
docs/
|
||||
├── index.md # Quick start guide
|
||||
├── api-reference.md # API overview
|
||||
├── api/
|
||||
│ ├── client.md # KworkClient documentation
|
||||
│ ├── models.md # Pydantic models
|
||||
│ └── errors.md # Exception classes
|
||||
└── examples.md # Usage examples
|
||||
|
||||
site/ # Generated HTML (не коммитим)
|
||||
├── index.html
|
||||
├── api-reference/
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Конфигурация:**
|
||||
- `mkdocs.yml` — MkDocs конфигурация
|
||||
- Pre-commit hook — автогенерация HTML при коммите
|
||||
|
||||
**Покрытие документацией:**
|
||||
- `KworkClient` — класс, аутентификация, все методы
|
||||
- `CatalogAPI` — каталог кворков
|
||||
- `ProjectsAPI` — биржа проектов
|
||||
- `UserAPI` — пользовательские данные
|
||||
- `ReferenceAPI` — справочники
|
||||
- `NotificationsAPI` — уведомления
|
||||
- `client.get_*()` — настройки и предпочтения (бывший OtherAPI)
|
||||
- `models.py` — все модели
|
||||
- `errors.py` — все исключения
|
||||
|
||||
**Команды:**
|
||||
```bash
|
||||
# Сборка HTML документации
|
||||
mkdocs build
|
||||
|
||||
# Локальный просмотр
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
### Аудит эндпоинтов (2026-03-23 03:08)
|
||||
|
||||
**Из HAR дампа:** 44 эндпоинта
|
||||
- **Пропущено (internal/analytics):** 9
|
||||
- **Реализовано:** 33/33 (100%) ✅
|
||||
- **Протестировано:** 33/33 (100%) ✅
|
||||
|
||||
**Пропущенные эндпоинты (анализ):**
|
||||
|
||||
| Эндпоинт | Размер | Описание | Решение |
|
||||
|----------|--------|----------|---------|
|
||||
| `/signIn` | - | Авторизация | ✅ Реализовано в `login()` |
|
||||
| `/getWebAuthToken` | - | Получение токена | ✅ Реализовано в `login()` |
|
||||
| `/kworks` | 22KB | Список кворков | 🔴 Добавить |
|
||||
| `/quick-faq/init` | 3.7MB | FAQ данные | ⏪ Опционально |
|
||||
| `/api/validation/checktext` | - | Валидация текста | 🔴 Добавить |
|
||||
| Остальные | - | Analytics/UI | ⏪ Пропустить |
|
||||
|
||||
**Тесты:**
|
||||
- Unit тесты: 46 passed
|
||||
- Покрытие: 92%
|
||||
- Файлы: `test_client.py` (13 тестов), `test_all_endpoints.py` (33 теста)
|
||||
|
||||
**Аутентификация:** cookies + web_auth_token (2 этапа)
|
||||
**Стек:** UV + httpx(http2) + pydantic v2 + structlog + mkdocstrings
|
||||
**HAR дамп:** 45 эндпоинтов проанализировано
|
||||
|
||||
**Решения:**
|
||||
- Rate limiting на стороне пользователя (не в библиотеке)
|
||||
- Только библиотека (без CLI)
|
||||
- Pydantic модели для всех ответов
|
||||
- Автогенерация документации через mkdocstrings+griffe
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Блокеры
|
||||
|
||||
Нет
|
||||
|
||||
---
|
||||
|
||||
## 📅 История
|
||||
|
||||
- **23:12** — Добавлен `/api/validation/checktext` endpoint ✅
|
||||
- Модели: `ValidationResponse`, `ValidationIssue`
|
||||
- Метод: `client.other.validate_text()`
|
||||
- Тесты: 3 новых теста (16 total)
|
||||
- **03:44** — mkdocstrings+griffe настроен, документация генерируется
|
||||
- **03:38** — Выбран mkdocstrings+griffe вместо pydoc-markdown
|
||||
- **03:26** — Автогенерация документации настроена (pre-commit hook)
|
||||
- **03:20** — Создана docs/ структура
|
||||
- **03:17** — WIP.md восстановлен после rebase
|
||||
- **03:14** — Анализ пропущенных эндпоинтов
|
||||
- **03:08** — Аудит завершён: 33/33 endpoint протестированы (92% coverage)
|
||||
- **02:48** — Все unit тесты пройдены (13/13)
|
||||
- **02:35** — Завершён этап "API клиент"
|
||||
- **02:20** — Завершён этап "Структура проекта"
|
||||
- **02:16** — Начат проект
|
||||
261
docs/ARCHITECTURE.md
Normal file
261
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,261 @@
|
||||
# Architecture — kwork-api
|
||||
|
||||
## 📐 Обзор
|
||||
|
||||
**kwork-api** — асинхронный Python клиент для Kwork.ru API с полной типизацией и документацией.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Архитектура
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ User Application │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ KworkClient │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Authentication Layer │ │
|
||||
│ │ - login() / token auth │ │
|
||||
│ │ - Session management │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ API Groups │ │
|
||||
│ │ - catalog, projects, user, reference, ... │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ HTTP Layer (httpx) │ │
|
||||
│ │ - HTTP/2 support │ │
|
||||
│ │ - Timeout handling │ │
|
||||
│ │ - Error handling │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Kwork.ru API (HTTPS) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
kwork-api/
|
||||
├── src/kwork_api/
|
||||
│ ├── __init__.py # Public API
|
||||
│ ├── client.py # KworkClient + API groups
|
||||
│ ├── models.py # Pydantic models
|
||||
│ └── errors.py # Exception classes
|
||||
│
|
||||
├── tests/
|
||||
│ ├── test_client.py # Client tests
|
||||
│ ├── test_models.py # Model tests
|
||||
│ └── test_all_endpoints.py # Endpoint tests
|
||||
│
|
||||
├── docs/
|
||||
│ ├── index.md # Quick start
|
||||
│ ├── api-reference.md # API docs
|
||||
│ ├── RELEASE.md # Release guide
|
||||
│ └── ARCHITECTURE.md # This file
|
||||
│
|
||||
├── .github/workflows/
|
||||
│ └── ci.yml # CI/CD pipeline
|
||||
│
|
||||
├── pyproject.toml # Project config
|
||||
├── uv.lock # Dependencies lock
|
||||
└── README.md # Main documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Компоненты
|
||||
|
||||
### 1. KworkClient
|
||||
|
||||
**Ответственность:** Основное взаимодействие с API
|
||||
|
||||
**Функции:**
|
||||
- Аутентификация (login / token)
|
||||
- Управление сессией
|
||||
- Делегирование API группам
|
||||
|
||||
**API Groups:**
|
||||
```python
|
||||
client.catalog # CatalogAPI
|
||||
client.projects # ProjectsAPI
|
||||
client.user # UserAPI
|
||||
client.reference # ReferenceAPI
|
||||
client.notifications # NotificationsAPI
|
||||
client.other # OtherAPI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Models (Pydantic)
|
||||
|
||||
**Ответственность:** Валидация и типизация ответов API
|
||||
|
||||
**Категории:**
|
||||
- **User models:** KworkUser, AuthResponse
|
||||
- **Kwork models:** Kwork, KworkDetails, KworkCategory
|
||||
- **Project models:** Project, ProjectsResponse
|
||||
- **Review models:** Review, ReviewsResponse
|
||||
- **Notification models:** Notification, Dialog
|
||||
- **Reference models:** City, Country, TimeZone, Feature, Badge
|
||||
- **Error models:** ErrorDetail, APIErrorResponse
|
||||
|
||||
---
|
||||
|
||||
### 3. Errors
|
||||
|
||||
**Ответственность:** Обработка ошибок API
|
||||
|
||||
**Иерархия:**
|
||||
```
|
||||
KworkError (base)
|
||||
├── KworkAuthError # 401, 403
|
||||
├── KworkApiError # 4xx, 5xx
|
||||
│ ├── KworkNotFoundError # 404
|
||||
│ ├── KworkRateLimitError # 429
|
||||
│ └── KworkValidationError # 400
|
||||
└── KworkNetworkError # Network issues
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. HTTP Layer (httpx)
|
||||
|
||||
**Ответственность:** HTTP запросы
|
||||
|
||||
**Функции:**
|
||||
- HTTP/2 поддержка
|
||||
- Таймауты
|
||||
- Cookies management
|
||||
- Token authentication
|
||||
- Error handling
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Flow: Аутентификация
|
||||
|
||||
```
|
||||
User → KworkClient.login(username, password)
|
||||
│
|
||||
▼
|
||||
POST /signIn (cookies)
|
||||
│
|
||||
▼
|
||||
POST /getWebAuthToken (token)
|
||||
│
|
||||
▼
|
||||
Store token + cookies
|
||||
│
|
||||
▼
|
||||
Return authenticated client
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Flow: API Request
|
||||
|
||||
```
|
||||
User → client.catalog.get_list(page=1)
|
||||
│
|
||||
▼
|
||||
CatalogAPI.get_list()
|
||||
│
|
||||
▼
|
||||
KworkClient._request()
|
||||
│
|
||||
▼
|
||||
httpx.AsyncClient.post()
|
||||
│
|
||||
▼
|
||||
KworkClient._handle_response()
|
||||
│
|
||||
▼
|
||||
CatalogResponse.model_validate()
|
||||
│
|
||||
▼
|
||||
Return typed response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 CI/CD Pipeline
|
||||
|
||||
```
|
||||
Push/Tag → GitHub Actions
|
||||
│
|
||||
├── Test Job
|
||||
│ ├── Install UV + Python
|
||||
│ ├── Run linters (ruff)
|
||||
│ ├── Run tests (pytest)
|
||||
│ └── Upload coverage
|
||||
│
|
||||
├── Build Job
|
||||
│ ├── Build wheel + sdist
|
||||
│ └── Upload artifacts
|
||||
│
|
||||
├── Publish Job (on tag)
|
||||
│ ├── Download artifacts
|
||||
│ └── Publish to Gitea Registry
|
||||
│
|
||||
└── Docs Job
|
||||
├── Build MkDocs
|
||||
└── Upload site
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Зависимости
|
||||
|
||||
### Runtime
|
||||
- `httpx[http2]>=0.26.0` — HTTP client
|
||||
- `pydantic>=2.0.0` — Data validation
|
||||
- `structlog>=24.0.0` — Logging
|
||||
|
||||
### Development
|
||||
- `pytest>=8.0.0` — Testing
|
||||
- `pytest-cov>=4.0.0` — Coverage
|
||||
- `pytest-asyncio>=0.23.0` — Async tests
|
||||
- `respx>=0.20.0` — HTTP mocking
|
||||
- `ruff>=0.3.0` — Linting
|
||||
- `mkdocs + mkdocstrings` — Documentation
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
- **Токены:** Не сохраняются в логах
|
||||
- **Пароли:** Передаются только при login()
|
||||
- **Сессии:** Token + cookies хранятся в клиенте
|
||||
- **HTTPS:** Все запросы через HTTPS
|
||||
|
||||
---
|
||||
|
||||
## 📈 Производительность
|
||||
|
||||
- **Async/Await:** Неблокирующие запросы
|
||||
- **HTTP/2:** Multiplexing запросов
|
||||
- **Connection pooling:** Переиспользование соединений
|
||||
- **Timeouts:** Защита от зависаний
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
- **Unit тесты:** 92% coverage
|
||||
- **Mock HTTP:** respx для изоляции
|
||||
- **Async tests:** pytest-asyncio
|
||||
- **CI:** Автоматический прогон при каждом коммите
|
||||
|
||||
---
|
||||
|
||||
## 📝 Лицензия
|
||||
|
||||
MIT License — свободное использование с указанием авторства.
|
||||
211
docs/GITEA_PAGES.md
Normal file
211
docs/GITEA_PAGES.md
Normal file
@ -0,0 +1,211 @@
|
||||
# Gitea Pages — Хостинг документации
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
**Gitea Pages** — аналог GitHub Pages для хостинга статических сайтов напрямую из Gitea.
|
||||
|
||||
**URL документации:** `https://git.much-data.ru/claw/kwork-api/`
|
||||
|
||||
---
|
||||
|
||||
## 🔧 НАСТРОЙКА
|
||||
|
||||
### **Шаг 1: Включить Pages в репозитории**
|
||||
|
||||
1. Зайди в https://git.much-data.ru/claw/kwork-api
|
||||
2. **Settings** → **Pages**
|
||||
3. Включить **Enable Pages**
|
||||
4. Выбрать источник:
|
||||
- **Source:** `gh-pages` branch
|
||||
- **Folder:** `/ (root)`
|
||||
5. **Save**
|
||||
|
||||
---
|
||||
|
||||
### **Шаг 2: Настроить Gitea Token**
|
||||
|
||||
1. https://git.much-data.ru → Settings → Applications
|
||||
2. Создать токен с правами:
|
||||
- `write:repository`
|
||||
- `write:package`
|
||||
3. Скопировать токен
|
||||
4. В репозитории: **Settings** → **Secrets**
|
||||
5. Добавить секрет: `GITEA_TOKEN` = твой токен
|
||||
|
||||
---
|
||||
|
||||
### **Шаг 3: Первый деплой**
|
||||
|
||||
```bash
|
||||
cd /root/kwork-api
|
||||
|
||||
# Собрать документацию
|
||||
uv run mkdocs build
|
||||
|
||||
# Проверить что site/ создан
|
||||
ls -la site/
|
||||
|
||||
# Закоммитить и запушить
|
||||
git add .
|
||||
git commit -m "docs: initial documentation"
|
||||
git push origin main
|
||||
|
||||
# CI/CD автоматически задеплоит на gh-pages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Шаг 4: Проверить**
|
||||
|
||||
После успешного CI/CD:
|
||||
|
||||
**Документация доступна:**
|
||||
```
|
||||
https://git.much-data.ru/claw/kwork-api/
|
||||
```
|
||||
|
||||
Или если включён custom domain:
|
||||
```
|
||||
https://kwork-api.much-data.ru/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 АВТОМАТИЧЕСКИЙ ДЕПЛОЙ
|
||||
|
||||
**Workflow срабатывает при:**
|
||||
- ✅ Push в `main` ветку
|
||||
- ✅ Создании тега релиза
|
||||
|
||||
**Что делает:**
|
||||
1. Собирает MkDocs документацию
|
||||
2. Пушит в `gh-pages` ветку
|
||||
3. Gitea Pages автоматически обновляет сайт
|
||||
|
||||
---
|
||||
|
||||
## 🌐 CUSTOM DOMAIN (опционально)
|
||||
|
||||
### **DNS настройка:**
|
||||
|
||||
```
|
||||
# В панели управления доменом much-data.ru
|
||||
|
||||
# CNAME запись:
|
||||
kwork-api CNAME git.much-data.ru
|
||||
|
||||
# Или A запись:
|
||||
kwork-api A 5.188.26.192
|
||||
```
|
||||
|
||||
### **В Gitea Pages:**
|
||||
|
||||
1. **Settings** → **Pages**
|
||||
2. **Custom Domain:** `kwork-api.much-data.ru`
|
||||
3. **Save**
|
||||
|
||||
### **Создать CNAME файл:**
|
||||
|
||||
```bash
|
||||
# В корне проекта (не в site/)
|
||||
echo "kwork-api.much-data.ru" > static/CNAME
|
||||
git add static/CNAME
|
||||
git commit -m "docs: add custom domain"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 СТРУКТУРА ВЕТКИ
|
||||
|
||||
```
|
||||
main (исходный код)
|
||||
├── src/
|
||||
├── docs/
|
||||
├── mkdocs.yml
|
||||
└── .github/workflows/ci.yml
|
||||
|
||||
gh-pages (автоматически, только сайт)
|
||||
├── index.html
|
||||
├── api-reference/
|
||||
├── search/
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 TROUBLESHOOTING
|
||||
|
||||
### **Pages не включаются:**
|
||||
|
||||
```bash
|
||||
# Проверить что Gitea версия >= 1.19
|
||||
# Админ должен включить Pages в настройках сервера
|
||||
```
|
||||
|
||||
### **CI/CD ошибка деплоя:**
|
||||
|
||||
```bash
|
||||
# Проверить токен
|
||||
# Проверить права токена (write:repository)
|
||||
# Проверить что gh-pages ветка существует
|
||||
```
|
||||
|
||||
### **404 на странице:**
|
||||
|
||||
```bash
|
||||
# Подождать 1-2 минуты (Gitea обрабатывает)
|
||||
# Проверить что site/ не пустой
|
||||
# Проверить workflow логи
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 ЧЕКЛИСТ
|
||||
|
||||
- [ ] Включить Pages в настройках репозитория
|
||||
- [ ] Создать Gitea Token
|
||||
- [ ] Добавить токен в Secrets (`GITEA_TOKEN`)
|
||||
- [ ] Запушить изменения в main
|
||||
- [ ] Дождаться CI/CD
|
||||
- [ ] Проверить https://git.much-data.ru/claw/kwork-api/
|
||||
- [ ] (Опционально) Настроить custom domain
|
||||
|
||||
---
|
||||
|
||||
## 🎯 АЛЬТЕРНАТИВЫ
|
||||
|
||||
Если Gitea Pages не работает:
|
||||
|
||||
### **1. Netlify (бесплатно)**
|
||||
```bash
|
||||
# Подключить репозиторий
|
||||
# Build command: uv run mkdocs build
|
||||
# Publish directory: site/
|
||||
```
|
||||
|
||||
### **2. Vercel (бесплатно)**
|
||||
```bash
|
||||
# Аналогично Netlify
|
||||
# Автоматический деплой из Git
|
||||
```
|
||||
|
||||
### **3. Cloudflare Pages (бесплатно)**
|
||||
```bash
|
||||
# Быстрый CDN
|
||||
# Автоматический HTTPS
|
||||
```
|
||||
|
||||
### **4. Своё сервер (nginx)**
|
||||
```bash
|
||||
# Скопировать site/ на сервер
|
||||
# Настроить nginx на /var/www/kwork-api-docs/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 ССЫЛКИ
|
||||
|
||||
- [Gitea Pages Documentation](https://docs.gitea.com/usage/pages)
|
||||
- [MkDocs Documentation](https://www.mkdocs.org/)
|
||||
- [Gitea Actions](https://docs.gitea.com/usage/actions)
|
||||
131
docs/RELEASE.md
Normal file
131
docs/RELEASE.md
Normal file
@ -0,0 +1,131 @@
|
||||
# Release Guide — kwork-api
|
||||
|
||||
## 📋 Стратегия версионирования
|
||||
|
||||
Используем **SemVer** (Semantic Versioning): `MAJOR.MINOR.PATCH`
|
||||
|
||||
- **MAJOR** — ломающие изменения API
|
||||
- **MINOR** — новая функциональность (обратно совместимая)
|
||||
- **PATCH** — багфиксы (обратно совместимые)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Процесс релиза
|
||||
|
||||
### 1. Подготовка
|
||||
|
||||
```bash
|
||||
# Убедись что все тесты проходят
|
||||
uv run pytest
|
||||
|
||||
# Проверь линтеры
|
||||
uv run ruff check src/ tests/
|
||||
|
||||
# Проверь сборку
|
||||
uv build
|
||||
```
|
||||
|
||||
### 2. Обновление версии
|
||||
|
||||
```bash
|
||||
# Обновить версию в pyproject.toml
|
||||
# Например: 0.1.0 → 0.1.1
|
||||
|
||||
# Создать тег
|
||||
git tag -a v0.1.1 -m "Release v0.1.1"
|
||||
|
||||
# Отпушить тег
|
||||
git push origin v0.1.1
|
||||
```
|
||||
|
||||
### 3. Автоматическая публикация
|
||||
|
||||
После пуша тега:
|
||||
1. ✅ Запускается CI/CD pipeline
|
||||
2. ✅ Прогоняются тесты
|
||||
3. ✅ Собирается пакет
|
||||
4. ✅ Публикуется в Gitea Registry
|
||||
|
||||
---
|
||||
|
||||
## 📦 Gitea Package Registry
|
||||
|
||||
**URL:** `https://git.much-data.ru/api/packages/claw/pypi`
|
||||
|
||||
**Установка:**
|
||||
```bash
|
||||
# Создать .pypirc в домашней директории
|
||||
cat > ~/.pypirc << EOF
|
||||
[pypi]
|
||||
username = claw
|
||||
password = YOUR_GITEA_TOKEN
|
||||
|
||||
[git.much-data.ru]
|
||||
repository = https://git.much-data.ru/api/packages/claw/pypi
|
||||
username = claw
|
||||
password = YOUR_GITEA_TOKEN
|
||||
EOF
|
||||
|
||||
# Установить из Gitea
|
||||
uv pip install kwork-api --index-url https://git.much-data.ru/api/packages/claw/pypi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Получение Gitea Token
|
||||
|
||||
1. Зайди в https://git.much-data.ru
|
||||
2. Профиль → Settings → Applications
|
||||
3. Создать токен с правами `write:package`
|
||||
4. Сохрани токен в секреты репозитория: `GITEA_TOKEN`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Changelog
|
||||
|
||||
Ведётся в `CHANGELOG.md` по формату [Keep a Changelog](https://keepachangelog.com/).
|
||||
|
||||
### Пример:
|
||||
```markdown
|
||||
## [0.1.1] - 2026-03-23
|
||||
|
||||
### Fixed
|
||||
- Исправлена ошибка аутентификации при истечении токена
|
||||
|
||||
### Changed
|
||||
- Обновлены зависимости
|
||||
|
||||
## [0.1.0] - 2026-03-23
|
||||
|
||||
### Added
|
||||
- Первый релиз
|
||||
- Базовая функциональность клиента
|
||||
- Документация 100%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Чеклист перед релизом
|
||||
|
||||
- [ ] Все тесты проходят
|
||||
- [ ] Линтеры без ошибок
|
||||
- [ ] Документация обновлена
|
||||
- [ ] CHANGELOG.md обновлён
|
||||
- [ ] Версия в pyproject.toml обновлена
|
||||
- [ ] Тег создан и отправлен
|
||||
- [ ] CI/CD pipeline успешен
|
||||
- [ ] Пакет опубликован
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Ручная публикация (если нужно)
|
||||
|
||||
```bash
|
||||
# Собрать
|
||||
uv build
|
||||
|
||||
# Опубликовать
|
||||
uv publish \
|
||||
--publish-url https://git.much-data.ru/api/packages/claw/pypi \
|
||||
--token YOUR_GITEA_TOKEN
|
||||
```
|
||||
281
docs/SEMANTIC_RELEASE.md
Normal file
281
docs/SEMANTIC_RELEASE.md
Normal file
@ -0,0 +1,281 @@
|
||||
# Semantic Release — Автоматическое версионирование
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
**python-semantic-release** автоматически определяет версию на основе Conventional Commits.
|
||||
|
||||
**Как работает:**
|
||||
```
|
||||
Commit → Анализ сообщения → Определение типа → Bump версии → Тег → Релиз
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 CONVENTIONAL COMMITS
|
||||
|
||||
### **Формат коммита:**
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
### **Типы коммитов и влияние на версию:**
|
||||
|
||||
| Тип | Влияние | Пример | Версия |
|
||||
|-----|---------|--------|--------|
|
||||
| `feat` | **MINOR** | `feat: add new API endpoint` | 0.1.0 → 0.2.0 |
|
||||
| `fix` | **PATCH** | `fix: handle timeout errors` | 0.1.0 → 0.1.1 |
|
||||
| `perf` | **PATCH** | `perf: optimize HTTP requests` | 0.1.0 → 0.1.1 |
|
||||
| `feat` + BREAKING | **MAJOR** | `feat: change auth method` | 0.1.0 → 1.0.0 |
|
||||
| `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `build` | Нет | `docs: update README` | Без изменений |
|
||||
|
||||
---
|
||||
|
||||
## 📝 ПРИМЕРЫ КОММИТОВ
|
||||
|
||||
### **PATCH (багфиксы):**
|
||||
|
||||
```bash
|
||||
git commit -m "fix: handle 404 error in catalog API"
|
||||
git commit -m "fix(auth): restore session from token correctly"
|
||||
git commit -m "perf: reduce HTTP connection overhead"
|
||||
```
|
||||
|
||||
### **MINOR (новая функциональность):**
|
||||
|
||||
```bash
|
||||
git commit -m "feat: add batch kwork details endpoint"
|
||||
git commit -m "feat(projects): add get_payer_orders method"
|
||||
git commit -m "feat: support HTTP/2 for faster requests"
|
||||
```
|
||||
|
||||
### **MAJOR (ломающие изменения):**
|
||||
|
||||
```bash
|
||||
git commit -m "feat: redesign authentication flow
|
||||
|
||||
BREAKING CHANGE: login() now returns KworkClient instead of tuple
|
||||
|
||||
Migration:
|
||||
Before: token, cookies = await login(user, pass)
|
||||
After: client = await KworkClient.login(user, pass)
|
||||
"
|
||||
```
|
||||
|
||||
### **Без влияния на версию:**
|
||||
|
||||
```bash
|
||||
git commit -m "docs: add usage examples to README"
|
||||
git commit -m "test: increase coverage to 95%"
|
||||
git commit -m "style: fix formatting with ruff"
|
||||
git commit -m "refactor: simplify error handling"
|
||||
git commit -m "chore: update dependencies"
|
||||
git commit -m "ci: add Gitea Actions workflow"
|
||||
git commit -m "build: configure UV build system"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 WORKFLOW
|
||||
|
||||
### **Разработка:**
|
||||
|
||||
```bash
|
||||
# Делай коммиты по Conventional Commits
|
||||
git commit -m "feat: add new endpoint"
|
||||
git commit -m "fix: handle edge case"
|
||||
git commit -m "docs: update documentation"
|
||||
|
||||
# Пуш в main
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### **CI/CD (автоматически):**
|
||||
|
||||
```
|
||||
1. Тесты запускаются
|
||||
2. Сборка пакета
|
||||
3. Semantic Release анализирует коммиты
|
||||
4. Определяет тип версии (MAJOR/MINOR/PATCH)
|
||||
5. Обновляет версию в pyproject.toml и __init__.py
|
||||
6. Создаёт Git тег
|
||||
7. Генерирует CHANGELOG
|
||||
8. Создаёт релиз в Gitea
|
||||
9. Публикует пакет в Gitea Registry
|
||||
```
|
||||
|
||||
### **Результат:**
|
||||
|
||||
```
|
||||
✅ v0.1.1 создан
|
||||
✅ CHANGELOG.md обновлён
|
||||
✅ Пакет опубликован
|
||||
✅ Релиз в Gitea создан
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 РУЧНОЕ УПРАВЛЕНИЕ
|
||||
|
||||
### **Проверить следующую версию:**
|
||||
|
||||
```bash
|
||||
cd /root/kwork-api
|
||||
uv run semantic-release version --print
|
||||
```
|
||||
|
||||
### **Сгенерировать CHANGELOG:**
|
||||
|
||||
```bash
|
||||
uv run semantic-release changelog
|
||||
```
|
||||
|
||||
### **Создать релиз вручную:**
|
||||
|
||||
```bash
|
||||
# Bump версии
|
||||
uv run semantic-release version --no-push
|
||||
|
||||
# Проверить что изменилось
|
||||
git diff
|
||||
|
||||
# Запушить
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 ПРИМЕР ИСТОРИИ ВЕРСИЙ
|
||||
|
||||
```
|
||||
v0.1.0 (2026-03-23)
|
||||
- Initial release
|
||||
- Complete API client
|
||||
- 100% documentation
|
||||
|
||||
v0.1.1 (2026-03-24)
|
||||
- fix: handle timeout errors
|
||||
- fix: restore session correctly
|
||||
|
||||
v0.2.0 (2026-03-25)
|
||||
- feat: add batch endpoint
|
||||
- feat: support HTTP/2
|
||||
|
||||
v0.2.1 (2026-03-26)
|
||||
- perf: optimize requests
|
||||
|
||||
v1.0.0 (2026-03-27)
|
||||
- feat: new authentication
|
||||
- BREAKING CHANGE: API changed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ КОНФИГУРАЦИЯ
|
||||
|
||||
**Файл:** `pyproject.toml`
|
||||
|
||||
```toml
|
||||
[tool.semantic_release]
|
||||
version_toml = ["pyproject.toml:project.version"]
|
||||
version_variables = ["src/kwork_api/__init__.py:__version__"]
|
||||
branch = "main"
|
||||
build_command = "uv build"
|
||||
commit_parser = "angular"
|
||||
tag_format = "v{version}"
|
||||
|
||||
[tool.semantic_release.commit_parser_options]
|
||||
minor_tags = ["feat"]
|
||||
patch_tags = ["fix", "perf"]
|
||||
breaking_change_tags = ["feat"]
|
||||
|
||||
[tool.semantic_release.remote]
|
||||
type = "gitea"
|
||||
domain = "https://git.much-data.ru"
|
||||
owner = "claw"
|
||||
repo_name = "kwork-api"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 TROUBLESHOOTING
|
||||
|
||||
### **Ошибка: "No commits to release"**
|
||||
|
||||
```bash
|
||||
# Значит не было коммитов с типами feat/fix/perf
|
||||
# Сделай коммит с правильным форматом
|
||||
git commit -m "feat: add something new"
|
||||
```
|
||||
|
||||
### **Ошибка: "Gitea token invalid"**
|
||||
|
||||
```bash
|
||||
# Проверь токен
|
||||
# Settings → Applications → Create new token
|
||||
# Права: write:repository, write:package
|
||||
# Обнови секрет в Gitea Actions
|
||||
```
|
||||
|
||||
### **Версия не обновляется**
|
||||
|
||||
```bash
|
||||
# Проверь конфигурацию
|
||||
uv run semantic-release --version
|
||||
|
||||
# Проверь что __version__ есть в __init__.py
|
||||
grep "__version__" src/kwork_api/__init__.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 ЧЕКЛИСТ ПЕРЕД ПУШЕМ
|
||||
|
||||
- [ ] Коммиты по Conventional Commits
|
||||
- [ ] Тесты проходят
|
||||
- [ ] CHANGELOG обновлён (автоматически)
|
||||
- [ ] Версия в __init__.py совпадает
|
||||
- [ ] GITEA_TOKEN настроен в секретах
|
||||
|
||||
---
|
||||
|
||||
## 🎯 BEST PRACTICES
|
||||
|
||||
### **✅ Делай:**
|
||||
|
||||
```bash
|
||||
# Атомарные коммиты
|
||||
git commit -m "feat: add user endpoint"
|
||||
git commit -m "fix: handle 404 error"
|
||||
|
||||
# Понятные описания
|
||||
git commit -m "fix: restore session from saved token"
|
||||
|
||||
# Scope для больших изменений
|
||||
git commit -m "feat(auth): add OAuth2 support"
|
||||
```
|
||||
|
||||
### **❌ Не делай:**
|
||||
|
||||
```bash
|
||||
# Слишком общие
|
||||
git commit -m "fix stuff"
|
||||
|
||||
# Несколько изменений в одном
|
||||
git commit -m "feat: add user endpoint and fix auth and update docs"
|
||||
|
||||
# Не по формату
|
||||
git commit -m "added new feature"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 ССЫЛКИ
|
||||
|
||||
- [python-semantic-release](https://python-semantic-release.readthedocs.io/)
|
||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
- [Angular Commit Guidelines](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit)
|
||||
9
docs/api-reference.md
Normal file
9
docs/api-reference.md
Normal file
@ -0,0 +1,9 @@
|
||||
# API Reference
|
||||
|
||||
Complete API documentation for Kwork API client.
|
||||
|
||||
## Modules
|
||||
|
||||
- [Client](api/client.md) — Main client class and API groups
|
||||
- [Models](api/models.md) — Pydantic models for API responses
|
||||
- [Errors](api/errors.md) — Exception classes
|
||||
3
docs/api/client.md
Normal file
3
docs/api/client.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Client API
|
||||
|
||||
::: kwork_api.client.KworkClient
|
||||
5
docs/api/errors.md
Normal file
5
docs/api/errors.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Errors
|
||||
|
||||
Exception classes for API errors.
|
||||
|
||||
::: kwork_api.errors
|
||||
5
docs/api/models.md
Normal file
5
docs/api/models.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Models
|
||||
|
||||
Pydantic models for API responses.
|
||||
|
||||
::: kwork_api.models
|
||||
0
docs/api_reference.md
Normal file
0
docs/api_reference.md
Normal file
109
docs/index.md
Normal file
109
docs/index.md
Normal file
@ -0,0 +1,109 @@
|
||||
# Kwork API — Python Client
|
||||
|
||||
Unofficial Python client for Kwork.ru API.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install kwork-api
|
||||
```
|
||||
|
||||
Or with UV:
|
||||
|
||||
```bash
|
||||
uv add 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
|
||||
async with await KworkClient.login("username", "password") as client:
|
||||
catalog = await client.catalog.get_list(page=1)
|
||||
# Client automatically closes
|
||||
```
|
||||
|
||||
### Save and restore session
|
||||
|
||||
```python
|
||||
# Save credentials after login
|
||||
client = await KworkClient.login("username", "password")
|
||||
|
||||
# Option 1: Save token only
|
||||
token = client.token
|
||||
|
||||
# Option 2: Save full credentials (token + cookies)
|
||||
creds = client.credentials
|
||||
import json
|
||||
with open("session.json", "w") as f:
|
||||
json.dump(creds, f)
|
||||
|
||||
# Later, restore session
|
||||
client = KworkClient(token=token)
|
||||
# or
|
||||
client = KworkClient(**creds)
|
||||
|
||||
user_info = await client.user.get_info()
|
||||
```
|
||||
|
||||
## API Overview
|
||||
|
||||
### Catalog API
|
||||
|
||||
- `client.catalog.get_list()` — Get kworks catalog
|
||||
- `client.catalog.get_details(kwork_id)` — Get kwork details
|
||||
|
||||
### Projects API
|
||||
|
||||
- `client.projects.get_list()` — Get freelance projects
|
||||
- `client.projects.get_payer_orders()` — Your orders as customer
|
||||
- `client.projects.get_worker_orders()` — Your orders as performer
|
||||
|
||||
### User API
|
||||
|
||||
- `client.user.get_info()` — Get user profile
|
||||
- `client.user.get_reviews()` — Get user reviews
|
||||
- `client.user.get_favorite_kworks()` — Get favorite kworks
|
||||
|
||||
### Settings & Preferences
|
||||
|
||||
- `client.get_wants()` — User preferences
|
||||
- `client.get_kworks_status()` — Kworks status
|
||||
- `client.update_settings()` — Update settings
|
||||
- `client.go_offline()` — Set offline status
|
||||
|
||||
See [API Reference](api-reference.md) for full documentation.
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
from kwork_api import KworkError, KworkAuthError, KworkApiError
|
||||
|
||||
try:
|
||||
await client.catalog.get_list()
|
||||
except KworkAuthError:
|
||||
print("Invalid credentials")
|
||||
except KworkApiError as e:
|
||||
print(f"API error: {e.status_code}")
|
||||
except KworkError as e:
|
||||
print(f"General error: {e.message}")
|
||||
```
|
||||
79
mkdocs.yml
Normal file
79
mkdocs.yml
Normal file
@ -0,0 +1,79 @@
|
||||
site_name: Kwork API
|
||||
site_description: Unofficial Python client for Kwork.ru API
|
||||
site_url: https://github.com/claw/kwork-api
|
||||
|
||||
repo_name: claw/kwork-api
|
||||
repo_url: https://github.com/claw/kwork-api
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.sections
|
||||
- toc.integrate
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
palette:
|
||||
- scheme: default
|
||||
toggle:
|
||||
icon: material/toggle-switch-off-outline
|
||||
name: Switch to dark mode
|
||||
- scheme: slate
|
||||
toggle:
|
||||
icon: material/toggle-switch
|
||||
name: Switch to light mode
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths: [src]
|
||||
options:
|
||||
docstring_style: google
|
||||
show_source: true
|
||||
show_root_heading: true
|
||||
show_category_heading: true
|
||||
merge_init_into_class: true
|
||||
separate_signature: true
|
||||
signature_crossrefs: true
|
||||
filters:
|
||||
- "!^_"
|
||||
- "^__init__"
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- def_list
|
||||
- footnotes
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.arithmatex:
|
||||
generic: true
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- pymdownx.caret
|
||||
- pymdownx.details
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.keys
|
||||
- pymdownx.magiclink:
|
||||
repo_url_shorthand: true
|
||||
user: claw
|
||||
repo: kwork-api
|
||||
- pymdownx.mark
|
||||
- pymdownx.smartsymbols
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.tilde
|
||||
|
||||
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
|
||||
22
pydoc-markdown.yml
Normal file
22
pydoc-markdown.yml
Normal file
@ -0,0 +1,22 @@
|
||||
loaders:
|
||||
- type: python
|
||||
search_path: [src]
|
||||
packages: [kwork_api]
|
||||
|
||||
processors:
|
||||
- type: filter
|
||||
skip_empty_modules: true
|
||||
documented_only: true
|
||||
do_not_filter_modules: true
|
||||
expression: "not (name.startswith('_') and not name.startswith('__'))"
|
||||
- type: smart
|
||||
- type: crossref
|
||||
|
||||
renderer:
|
||||
type: markdown
|
||||
filename: api_reference.md
|
||||
render_toc: true
|
||||
descriptive_class_title: false
|
||||
descriptive_module_title: true
|
||||
add_method_class_prefix: true
|
||||
add_member_class_prefix: false
|
||||
154
pyproject.toml
Normal file
154
pyproject.toml
Normal file
@ -0,0 +1,154 @@
|
||||
[project]
|
||||
name = "kwork-api"
|
||||
version = "0.1.0"
|
||||
description = "Unofficial Kwork.ru API client"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{name = "Claw", email = "claw@localhost"}
|
||||
]
|
||||
keywords = ["kwork", "api", "client", "parsing", "freelance"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"httpx[http2]>=0.26.0",
|
||||
"pydantic>=2.0.0",
|
||||
"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"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/kwork_api"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"respx>=0.20.0",
|
||||
"ruff>=0.3.0",
|
||||
"pydoc-markdown>=4.8.2",
|
||||
"mkdocs>=1.6.1",
|
||||
"mkdocs-material>=9.7.6",
|
||||
"mkdocstrings>=1.0.3",
|
||||
"mkdocstrings-python>=2.0.3",
|
||||
"python-semantic-release>=10.5.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.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["kwork_api"]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"raise NotImplementedError",
|
||||
"if __name__ == .__main__.:",
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
|
||||
# ============================================
|
||||
# Python Semantic Release Configuration
|
||||
# ============================================
|
||||
[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"
|
||||
upload_to_vcs_release = true
|
||||
tag_format = "v{version}"
|
||||
|
||||
[tool.semantic_release.branches.main]
|
||||
match = "main"
|
||||
prerelease = false
|
||||
|
||||
[tool.semantic_release.commit_parser_options]
|
||||
allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"]
|
||||
minor_tags = ["feat"]
|
||||
patch_tags = ["fix", "perf"]
|
||||
breaking_change_tags = ["feat"]
|
||||
|
||||
[tool.semantic_release.remote]
|
||||
type = "gitea"
|
||||
domain = "https://git.much-data.ru"
|
||||
owner = "claw"
|
||||
repo_name = "kwork-api"
|
||||
token = { env = "GITEA_TOKEN" }
|
||||
|
||||
[tool.semantic_release.publish]
|
||||
dist_glob_patterns = ["dist/*"]
|
||||
upload_to_vcs_release = true
|
||||
|
||||
[tool.semantic_release.changelog]
|
||||
template_dir = "templates"
|
||||
changelog_file = "CHANGELOG.md"
|
||||
exclude_commit_patterns = [
|
||||
"chore\\(release\\):.*",
|
||||
"ci\\(release\\):.*",
|
||||
]
|
||||
|
||||
[tool.semantic_release.changelog.environment]
|
||||
trim_blocks = true
|
||||
lstrip_blocks = true
|
||||
|
||||
|
||||
44
src/kwork_api/__init__.py
Normal file
44
src/kwork_api/__init__.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""
|
||||
Kwork.ru API Client
|
||||
|
||||
Unofficial Python client for Kwork.ru API.
|
||||
|
||||
Example:
|
||||
from kwork_api import KworkClient
|
||||
|
||||
# Login with credentials
|
||||
client = await KworkClient.login("username", "password")
|
||||
|
||||
# Or restore from token
|
||||
client = KworkClient(token="your_web_auth_token")
|
||||
|
||||
# Get catalog
|
||||
catalog = await client.catalog.get_list(page=1)
|
||||
"""
|
||||
|
||||
from .client import KworkClient
|
||||
from .errors import KworkError, KworkAuthError, KworkApiError
|
||||
from .models import (
|
||||
ValidationResponse,
|
||||
ValidationIssue,
|
||||
Kwork,
|
||||
KworkDetails,
|
||||
Project,
|
||||
CatalogResponse,
|
||||
ProjectsResponse,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = [
|
||||
"KworkClient",
|
||||
"KworkError",
|
||||
"KworkAuthError",
|
||||
"KworkApiError",
|
||||
"ValidationResponse",
|
||||
"ValidationIssue",
|
||||
"Kwork",
|
||||
"KworkDetails",
|
||||
"Project",
|
||||
"CatalogResponse",
|
||||
"ProjectsResponse",
|
||||
]
|
||||
BIN
src/kwork_api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
src/kwork_api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/kwork_api/__pycache__/client.cpython-312.pyc
Normal file
BIN
src/kwork_api/__pycache__/client.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/kwork_api/__pycache__/errors.cpython-312.pyc
Normal file
BIN
src/kwork_api/__pycache__/errors.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/kwork_api/__pycache__/models.cpython-312.pyc
Normal file
BIN
src/kwork_api/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
1254
src/kwork_api/client.py
Normal file
1254
src/kwork_api/client.py
Normal file
File diff suppressed because it is too large
Load Diff
192
src/kwork_api/errors.py
Normal file
192
src/kwork_api/errors.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""
|
||||
Исключения Kwork API.
|
||||
|
||||
Все исключения предоставляют понятные сообщения для отладки.
|
||||
Иерархия исключений:
|
||||
|
||||
KworkError (базовое)
|
||||
├── KworkAuthError (ошибки аутентификации)
|
||||
├── KworkApiError (HTTP ошибки API)
|
||||
│ ├── KworkNotFoundError (404)
|
||||
│ ├── KworkRateLimitError (429)
|
||||
│ └── KworkValidationError (400)
|
||||
└── KworkNetworkError (ошибки сети)
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class KworkError(Exception):
|
||||
"""
|
||||
Базовое исключение для всех ошибок Kwork API.
|
||||
|
||||
Все остальные исключения наследуются от этого класса.
|
||||
|
||||
Attributes:
|
||||
message: Сообщение об ошибке.
|
||||
response: Оригинальный HTTP response (если есть).
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_list()
|
||||
except KworkError as e:
|
||||
print(f"Ошибка: {e.message}")
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, response: Optional[Any] = None):
|
||||
self.message = message
|
||||
self.response = response
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"KworkError: {self.message}"
|
||||
|
||||
|
||||
class KworkAuthError(KworkError):
|
||||
"""
|
||||
Ошибка аутентификации/авторизации.
|
||||
|
||||
Возникает при:
|
||||
- Неверном логине или пароле
|
||||
- Истёкшем или невалидном токене
|
||||
- Отсутствии прав доступа (403)
|
||||
|
||||
Example:
|
||||
try:
|
||||
client = await KworkClient.login("user", "wrong_password")
|
||||
except KworkAuthError:
|
||||
print("Неверные учётные данные")
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "Authentication failed", response: Optional[Any] = None):
|
||||
super().__init__(message, response)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"KworkAuthError: {self.message}"
|
||||
|
||||
|
||||
class KworkApiError(KworkError):
|
||||
"""
|
||||
Ошибка HTTP запроса к API (4xx, 5xx).
|
||||
|
||||
Базовый класс для HTTP ошибок API. Содержит код статуса.
|
||||
|
||||
Attributes:
|
||||
status_code: HTTP код ответа (400, 404, 500, etc.)
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_details(999999)
|
||||
except KworkApiError as e:
|
||||
print(f"HTTP {e.status_code}: {e.message}")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: Optional[int] = None,
|
||||
response: Optional[Any] = None,
|
||||
):
|
||||
self.status_code = status_code
|
||||
super().__init__(message, response)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.status_code:
|
||||
return f"KworkApiError [{self.status_code}]: {self.message}"
|
||||
return f"KworkApiError: {self.message}"
|
||||
|
||||
|
||||
class KworkNotFoundError(KworkApiError):
|
||||
"""
|
||||
Ресурс не найден (404).
|
||||
|
||||
Возникает при запросе несуществующего кворка,
|
||||
пользователя или другого ресурса.
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_details(999999)
|
||||
except KworkNotFoundError:
|
||||
print("Кворк не найден")
|
||||
"""
|
||||
|
||||
def __init__(self, resource: str, response: Optional[Any] = None):
|
||||
super().__init__(f"Resource not found: {resource}", 404, response)
|
||||
|
||||
|
||||
class KworkRateLimitError(KworkApiError):
|
||||
"""
|
||||
Превышен лимит запросов (429).
|
||||
|
||||
Возникает при слишком частых запросах к API.
|
||||
Рекомендуется сделать паузу перед повторным запросом.
|
||||
|
||||
Example:
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
await client.catalog.get_list()
|
||||
except KworkRateLimitError:
|
||||
await asyncio.sleep(5) # Пауза 5 секунд
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "Rate limit exceeded", response: Optional[Any] = None):
|
||||
super().__init__(message, 429, response)
|
||||
|
||||
|
||||
class KworkValidationError(KworkApiError):
|
||||
"""
|
||||
Ошибка валидации (400).
|
||||
|
||||
Возникает при некорректных данных запроса.
|
||||
|
||||
Attributes:
|
||||
fields: Словарь ошибок по полям {field: [errors]}.
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_list(page=-1)
|
||||
except KworkValidationError as e:
|
||||
if e.fields:
|
||||
for field, errors in e.fields.items():
|
||||
print(f"{field}: {errors[0]}")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Validation failed",
|
||||
fields: Optional[dict[str, list[str]]] = None,
|
||||
response: Optional[Any] = None,
|
||||
):
|
||||
self.fields = fields or {}
|
||||
super().__init__(message, 400, response)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.fields:
|
||||
field_errors = ", ".join(f"{k}: {v[0]}" for k, v in self.fields.items())
|
||||
return f"KworkValidationError: {field_errors}"
|
||||
return f"KworkValidationError: {self.message}"
|
||||
|
||||
|
||||
class KworkNetworkError(KworkError):
|
||||
"""
|
||||
Ошибка сети/подключения.
|
||||
|
||||
Возникает при:
|
||||
- Отсутствии соединения
|
||||
- Таймауте запроса
|
||||
- Ошибке DNS
|
||||
- Проблемах с SSL
|
||||
|
||||
Example:
|
||||
try:
|
||||
await client.catalog.get_list()
|
||||
except KworkNetworkError:
|
||||
print("Проверьте подключение к интернету")
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "Network error", response: Optional[Any] = None):
|
||||
super().__init__(message, response)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"KworkNetworkError: {self.message}"
|
||||
450
src/kwork_api/models.py
Normal file
450
src/kwork_api/models.py
Normal file
@ -0,0 +1,450 @@
|
||||
"""
|
||||
Pydantic модели для ответов Kwork API.
|
||||
|
||||
Все модели соответствуют структуре, найденной при анализе HAR дампа.
|
||||
Используются для валидации и типизации ответов API.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class KworkUser(BaseModel):
|
||||
"""
|
||||
Информация о пользователе Kwork.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID пользователя.
|
||||
username: Имя пользователя (логин).
|
||||
avatar_url: URL аватара или None.
|
||||
is_online: Статус онлайн.
|
||||
rating: Рейтинг пользователя (0-5).
|
||||
|
||||
Example:
|
||||
user = KworkUser(id=123, username="seller", rating=4.9)
|
||||
print(f"{user.username}: {user.rating} ★")
|
||||
"""
|
||||
id: int
|
||||
username: str
|
||||
avatar_url: Optional[str] = None
|
||||
is_online: bool = False
|
||||
rating: Optional[float] = None
|
||||
|
||||
|
||||
class KworkCategory(BaseModel):
|
||||
"""
|
||||
Категория кворков.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID категории.
|
||||
name: Название категории.
|
||||
slug: URL-safe идентификатор.
|
||||
parent_id: ID родительской категории для вложенности.
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
parent_id: Optional[int] = None
|
||||
|
||||
|
||||
class Kwork(BaseModel):
|
||||
"""
|
||||
Кворк — услуга на Kwork.
|
||||
|
||||
Базовая модель кворка с основной информацией.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID кворка.
|
||||
title: Заголовок кворка.
|
||||
description: Краткое описание.
|
||||
price: Цена в рублях.
|
||||
currency: Валюта (по умолчанию RUB).
|
||||
category_id: ID категории.
|
||||
seller: Информация о продавце.
|
||||
images: Список URL изображений.
|
||||
rating: Рейтинг кворка (0-5).
|
||||
reviews_count: Количество отзывов.
|
||||
created_at: Дата создания.
|
||||
updated_at: Дата последнего обновления.
|
||||
"""
|
||||
id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
price: float
|
||||
currency: str = "RUB"
|
||||
category_id: Optional[int] = None
|
||||
seller: Optional[KworkUser] = None
|
||||
images: list[str] = Field(default_factory=list)
|
||||
rating: Optional[float] = None
|
||||
reviews_count: int = 0
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class KworkDetails(Kwork):
|
||||
"""
|
||||
Расширенная информация о кворке.
|
||||
|
||||
Наследует все поля Kwork плюс дополнительные детали.
|
||||
|
||||
Attributes:
|
||||
full_description: Полное описание услуги.
|
||||
requirements: Требования к заказчику.
|
||||
delivery_time: Срок выполнения в днях.
|
||||
revisions: Количество бесплатных правок.
|
||||
features: Список дополнительных опций.
|
||||
faq: Список вопросов и ответов.
|
||||
"""
|
||||
full_description: Optional[str] = None
|
||||
requirements: Optional[str] = None
|
||||
delivery_time: Optional[int] = None
|
||||
revisions: Optional[int] = None
|
||||
features: list[str] = Field(default_factory=list)
|
||||
faq: list[dict[str, str]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PaginationInfo(BaseModel):
|
||||
"""
|
||||
Информация о пагинации.
|
||||
|
||||
Attributes:
|
||||
current_page: Текущая страница (начиная с 1).
|
||||
total_pages: Общее количество страниц.
|
||||
total_items: Общее количество элементов.
|
||||
items_per_page: Элементов на странице.
|
||||
has_next: Есть ли следующая страница.
|
||||
has_prev: Есть ли предыдущая страница.
|
||||
"""
|
||||
current_page: int = 1
|
||||
total_pages: int = 1
|
||||
total_items: int = 0
|
||||
items_per_page: int = 20
|
||||
has_next: bool = False
|
||||
has_prev: bool = False
|
||||
|
||||
|
||||
class CatalogResponse(BaseModel):
|
||||
"""
|
||||
Ответ API каталога кворков.
|
||||
|
||||
Attributes:
|
||||
kworks: Список кворков на странице.
|
||||
pagination: Информация о пагинации.
|
||||
filters: Доступные фильтры.
|
||||
sort_options: Доступные опции сортировки.
|
||||
"""
|
||||
kworks: list[Kwork] = Field(default_factory=list)
|
||||
pagination: Optional[PaginationInfo] = None
|
||||
filters: Optional[dict[str, Any]] = None
|
||||
sort_options: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Project(BaseModel):
|
||||
"""
|
||||
Проект (заказ на бирже фриланса).
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID проекта.
|
||||
title: Заголовок проекта.
|
||||
description: Описание задачи.
|
||||
budget: Бюджет проекта.
|
||||
budget_type: Тип бюджета: "fixed" (фиксированный) или "hourly" (почасовой).
|
||||
category_id: ID категории.
|
||||
customer: Информация о заказчике.
|
||||
status: Статус проекта: "open", "in_progress", "completed", "cancelled".
|
||||
created_at: Дата создания.
|
||||
updated_at: Дата обновления.
|
||||
bids_count: Количество откликов.
|
||||
skills: Требуемые навыки.
|
||||
"""
|
||||
id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
budget: Optional[float] = None
|
||||
budget_type: str = "fixed"
|
||||
category_id: Optional[int] = None
|
||||
customer: Optional[KworkUser] = None
|
||||
status: str = "open"
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
bids_count: int = 0
|
||||
skills: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ProjectsResponse(BaseModel):
|
||||
"""
|
||||
Ответ API списка проектов.
|
||||
|
||||
Attributes:
|
||||
projects: Список проектов.
|
||||
pagination: Информация о пагинации.
|
||||
"""
|
||||
projects: list[Project] = Field(default_factory=list)
|
||||
pagination: Optional[PaginationInfo] = None
|
||||
|
||||
|
||||
class Review(BaseModel):
|
||||
"""
|
||||
Отзыв о кворке или проекте.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID отзыва.
|
||||
rating: Оценка от 1 до 5.
|
||||
comment: Текст отзыва.
|
||||
author: Автор отзыва.
|
||||
kwork_id: ID кворка (если отзыв о кворке).
|
||||
created_at: Дата создания.
|
||||
"""
|
||||
id: int
|
||||
rating: int = Field(ge=1, le=5)
|
||||
comment: Optional[str] = None
|
||||
author: Optional[KworkUser] = None
|
||||
kwork_id: Optional[int] = None
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class ReviewsResponse(BaseModel):
|
||||
"""
|
||||
Ответ API списка отзывов.
|
||||
|
||||
Attributes:
|
||||
reviews: Список отзывов.
|
||||
pagination: Информация о пагинации.
|
||||
average_rating: Средний рейтинг.
|
||||
"""
|
||||
reviews: list[Review] = Field(default_factory=list)
|
||||
pagination: Optional[PaginationInfo] = None
|
||||
average_rating: Optional[float] = None
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
"""
|
||||
Уведомление пользователя.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID уведомления.
|
||||
type: Тип уведомления: "message", "order", "system", etc.
|
||||
title: Заголовок уведомления.
|
||||
message: Текст уведомления.
|
||||
is_read: Прочитано ли уведомление.
|
||||
created_at: Дата создания.
|
||||
link: Ссылка для перехода (если есть).
|
||||
"""
|
||||
id: int
|
||||
type: str
|
||||
title: str
|
||||
message: str
|
||||
is_read: bool = False
|
||||
created_at: Optional[datetime] = None
|
||||
link: Optional[str] = None
|
||||
|
||||
|
||||
class NotificationsResponse(BaseModel):
|
||||
"""
|
||||
Ответ API списка уведомлений.
|
||||
|
||||
Attributes:
|
||||
notifications: Список уведомлений.
|
||||
unread_count: Количество непрочитанных уведомлений.
|
||||
"""
|
||||
notifications: list[Notification] = Field(default_factory=list)
|
||||
unread_count: int = 0
|
||||
|
||||
|
||||
class Dialog(BaseModel):
|
||||
"""
|
||||
Диалог (чат) с пользователем.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID диалога.
|
||||
participant: Собеседник.
|
||||
last_message: Текст последнего сообщения.
|
||||
unread_count: Количество непрочитанных сообщений.
|
||||
updated_at: Время последнего сообщения.
|
||||
"""
|
||||
id: int
|
||||
participant: Optional[KworkUser] = None
|
||||
last_message: Optional[str] = None
|
||||
unread_count: int = 0
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class AuthResponse(BaseModel):
|
||||
"""
|
||||
Ответ API аутентификации.
|
||||
|
||||
Attributes:
|
||||
success: Успешность аутентификации.
|
||||
user_id: ID пользователя.
|
||||
username: Имя пользователя.
|
||||
web_auth_token: Токен для последующих запросов.
|
||||
message: Сообщение (например, об ошибке).
|
||||
"""
|
||||
success: bool
|
||||
user_id: Optional[int] = None
|
||||
username: Optional[str] = None
|
||||
web_auth_token: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class ErrorDetail(BaseModel):
|
||||
"""
|
||||
Детали ошибки API.
|
||||
|
||||
Attributes:
|
||||
code: Код ошибки.
|
||||
message: Сообщение об ошибке.
|
||||
field: Поле, вызвавшее ошибку (если применимо).
|
||||
"""
|
||||
code: str
|
||||
message: str
|
||||
field: Optional[str] = None
|
||||
|
||||
|
||||
class APIErrorResponse(BaseModel):
|
||||
"""
|
||||
Стандартный ответ API об ошибке.
|
||||
|
||||
Attributes:
|
||||
success: Всегда False для ошибок.
|
||||
errors: Список деталей ошибок.
|
||||
message: Общее сообщение об ошибке.
|
||||
"""
|
||||
success: bool = False
|
||||
errors: list[ErrorDetail] = Field(default_factory=list)
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class City(BaseModel):
|
||||
"""
|
||||
Город из справочника.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID города.
|
||||
name: Название города.
|
||||
country_id: ID страны.
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
country_id: Optional[int] = None
|
||||
|
||||
|
||||
class Country(BaseModel):
|
||||
"""
|
||||
Страна из справочника.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID страны.
|
||||
name: Название страны.
|
||||
code: Код страны (ISO).
|
||||
cities: Список городов в стране.
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
code: Optional[str] = None
|
||||
cities: list[City] = Field(default_factory=list)
|
||||
|
||||
|
||||
class TimeZone(BaseModel):
|
||||
"""
|
||||
Часовой пояс.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID.
|
||||
name: Название пояса.
|
||||
offset: Смещение от UTC (например, "+03:00").
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
offset: str
|
||||
|
||||
|
||||
class Feature(BaseModel):
|
||||
"""
|
||||
Дополнительная функция (feature) для кворка.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID функции.
|
||||
name: Название.
|
||||
description: Описание.
|
||||
price: Стоимость в рублях.
|
||||
type: Тип: "extra", "premium", etc.
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
price: float
|
||||
type: str
|
||||
|
||||
|
||||
class Badge(BaseModel):
|
||||
"""
|
||||
Значок (достижение) пользователя.
|
||||
|
||||
Attributes:
|
||||
id: Уникальный ID значка.
|
||||
name: Название значка.
|
||||
description: Описание достижения.
|
||||
icon_url: URL иконки значка.
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
icon_url: Optional[str] = None
|
||||
|
||||
|
||||
# Generic response wrapper
|
||||
class DataResponse(BaseModel):
|
||||
"""
|
||||
Универсальный ответ API с данными.
|
||||
|
||||
Используется как обёртка для различных ответов API.
|
||||
|
||||
Attributes:
|
||||
success: Успешность запроса.
|
||||
data: Полезные данные (словарь).
|
||||
message: Дополнительное сообщение.
|
||||
"""
|
||||
success: bool = True
|
||||
data: Optional[dict[str, Any]] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class ValidationIssue(BaseModel):
|
||||
"""
|
||||
Проблема, найденная при валидации текста.
|
||||
|
||||
Attributes:
|
||||
type: Тип проблемы: "error", "warning", "suggestion".
|
||||
code: Код ошибки (например, "SPELLING", "GRAMMAR", "LENGTH").
|
||||
message: Описание проблемы.
|
||||
position: Позиция в тексте (если применимо).
|
||||
suggestion: Предлагаемое исправление (если есть).
|
||||
"""
|
||||
type: str = "error"
|
||||
code: str
|
||||
message: str
|
||||
position: Optional[int] = None
|
||||
suggestion: Optional[str] = None
|
||||
|
||||
|
||||
class ValidationResponse(BaseModel):
|
||||
"""
|
||||
Ответ API валидации текста.
|
||||
|
||||
Используется для эндпоинта /api/validation/checktext.
|
||||
|
||||
Attributes:
|
||||
success: Успешность валидации.
|
||||
is_valid: Текст проходит валидацию (нет критических ошибок).
|
||||
issues: Список найденных проблем.
|
||||
score: Оценка качества текста (0-100, если доступна).
|
||||
message: Дополнительное сообщение.
|
||||
"""
|
||||
success: bool = True
|
||||
is_valid: bool = True
|
||||
issues: list[ValidationIssue] = Field(default_factory=list)
|
||||
score: Optional[int] = None
|
||||
message: Optional[str] = None
|
||||
255
tests/integration/test_real_api.py
Normal file
255
tests/integration/test_real_api.py
Normal file
@ -0,0 +1,255 @@
|
||||
"""
|
||||
Integration tests with real Kwork API.
|
||||
|
||||
These tests require valid credentials and make real API calls.
|
||||
Skip these tests in CI/CD or when running unit tests only.
|
||||
|
||||
Usage:
|
||||
pytest tests/integration/ -m integration
|
||||
|
||||
Or with credentials:
|
||||
KWORK_USERNAME=user KWORK_PASSWORD=pass pytest tests/integration/ -m integration
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from kwork_api import KworkClient, KworkAuthError
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client() -> Optional[KworkClient]:
|
||||
"""
|
||||
Create authenticated client for integration tests.
|
||||
|
||||
Requires KWORK_USERNAME and KWORK_PASSWORD environment variables.
|
||||
Skip tests if not provided.
|
||||
"""
|
||||
username = os.getenv("KWORK_USERNAME")
|
||||
password = os.getenv("KWORK_PASSWORD")
|
||||
|
||||
if not username or not password:
|
||||
pytest.skip("KWORK_USERNAME and KWORK_PASSWORD not set")
|
||||
|
||||
# Create client
|
||||
import asyncio
|
||||
|
||||
async def create_client():
|
||||
return await KworkClient.login(username, password)
|
||||
|
||||
return asyncio.run(create_client())
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAuthentication:
|
||||
"""Test authentication with real API."""
|
||||
|
||||
def test_login_with_credentials(self):
|
||||
"""Test login with real credentials."""
|
||||
username = os.getenv("KWORK_USERNAME")
|
||||
password = os.getenv("KWORK_PASSWORD")
|
||||
|
||||
if not username or not password:
|
||||
pytest.skip("Credentials not set")
|
||||
|
||||
import asyncio
|
||||
|
||||
async def login():
|
||||
client = await KworkClient.login(username, password)
|
||||
assert client._token is not None
|
||||
assert "userId" in client._cookies
|
||||
await client.close()
|
||||
return True
|
||||
|
||||
result = asyncio.run(login())
|
||||
assert result
|
||||
|
||||
def test_invalid_credentials(self):
|
||||
"""Test login with invalid credentials."""
|
||||
import asyncio
|
||||
|
||||
async def try_login():
|
||||
try:
|
||||
await KworkClient.login("invalid_user_12345", "wrong_password")
|
||||
return False
|
||||
except KworkAuthError:
|
||||
return True
|
||||
|
||||
result = asyncio.run(try_login())
|
||||
assert result # Should raise auth error
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestCatalogAPI:
|
||||
"""Test catalog endpoints with real API."""
|
||||
|
||||
def test_get_catalog_list(self, client: KworkClient):
|
||||
"""Test getting catalog list."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
|
||||
async def fetch():
|
||||
result = await client.catalog.get_list(page=1)
|
||||
return result
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
assert result.kworks is not None
|
||||
assert len(result.kworks) > 0
|
||||
assert result.pagination is not None
|
||||
|
||||
def test_get_kwork_details(self, client: KworkClient):
|
||||
"""Test getting kwork details."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
|
||||
async def fetch():
|
||||
# First get a kwork ID from catalog
|
||||
catalog = await client.catalog.get_list(page=1)
|
||||
if not catalog.kworks:
|
||||
return None
|
||||
|
||||
kwork_id = catalog.kworks[0].id
|
||||
details = await client.catalog.get_details(kwork_id)
|
||||
return details
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
if result:
|
||||
assert result.id is not None
|
||||
assert result.title is not None
|
||||
assert result.price is not None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestProjectsAPI:
|
||||
"""Test projects endpoints with real API."""
|
||||
|
||||
def test_get_projects_list(self, client: KworkClient):
|
||||
"""Test getting projects list."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
|
||||
async def fetch():
|
||||
return await client.projects.get_list(page=1)
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
assert result.projects is not None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestReferenceAPI:
|
||||
"""Test reference data endpoints."""
|
||||
|
||||
def test_get_cities(self, client: KworkClient):
|
||||
"""Test getting cities."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
|
||||
async def fetch():
|
||||
return await client.reference.get_cities()
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
|
||||
assert isinstance(result, list)
|
||||
# Kwork has many cities, should have at least some
|
||||
assert len(result) > 0
|
||||
|
||||
def test_get_countries(self, client: KworkClient):
|
||||
"""Test getting countries."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
result = asyncio.run(client.reference.get_countries())
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_get_timezones(self, client: KworkClient):
|
||||
"""Test getting timezones."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
result = asyncio.run(client.reference.get_timezones())
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestUserAPI:
|
||||
"""Test user endpoints."""
|
||||
|
||||
def test_get_user_info(self, client: KworkClient):
|
||||
"""Test getting current user info."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
result = asyncio.run(client.user.get_info())
|
||||
|
||||
assert isinstance(result, dict)
|
||||
# Should have user data
|
||||
assert result # Not empty
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestErrorHandling:
|
||||
"""Test error handling with real API."""
|
||||
|
||||
def test_invalid_kwork_id(self, client: KworkClient):
|
||||
"""Test getting non-existent kwork."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
|
||||
async def fetch():
|
||||
try:
|
||||
await client.catalog.get_details(999999999)
|
||||
return False
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
result = asyncio.run(fetch())
|
||||
# May or may not raise error depending on API behavior
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestRateLimiting:
|
||||
"""Test rate limiting behavior."""
|
||||
|
||||
def test_multiple_requests(self, client: KworkClient):
|
||||
"""Test making multiple requests."""
|
||||
if not client:
|
||||
pytest.skip("No client")
|
||||
|
||||
import asyncio
|
||||
|
||||
async def fetch_multiple():
|
||||
results = []
|
||||
for page in range(1, 4):
|
||||
catalog = await client.catalog.get_list(page=page)
|
||||
results.append(catalog)
|
||||
# Small delay to avoid rate limiting
|
||||
await asyncio.sleep(0.5)
|
||||
return results
|
||||
|
||||
results = asyncio.run(fetch_multiple())
|
||||
|
||||
assert len(results) == 3
|
||||
for result in results:
|
||||
assert result.kworks is not None
|
||||
BIN
tests/unit/__pycache__/test_client.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
tests/unit/__pycache__/test_client.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
332
tests/unit/test_client.py
Normal file
332
tests/unit/test_client.py
Normal file
@ -0,0 +1,332 @@
|
||||
"""
|
||||
Unit tests for KworkClient with mocks.
|
||||
|
||||
These tests use respx for HTTP mocking and don't require real API access.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
from httpx import Response
|
||||
|
||||
from kwork_api import KworkClient, KworkAuthError, KworkApiError
|
||||
from kwork_api.models import CatalogResponse, Kwork, ValidationResponse, ValidationIssue
|
||||
|
||||
|
||||
class TestAuthentication:
|
||||
"""Test authentication flows."""
|
||||
|
||||
@respx.mock
|
||||
async def test_login_success(self):
|
||||
"""Test successful login."""
|
||||
import httpx
|
||||
|
||||
# Mock login endpoint
|
||||
login_route = respx.post("https://kwork.ru/signIn")
|
||||
login_route.mock(return_value=httpx.Response(
|
||||
200,
|
||||
headers={"Set-Cookie": "userId=12345; slrememberme=token123"},
|
||||
))
|
||||
|
||||
# Mock token endpoint
|
||||
token_route = respx.post("https://kwork.ru/getWebAuthToken").mock(
|
||||
return_value=Response(
|
||||
200,
|
||||
json={"web_auth_token": "test_token_abc123"},
|
||||
)
|
||||
)
|
||||
|
||||
# Login
|
||||
client = await KworkClient.login("testuser", "testpass")
|
||||
|
||||
# Verify
|
||||
assert login_route.called
|
||||
assert token_route.called
|
||||
assert client._token == "test_token_abc123"
|
||||
|
||||
@respx.mock
|
||||
async def test_login_invalid_credentials(self):
|
||||
"""Test login with invalid credentials."""
|
||||
respx.post("https://kwork.ru/signIn").mock(
|
||||
return_value=Response(401, json={"error": "Invalid credentials"})
|
||||
)
|
||||
|
||||
with pytest.raises(KworkAuthError):
|
||||
await KworkClient.login("wrong", "wrong")
|
||||
|
||||
@respx.mock
|
||||
async def test_login_no_userid(self):
|
||||
"""Test login without userId in cookies."""
|
||||
import httpx
|
||||
|
||||
respx.post("https://kwork.ru/signIn").mock(
|
||||
return_value=httpx.Response(200, headers={"Set-Cookie": "other=value"})
|
||||
)
|
||||
|
||||
with pytest.raises(KworkAuthError, match="no userId"):
|
||||
await KworkClient.login("test", "test")
|
||||
|
||||
@respx.mock
|
||||
async def test_login_no_token(self):
|
||||
"""Test login without web_auth_token in response."""
|
||||
import httpx
|
||||
|
||||
respx.post("https://kwork.ru/signIn").mock(
|
||||
return_value=httpx.Response(200, headers={"Set-Cookie": "userId=123"})
|
||||
)
|
||||
|
||||
respx.post("https://kwork.ru/getWebAuthToken").mock(
|
||||
return_value=Response(200, json={"other": "data"})
|
||||
)
|
||||
|
||||
with pytest.raises(KworkAuthError, match="No web_auth_token"):
|
||||
await KworkClient.login("test", "test")
|
||||
|
||||
def test_init_with_token(self):
|
||||
"""Test client initialization with token."""
|
||||
client = KworkClient(token="test_token")
|
||||
assert client._token == "test_token"
|
||||
|
||||
|
||||
class TestCatalogAPI:
|
||||
"""Test catalog endpoints."""
|
||||
|
||||
@respx.mock
|
||||
async def test_get_catalog(self):
|
||||
"""Test getting catalog list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
mock_data = {
|
||||
"kworks": [
|
||||
{"id": 1, "title": "Test Kwork", "price": 1000.0},
|
||||
{"id": 2, "title": "Another Kwork", "price": 2000.0},
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"total_pages": 5,
|
||||
"total_items": 100,
|
||||
},
|
||||
}
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
result = await client.catalog.get_list(page=1)
|
||||
|
||||
assert isinstance(result, CatalogResponse)
|
||||
assert len(result.kworks) == 2
|
||||
assert result.kworks[0].id == 1
|
||||
assert result.pagination.total_pages == 5
|
||||
|
||||
@respx.mock
|
||||
async def test_get_kwork_details(self):
|
||||
"""Test getting kwork details."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
mock_data = {
|
||||
"id": 123,
|
||||
"title": "Detailed Kwork",
|
||||
"price": 5000.0,
|
||||
"full_description": "Full description here",
|
||||
"delivery_time": 3,
|
||||
}
|
||||
|
||||
respx.post(f"{client.base_url}/getKworkDetails").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
result = await client.catalog.get_details(123)
|
||||
|
||||
assert result.id == 123
|
||||
assert result.full_description == "Full description here"
|
||||
assert result.delivery_time == 3
|
||||
|
||||
@respx.mock
|
||||
async def test_catalog_error(self):
|
||||
"""Test catalog API error handling."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
return_value=Response(400, json={"message": "Invalid category"})
|
||||
)
|
||||
|
||||
with pytest.raises(KworkApiError):
|
||||
await client.catalog.get_list(category_id=99999)
|
||||
|
||||
|
||||
class TestProjectsAPI:
|
||||
"""Test projects endpoints."""
|
||||
|
||||
@respx.mock
|
||||
async def test_get_projects(self):
|
||||
"""Test getting projects list."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
mock_data = {
|
||||
"projects": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Test Project",
|
||||
"description": "Test description",
|
||||
"budget": 10000.0,
|
||||
"status": "open",
|
||||
}
|
||||
],
|
||||
"pagination": {"current_page": 1},
|
||||
}
|
||||
|
||||
respx.post(f"{client.base_url}/projects").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
result = await client.projects.get_list()
|
||||
|
||||
assert len(result.projects) == 1
|
||||
assert result.projects[0].budget == 10000.0
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling."""
|
||||
|
||||
@respx.mock
|
||||
async def test_404_error(self):
|
||||
"""Test 404 error handling."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
respx.post(f"{client.base_url}/getKworkDetails").mock(
|
||||
return_value=Response(404)
|
||||
)
|
||||
|
||||
with pytest.raises(KworkApiError) as exc_info:
|
||||
await client.catalog.get_details(999)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
@respx.mock
|
||||
async def test_401_error(self):
|
||||
"""Test 401 error handling."""
|
||||
client = KworkClient(token="invalid")
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
return_value=Response(401)
|
||||
)
|
||||
|
||||
with pytest.raises(KworkAuthError):
|
||||
await client.catalog.get_list()
|
||||
|
||||
@respx.mock
|
||||
async def test_network_error(self):
|
||||
"""Test network error handling."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
respx.post(f"{client.base_url}/catalogMainv2").mock(
|
||||
side_effect=Exception("Connection refused")
|
||||
)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
await client.catalog.get_list()
|
||||
|
||||
|
||||
class TestContextManager:
|
||||
"""Test async context manager."""
|
||||
|
||||
async def test_context_manager(self):
|
||||
"""Test using client as context manager."""
|
||||
async with KworkClient(token="test") as client:
|
||||
assert client._client is None # Not created yet
|
||||
|
||||
# Client should be created on first request
|
||||
# (but we don't make actual requests in this test)
|
||||
|
||||
# Client should be closed after context
|
||||
assert client._client is None or client._client.is_closed
|
||||
|
||||
|
||||
class TestValidationAPI:
|
||||
"""Test text validation endpoint."""
|
||||
|
||||
@respx.mock
|
||||
async def test_validate_text_success(self):
|
||||
"""Test successful text validation."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
mock_data = {
|
||||
"success": True,
|
||||
"is_valid": True,
|
||||
"issues": [],
|
||||
"score": 95,
|
||||
}
|
||||
|
||||
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
result = await client.other.validate_text("Хороший текст для кворка")
|
||||
|
||||
assert isinstance(result, ValidationResponse)
|
||||
assert result.success is True
|
||||
assert result.is_valid is True
|
||||
assert len(result.issues) == 0
|
||||
assert result.score == 95
|
||||
|
||||
@respx.mock
|
||||
async def test_validate_text_with_issues(self):
|
||||
"""Test text validation with found issues."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
mock_data = {
|
||||
"success": True,
|
||||
"is_valid": False,
|
||||
"issues": [
|
||||
{
|
||||
"type": "error",
|
||||
"code": "CONTACT_INFO",
|
||||
"message": "Текст содержит контактную информацию",
|
||||
"position": 25,
|
||||
"suggestion": "Удалите номер телефона",
|
||||
},
|
||||
{
|
||||
"type": "warning",
|
||||
"code": "LENGTH",
|
||||
"message": "Текст слишком короткий",
|
||||
},
|
||||
],
|
||||
"score": 45,
|
||||
}
|
||||
|
||||
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
result = await client.other.validate_text(
|
||||
"Звоните +7-999-000-00-00",
|
||||
context="kwork_description",
|
||||
)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert len(result.issues) == 2
|
||||
assert result.issues[0].code == "CONTACT_INFO"
|
||||
assert result.issues[0].type == "error"
|
||||
assert result.issues[1].type == "warning"
|
||||
assert result.score == 45
|
||||
|
||||
@respx.mock
|
||||
async def test_validate_text_empty(self):
|
||||
"""Test validation of empty text."""
|
||||
client = KworkClient(token="test")
|
||||
|
||||
mock_data = {
|
||||
"success": False,
|
||||
"is_valid": False,
|
||||
"message": "Текст не может быть пустым",
|
||||
"issues": [],
|
||||
}
|
||||
|
||||
respx.post(f"{client.base_url}/api/validation/checktext").mock(
|
||||
return_value=Response(200, json=mock_data)
|
||||
)
|
||||
|
||||
result = await client.other.validate_text("")
|
||||
|
||||
assert result.success is False
|
||||
assert result.message is not None
|
||||
Loading…
Reference in New Issue
Block a user