Compare commits

...

No commits in common. "6ac9c533fa5f30305538a46f33349aff5937c430" and "20c3e815e18bb76ef317527bb426d3dc150c0b61" have entirely different histories.

32 changed files with 2 additions and 6220 deletions

BIN
.coverage

Binary file not shown.

View File

@ -1,34 +0,0 @@
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/

View File

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

35
.gitignore vendored
View File

@ -1,35 +0,0 @@
# 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/

View File

@ -1,70 +0,0 @@
# 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
View File

@ -1,267 +1,3 @@
# 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.
Kwork API парсер

148
WIP.md
View File

@ -1,148 +0,0 @@
# 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** — Начат проект

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
# 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

View File

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

View File

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

View File

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

View File

View File

@ -1,109 +0,0 @@
# 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}")
```

View File

@ -1,79 +0,0 @@
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

View File

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

View File

@ -1,154 +0,0 @@
[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

View File

@ -1,44 +0,0 @@
"""
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",
]

File diff suppressed because it is too large Load Diff

View File

@ -1,192 +0,0 @@
"""
Исключения 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}"

View File

@ -1,450 +0,0 @@
"""
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

View File

@ -1,255 +0,0 @@
"""
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

View File

@ -1,332 +0,0 @@
"""
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

1714
uv.lock generated

File diff suppressed because it is too large Load Diff