fix: auto-format code and fix linter errors
Some checks failed
PR Checks / test (pull_request) Failing after 25s
PR Checks / security (pull_request) Failing after 7s

This commit is contained in:
root 2026-03-29 08:58:35 +00:00
parent b67f0e5031
commit 3995d60b6b
15 changed files with 1914 additions and 809 deletions

View File

@ -17,15 +17,23 @@ Example:
""" """
from .client import KworkClient from .client import KworkClient
from .errors import KworkError, KworkAuthError, KworkApiError from .errors import (
KworkApiError,
KworkAuthError,
KworkError,
KworkNetworkError,
KworkNotFoundError,
KworkRateLimitError,
KworkValidationError,
)
from .models import ( from .models import (
ValidationResponse, CatalogResponse,
ValidationIssue,
Kwork, Kwork,
KworkDetails, KworkDetails,
Project, Project,
CatalogResponse,
ProjectsResponse, ProjectsResponse,
ValidationIssue,
ValidationResponse,
) )
__version__ = "0.1.0" # Updated by semantic-release __version__ = "0.1.0" # Updated by semantic-release
@ -34,6 +42,10 @@ __all__ = [
"KworkError", "KworkError",
"KworkAuthError", "KworkAuthError",
"KworkApiError", "KworkApiError",
"KworkNetworkError",
"KworkNotFoundError",
"KworkRateLimitError",
"KworkValidationError",
"ValidationResponse", "ValidationResponse",
"ValidationIssue", "ValidationIssue",
"Kwork", "Kwork",

View File

@ -5,10 +5,9 @@ Main client class with authentication and all API endpoints.
""" """
import logging import logging
from typing import Any, Optional from typing import Any
import httpx import httpx
from pydantic import HttpUrl
from .errors import ( from .errors import (
KworkApiError, KworkApiError,
@ -20,13 +19,10 @@ from .errors import (
KworkValidationError, KworkValidationError,
) )
from .models import ( from .models import (
APIErrorResponse,
AuthResponse,
Badge, Badge,
CatalogResponse, CatalogResponse,
City, City,
Country, Country,
DataResponse,
Dialog, Dialog,
Feature, Feature,
Kwork, Kwork,
@ -34,7 +30,6 @@ from .models import (
NotificationsResponse, NotificationsResponse,
Project, Project,
ProjectsResponse, ProjectsResponse,
Review,
ReviewsResponse, ReviewsResponse,
TimeZone, TimeZone,
ValidationResponse, ValidationResponse,
@ -91,10 +86,10 @@ class KworkClient:
def __init__( def __init__(
self, self,
token: Optional[str] = None, token: str | None = None,
cookies: Optional[dict[str, str]] = None, cookies: dict[str, str] | None = None,
timeout: float = 30.0, timeout: float = 30.0,
base_url: Optional[str] = None, base_url: str | None = None,
): ):
""" """
Инициализация клиента. Инициализация клиента.
@ -130,10 +125,10 @@ class KworkClient:
self._cookies = cookies or {} self._cookies = cookies or {}
# Initialize HTTP client # Initialize HTTP client
self._client: Optional[httpx.AsyncClient] = None self._client: httpx.AsyncClient | None = None
@property @property
def token(self) -> Optional[str]: def token(self) -> str | None:
""" """
Web auth token для аутентификации. Web auth token для аутентификации.
@ -169,7 +164,7 @@ class KworkClient:
return self._cookies.copy() return self._cookies.copy()
@property @property
def credentials(self) -> dict[str, Optional[str]]: def credentials(self) -> dict[str, str | None]:
""" """
Учётные данные для восстановления сессии. Учётные данные для восстановления сессии.
@ -286,7 +281,7 @@ class KworkClient:
return cls(token=web_token, cookies=cookies, timeout=timeout) return cls(token=web_token, cookies=cookies, timeout=timeout)
except httpx.RequestError as e: 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: def _get_httpx_client(self) -> httpx.AsyncClient:
"""Get or create HTTP client with proper headers.""" """Get or create HTTP client with proper headers."""
@ -367,7 +362,7 @@ class KworkClient:
try: try:
return response.json() return response.json()
except Exception as e: 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( async def _request(
self, self,
@ -392,7 +387,7 @@ class KworkClient:
response = await http_client.request(method, endpoint, **kwargs) response = await http_client.request(method, endpoint, **kwargs)
return self._handle_response(response) return self._handle_response(response)
except httpx.RequestError as e: except httpx.RequestError as e:
raise KworkNetworkError(f"Request failed: {e}") raise KworkNetworkError(f"Request failed: {e}") from e
# ========== Catalog Endpoints ========== # ========== Catalog Endpoints ==========
@ -422,7 +417,7 @@ class KworkClient:
async def get_list( async def get_list(
self, self,
page: int = 1, page: int = 1,
category_id: Optional[int] = None, category_id: int | None = None,
sort: str = "recommend", sort: str = "recommend",
) -> CatalogResponse: ) -> CatalogResponse:
""" """
@ -564,7 +559,7 @@ class KworkClient:
async def get_list( async def get_list(
self, self,
page: int = 1, page: int = 1,
category_id: Optional[int] = None, category_id: int | None = None,
) -> ProjectsResponse: ) -> ProjectsResponse:
""" """
Получить список проектов с биржи. Получить список проектов с биржи.
@ -689,7 +684,7 @@ class KworkClient:
async def get_reviews( async def get_reviews(
self, self,
user_id: Optional[int] = None, user_id: int | None = None,
page: int = 1, page: int = 1,
) -> ReviewsResponse: ) -> ReviewsResponse:
""" """
@ -1179,7 +1174,9 @@ class KworkClient:
""" """
return await self.client._request("POST", "/actor") 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. Проверить текст на соответствие требованиям Kwork.

View File

@ -13,7 +13,7 @@
KworkNetworkError (ошибки сети) KworkNetworkError (ошибки сети)
""" """
from typing import Any, Optional from typing import Any
__all__ = [ __all__ = [
"KworkError", "KworkError",
@ -43,7 +43,7 @@ class KworkError(Exception):
print(f"Ошибка: {e.message}") 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.message = message
self.response = response self.response = response
super().__init__(self.message) super().__init__(self.message)
@ -68,7 +68,7 @@ class KworkAuthError(KworkError):
print("Неверные учётные данные") 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) super().__init__(message, response)
def __str__(self) -> str: def __str__(self) -> str:
@ -94,8 +94,8 @@ class KworkApiError(KworkError):
def __init__( def __init__(
self, self,
message: str, message: str,
status_code: Optional[int] = None, status_code: int | None = None,
response: Optional[Any] = None, response: Any | None = None,
): ):
self.status_code = status_code self.status_code = status_code
super().__init__(message, response) super().__init__(message, response)
@ -120,7 +120,7 @@ class KworkNotFoundError(KworkApiError):
print("Кворк не найден") 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) super().__init__(f"Resource not found: {resource}", 404, response)
@ -147,8 +147,8 @@ class KworkRateLimitError(KworkApiError):
def __init__( def __init__(
self, self,
message: str = "Rate limit exceeded", message: str = "Rate limit exceeded",
response: Optional[Any] = None, response: Any | None = None,
retry_after: Optional[int] = None, retry_after: int | None = None,
): ):
self.retry_after = retry_after self.retry_after = retry_after
super().__init__(message, 429, response) super().__init__(message, 429, response)
@ -175,8 +175,8 @@ class KworkValidationError(KworkApiError):
def __init__( def __init__(
self, self,
message: str = "Validation failed", message: str = "Validation failed",
fields: Optional[dict[str, list[str]]] = None, fields: dict[str, list[str]] | None = None,
response: Optional[Any] = None, response: Any | None = None,
): ):
self.fields = fields or {} self.fields = fields or {}
super().__init__(message, 400, response) super().__init__(message, 400, response)
@ -205,7 +205,7 @@ class KworkNetworkError(KworkError):
print("Проверьте подключение к интернету") 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) super().__init__(message, response)
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -6,7 +6,7 @@ Pydantic модели для ответов Kwork API.
""" """
from datetime import datetime from datetime import datetime
from typing import Any, Optional from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -26,11 +26,12 @@ class KworkUser(BaseModel):
user = KworkUser(id=123, username="seller", rating=4.9) user = KworkUser(id=123, username="seller", rating=4.9)
print(f"{user.username}: {user.rating}") print(f"{user.username}: {user.rating}")
""" """
id: int id: int
username: str username: str
avatar_url: Optional[str] = None avatar_url: str | None = None
is_online: bool = False is_online: bool = False
rating: Optional[float] = None rating: float | None = None
class KworkCategory(BaseModel): class KworkCategory(BaseModel):
@ -43,10 +44,11 @@ class KworkCategory(BaseModel):
slug: URL-safe идентификатор. slug: URL-safe идентификатор.
parent_id: ID родительской категории для вложенности. parent_id: ID родительской категории для вложенности.
""" """
id: int id: int
name: str name: str
slug: str slug: str
parent_id: Optional[int] = None parent_id: int | None = None
class Kwork(BaseModel): class Kwork(BaseModel):
@ -69,18 +71,19 @@ class Kwork(BaseModel):
created_at: Дата создания. created_at: Дата создания.
updated_at: Дата последнего обновления. updated_at: Дата последнего обновления.
""" """
id: int id: int
title: str title: str
description: Optional[str] = None description: str | None = None
price: float price: float
currency: str = "RUB" currency: str = "RUB"
category_id: Optional[int] = None category_id: int | None = None
seller: Optional[KworkUser] = None seller: KworkUser | None = None
images: list[str] = Field(default_factory=list) images: list[str] = Field(default_factory=list)
rating: Optional[float] = None rating: float | None = None
reviews_count: int = 0 reviews_count: int = 0
created_at: Optional[datetime] = None created_at: datetime | None = None
updated_at: Optional[datetime] = None updated_at: datetime | None = None
class KworkDetails(Kwork): class KworkDetails(Kwork):
@ -97,10 +100,11 @@ class KworkDetails(Kwork):
features: Список дополнительных опций. features: Список дополнительных опций.
faq: Список вопросов и ответов. faq: Список вопросов и ответов.
""" """
full_description: Optional[str] = None
requirements: Optional[str] = None full_description: str | None = None
delivery_time: Optional[int] = None requirements: str | None = None
revisions: Optional[int] = None delivery_time: int | None = None
revisions: int | None = None
features: list[str] = Field(default_factory=list) features: list[str] = Field(default_factory=list)
faq: list[dict[str, str]] = Field(default_factory=list) faq: list[dict[str, str]] = Field(default_factory=list)
@ -117,6 +121,7 @@ class PaginationInfo(BaseModel):
has_next: Есть ли следующая страница. has_next: Есть ли следующая страница.
has_prev: Есть ли предыдущая страница. has_prev: Есть ли предыдущая страница.
""" """
current_page: int = 1 current_page: int = 1
total_pages: int = 1 total_pages: int = 1
total_items: int = 0 total_items: int = 0
@ -135,9 +140,10 @@ class CatalogResponse(BaseModel):
filters: Доступные фильтры. filters: Доступные фильтры.
sort_options: Доступные опции сортировки. sort_options: Доступные опции сортировки.
""" """
kworks: list[Kwork] = Field(default_factory=list) kworks: list[Kwork] = Field(default_factory=list)
pagination: Optional[PaginationInfo] = None pagination: PaginationInfo | None = None
filters: Optional[dict[str, Any]] = None filters: dict[str, Any] | None = None
sort_options: list[str] = Field(default_factory=list) sort_options: list[str] = Field(default_factory=list)
@ -159,16 +165,17 @@ class Project(BaseModel):
bids_count: Количество откликов. bids_count: Количество откликов.
skills: Требуемые навыки. skills: Требуемые навыки.
""" """
id: int id: int
title: str title: str
description: Optional[str] = None description: str | None = None
budget: Optional[float] = None budget: float | None = None
budget_type: str = "fixed" budget_type: str = "fixed"
category_id: Optional[int] = None category_id: int | None = None
customer: Optional[KworkUser] = None customer: KworkUser | None = None
status: str = "open" status: str = "open"
created_at: Optional[datetime] = None created_at: datetime | None = None
updated_at: Optional[datetime] = None updated_at: datetime | None = None
bids_count: int = 0 bids_count: int = 0
skills: list[str] = Field(default_factory=list) skills: list[str] = Field(default_factory=list)
@ -181,8 +188,9 @@ class ProjectsResponse(BaseModel):
projects: Список проектов. projects: Список проектов.
pagination: Информация о пагинации. pagination: Информация о пагинации.
""" """
projects: list[Project] = Field(default_factory=list) projects: list[Project] = Field(default_factory=list)
pagination: Optional[PaginationInfo] = None pagination: PaginationInfo | None = None
class Review(BaseModel): class Review(BaseModel):
@ -197,12 +205,13 @@ class Review(BaseModel):
kwork_id: ID кворка (если отзыв о кворке). kwork_id: ID кворка (если отзыв о кворке).
created_at: Дата создания. created_at: Дата создания.
""" """
id: int id: int
rating: int = Field(ge=1, le=5) rating: int = Field(ge=1, le=5)
comment: Optional[str] = None comment: str | None = None
author: Optional[KworkUser] = None author: KworkUser | None = None
kwork_id: Optional[int] = None kwork_id: int | None = None
created_at: Optional[datetime] = None created_at: datetime | None = None
class ReviewsResponse(BaseModel): class ReviewsResponse(BaseModel):
@ -214,9 +223,10 @@ class ReviewsResponse(BaseModel):
pagination: Информация о пагинации. pagination: Информация о пагинации.
average_rating: Средний рейтинг. average_rating: Средний рейтинг.
""" """
reviews: list[Review] = Field(default_factory=list) reviews: list[Review] = Field(default_factory=list)
pagination: Optional[PaginationInfo] = None pagination: PaginationInfo | None = None
average_rating: Optional[float] = None average_rating: float | None = None
class Notification(BaseModel): class Notification(BaseModel):
@ -232,13 +242,14 @@ class Notification(BaseModel):
created_at: Дата создания. created_at: Дата создания.
link: Ссылка для перехода (если есть). link: Ссылка для перехода (если есть).
""" """
id: int id: int
type: str type: str
title: str title: str
message: str message: str
is_read: bool = False is_read: bool = False
created_at: Optional[datetime] = None created_at: datetime | None = None
link: Optional[str] = None link: str | None = None
class NotificationsResponse(BaseModel): class NotificationsResponse(BaseModel):
@ -249,6 +260,7 @@ class NotificationsResponse(BaseModel):
notifications: Список уведомлений. notifications: Список уведомлений.
unread_count: Количество непрочитанных уведомлений. unread_count: Количество непрочитанных уведомлений.
""" """
notifications: list[Notification] = Field(default_factory=list) notifications: list[Notification] = Field(default_factory=list)
unread_count: int = 0 unread_count: int = 0
@ -264,11 +276,12 @@ class Dialog(BaseModel):
unread_count: Количество непрочитанных сообщений. unread_count: Количество непрочитанных сообщений.
updated_at: Время последнего сообщения. updated_at: Время последнего сообщения.
""" """
id: int id: int
participant: Optional[KworkUser] = None participant: KworkUser | None = None
last_message: Optional[str] = None last_message: str | None = None
unread_count: int = 0 unread_count: int = 0
updated_at: Optional[datetime] = None updated_at: datetime | None = None
class AuthResponse(BaseModel): class AuthResponse(BaseModel):
@ -282,11 +295,12 @@ class AuthResponse(BaseModel):
web_auth_token: Токен для последующих запросов. web_auth_token: Токен для последующих запросов.
message: Сообщение (например, об ошибке). message: Сообщение (например, об ошибке).
""" """
success: bool success: bool
user_id: Optional[int] = None user_id: int | None = None
username: Optional[str] = None username: str | None = None
web_auth_token: Optional[str] = None web_auth_token: str | None = None
message: Optional[str] = None message: str | None = None
class ErrorDetail(BaseModel): class ErrorDetail(BaseModel):
@ -298,9 +312,10 @@ class ErrorDetail(BaseModel):
message: Сообщение об ошибке. message: Сообщение об ошибке.
field: Поле, вызвавшее ошибку (если применимо). field: Поле, вызвавшее ошибку (если применимо).
""" """
code: str code: str
message: str message: str
field: Optional[str] = None field: str | None = None
class APIErrorResponse(BaseModel): class APIErrorResponse(BaseModel):
@ -312,9 +327,10 @@ class APIErrorResponse(BaseModel):
errors: Список деталей ошибок. errors: Список деталей ошибок.
message: Общее сообщение об ошибке. message: Общее сообщение об ошибке.
""" """
success: bool = False success: bool = False
errors: list[ErrorDetail] = Field(default_factory=list) errors: list[ErrorDetail] = Field(default_factory=list)
message: Optional[str] = None message: str | None = None
class City(BaseModel): class City(BaseModel):
@ -326,9 +342,10 @@ class City(BaseModel):
name: Название города. name: Название города.
country_id: ID страны. country_id: ID страны.
""" """
id: int id: int
name: str name: str
country_id: Optional[int] = None country_id: int | None = None
class Country(BaseModel): class Country(BaseModel):
@ -341,9 +358,10 @@ class Country(BaseModel):
code: Код страны (ISO). code: Код страны (ISO).
cities: Список городов в стране. cities: Список городов в стране.
""" """
id: int id: int
name: str name: str
code: Optional[str] = None code: str | None = None
cities: list[City] = Field(default_factory=list) cities: list[City] = Field(default_factory=list)
@ -356,6 +374,7 @@ class TimeZone(BaseModel):
name: Название пояса. name: Название пояса.
offset: Смещение от UTC (например, "+03:00"). offset: Смещение от UTC (например, "+03:00").
""" """
id: int id: int
name: str name: str
offset: str offset: str
@ -372,9 +391,10 @@ class Feature(BaseModel):
price: Стоимость в рублях. price: Стоимость в рублях.
type: Тип: "extra", "premium", etc. type: Тип: "extra", "premium", etc.
""" """
id: int id: int
name: str name: str
description: Optional[str] = None description: str | None = None
price: float price: float
type: str type: str
@ -389,10 +409,11 @@ class Badge(BaseModel):
description: Описание достижения. description: Описание достижения.
icon_url: URL иконки значка. icon_url: URL иконки значка.
""" """
id: int id: int
name: str name: str
description: Optional[str] = None description: str | None = None
icon_url: Optional[str] = None icon_url: str | None = None
# Generic response wrapper # Generic response wrapper
@ -407,9 +428,10 @@ class DataResponse(BaseModel):
data: Полезные данные (словарь). data: Полезные данные (словарь).
message: Дополнительное сообщение. message: Дополнительное сообщение.
""" """
success: bool = True success: bool = True
data: Optional[dict[str, Any]] = None data: dict[str, Any] | None = None
message: Optional[str] = None message: str | None = None
class ValidationIssue(BaseModel): class ValidationIssue(BaseModel):
@ -423,11 +445,12 @@ class ValidationIssue(BaseModel):
position: Позиция в тексте (если применимо). position: Позиция в тексте (если применимо).
suggestion: Предлагаемое исправление (если есть). suggestion: Предлагаемое исправление (если есть).
""" """
type: str = "error" type: str = "error"
code: str code: str
message: str message: str
position: Optional[int] = None position: int | None = None
suggestion: Optional[str] = None suggestion: str | None = None
class ValidationResponse(BaseModel): class ValidationResponse(BaseModel):
@ -443,8 +466,9 @@ class ValidationResponse(BaseModel):
score: Оценка качества текста (0-100, если доступна). score: Оценка качества текста (0-100, если доступна).
message: Дополнительное сообщение. message: Дополнительное сообщение.
""" """
success: bool = True success: bool = True
is_valid: bool = True is_valid: bool = True
issues: list[ValidationIssue] = Field(default_factory=list) issues: list[ValidationIssue] = Field(default_factory=list)
score: Optional[int] = None score: int | None = None
message: Optional[str] = None message: str | None = None

1094
test-results/report.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,9 @@ E2E тесты для Kwork API.
""" """
import os import os
import pytest
from pathlib import Path from pathlib import Path
import pytest
from dotenv import load_dotenv from dotenv import load_dotenv
# Загружаем .env # Загружаем .env
@ -39,21 +40,17 @@ def slowmo(request):
slowmo = request.config.getoption("--slowmo", default=0) slowmo = request.config.getoption("--slowmo", default=0)
if slowmo > 0: if slowmo > 0:
import time import time
time.sleep(slowmo) time.sleep(slowmo)
def pytest_configure(config): def pytest_configure(config):
"""Регистрация маркера e2e.""" """Регистрация маркера e2e."""
config.addinivalue_line( config.addinivalue_line("markers", "e2e: mark test as end-to-end (requires credentials)")
"markers", "e2e: mark test as end-to-end (requires credentials)"
)
def pytest_addoption(parser): def pytest_addoption(parser):
"""Добавляет опцию --slowmo.""" """Добавляет опцию --slowmo."""
parser.addoption( parser.addoption(
"--slowmo", "--slowmo", type=float, default=0, help="Delay between tests in seconds (for rate limiting)"
type=float,
default=0,
help="Delay between tests in seconds (for rate limiting)"
) )

View File

@ -3,6 +3,7 @@ E2E тесты аутентификации.
""" """
import pytest import pytest
from kwork_api import KworkClient from kwork_api import KworkClient
from kwork_api.errors import KworkAuthError from kwork_api.errors import KworkAuthError
@ -11,8 +12,7 @@ from kwork_api.errors import KworkAuthError
async def test_login_success(require_credentials): async def test_login_success(require_credentials):
"""E2E: Успешная аутентификация.""" """E2E: Успешная аутентификация."""
client = await KworkClient.login( client = await KworkClient.login(
username=require_credentials["username"], username=require_credentials["username"], password=require_credentials["password"]
password=require_credentials["password"]
) )
try: try:
@ -26,10 +26,7 @@ async def test_login_success(require_credentials):
async def test_login_invalid_credentials(): async def test_login_invalid_credentials():
"""E2E: Неверные credentials.""" """E2E: Неверные credentials."""
with pytest.raises(KworkAuthError): with pytest.raises(KworkAuthError):
await KworkClient.login( await KworkClient.login(username="invalid_user_12345", password="invalid_pass_12345")
username="invalid_user_12345",
password="invalid_pass_12345"
)
@pytest.mark.e2e @pytest.mark.e2e
@ -37,8 +34,7 @@ async def test_restore_session(require_credentials):
"""E2E: Восстановление сессии из токена.""" """E2E: Восстановление сессии из токена."""
# First login # First login
client1 = await KworkClient.login( client1 = await KworkClient.login(
username=require_credentials["username"], username=require_credentials["username"], password=require_credentials["password"]
password=require_credentials["password"]
) )
token = client1.token token = client1.token
await client1.aclose() await client1.aclose()

View File

@ -12,15 +12,14 @@ Usage:
""" """
import os import os
from typing import Optional
import pytest import pytest
from kwork_api import KworkClient, KworkAuthError from kwork_api import KworkAuthError, KworkClient
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def client() -> Optional[KworkClient]: def client() -> KworkClient | None:
""" """
Create authenticated client for integration tests. Create authenticated client for integration tests.
@ -172,6 +171,7 @@ class TestReferenceAPI:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
result = asyncio.run(client.reference.get_countries()) result = asyncio.run(client.reference.get_countries())
assert isinstance(result, list) assert isinstance(result, list)
@ -183,6 +183,7 @@ class TestReferenceAPI:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
result = asyncio.run(client.reference.get_timezones()) result = asyncio.run(client.reference.get_timezones())
assert isinstance(result, list) assert isinstance(result, list)
@ -199,6 +200,7 @@ class TestUserAPI:
pytest.skip("No client") pytest.skip("No client")
import asyncio import asyncio
result = asyncio.run(client.user.get_info()) result = asyncio.run(client.user.get_info())
assert isinstance(result, dict) assert isinstance(result, dict)
@ -224,7 +226,7 @@ class TestErrorHandling:
except Exception: except Exception:
return True return True
result = asyncio.run(fetch()) asyncio.run(fetch())
# May or may not raise error depending on API behavior # May or may not raise error depending on API behavior

View File

@ -8,8 +8,8 @@ import pytest
import respx import respx
from httpx import Response from httpx import Response
from kwork_api import KworkClient, KworkAuthError, KworkApiError from kwork_api import KworkApiError, KworkAuthError, KworkClient, KworkNetworkError
from kwork_api.models import CatalogResponse, Kwork, ValidationResponse, ValidationIssue from kwork_api.models import CatalogResponse, ValidationResponse
class TestAuthentication: class TestAuthentication:
@ -22,10 +22,12 @@ class TestAuthentication:
# Mock login endpoint # Mock login endpoint
login_route = respx.post("https://kwork.ru/signIn") login_route = respx.post("https://kwork.ru/signIn")
login_route.mock(return_value=httpx.Response( login_route.mock(
return_value=httpx.Response(
200, 200,
headers={"Set-Cookie": "userId=12345; slrememberme=token123"}, headers={"Set-Cookie": "userId=12345; slrememberme=token123"},
)) )
)
# Mock token endpoint # Mock token endpoint
token_route = respx.post("https://kwork.ru/getWebAuthToken").mock( token_route = respx.post("https://kwork.ru/getWebAuthToken").mock(
@ -175,9 +177,7 @@ class TestProjectsAPI:
"pagination": {"current_page": 1}, "pagination": {"current_page": 1},
} }
respx.post(f"{client.base_url}/projects").mock( respx.post(f"{client.base_url}/projects").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_list() result = await client.projects.get_list()
@ -193,9 +193,7 @@ class TestErrorHandling:
"""Test 404 error handling.""" """Test 404 error handling."""
client = KworkClient(token="test") client = KworkClient(token="test")
respx.post(f"{client.base_url}/getKworkDetails").mock( respx.post(f"{client.base_url}/getKworkDetails").mock(return_value=Response(404))
return_value=Response(404)
)
with pytest.raises(KworkApiError) as exc_info: with pytest.raises(KworkApiError) as exc_info:
await client.catalog.get_details(999) await client.catalog.get_details(999)
@ -207,9 +205,7 @@ class TestErrorHandling:
"""Test 401 error handling.""" """Test 401 error handling."""
client = KworkClient(token="invalid") client = KworkClient(token="invalid")
respx.post(f"{client.base_url}/catalogMainv2").mock( respx.post(f"{client.base_url}/catalogMainv2").mock(return_value=Response(401))
return_value=Response(401)
)
with pytest.raises(KworkAuthError): with pytest.raises(KworkAuthError):
await client.catalog.get_list() await client.catalog.get_list()
@ -217,13 +213,15 @@ class TestErrorHandling:
@respx.mock @respx.mock
async def test_network_error(self): async def test_network_error(self):
"""Test network error handling.""" """Test network error handling."""
import httpx
client = KworkClient(token="test") client = KworkClient(token="test")
respx.post(f"{client.base_url}/catalogMainv2").mock( respx.post(f"{client.base_url}/catalogMainv2").mock(
side_effect=Exception("Connection refused") side_effect=httpx.RequestError("Connection refused", request=None)
) )
with pytest.raises(Exception): with pytest.raises(KworkNetworkError):
await client.catalog.get_list() await client.catalog.get_list()

View File

@ -8,17 +8,17 @@ import pytest
import respx import respx
from httpx import Response from httpx import Response
from kwork_api import KworkClient, KworkAuthError, KworkApiError from kwork_api import KworkApiError, KworkClient
from kwork_api.models import ( from kwork_api.models import (
NotificationsResponse, Badge,
Kwork,
Dialog,
City, City,
Country, Country,
TimeZone, Dialog,
Feature, Feature,
Badge, Kwork,
NotificationsResponse,
Project, Project,
TimeZone,
) )
BASE_URL = "https://api.kwork.ru" BASE_URL = "https://api.kwork.ru"
@ -103,9 +103,7 @@ class TestProjectsAPIExtended:
] ]
} }
respx.post(f"{BASE_URL}/payerOrders").mock( respx.post(f"{BASE_URL}/payerOrders").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_payer_orders() result = await client.projects.get_payer_orders()
@ -124,9 +122,7 @@ class TestProjectsAPIExtended:
] ]
} }
respx.post(f"{BASE_URL}/workerOrders").mock( respx.post(f"{BASE_URL}/workerOrders").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.projects.get_worker_orders() result = await client.projects.get_worker_orders()
@ -149,9 +145,7 @@ class TestUserAPI:
"balance": 50000.0, "balance": 50000.0,
} }
respx.post(f"{BASE_URL}/user").mock( respx.post(f"{BASE_URL}/user").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.user.get_info() result = await client.user.get_info()
@ -165,14 +159,17 @@ class TestUserAPI:
mock_data = { mock_data = {
"reviews": [ "reviews": [
{"id": 1, "rating": 5, "comment": "Great work!", "author": {"id": 999, "username": "client1"}}, {
"id": 1,
"rating": 5,
"comment": "Great work!",
"author": {"id": 999, "username": "client1"},
},
], ],
"pagination": {"current_page": 1, "total_pages": 5}, "pagination": {"current_page": 1, "total_pages": 5},
} }
respx.post(f"{BASE_URL}/userReviews").mock( respx.post(f"{BASE_URL}/userReviews").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.user.get_reviews(user_id=12345, page=1) result = await client.user.get_reviews(user_id=12345, page=1)
@ -191,9 +188,7 @@ class TestUserAPI:
] ]
} }
respx.post(f"{BASE_URL}/favoriteKworks").mock( respx.post(f"{BASE_URL}/favoriteKworks").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.user.get_favorite_kworks() result = await client.user.get_favorite_kworks()
@ -217,9 +212,7 @@ class TestReferenceAPI:
] ]
} }
respx.post(f"{BASE_URL}/cities").mock( respx.post(f"{BASE_URL}/cities").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_cities() result = await client.reference.get_cities()
@ -239,9 +232,7 @@ class TestReferenceAPI:
] ]
} }
respx.post(f"{BASE_URL}/countries").mock( respx.post(f"{BASE_URL}/countries").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_countries() result = await client.reference.get_countries()
@ -261,9 +252,7 @@ class TestReferenceAPI:
] ]
} }
respx.post(f"{BASE_URL}/timezones").mock( respx.post(f"{BASE_URL}/timezones").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_timezones() result = await client.reference.get_timezones()
@ -300,13 +289,17 @@ class TestReferenceAPI:
mock_data = { mock_data = {
"features": [ "features": [
{"id": 10, "name": "Public Feature", "is_public": True, "price": 500, "type": "extra"}, {
"id": 10,
"name": "Public Feature",
"is_public": True,
"price": 500,
"type": "extra",
},
] ]
} }
respx.post(f"{BASE_URL}/getPublicFeatures").mock( respx.post(f"{BASE_URL}/getPublicFeatures").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_public_features() result = await client.reference.get_public_features()
@ -325,9 +318,7 @@ class TestReferenceAPI:
] ]
} }
respx.post(f"{BASE_URL}/getBadgesInfo").mock( respx.post(f"{BASE_URL}/getBadgesInfo").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.reference.get_badges_info() result = await client.reference.get_badges_info()
@ -346,15 +337,25 @@ class TestNotificationsAPI:
mock_data = { mock_data = {
"notifications": [ "notifications": [
{"id": 1, "type": "order", "title": "New Order", "message": "New order received", "is_read": False}, {
{"id": 2, "type": "message", "title": "New Message", "message": "You have a new message", "is_read": True}, "id": 1,
"type": "order",
"title": "New Order",
"message": "New order received",
"is_read": False,
},
{
"id": 2,
"type": "message",
"title": "New Message",
"message": "You have a new message",
"is_read": True,
},
], ],
"unread_count": 5, "unread_count": 5,
} }
respx.post(f"{BASE_URL}/notifications").mock( respx.post(f"{BASE_URL}/notifications").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_list() result = await client.notifications.get_list()
@ -368,7 +369,13 @@ class TestNotificationsAPI:
mock_data = { mock_data = {
"notifications": [ "notifications": [
{"id": 3, "type": "system", "title": "System Update", "message": "System update available", "is_read": False}, {
"id": 3,
"type": "system",
"title": "System Update",
"message": "System update available",
"is_read": False,
},
], ],
"unread_count": 1, "unread_count": 1,
} }
@ -394,9 +401,7 @@ class TestNotificationsAPI:
] ]
} }
respx.post(f"{BASE_URL}/dialogs").mock( respx.post(f"{BASE_URL}/dialogs").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_dialogs() result = await client.notifications.get_dialogs()
@ -415,9 +420,7 @@ class TestNotificationsAPI:
] ]
} }
respx.post(f"{BASE_URL}/blockedDialogList").mock( respx.post(f"{BASE_URL}/blockedDialogList").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.notifications.get_blocked_dialogs() result = await client.notifications.get_blocked_dialogs()
@ -438,9 +441,7 @@ class TestOtherAPI:
"count": 1, "count": 1,
} }
respx.post(f"{BASE_URL}/myWants").mock( respx.post(f"{BASE_URL}/myWants").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_wants() result = await client.other.get_wants()
@ -457,9 +458,7 @@ class TestOtherAPI:
"completed_wants": 10, "completed_wants": 10,
} }
respx.post(f"{BASE_URL}/wantsStatusList").mock( respx.post(f"{BASE_URL}/wantsStatusList").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_wants_status() result = await client.other.get_wants_status()
@ -475,9 +474,7 @@ class TestOtherAPI:
"total_sales": 50, "total_sales": 50,
} }
respx.post(f"{BASE_URL}/kworksStatusList").mock( respx.post(f"{BASE_URL}/kworksStatusList").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_kworks_status() result = await client.other.get_kworks_status()
@ -492,9 +489,7 @@ class TestOtherAPI:
"offers": [{"id": 1, "title": "Special offer"}], "offers": [{"id": 1, "title": "Special offer"}],
} }
respx.post(f"{BASE_URL}/offers").mock( respx.post(f"{BASE_URL}/offers").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_offers() result = await client.other.get_offers()
@ -510,9 +505,7 @@ class TestOtherAPI:
"eur_rate": 98.2, "eur_rate": 98.2,
} }
respx.post(f"{BASE_URL}/exchangeInfo").mock( respx.post(f"{BASE_URL}/exchangeInfo").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_exchange_info() result = await client.other.get_exchange_info()
@ -528,9 +521,7 @@ class TestOtherAPI:
"name": "Main Channel", "name": "Main Channel",
} }
respx.post(f"{BASE_URL}/getChannel").mock( respx.post(f"{BASE_URL}/getChannel").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_channel() result = await client.other.get_channel()
@ -647,9 +638,7 @@ class TestOtherAPI:
"updated": {"notifications_enabled": False}, "updated": {"notifications_enabled": False},
} }
respx.post(f"{BASE_URL}/updateSettings").mock( respx.post(f"{BASE_URL}/updateSettings").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
settings = {"notifications_enabled": False, "theme": "dark"} settings = {"notifications_enabled": False, "theme": "dark"}
result = await client.other.update_settings(settings) result = await client.other.update_settings(settings)
@ -666,9 +655,7 @@ class TestOtherAPI:
"status": "offline", "status": "offline",
} }
respx.post(f"{BASE_URL}/offline").mock( respx.post(f"{BASE_URL}/offline").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.go_offline() result = await client.other.go_offline()
@ -685,9 +672,7 @@ class TestOtherAPI:
"name": "Test Actor", "name": "Test Actor",
} }
respx.post(f"{BASE_URL}/actor").mock( respx.post(f"{BASE_URL}/actor").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client.other.get_actor() result = await client.other.get_actor()
@ -700,6 +685,7 @@ class TestClientInternals:
def test_handle_response_success(self): def test_handle_response_success(self):
"""Test _handle_response with successful response.""" """Test _handle_response with successful response."""
import httpx import httpx
client = KworkClient(token="test") client = KworkClient(token="test")
response = httpx.Response(200, json={"success": True, "data": "test"}) response = httpx.Response(200, json={"success": True, "data": "test"})
@ -711,6 +697,7 @@ class TestClientInternals:
def test_handle_response_error(self): def test_handle_response_error(self):
"""Test _handle_response with error response.""" """Test _handle_response with error response."""
import httpx import httpx
client = KworkClient(token="test") client = KworkClient(token="test")
response = httpx.Response(400, json={"message": "Bad request"}) response = httpx.Response(400, json={"message": "Bad request"})
@ -727,9 +714,7 @@ class TestClientInternals:
mock_data = {"result": "success"} mock_data = {"result": "success"}
respx.post(f"{BASE_URL}/test-endpoint").mock( respx.post(f"{BASE_URL}/test-endpoint").mock(return_value=Response(200, json=mock_data))
return_value=Response(200, json=mock_data)
)
result = await client._request("POST", "/test-endpoint", json={"param": "value"}) result = await client._request("POST", "/test-endpoint", json={"param": "value"})