diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..0f3aa8b --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,487 @@ +# API Reference + +Auto-generated API documentation. + +Last updated: kwork-api + +--- + +## KworkClient + +Kwork.ru API client. + +Usage: + # Login with credentials + client = await KworkClient.login("username", "password") + + # Or restore from token + client = KworkClient(token="your_web_auth_token") + + # Make requests + catalog = await client.catalog.get_list(page=1) + +### Methods + +#### `catalog()` + +Catalog API. + +#### `projects()` + +Projects API. + +#### `user()` + +User API. + +#### `reference()` + +Reference data API. + +#### `notifications()` + +Notifications API. + +#### `other()` + +Other endpoints. + + +--- + +## CatalogAPI + +Catalog/Kworks API endpoints. + +--- + +## ProjectsAPI + +Projects (freelance orders) API endpoints. + +--- + +## UserAPI + +User API endpoints. + +--- + +## ReferenceAPI + +Reference data (cities, countries, etc.) endpoints. + +--- + +## NotificationsAPI + +Notifications and messages endpoints. + +--- + +## OtherAPI + +Other API endpoints. + +--- + + +# Models + +Pydantic models used in API responses. + +## KworkUser + +User information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `username` | - | - | +| `avatar_url` | - | - | +| `is_online` | - | - | +| `rating` | - | - | + +--- + +## KworkCategory + +Category information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `name` | - | - | +| `slug` | - | - | +| `parent_id` | - | - | + +--- + +## Kwork + +Kwork (service) information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `title` | - | - | +| `description` | - | - | +| `price` | - | - | +| `currency` | - | - | +| `category_id` | - | - | +| `seller` | - | - | +| `images` | - | - | +| `rating` | - | - | +| `reviews_count` | - | - | +| `created_at` | - | - | +| `updated_at` | - | - | + +--- + +## KworkDetails + +Extended kwork details. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `full_description` | - | - | +| `requirements` | - | - | +| `delivery_time` | - | - | +| `revisions` | - | - | +| `features` | - | - | +| `faq` | - | - | + +--- + +## PaginationInfo + +Pagination metadata. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `current_page` | - | - | +| `total_pages` | - | - | +| `total_items` | - | - | +| `items_per_page` | - | - | +| `has_next` | - | - | +| `has_prev` | - | - | + +--- + +## CatalogResponse + +Catalog response with kworks and pagination. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `kworks` | - | - | +| `pagination` | - | - | +| `filters` | - | - | +| `sort_options` | - | - | + +--- + +## Project + +Project (freelance order) information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `title` | - | - | +| `description` | - | - | +| `budget` | - | - | +| `budget_type` | - | - | +| `category_id` | - | - | +| `customer` | - | - | +| `status` | - | - | +| `created_at` | - | - | +| `updated_at` | - | - | +| `bids_count` | - | - | +| `skills` | - | - | + +--- + +## ProjectsResponse + +Projects list response. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `projects` | - | - | +| `pagination` | - | - | + +--- + +## Review + +Review information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `rating` | - | - | +| `comment` | - | - | +| `author` | - | - | +| `kwork_id` | - | - | +| `created_at` | - | - | + +--- + +## ReviewsResponse + +Reviews list response. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `reviews` | - | - | +| `pagination` | - | - | +| `average_rating` | - | - | + +--- + +## Notification + +Notification information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `type` | - | - | +| `title` | - | - | +| `message` | - | - | +| `is_read` | - | - | +| `created_at` | - | - | +| `link` | - | - | + +--- + +## NotificationsResponse + +Notifications list response. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `notifications` | - | - | +| `unread_count` | - | - | + +--- + +## Dialog + +Dialog (chat) information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `participant` | - | - | +| `last_message` | - | - | +| `unread_count` | - | - | +| `updated_at` | - | - | + +--- + +## AuthResponse + +Authentication response. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `success` | - | - | +| `user_id` | - | - | +| `username` | - | - | +| `web_auth_token` | - | - | +| `message` | - | - | + +--- + +## ErrorDetail + +Error detail from API. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `code` | - | - | +| `message` | - | - | +| `field` | - | - | + +--- + +## APIErrorResponse + +Standard API error response. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `success` | - | - | +| `errors` | - | - | +| `message` | - | - | + +--- + +## City + +City information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `name` | - | - | +| `country_id` | - | - | + +--- + +## Country + +Country information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `name` | - | - | +| `code` | - | - | +| `cities` | - | - | + +--- + +## TimeZone + +Timezone information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `name` | - | - | +| `offset` | - | - | + +--- + +## Feature + +Feature/addon information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `name` | - | - | +| `description` | - | - | +| `price` | - | - | +| `type` | - | - | + +--- + +## Badge + +User badge information. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | - | - | +| `name` | - | - | +| `description` | - | - | +| `icon_url` | - | - | + +--- + +## DataResponse + +Generic data response wrapper. + +### Fields + +| Field | Type | Description | +|-------|------|-------------| +| `success` | - | - | +| `data` | - | - | +| `message` | - | - | + +--- + + +# Errors + +Exception classes for error handling. + +## KworkError + +Base exception for all Kwork API errors. + +--- + +## KworkAuthError + +Authentication/authorization error. + +--- + +## KworkApiError + +API request error (4xx, 5xx). + +--- + +## KworkNotFoundError + +Resource not found (404). + +--- + +## KworkRateLimitError + +Rate limit exceeded (429). + +--- + +## KworkValidationError + +Validation error (400). + +--- + +## KworkNetworkError + +Network/connection error. + +--- diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..3d0b311 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,212 @@ +# Usage Examples + +## Catalog + +### Get Catalog List + +```python +from kwork_api import KworkClient + +async with KworkClient(token="token") as client: + catalog = await client.catalog.get_list(page=1, category_id=5) + + for kwork in catalog.kworks: + print(f"{kwork.title}: {kwork.price} RUB") + + # Pagination + if catalog.pagination: + print(f"Page {catalog.pagination.current_page} of {catalog.pagination.total_pages}") +``` + +### Get Kwork Details + +```python +details = await client.catalog.get_details(kwork_id=123) + +print(f"Title: {details.title}") +print(f"Price: {details.price}") +print(f"Description: {details.full_description}") +print(f"Delivery: {details.delivery_time} days") +``` + +## Projects + +### Get Projects List + +```python +projects = await client.projects.get_list(page=1) + +for project in projects.projects: + print(f"{project.title} - {project.budget} RUB") +``` + +### Get Customer Orders + +```python +orders = await client.projects.get_payer_orders() + +for order in orders: + print(f"Order #{order.id}: {order.status}") +``` + +### Get Performer Orders + +```python +orders = await client.projects.get_worker_orders() + +for order in orders: + print(f"Work #{order.id}: {order.status}") +``` + +## User + +### Get User Info + +```python +user_info = await client.user.get_info() +print(f"Username: {user_info.get('username')}") +``` + +### Get Reviews + +```python +reviews = await client.user.get_reviews(page=1) + +for review in reviews.reviews: + print(f"Rating: {review.rating}/5 - {review.comment}") +``` + +### Get Favorite Kworks + +```python +favorites = await client.user.get_favorite_kworks() + +for kwork in favorites: + print(f"Favorite: {kwork.title}") +``` + +## Reference Data + +### Get Cities + +```python +cities = await client.reference.get_cities() + +for city in cities: + print(f"{city.id}: {city.name}") +``` + +### Get Countries + +```python +countries = await client.reference.get_countries() + +for country in countries: + print(f"{country.id}: {country.name}") +``` + +### Get Timezones + +```python +timezones = await client.reference.get_timezones() + +for tz in timezones: + print(f"{tz.id}: {tz.name} ({tz.offset})") +``` + +## Notifications + +### Get Notifications + +```python +notifications = await client.notifications.get_list() + +for notif in notifications.notifications: + print(f"{notif.title}: {notif.message}") + +print(f"Unread: {notifications.unread_count}") +``` + +### Fetch New Notifications + +```python +new_notifications = await client.notifications.fetch() +print(f"New: {len(new_notifications.notifications)}") +``` + +### Get Dialogs + +```python +dialogs = await client.notifications.get_dialogs() + +for dialog in dialogs: + print(f"Dialog with {dialog.participant.username}: {dialog.last_message}") +``` + +## Error Handling + +```python +from kwork_api import KworkAuthError, KworkApiError, KworkNotFoundError + +try: + catalog = await client.catalog.get_list() +except KworkAuthError as e: + print(f"Authentication failed: {e}") +except KworkNotFoundError as e: + print(f"Resource not found: {e}") +except KworkApiError as e: + print(f"API error [{e.status_code}]: {e.message}") +except Exception as e: + print(f"Unexpected error: {e}") +``` + +## Rate Limiting + +```python +import asyncio + +async def fetch_all_pages(): + all_kworks = [] + + for page in range(1, 10): + try: + catalog = await client.catalog.get_list(page=page) + all_kworks.extend(catalog.kworks) + + if not catalog.pagination or not catalog.pagination.has_next: + break + + # Delay to avoid rate limiting + await asyncio.sleep(1) + + except KworkRateLimitError: + print("Rate limited, waiting...") + await asyncio.sleep(5) + + return all_kworks +``` + +## Pagination Helper + +```python +async def fetch_all_catalog(): + """Fetch all kworks from catalog with pagination.""" + all_kworks = [] + page = 1 + + while True: + catalog = await client.catalog.get_list(page=page) + all_kworks.extend(catalog.kworks) + + if not catalog.pagination or not catalog.pagination.has_next: + break + + page += 1 + await asyncio.sleep(0.5) # Rate limiting + + return all_kworks +``` + +--- + +*More examples in the API Reference.* diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9a3a421 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,81 @@ +# Kwork API Documentation + +Unofficial Python client for Kwork.ru API. + +## Quick Start + +### Installation + +```bash +pip install kwork-api +``` + +### Authentication + +```python +from kwork_api import KworkClient + +# Login with credentials +client = await KworkClient.login("username", "password") + +# Or restore from token +client = KworkClient(token="your_web_auth_token") +``` + +### Basic Usage + +```python +async with KworkClient(token="token") as client: + # Get catalog + catalog = await client.catalog.get_list(page=1) + + # Get kwork details + details = await client.catalog.get_details(kwork_id=123) + + # Get projects + projects = await client.projects.get_list() +``` + +## Documentation Sections + +- **[API Reference](api-reference.md)** — All endpoints and methods +- **[Models](api-reference.md#models)** — Pydantic models +- **[Errors](api-reference.md#errors)** — Exception classes +- **[Examples](examples.md)** — Usage examples + +## Features + +- ✅ Full API coverage (45 endpoints) +- ✅ Async/await support +- ✅ Pydantic models for type safety +- ✅ Clear error handling +- ✅ Session management + +## Rate Limiting + +Rate limiting is **not** implemented in the library. Handle it in your code: + +```python +import asyncio + +for page in range(1, 10): + catalog = await client.catalog.get_list(page=page) + await asyncio.sleep(1) # 1 second delay +``` + +## Error Handling + +```python +from kwork_api import KworkAuthError, KworkApiError + +try: + catalog = await client.catalog.get_list() +except KworkAuthError as e: + print(f"Auth failed: {e}") +except KworkApiError as e: + print(f"API error [{e.status_code}]: {e.message}") +``` + +--- + +*Documentation auto-generated from source code.* diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py new file mode 100644 index 0000000..fd47c06 --- /dev/null +++ b/scripts/generate_docs.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Generate API documentation from docstrings. + +Usage: + python scripts/generate_docs.py + +Generates docs/api-reference.md from source code docstrings. +""" + +import ast +import inspect +from pathlib import Path +from typing import List, Tuple + + +def extract_class_info(class_node: ast.ClassDef, source_lines: List[str]) -> dict: + """Extract class information including docstring and methods.""" + info = { + "name": class_node.name, + "docstring": ast.get_docstring(class_node) or "", + "methods": [], + } + + for node in class_node.body: + if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"): + method_info = { + "name": node.name, + "docstring": ast.get_docstring(node) or "", + "args": [], + "returns": "", + } + + # Extract arguments + for arg in node.args.args: + if arg.arg not in ("self", "cls"): + method_info["args"].append(arg.arg) + + info["methods"].append(method_info) + + return info + + +def parse_module(file_path: Path) -> List[dict]: + """Parse Python module and extract classes with methods.""" + source = file_path.read_text() + source_lines = source.splitlines() + tree = ast.parse(source) + + classes = [] + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + classes.append(extract_class_info(node, source_lines)) + + return classes + + +def generate_api_reference(src_dir: Path, output_path: Path): + """Generate API reference markdown from source code.""" + + # Parse client.py + client_path = src_dir / "client.py" + classes = parse_module(client_path) + + # Generate markdown + md_lines = [ + "# API Reference", + "", + "Auto-generated API documentation.", + "", + "Last updated: " + Path().cwd().name, # Will be updated with date + "", + "---", + "", + ] + + for cls in classes: + if not cls["docstring"]: + continue + + md_lines.append(f"## {cls['name']}") + md_lines.append("") + md_lines.append(cls["docstring"]) + md_lines.append("") + + if cls["methods"]: + md_lines.append("### Methods") + md_lines.append("") + + for method in cls["methods"]: + args_str = ", ".join(method["args"]) + md_lines.append(f"#### `{method['name']}({args_str})`") + md_lines.append("") + + if method["docstring"]: + md_lines.append(method["docstring"]) + md_lines.append("") + + md_lines.append("") + + md_lines.append("---") + md_lines.append("") + + # Parse models.py + models_path = src_dir / "models.py" + if models_path.exists(): + model_classes = parse_module(models_path) + + md_lines.append("") + md_lines.append("# Models") + md_lines.append("") + md_lines.append("Pydantic models used in API responses.") + md_lines.append("") + + for cls in model_classes: + if cls["name"].startswith("_"): + continue + + md_lines.append(f"## {cls['name']}") + md_lines.append("") + + if cls["docstring"]: + md_lines.append(cls["docstring"]) + md_lines.append("") + + # Show fields + md_lines.append("### Fields") + md_lines.append("") + md_lines.append("| Field | Type | Description |") + md_lines.append("|-------|------|-------------|") + + # Extract fields from class body + for node in ast.walk(ast.parse(models_path.read_text())): + if isinstance(node, ast.ClassDef) and node.name == cls["name"]: + for item in node.body: + if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name): + field_name = item.target.id + if not field_name.startswith("_"): + md_lines.append(f"| `{field_name}` | - | - |") + + md_lines.append("") + md_lines.append("---") + md_lines.append("") + + # Parse errors.py + errors_path = src_dir / "errors.py" + if errors_path.exists(): + error_classes = parse_module(errors_path) + + md_lines.append("") + md_lines.append("# Errors") + md_lines.append("") + md_lines.append("Exception classes for error handling.") + md_lines.append("") + + for cls in error_classes: + if cls["name"].startswith("_"): + continue + + md_lines.append(f"## {cls['name']}") + md_lines.append("") + + if cls["docstring"]: + md_lines.append(cls["docstring"]) + md_lines.append("") + + md_lines.append("---") + md_lines.append("") + + # Write output + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text("\n".join(md_lines)) + print(f"✅ Generated {output_path}") + + +def main(): + """Main entry point.""" + import sys + + # Paths + root_dir = Path(__file__).parent.parent + src_dir = root_dir / "src" / "kwork_api" + output_path = root_dir / "docs" / "api-reference.md" + + if not src_dir.exists(): + print(f"❌ Source directory not found: {src_dir}") + sys.exit(1) + + generate_api_reference(src_dir, output_path) + + +if __name__ == "__main__": + main()