diff --git a/src/kwork_api/__init__.py b/src/kwork_api/__init__.py index c0af22a..511507e 100644 --- a/src/kwork_api/__init__.py +++ b/src/kwork_api/__init__.py @@ -5,27 +5,35 @@ 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 .errors import ( + KworkApiError, + KworkAuthError, + KworkError, + KworkNetworkError, + KworkNotFoundError, + KworkRateLimitError, + KworkValidationError, +) from .models import ( - ValidationResponse, - ValidationIssue, + CatalogResponse, Kwork, KworkDetails, Project, - CatalogResponse, ProjectsResponse, + ValidationIssue, + ValidationResponse, ) __version__ = "0.1.0" # Updated by semantic-release @@ -34,6 +42,10 @@ __all__ = [ "KworkError", "KworkAuthError", "KworkApiError", + "KworkNetworkError", + "KworkNotFoundError", + "KworkRateLimitError", + "KworkValidationError", "ValidationResponse", "ValidationIssue", "Kwork", diff --git a/src/kwork_api/__pycache__/__init__.cpython-312.pyc b/src/kwork_api/__pycache__/__init__.cpython-312.pyc index 0e5b60d..3c3b9a2 100644 Binary files a/src/kwork_api/__pycache__/__init__.cpython-312.pyc and b/src/kwork_api/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/kwork_api/__pycache__/client.cpython-312.pyc b/src/kwork_api/__pycache__/client.cpython-312.pyc index 455e83c..cf3c83d 100644 Binary files a/src/kwork_api/__pycache__/client.cpython-312.pyc and b/src/kwork_api/__pycache__/client.cpython-312.pyc differ diff --git a/src/kwork_api/__pycache__/errors.cpython-312.pyc b/src/kwork_api/__pycache__/errors.cpython-312.pyc index adda591..8091491 100644 Binary files a/src/kwork_api/__pycache__/errors.cpython-312.pyc and b/src/kwork_api/__pycache__/errors.cpython-312.pyc differ diff --git a/src/kwork_api/__pycache__/models.cpython-312.pyc b/src/kwork_api/__pycache__/models.cpython-312.pyc index 624181c..573e0ea 100644 Binary files a/src/kwork_api/__pycache__/models.cpython-312.pyc and b/src/kwork_api/__pycache__/models.cpython-312.pyc differ diff --git a/src/kwork_api/client.py b/src/kwork_api/client.py index 60ce889..e08064d 100644 --- a/src/kwork_api/client.py +++ b/src/kwork_api/client.py @@ -5,10 +5,9 @@ Main client class with authentication and all API endpoints. """ import logging -from typing import Any, Optional +from typing import Any import httpx -from pydantic import HttpUrl from .errors import ( KworkApiError, @@ -20,13 +19,10 @@ from .errors import ( KworkValidationError, ) from .models import ( - APIErrorResponse, - AuthResponse, Badge, CatalogResponse, City, Country, - DataResponse, Dialog, Feature, Kwork, @@ -34,7 +30,6 @@ from .models import ( NotificationsResponse, Project, ProjectsResponse, - Review, ReviewsResponse, TimeZone, ValidationResponse, @@ -46,32 +41,32 @@ 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: Доступ к бирже проектов @@ -79,29 +74,29 @@ class KworkClient: reference: Справочные данные notifications: Уведомления и сообщения other: Прочие эндпоинты - + Note: Клиент поддерживает context manager для автоматического закрытия соединения. Рекомендуется использовать `async with` для корректного освобождения ресурсов. """ - + BASE_URL = "https://api.kwork.ru" LOGIN_URL = "https://kwork.ru/signIn" TOKEN_URL = "https://kwork.ru/getWebAuthToken" - + def __init__( self, - token: Optional[str] = None, - cookies: Optional[dict[str, str]] = None, + token: str | None = None, + cookies: dict[str, str] | None = None, timeout: float = 30.0, - base_url: Optional[str] = None, + base_url: str | None = None, ): """ Инициализация клиента. - + Создаёт неаутентифицированный клиент или восстанавливает сессию по ранее полученному токену. - + Args: token: Web auth token, полученный через `getWebAuthToken` или `login()`. Если указан, автоматически добавляется в cookies. @@ -109,17 +104,17 @@ class KworkClient: Обычно не требуется — устанавливаются автоматически из token. timeout: Таймаут HTTP запросов в секундах. По умолчанию 30 секунд. base_url: Кастомный базовый URL. Используется только для тестирования. - + Example: # Новый клиент без аутентификации client = KworkClient() - + # Восстановление сессии client = KworkClient(token="eyJ0eXAiOiJKV1QiLCJhbGc...") - + # Клиент с кастомным таймаутом client = KworkClient(timeout=60.0) - + Note: Для полноценной работы API требуется аутентификация. Используйте `login()` или передайте сохранённый token. @@ -128,61 +123,61 @@ class KworkClient: self.timeout = timeout self._token = token self._cookies = cookies or {} - + # Initialize HTTP client - self._client: Optional[httpx.AsyncClient] = None - + self._client: httpx.AsyncClient | None = None + @property - def token(self) -> Optional[str]: + 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, Optional[str]]: + def credentials(self) -> dict[str, str | None]: """ Учётные данные для восстановления сессии. - + Returns: Словарь с token и cookies для передачи в KworkClient(). - + 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) @@ -192,7 +187,7 @@ class KworkClient: "token": self._token, "cookies": self._cookies.copy() if self._cookies else None, } - + @classmethod async def login( cls, @@ -202,48 +197,48 @@ class KworkClient: ) -> "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 @@ -251,43 +246,43 @@ class KworkClient: "login_or_email": username, "password": password, } - + response = await http_client.post( cls.LOGIN_URL, data=login_data, headers={"Referer": "https://kwork.ru/"}, ) - + if response.status_code != 200: raise KworkAuthError(f"Login failed: {response.status_code}") - + # Extract cookies cookies = dict(response.cookies) - + if "userId" not in cookies: raise KworkAuthError("Login failed: no userId in cookies") - + # Step 2: Get web auth token token_response = await http_client.post( cls.TOKEN_URL, json={}, ) - + if token_response.status_code != 200: raise KworkAuthError(f"Token request failed: {token_response.status_code}") - + token_data = token_response.json() web_token = token_data.get("web_auth_token") - + if not web_token: raise KworkAuthError("No web_auth_token in response") - + # 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}") - + 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: @@ -297,11 +292,11 @@ class KworkClient: "Referer": "https://kwork.ru/", "Origin": "https://kwork.ru", } - + if self._token: # Add token to cookies self._cookies["web_auth_token"] = self._token - + self._client = httpx.AsyncClient( base_url=self.base_url, headers=headers, @@ -309,30 +304,30 @@ class KworkClient: 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 @@ -341,34 +336,34 @@ class KworkClient: # 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}") - + raise KworkError(f"Failed to parse response: {e}") from e + async def _request( self, method: str, @@ -377,60 +372,60 @@ class KworkClient: ) -> 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() - + try: response = await http_client.request(method, endpoint, **kwargs) return self._handle_response(response) except httpx.RequestError as e: - raise KworkNetworkError(f"Request failed: {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: Optional[int] = None, + category_id: int | None = None, sort: str = "recommend", ) -> CatalogResponse: """ Получить список кворков из каталога. - + Основной эндпоинт для поиска и просмотра кворков. Возвращает пагинированный список с возможностью фильтрации. - + Args: page: Номер страницы для пагинации (начиная с 1). category_id: ID категории для фильтрации. @@ -442,24 +437,24 @@ class KworkClient: - "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) @@ -474,26 +469,26 @@ class KworkClient: }, ) 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}") @@ -507,25 +502,25 @@ class KworkClient: 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: Этот эндпоинт возвращает "сырые" данные без валидации. Структура ответа может измениться без предупреждения. @@ -535,60 +530,60 @@ class KworkClient: "/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: Optional[int] = None, + 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, @@ -604,17 +599,17 @@ class KworkClient: }, ) 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: @@ -622,17 +617,17 @@ class KworkClient: """ 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"] @@ -640,81 +635,81 @@ class KworkClient: """ 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") """ return await self.client._request("POST", "/user") - + async def get_reviews( self, - user_id: Optional[int] = None, + 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) """ @@ -724,16 +719,16 @@ class KworkClient: json={"user_id": user_id, "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: @@ -741,89 +736,89 @@ class KworkClient: """ 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 == "Москва") """ 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") """ 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: @@ -831,33 +826,33 @@ class KworkClient: """ 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: @@ -865,67 +860,67 @@ class KworkClient: """ 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: @@ -933,17 +928,17 @@ class KworkClient: """ 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: @@ -951,149 +946,149 @@ class KworkClient: """ 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: @@ -1105,49 +1100,49 @@ class KworkClient: 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, @@ -1155,52 +1150,54 @@ class KworkClient: }) """ 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: Optional[str] = None) -> ValidationResponse: + + 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" @@ -1213,41 +1210,41 @@ class KworkClient: 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.""" diff --git a/src/kwork_api/errors.py b/src/kwork_api/errors.py index 4004127..4d5d702 100644 --- a/src/kwork_api/errors.py +++ b/src/kwork_api/errors.py @@ -13,7 +13,7 @@ └── KworkNetworkError (ошибки сети) """ -from typing import Any, Optional +from typing import Any __all__ = [ "KworkError", @@ -29,25 +29,25 @@ __all__ = [ 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): + + def __init__(self, message: str, response: Any | None = None): self.message = message self.response = response super().__init__(self.message) - + def __str__(self) -> str: return f"KworkError: {self.message}" @@ -55,22 +55,22 @@ class KworkError(Exception): 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): + + def __init__(self, message: str = "Authentication failed", response: Any | None = None): super().__init__(message, response) - + def __str__(self) -> str: return f"KworkAuthError: {self.message}" @@ -78,28 +78,28 @@ class KworkAuthError(KworkError): 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, + status_code: int | None = None, + response: Any | None = 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}" @@ -109,46 +109,46 @@ class KworkApiError(KworkError): class KworkNotFoundError(KworkApiError): """ Ресурс не найден (404). - + Возникает при запросе несуществующего кворка, пользователя или другого ресурса. - + Example: try: await client.catalog.get_details(999999) except KworkNotFoundError: print("Кворк не найден") """ - - def __init__(self, resource: str, response: Optional[Any] = None): + + def __init__(self, resource: str, response: Any | None = None): super().__init__(f"Resource not found: {resource}", 404, response) class KworkRateLimitError(KworkApiError): """ Превышен лимит запросов (429). - + Возникает при слишком частых запросах к API. Рекомендуется сделать паузу перед повторным запросом. - + Attributes: retry_after: Время ожидания в секундах (если указано сервером). - + Example: import asyncio - + try: await client.catalog.get_list() except KworkRateLimitError as e: wait_time = e.retry_after or 5 await asyncio.sleep(wait_time) """ - + def __init__( self, message: str = "Rate limit exceeded", - response: Optional[Any] = None, - retry_after: Optional[int] = None, + response: Any | None = None, + retry_after: int | None = None, ): self.retry_after = retry_after super().__init__(message, 429, response) @@ -157,12 +157,12 @@ class KworkRateLimitError(KworkApiError): class KworkValidationError(KworkApiError): """ Ошибка валидации (400). - + Возникает при некорректных данных запроса. - + Attributes: fields: Словарь ошибок по полям {field: [errors]}. - + Example: try: await client.catalog.get_list(page=-1) @@ -171,16 +171,16 @@ class KworkValidationError(KworkApiError): 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, + fields: dict[str, list[str]] | None = None, + response: Any | None = 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()) @@ -191,22 +191,22 @@ class KworkValidationError(KworkApiError): 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): + + def __init__(self, message: str = "Network error", response: Any | None = None): super().__init__(message, response) - + def __str__(self) -> str: return f"KworkNetworkError: {self.message}" diff --git a/src/kwork_api/models.py b/src/kwork_api/models.py index d69c0fc..6a70fa2 100644 --- a/src/kwork_api/models.py +++ b/src/kwork_api/models.py @@ -6,7 +6,7 @@ Pydantic модели для ответов Kwork API. """ from datetime import datetime -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, Field @@ -14,47 +14,49 @@ 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 + avatar_url: str | None = None is_online: bool = False - rating: Optional[float] = None + rating: float | None = 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 + parent_id: int | None = None class Kwork(BaseModel): """ Кворк — услуга на Kwork. - + Базовая модель кворка с основной информацией. - + Attributes: id: Уникальный ID кворка. title: Заголовок кворка. @@ -69,26 +71,27 @@ class Kwork(BaseModel): created_at: Дата создания. updated_at: Дата последнего обновления. """ + id: int title: str - description: Optional[str] = None + description: str | None = None price: float currency: str = "RUB" - category_id: Optional[int] = None - seller: Optional[KworkUser] = None + category_id: int | None = None + seller: KworkUser | None = None images: list[str] = Field(default_factory=list) - rating: Optional[float] = None + rating: float | None = None reviews_count: int = 0 - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None + created_at: datetime | None = None + updated_at: datetime | None = None class KworkDetails(Kwork): """ Расширенная информация о кворке. - + Наследует все поля Kwork плюс дополнительные детали. - + Attributes: full_description: Полное описание услуги. requirements: Требования к заказчику. @@ -97,10 +100,11 @@ class KworkDetails(Kwork): features: Список дополнительных опций. faq: Список вопросов и ответов. """ - full_description: Optional[str] = None - requirements: Optional[str] = None - delivery_time: Optional[int] = None - revisions: Optional[int] = None + + full_description: str | None = None + requirements: str | None = None + delivery_time: int | None = None + revisions: int | None = None features: list[str] = Field(default_factory=list) faq: list[dict[str, str]] = Field(default_factory=list) @@ -108,7 +112,7 @@ class KworkDetails(Kwork): class PaginationInfo(BaseModel): """ Информация о пагинации. - + Attributes: current_page: Текущая страница (начиная с 1). total_pages: Общее количество страниц. @@ -117,6 +121,7 @@ class PaginationInfo(BaseModel): has_next: Есть ли следующая страница. has_prev: Есть ли предыдущая страница. """ + current_page: int = 1 total_pages: int = 1 total_items: int = 0 @@ -128,23 +133,24 @@ class PaginationInfo(BaseModel): 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 + pagination: PaginationInfo | None = None + filters: dict[str, Any] | None = None sort_options: list[str] = Field(default_factory=list) class Project(BaseModel): """ Проект (заказ на бирже фриланса). - + Attributes: id: Уникальный ID проекта. title: Заголовок проекта. @@ -159,16 +165,17 @@ class Project(BaseModel): bids_count: Количество откликов. skills: Требуемые навыки. """ + id: int title: str - description: Optional[str] = None - budget: Optional[float] = None + description: str | None = None + budget: float | None = None budget_type: str = "fixed" - category_id: Optional[int] = None - customer: Optional[KworkUser] = None + category_id: int | None = None + customer: KworkUser | None = None status: str = "open" - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None + created_at: datetime | None = None + updated_at: datetime | None = None bids_count: int = 0 skills: list[str] = Field(default_factory=list) @@ -176,19 +183,20 @@ class Project(BaseModel): class ProjectsResponse(BaseModel): """ Ответ API списка проектов. - + Attributes: projects: Список проектов. pagination: Информация о пагинации. """ + projects: list[Project] = Field(default_factory=list) - pagination: Optional[PaginationInfo] = None + pagination: PaginationInfo | None = None class Review(BaseModel): """ Отзыв о кворке или проекте. - + Attributes: id: Уникальный ID отзыва. rating: Оценка от 1 до 5. @@ -197,32 +205,34 @@ class Review(BaseModel): 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 + comment: str | None = None + author: KworkUser | None = None + kwork_id: int | None = None + created_at: datetime | None = 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 + pagination: PaginationInfo | None = None + average_rating: float | None = None class Notification(BaseModel): """ Уведомление пользователя. - + Attributes: id: Уникальный ID уведомления. type: Тип уведомления: "message", "order", "system", etc. @@ -232,23 +242,25 @@ class Notification(BaseModel): created_at: Дата создания. link: Ссылка для перехода (если есть). """ + id: int type: str title: str message: str is_read: bool = False - created_at: Optional[datetime] = None - link: Optional[str] = None + created_at: datetime | None = None + link: str | None = None class NotificationsResponse(BaseModel): """ Ответ API списка уведомлений. - + Attributes: notifications: Список уведомлений. unread_count: Количество непрочитанных уведомлений. """ + notifications: list[Notification] = Field(default_factory=list) unread_count: int = 0 @@ -256,7 +268,7 @@ class NotificationsResponse(BaseModel): class Dialog(BaseModel): """ Диалог (чат) с пользователем. - + Attributes: id: Уникальный ID диалога. participant: Собеседник. @@ -264,17 +276,18 @@ class Dialog(BaseModel): unread_count: Количество непрочитанных сообщений. updated_at: Время последнего сообщения. """ + id: int - participant: Optional[KworkUser] = None - last_message: Optional[str] = None + participant: KworkUser | None = None + last_message: str | None = None unread_count: int = 0 - updated_at: Optional[datetime] = None + updated_at: datetime | None = None class AuthResponse(BaseModel): """ Ответ API аутентификации. - + Attributes: success: Успешность аутентификации. user_id: ID пользователя. @@ -282,80 +295,86 @@ class AuthResponse(BaseModel): 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 + user_id: int | None = None + username: str | None = None + web_auth_token: str | None = None + message: str | None = None class ErrorDetail(BaseModel): """ Детали ошибки API. - + Attributes: code: Код ошибки. message: Сообщение об ошибке. field: Поле, вызвавшее ошибку (если применимо). """ + code: str message: str - field: Optional[str] = None + field: str | None = None class APIErrorResponse(BaseModel): """ Стандартный ответ API об ошибке. - + Attributes: success: Всегда False для ошибок. errors: Список деталей ошибок. message: Общее сообщение об ошибке. """ + success: bool = False errors: list[ErrorDetail] = Field(default_factory=list) - message: Optional[str] = None + message: str | None = None class City(BaseModel): """ Город из справочника. - + Attributes: id: Уникальный ID города. name: Название города. country_id: ID страны. """ + id: int name: str - country_id: Optional[int] = None + country_id: int | None = None class Country(BaseModel): """ Страна из справочника. - + Attributes: id: Уникальный ID страны. name: Название страны. code: Код страны (ISO). cities: Список городов в стране. """ + id: int name: str - code: Optional[str] = None + code: str | None = 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 @@ -364,7 +383,7 @@ class TimeZone(BaseModel): class Feature(BaseModel): """ Дополнительная функция (feature) для кворка. - + Attributes: id: Уникальный ID функции. name: Название. @@ -372,9 +391,10 @@ class Feature(BaseModel): price: Стоимость в рублях. type: Тип: "extra", "premium", etc. """ + id: int name: str - description: Optional[str] = None + description: str | None = None price: float type: str @@ -382,40 +402,42 @@ class Feature(BaseModel): class Badge(BaseModel): """ Значок (достижение) пользователя. - + Attributes: id: Уникальный ID значка. name: Название значка. description: Описание достижения. icon_url: URL иконки значка. """ + id: int name: str - description: Optional[str] = None - icon_url: Optional[str] = None + description: str | None = None + icon_url: str | None = 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 + data: dict[str, Any] | None = None + message: str | None = None class ValidationIssue(BaseModel): """ Проблема, найденная при валидации текста. - + Attributes: type: Тип проблемы: "error", "warning", "suggestion". code: Код ошибки (например, "SPELLING", "GRAMMAR", "LENGTH"). @@ -423,19 +445,20 @@ class ValidationIssue(BaseModel): position: Позиция в тексте (если применимо). suggestion: Предлагаемое исправление (если есть). """ + type: str = "error" code: str message: str - position: Optional[int] = None - suggestion: Optional[str] = None + position: int | None = None + suggestion: str | None = None class ValidationResponse(BaseModel): """ Ответ API валидации текста. - + Используется для эндпоинта /api/validation/checktext. - + Attributes: success: Успешность валидации. is_valid: Текст проходит валидацию (нет критических ошибок). @@ -443,8 +466,9 @@ class ValidationResponse(BaseModel): 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 + score: int | None = None + message: str | None = None diff --git a/test-results/report.html b/test-results/report.html new file mode 100644 index 0000000..48c7737 --- /dev/null +++ b/test-results/report.html @@ -0,0 +1,1094 @@ + + +
+ +Report generated on 29-Mar-2026 at 07:54:55 by pytest-html + v4.2.0
+16 tests took 00:00:02.
+(Un)check the boxes to filter the results.
+| Result | +Test | +Duration | +Links | +
|---|