kwork-api/src/kwork_api/client.py

1321 lines
52 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Kwork API Client.
Main client class with authentication and all API endpoints.
"""
import logging
from typing import Any
import httpx
from .errors import (
KworkApiError,
KworkAuthError,
KworkError,
KworkNetworkError,
KworkNotFoundError,
KworkRateLimitError,
KworkValidationError,
)
from .models import (
Badge,
CatalogResponse,
City,
Country,
Dialog,
Feature,
Kwork,
KworkDetails,
NotificationsResponse,
Project,
ProjectsResponse,
ReviewsResponse,
TimeZone,
ValidationResponse,
)
logger = logging.getLogger(__name__)
class KworkClient:
"""
Асинхронный клиент для Kwork.ru API.
Предоставляет доступ ко всем основным эндпоинтам Kwork API:
- Каталог кворков и поиск
- Биржа проектов (фриланс заказы)
- Пользовательские данные и отзывы
- Уведомления и сообщения
- Справочные данные (города, страны, категории)
Аутентификация:
Клиент использует двухэтапную аутентификацию Kwork:
1. POST /signIn - получение session cookies
2. POST /getWebAuthToken - получение web_auth_token
Примеры использования:
# Вход по логину/паролю
async with await KworkClient.login("username", "password") as client:
catalog = await client.catalog.get_list(page=1)
# Восстановление сессии по токену
client = KworkClient(token="saved_web_auth_token")
user_info = await client.user.get_info()
# Работа с проектами
projects = await client.projects.get_list(page=1)
my_orders = await client.projects.get_payer_orders()
Attributes:
catalog: Доступ к каталогу кворков
projects: Доступ к бирже проектов
user: Пользовательские эндпоинты
reference: Справочные данные
notifications: Уведомления и сообщения
other: Прочие эндпоинты
Note:
Клиент поддерживает context manager для автоматического закрытия соединения.
Рекомендуется использовать `async with` для корректного освобождения ресурсов.
"""
BASE_URL = "https://api.kwork.ru" # HAR shows all API endpoints use api.kwork.ru
def __init__(
self,
token: str | None = None,
cookies: dict[str, str] | None = None,
timeout: float = 30.0,
base_url: str | None = None,
):
"""
Инициализация клиента.
Создаёт неаутентифицированный клиент или восстанавливает сессию
по ранее полученному токену.
Args:
token: Web auth token, полученный через `getWebAuthToken` или `login()`.
Если указан, автоматически добавляется в cookies.
cookies: Session cookies из предыдущей аутентификации.
Обычно не требуется - устанавливаются автоматически из token.
timeout: Таймаут HTTP запросов в секундах. По умолчанию 30 секунд.
base_url: Кастомный базовый URL. Используется только для тестирования.
Example:
# Новый клиент без аутентификации
client = KworkClient()
# Восстановление сессии
client = KworkClient(token="eyJ0eXAiOiJKV1QiLCJhbGc...")
# Клиент с кастомным таймаутом
client = KworkClient(timeout=60.0)
Note:
Для полноценной работы API требуется аутентификация.
Используйте `login()` или передайте сохранённый token.
"""
self.base_url = base_url or self.BASE_URL
self.timeout = timeout
self._token = token
self._cookies = cookies or {}
# Initialize HTTP client
self._client: httpx.AsyncClient | None = None
@property
def token(self) -> str | None:
"""
Web auth token для аутентификации.
Returns:
Токен или None если клиент не аутентифицирован.
Example:
# Сохранение токена для последующего использования
client = await KworkClient.login("user", "pass")
token = client.token
# Позже: восстановление сессии
client = KworkClient(token=token)
"""
return self._token
@property
def cookies(self) -> dict[str, str]:
"""
Session cookies.
Returns:
Словарь cookies включая web_auth_token.
Example:
# Сохранение полной сессии
client = await KworkClient.login("user", "pass")
creds = client.credentials
# Восстановление
client = KworkClient(**creds)
"""
return self._cookies.copy()
@property
def credentials(self) -> dict[str, str]:
"""
Учётные данные для восстановления сессии.
Returns:
Словарь со всеми cookies (включая slrememberme и userId) для передачи в KworkClient(cookies=...).
Example:
# Сохранение
client = await KworkClient.login("user", "pass")
import json
with open("session.json", "w") as f:
json.dump(client.credentials, f)
# Восстановление
with open("session.json") as f:
creds = json.load(f)
client = KworkClient(cookies=creds)
"""
return self._cookies.copy() if self._cookies else {}
@classmethod
async def login(
cls,
username: str,
password: str,
timeout: float = 30.0,
) -> "KworkClient":
"""
Аутентификация по логину и паролю.
Выполняет двухэтапный процесс аутентификации Kwork:
1. POST /signIn - проверка учётных данных, получение session cookies
2. POST /getWebAuthToken - обмен cookies на web_auth_token
Полученный токен и cookies сохраняются в клиенте для последующих запросов.
Args:
username: Логин или email аккаунта Kwork.
password: Пароль аккаунта Kwork.
timeout: Таймаут запросов в секундах. Применяется к каждому этапу.
Returns:
Полностью аутентифицированный экземпляр KworkClient,
готовый к работе с API.
Raises:
KworkAuthError: Если логин/пароль неверны или токен не получен.
KworkNetworkError: Если произошла ошибка сети.
Example:
# Базовое использование
client = await KworkClient.login("myuser", "mypassword")
# С кастомным таймаутом
client = await KworkClient.login("user", "pass", timeout=60.0)
# Сохранение токена для повторного использования
token = client._token
# Позже: client = KworkClient(token=token)
Security:
Пароль не сохраняется в клиенте. Только token и cookies.
Рекомендуется сохранять token для повторного использования
вместо хранения пароля.
Note:
Токен имеет ограниченное время жизни. При получении 401 ошибки
необходимо выполнить повторный login().
"""
client = cls(timeout=timeout)
try:
async with client._get_httpx_client() as http_client:
# Step 1: Login to get session cookies and token
# HAR analysis (mitmproxy + har-analyzer skill):
# POST https://api.kwork.ru/signIn
# Required headers: Authorization (Basic mobile_api:qFvfRl7w), User-Agent (Android), OS-Version
# Content-Type: application/x-www-form-urlencoded
login_data = {
"login": username,
"password": password,
"uad": "",
"device": "",
}
logger.info(f"Login request: POST https://api.kwork.ru/signIn (user: {username})")
logger.debug(f"Login payload: {login_data}")
response = await http_client.post(
"https://api.kwork.ru/signIn",
data=login_data,
headers={
"Authorization": "Basic bW9iaWxlX2FwaTpxRnZmUmw3dw==",
"User-Agent": "Kwork android client, version: 3.8.1",
"OS-Version": "30",
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
},
)
logger.debug(f"Login response status: {response.status_code}")
logger.debug(f"Login response headers: {dict(response.headers)}")
if response.status_code != 200:
logger.error(
f"Login failed with status {response.status_code}: {response.text[:200]}"
)
raise KworkAuthError(f"Login failed: {response.status_code}")
response_data = response.json()
cookies = dict(response.cookies)
logger.info(
f"Login successful: user_id={cookies.get('userId')}, csrf_token={response_data.get('csrftoken', 'N/A')[:20] if response_data.get('csrftoken') else 'N/A'}"
)
logger.debug(f"Login response data: {response_data}")
logger.debug(f"Login cookies: {list(cookies.keys())}")
# Extract userId from cookies
user_id = cookies.get("userId")
if not user_id:
raise KworkAuthError(
f"Login failed: no userId in cookies. Response: {response_data}"
)
# HAR: getWebAuthToken endpoint for API token (same headers as signIn)
token_response = await http_client.post(
"https://api.kwork.ru/getWebAuthToken",
json={},
headers={
"Authorization": "Basic bW9iaWxlX2FwaTpxRnZmUmw3dw==",
"User-Agent": "Kwork android client, version: 3.8.1",
"OS-Version": "30",
"Accept": "application/json",
},
)
if token_response.status_code != 200:
raise KworkAuthError(f"Token request failed: {token_response.status_code}")
token_data = token_response.json()
# HAR shows: {"success":true,"response":{"token":"...", "expires_at":..., "url":...}}
web_token = token_data.get("response", {}).get("token")
if not web_token:
raise KworkAuthError(f"No token in response: {token_data}")
logger.info(f"Got web_auth_token: {web_token[:20]}...")
# Create new client with token
return cls(token=web_token, cookies=cookies, timeout=timeout)
except httpx.RequestError as e:
raise KworkNetworkError(f"Login request failed: {e}") from e
def _get_httpx_client(self) -> httpx.AsyncClient:
"""Get or create HTTP client with proper headers."""
if self._client is None or self._client.is_closed:
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Referer": "https://kwork.ru/",
"Origin": "https://kwork.ru",
# HAR: All API requests require Authorization header
"Authorization": "Basic bW9iaWxlX2FwaTpxRnZmUmw3dw==",
"User-Agent": "Kwork android client, version: 3.8.1",
"OS-Version": "30",
}
if self._token:
# HAR: API requires both Authorization header AND slrememberme cookie
self._cookies["slrememberme"] = self._token
# Convert cookies to Cookie header string for cross-domain compatibility
if self._cookies:
cookie_header = "; ".join(f"{k}={v}" for k, v in self._cookies.items())
headers["Cookie"] = cookie_header
logger.debug(f"Setting Cookie header: {cookie_header[:100]}...")
self._client = httpx.AsyncClient(
base_url=self.base_url,
headers=headers,
timeout=self.timeout,
http2=True,
)
return self._client
async def close(self) -> None:
"""Close HTTP client."""
if self._client and not self._client.is_closed:
await self._client.aclose()
async def __aenter__(self) -> "KworkClient":
return self
async def __aexit__(self, *args: Any) -> None:
await self.close()
def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
"""
Handle HTTP response and raise appropriate errors.
Args:
response: HTTP response
Returns:
Response JSON data
Raises:
KworkApiError: For HTTP errors
KworkAuthError: For auth errors
KworkNetworkError: For network errors
"""
# Check for common error statuses
if response.status_code == 401:
raise KworkAuthError("Unauthorized: invalid or expired token")
if response.status_code == 403:
raise KworkAuthError("Forbidden: access denied")
if response.status_code == 404:
raise KworkNotFoundError(f"Resource not found: {response.url}")
if response.status_code == 429:
raise KworkRateLimitError("Too many requests")
if response.status_code >= 400:
try:
error_data = response.json()
message = error_data.get("message", str(error_data))
except Exception:
message = response.text
if response.status_code == 400:
raise KworkValidationError(message, response=response)
raise KworkApiError(message, response.status_code, response)
# Parse successful response
try:
return response.json()
except Exception as e:
raise KworkError(f"Failed to parse response: {e}") from e
async def _request(
self,
method: str,
endpoint: str,
**kwargs: Any,
) -> dict[str, Any]:
"""
Make HTTP request.
Args:
method: HTTP method
endpoint: API endpoint
**kwargs: Additional arguments for httpx
Returns:
Response JSON data
"""
http_client = self._get_httpx_client()
# Log request
full_url = f"{self.base_url}{endpoint}" if not endpoint.startswith("http") else endpoint
logger.debug(f"API Request: {method} {full_url}")
logger.debug(f"Request kwargs: {kwargs}")
try:
response = await http_client.request(method, endpoint, **kwargs)
# Log response
logger.debug(f"API Response: {response.status_code} {response.reason_phrase}")
logger.debug(f"Response headers: {dict(response.headers)}")
# Log response body (truncated for large responses)
try:
response_text = response.text
if len(response_text) > 500:
logger.debug(f"Response body (truncated): {response_text[:500]}...")
else:
logger.debug(f"Response body: {response_text}")
except Exception:
logger.debug("Response body: <not available>")
return self._handle_response(response)
except httpx.RequestError as e:
logger.error(f"Network error for {method} {full_url}: {e}")
raise KworkNetworkError(f"Request failed: {e}") from e
# ========== Catalog Endpoints ==========
class CatalogAPI:
"""
API каталога кворков.
Предоставляет доступ к каталогу услуг Kwork:
- Поиск и фильтрация кворков
- Получение детальной информации
- Категории и сортировка
Example:
# Получить первую страницу каталога
catalog = await client.catalog.get_list(page=1)
# Фильтрация по категории
catalog = await client.catalog.get_list(category_id=5)
# Детали конкретного кворка
details = await client.catalog.get_details(kwork_id=12345)
"""
def __init__(self, client: "KworkClient"):
self.client = client
async def get_list(
self,
page: int = 1,
category_id: int | None = None,
sort: str = "recommend",
) -> CatalogResponse:
"""
Получить список кворков из каталога.
Основной эндпоинт для поиска и просмотра кворков.
Возвращает пагинированный список с возможностью фильтрации.
Args:
page: Номер страницы для пагинации (начиная с 1).
category_id: ID категории для фильтрации.
Если None - все категории.
sort: Опция сортировки. Варианты:
- "recommend" - по рекомендации (по умолчанию)
- "price_asc" - по возрастанию цены
- "price_desc" - по убыванию цены
- "rating" - по рейтингу
- "reviews" - по количеству отзывов
- "newest" - по дате создания
Returns:
CatalogResponse содержащий:
- kworks: список кворков на странице
- pagination: информация о пагинации
- filters: доступные фильтры
- sort_options: доступные опции сортировки
Example:
# Первая страница, сортировка по цене
response = await client.catalog.get_list(
page=1,
sort="price_asc"
)
for kwork in response.kworks:
print(f"{kwork.title}: {kwork.price} RUB")
# Пагинация
if response.pagination and response.pagination.has_next:
next_page = await client.catalog.get_list(page=2)
"""
data = await self.client._request(
"POST",
"/catalogMainv2", # TODO: 404 - need to find correct endpoint (HAR shows GET /categories/{slug})
json={
"page": page,
"category_id": category_id,
"sort": sort,
},
)
return CatalogResponse.model_validate(data)
async def get_details(self, kwork_id: int) -> KworkDetails:
"""
Получить полную информацию о кворке.
Возвращает расширенную информацию о кворке включая:
- Полное описание и требования
- Сроки выполнения и количество правок
- Дополнительные опции (features)
- FAQ от продавца
Args:
kwork_id: Уникальный идентификатор кворка.
Returns:
KworkDetails с полной информацией о кворке.
Raises:
KworkNotFoundError: Если кворк с таким ID не найден.
Example:
details = await client.catalog.get_details(12345)
print(f"Название: {details.title}")
print(f"Цена: {details.price} {details.currency}")
print(f"Срок: {details.delivery_time} дней")
print(f"Правки: {details.revisions}")
"""
data = await self.client._request(
"POST",
"/getKworkDetails", # TODO: 404 - HAR shows GET /projects/{id}/view
json={"kwork_id": kwork_id},
)
return KworkDetails.model_validate(data)
async def get_details_extra(self, kwork_id: int) -> dict[str, Any]:
"""
Получить дополнительные детали кворка.
Возвращает расширенную информацию, которая не включена
в основной ответ get_details(). Может содержать:
- Дополнительные изображения
- Видео обзоры
- Детали пакетов услуг
- Статистику продаж
Args:
kwork_id: Уникальный идентификатор кворка.
Returns:
Словарь с дополнительными данными. Структура зависит
от конкретного кворка и не гарантируется стабильной.
Note:
Этот эндпоинт возвращает "сырые" данные без валидации.
Структура ответа может измениться без предупреждения.
"""
return await self.client._request(
"POST",
"/getKworkDetailsExtra",
json={"kwork_id": kwork_id},
)
# ========== Projects Endpoints ==========
class ProjectsAPI:
"""
API биржи проектов (фриланс заказы).
Предоставляет доступ к заказам на фриланс:
- Просмотр открытых проектов
- Заказы где вы заказчик (payer)
- Заказы где вы исполнитель (worker)
Example:
# Новые проекты
projects = await client.projects.get_list(page=1)
# Мои заказы как заказчика
my_orders = await client.projects.get_payer_orders()
# Мои заказы как исполнителя
my_work = await client.projects.get_worker_orders()
"""
def __init__(self, client: "KworkClient"):
self.client = client
async def get_list(
self,
page: int = 1,
category_id: int | None = None,
) -> ProjectsResponse:
"""
Получить список проектов с биржи.
Основной эндпоинт для просмотра доступных заказов.
Возвращает пагинированный список проектов.
Args:
page: Номер страницы (начиная с 1).
category_id: ID категории для фильтрации.
Если None - все категории.
Returns:
ProjectsResponse содержащий:
- projects: список проектов на странице
- pagination: информация о пагинации
Example:
# Все новые проекты
response = await client.projects.get_list(page=1)
for project in response.projects:
print(f"{project.title}: {project.budget} {project.budget_type}")
# Только категория "Разработка"
dev_projects = await client.projects.get_list(
page=1,
category_id=5
)
"""
data = await self.client._request(
"POST",
"/projects",
json={
"page": page,
"category_id": category_id,
},
)
return ProjectsResponse.model_validate(data)
async def get_payer_orders(self) -> list[Project]:
"""
Получить заказы где вы являетесь заказчиком.
Возвращает все проекты, созданные текущим пользователем,
независимо от их статуса (открыт, в работе, завершён).
Returns:
Список проектов где текущий пользователь - заказчик.
Example:
orders = await client.projects.get_payer_orders()
for order in orders:
print(f"Заказ #{order.id}: {order.status}")
"""
data = await self.client._request("POST", "/payerOrders")
return [Project.model_validate(p) for p in data.get("orders", [])]
async def get_worker_orders(self) -> list[Project]:
"""
Получить заказы где вы являетесь исполнителем.
Возвращает все проекты, где текущий пользователь
назначен исполнителем.
Returns:
Список проектов где текущий пользователь - исполнитель.
Example:
work = await client.projects.get_worker_orders()
active = [p for p in work if p.status == "in_progress"]
print(f"Активных заказов: {len(active)}")
"""
data = await self.client._request("POST", "/workerOrders")
return [Project.model_validate(p) for p in data.get("orders", [])]
# ========== User Endpoints ==========
class UserAPI:
"""
Пользовательское API.
Предоставляет доступ к данным текущего пользователя:
- Профиль и информация об аккаунте
- Отзывы (полученные и оставленные)
- Избранные кворки
Example:
# Информация о пользователе
info = await client.user.get_info()
# Мои отзывы
reviews = await client.user.get_reviews()
# Избранные кворки
favorites = await client.user.get_favorite_kworks()
"""
def __init__(self, client: "KworkClient"):
self.client = client
async def get_info(self) -> dict[str, Any]:
"""
Получить информацию о текущем пользователе.
Возвращает основные данные аккаунта:
- ID, username, email
- Баланс, рейтинг
- Статус верификации
- Настройки профиля
Returns:
Словарь с информацией о пользователе.
Структура зависит от ответа API.
Example:
info = await client.user.get_info()
print(f"User: {info.get('username')}")
print(f"Balance: {info.get('balance')} RUB")
"""
# HAR: POST https://api.kwork.ru/user
return await self.client._request("POST", "/user")
async def get_reviews(
self,
user_id: int | None = None,
page: int = 1,
) -> ReviewsResponse:
"""
Получить отзывы пользователя.
Если user_id не указан - возвращает отзывы текущего пользователя.
Если указан - отзывы другого пользователя по ID.
Args:
user_id: ID пользователя. Если None - текущий пользователь.
page: Номер страницы для пагинации (начиная с 1).
Returns:
ReviewsResponse содержащий:
- reviews: список отзывов на странице
- pagination: информация о пагинации
- average_rating: средний рейтинг
Example:
# Мои отзывы
my_reviews = await client.user.get_reviews()
# Отзывы другого пользователя
user_reviews = await client.user.get_reviews(user_id=12345)
# С пагинацией
page2 = await client.user.get_reviews(page=2)
"""
# HAR: POST https://api.kwork.ru/userReviews
data = await self.client._request(
"POST",
"/userReviews",
json={"user_id": user_id, "page": page} if user_id else {"page": page},
)
return ReviewsResponse.model_validate(data)
async def get_favorite_kworks(self) -> list[Kwork]:
"""
Получить список избранных кворков.
Возвращает все кворки, добавленные пользователем в избранное.
Returns:
Список избранных кворков.
Example:
favorites = await client.user.get_favorite_kworks()
for kwork in favorites:
print(f"{kwork.title}: {kwork.price} RUB")
"""
data = await self.client._request("POST", "/favoriteKworks")
return [Kwork.model_validate(k) for k in data.get("kworks", [])]
# ========== Reference Data Endpoints ==========
class ReferenceAPI:
"""
Справочное API.
Предоставляет доступ к справочным данным Kwork:
- Города, страны, часовые пояса
- Доступные функции и дополнения
- Значки пользователей
Эти данные редко меняются и могут быть закэшированы.
Example:
# Все страны
countries = await client.reference.get_countries()
# Все города
cities = await client.reference.get_cities()
# Доступные фичи
features = await client.reference.get_features()
"""
def __init__(self, client: "KworkClient"):
self.client = client
async def get_cities(self) -> list[City]:
"""
Получить список всех городов.
Returns:
Список всех городов из справочника Kwork.
Example:
cities = await client.reference.get_cities()
moscow = next(c for c in cities if c.name == "Москва")
"""
# TODO: 404 - endpoint not found in HAR, may need to parse HTML or find JS data
data = await self.client._request("POST", "/cities")
return [City.model_validate(c) for c in data.get("cities", [])]
async def get_countries(self) -> list[Country]:
"""
Получить список всех стран.
Returns:
Список всех стран с кодами и городами.
Example:
countries = await client.reference.get_countries()
russia = next(c for c in countries if c.code == "RU")
"""
# TODO: 404 - endpoint not found in HAR, may need to parse HTML or find JS data
data = await self.client._request("POST", "/countries")
return [Country.model_validate(c) for c in data.get("countries", [])]
async def get_timezones(self) -> list[TimeZone]:
"""
Получить список всех часовых поясов.
Returns:
Список часовых поясов с названиями и смещениями.
Example:
timezones = await client.reference.get_timezones()
msks = next(tz for tz in timezones if "Moscow" in tz.name)
"""
data = await self.client._request("POST", "/timezones")
return [TimeZone.model_validate(t) for t in data.get("timezones", [])]
async def get_features(self) -> list[Feature]:
"""
Получить доступные дополнительные функции (features).
Features - это платные дополнения к кворкам:
- Увеличенные сроки
- Дополнительные правки
- Приоритетная поддержка
- и т.д.
Returns:
Список доступных features с названиями и ценами.
Example:
features = await client.reference.get_features()
for f in features:
print(f"{f.name}: {f.price} RUB")
"""
data = await self.client._request("POST", "/getAvailableFeatures")
return [Feature.model_validate(f) for f in data.get("features", [])]
async def get_public_features(self) -> list[Feature]:
"""
Получить публичные функции.
Аналогично get_features(), но возвращает только
публично доступные опции.
Returns:
Список публичных features.
"""
data = await self.client._request("POST", "/getPublicFeatures")
return [Feature.model_validate(f) for f in data.get("features", [])]
async def get_badges_info(self) -> list[Badge]:
"""
Получить информацию о значках пользователей.
Значки (badges) отображают достижения и статусы:
- "Профессионал"
- "Быстрый ответ"
- "Надёжный продавец"
- и т.д.
Returns:
Список значков с описаниями и иконками.
Example:
badges = await client.reference.get_badges_info()
for badge in badges:
print(f"{badge.name}: {badge.description}")
"""
data = await self.client._request("POST", "/getBadgesInfo")
return [Badge.model_validate(b) for b in data.get("badges", [])]
# ========== Notifications & Messages ==========
class NotificationsAPI:
"""
API уведомлений и сообщений.
Предоставляет доступ к системе уведомлений Kwork:
- Список уведомлений
- Получение новых уведомлений
- Диалоги с пользователями
- Заблокированные диалоги
Example:
# Все уведомления
notifications = await client.notifications.get_list()
# Новые уведомления
new = await client.notifications.fetch()
# Диалоги
dialogs = await client.notifications.get_dialogs()
"""
def __init__(self, client: "KworkClient"):
self.client = client
async def get_list(self) -> NotificationsResponse:
"""
Получить список всех уведомлений.
Возвращает все уведомления пользователя с информацией
о прочтении.
Returns:
NotificationsResponse содержащий:
- notifications: список уведомлений
- unread_count: количество непрочитанных
Example:
notifs = await client.notifications.get_list()
print(f"Непрочитанных: {notifs.unread_count}")
for n in notifs.notifications:
if not n.is_read:
print(f"Новое: {n.title}")
"""
data = await self.client._request("POST", "/notifications")
return NotificationsResponse.model_validate(data)
async def fetch(self) -> NotificationsResponse:
"""
Получить новые уведомления.
Отличается от get_list() тем, что возвращает только
уведомления, появившиеся с момента последнего запроса.
Также может обновлять счётчик непрочитанных.
Returns:
NotificationsResponse с новыми уведомлениями.
Example:
new_notifs = await client.notifications.fetch()
if new_notifs.unread_count > 0:
print(f"У вас {new_notifs.unread_count} новых уведомлений!")
"""
data = await self.client._request("POST", "/notificationsFetch")
return NotificationsResponse.model_validate(data)
async def get_dialogs(self) -> list[Dialog]:
"""
Получить список диалогов (чатов).
Возвращает все активные диалоги пользователя с другими
пользователями Kwork.
Returns:
Список диалогов с последней перепиской.
Example:
dialogs = await client.notifications.get_dialogs()
for d in dialogs:
print(f"С {d.participant.username}: {d.last_message}")
"""
data = await self.client._request("POST", "/dialogs")
return [Dialog.model_validate(d) for d in data.get("dialogs", [])]
async def get_blocked_dialogs(self) -> list[Dialog]:
"""
Получить список заблокированных диалогов.
Возвращает диалоги с пользователями, которые были
заблокированы текущим пользователем.
Returns:
Список заблокированных диалогов.
Example:
blocked = await client.notifications.get_blocked_dialogs()
print(f"Заблокировано: {len(blocked)} пользователей")
"""
data = await self.client._request("POST", "/blockedDialogList")
return [Dialog.model_validate(d) for d in data.get("dialogs", [])]
# ========== Other Endpoints ==========
class OtherAPI:
"""
Прочее API.
Вспомогательные эндпоинты которые не вошли в другие категории:
- Пользовательские настройки и предпочтения (wants)
- Статусы кворков и заказов
- Персональные предложения (offers)
- Настройки профиля
- Статус онлайн/оффлайн
Example:
# Пользовательские предпочтения
wants = await client.other.get_wants()
# Статус кворков
status = await client.other.get_kworks_status()
# Установить статус оффлайн
await client.other.go_offline()
"""
def __init__(self, client: "KworkClient"):
self.client = client
async def get_wants(self) -> dict[str, Any]:
"""
Получить пользовательские предпочтения (wants).
Wants - это настройки интересов пользователя:
- Предпочитаемые категории
- Ключевые слова для мониторинга
- Фильтры для поиска
Returns:
Словарь с настройками предпочтений.
Example:
wants = await client.other.get_wants()
print(wants)
"""
return await self.client._request("POST", "/myWants")
async def get_wants_status(self) -> dict[str, Any]:
"""
Получить статус предпочтений.
Returns:
Статус wants с метаданными.
"""
return await self.client._request("POST", "/wantsStatusList")
async def get_kworks_status(self) -> dict[str, Any]:
"""
Получить статусы кворков пользователя.
Возвращает информацию о статусах всех кворков
текущего пользователя (активен, на модерации, и т.д.).
Returns:
Словарь со статусами кворков.
Example:
status = await client.other.get_kworks_status()
print(status)
"""
return await self.client._request("POST", "/kworksStatusList")
async def get_offers(self) -> dict[str, Any]:
"""
Получить персональные предложения.
Returns:
Список персональных предложений от Kwork.
"""
return await self.client._request("POST", "/offers")
async def get_exchange_info(self) -> dict[str, Any]:
"""
Получить информацию об обмене валюты.
Returns:
Информация о курсах валют и обмене.
"""
return await self.client._request("POST", "/exchangeInfo")
async def get_channel(self) -> dict[str, Any]:
"""
Получить информацию о канале пользователя.
Returns:
Данные канала (если есть).
"""
return await self.client._request("POST", "/getChannel")
async def get_in_app_notification(self) -> dict[str, Any]:
"""
Получить внутриприложенное уведомление.
Returns:
Данные in-app уведомления.
"""
return await self.client._request("POST", "/getInAppNotification")
async def get_security_user_data(self) -> dict[str, Any]:
"""
Получить данные безопасности пользователя.
Returns:
Информация о безопасности аккаунта.
"""
return await self.client._request("POST", "/getSecurityUserData")
async def is_dialog_allow(self, user_id: int) -> bool:
"""
Проверить возможность начала диалога с пользователем.
Args:
user_id: ID пользователя для проверки.
Returns:
True если диалог разрешён, False иначе.
Example:
allowed = await client.other.is_dialog_allow(12345)
if allowed:
print("Можно написать сообщение")
"""
data = await self.client._request(
"POST",
"/isDialogAllow",
json={"user_id": user_id},
)
return data.get("allowed", False)
async def get_viewed_kworks(self) -> list[Kwork]:
"""
Получить просмотренные кворки.
Возвращает список кворков, которые пользователь
просматривал ранее.
Returns:
Список просмотренных кворков.
Example:
viewed = await client.other.get_viewed_kworks()
print(f"Просмотрено: {len(viewed)} кворков")
"""
data = await self.client._request("POST", "/viewedCatalogKworks")
return [Kwork.model_validate(k) for k in data.get("kworks", [])]
async def get_favorite_categories(self) -> list[int]:
"""
Получить ID избранных категорий.
Returns:
Список ID категорий, добавленных в избранное.
Example:
cats = await client.other.get_favorite_categories()
print(f"Избранные категории: {cats}")
"""
data = await self.client._request("POST", "/favoriteCategories")
return data.get("categories", [])
async def update_settings(self, settings: dict[str, Any]) -> dict[str, Any]:
"""
Обновить настройки пользователя.
Args:
settings: Словарь с настройками для обновления.
Структура зависит от конкретных настроек.
Returns:
Ответ API с подтверждением обновления.
Example:
await client.other.update_settings({
"email_notifications": True,
"language": "ru"
})
"""
return await self.client._request("POST", "/updateSettings", json=settings)
async def go_offline(self) -> dict[str, Any]:
"""
Установить статус пользователя "оффлайн".
Скрывает онлайн-статус от других пользователей.
Returns:
Подтверждение изменения статуса.
Example:
await client.other.go_offline()
"""
return await self.client._request("POST", "/offline")
async def get_actor(self) -> dict[str, Any]:
"""
Получить информацию об актёре (текущем пользователе).
Returns:
Данные актёра/пользователя.
"""
return await self.client._request("POST", "/actor")
async def validate_text(self, text: str, context: str | None = None) -> ValidationResponse:
"""
Проверить текст на соответствие требованиям Kwork.
Эндпоинт валидации текста используется для проверки:
- Описаний кворков
- Текстов проектов
- Сообщений и отзывов
Находит потенциальные проблемы:
- Запрещённые слова и контакты
- Нарушения правил площадки
- Проблемы с форматированием
Args:
text: Текст для проверки.
context: Контекст использования (опционально).
Например: "kwork_description", "project_text", "message".
Returns:
ValidationResponse с результатами валидации.
Example:
result = await client.other.validate_text(
"Отличный сервис, звоните +7-999-000-00-00"
)
if not result.is_valid:
print("Текст не прошёл валидацию:")
for issue in result.issues:
print(f" - {issue.message}")
"""
payload = {"text": text}
if context:
payload["context"] = context
data = await self.client._request(
"POST",
"/api/validation/checktext",
json=payload,
)
return ValidationResponse.model_validate(data)
# ========== API Property Accessors ==========
@property
def catalog(self) -> CatalogAPI:
"""API каталога кворков."""
return self.CatalogAPI(self)
@property
def projects(self) -> ProjectsAPI:
"""API биржи проектов."""
return self.ProjectsAPI(self)
@property
def user(self) -> UserAPI:
"""Пользовательское API."""
return self.UserAPI(self)
@property
def reference(self) -> ReferenceAPI:
"""Справочное API."""
return self.ReferenceAPI(self)
@property
def notifications(self) -> NotificationsAPI:
"""API уведомлений."""
return self.NotificationsAPI(self)
@property
def other(self) -> OtherAPI:
"""Прочее API."""
return self.OtherAPI(self)