Implement Assets API with file/asset management operations
Implementation: - Asset data models (wikijs/models/asset.py) - Asset, AssetFolder models - AssetUpload, AssetRename, AssetMove models - FolderCreate model - File size helpers (size_mb, size_kb) - Field validation and normalization - Sync AssetsEndpoint (wikijs/endpoints/assets.py) - list(folder_id, kind) - List assets with filtering - get(asset_id) - Get single asset - rename(asset_id, new_filename) - Rename asset - move(asset_id, folder_id) - Move asset between folders - delete(asset_id) - Delete asset - list_folders() - List all folders - create_folder(slug, name) - Create new folder - delete_folder(folder_id) - Delete folder - Note: upload/download require multipart support (future enhancement) - Async AsyncAssetsEndpoint (wikijs/aio/endpoints/assets.py) - Complete async implementation - Identical interface to sync version - All asset and folder management operations - Integration with clients - WikiJSClient.assets - AsyncWikiJSClient.assets GraphQL operations for asset and folder management. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@ from ..utils import (
|
|||||||
parse_wiki_response,
|
parse_wiki_response,
|
||||||
)
|
)
|
||||||
from ..version import __version__
|
from ..version import __version__
|
||||||
from .endpoints import AsyncGroupsEndpoint, AsyncPagesEndpoint, AsyncUsersEndpoint
|
from .endpoints import AsyncAssetsEndpoint, AsyncGroupsEndpoint, AsyncPagesEndpoint, AsyncUsersEndpoint
|
||||||
|
|
||||||
|
|
||||||
class AsyncWikiJSClient:
|
class AsyncWikiJSClient:
|
||||||
@@ -105,8 +105,7 @@ class AsyncWikiJSClient:
|
|||||||
self.pages = AsyncPagesEndpoint(self)
|
self.pages = AsyncPagesEndpoint(self)
|
||||||
self.users = AsyncUsersEndpoint(self)
|
self.users = AsyncUsersEndpoint(self)
|
||||||
self.groups = AsyncGroupsEndpoint(self)
|
self.groups = AsyncGroupsEndpoint(self)
|
||||||
# Future endpoints:
|
self.assets = AsyncAssetsEndpoint(self)
|
||||||
# self.assets = AsyncAssetsEndpoint(self)
|
|
||||||
|
|
||||||
def _get_session(self) -> aiohttp.ClientSession:
|
def _get_session(self) -> aiohttp.ClientSession:
|
||||||
"""Get or create aiohttp session.
|
"""Get or create aiohttp session.
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"""Async endpoint handlers for Wiki.js API."""
|
"""Async endpoint handlers for Wiki.js API."""
|
||||||
|
|
||||||
|
from .assets import AsyncAssetsEndpoint
|
||||||
from .base import AsyncBaseEndpoint
|
from .base import AsyncBaseEndpoint
|
||||||
from .groups import AsyncGroupsEndpoint
|
from .groups import AsyncGroupsEndpoint
|
||||||
from .pages import AsyncPagesEndpoint
|
from .pages import AsyncPagesEndpoint
|
||||||
from .users import AsyncUsersEndpoint
|
from .users import AsyncUsersEndpoint
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"AsyncAssetsEndpoint",
|
||||||
"AsyncBaseEndpoint",
|
"AsyncBaseEndpoint",
|
||||||
"AsyncGroupsEndpoint",
|
"AsyncGroupsEndpoint",
|
||||||
"AsyncPagesEndpoint",
|
"AsyncPagesEndpoint",
|
||||||
|
|||||||
314
wikijs/aio/endpoints/assets.py
Normal file
314
wikijs/aio/endpoints/assets.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
"""Async assets endpoint for Wiki.js API."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from ...exceptions import APIError, ValidationError
|
||||||
|
from ...models import Asset, AssetFolder
|
||||||
|
from .base import AsyncBaseEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncAssetsEndpoint(AsyncBaseEndpoint):
|
||||||
|
"""Async endpoint for managing Wiki.js assets."""
|
||||||
|
|
||||||
|
async def list(
|
||||||
|
self, folder_id: Optional[int] = None, kind: Optional[str] = None
|
||||||
|
) -> List[Asset]:
|
||||||
|
"""List all assets asynchronously."""
|
||||||
|
if folder_id is not None and folder_id < 0:
|
||||||
|
raise ValidationError("folder_id must be non-negative")
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query ($folderId: Int, $kind: AssetKind) {
|
||||||
|
assets {
|
||||||
|
list(folderId: $folderId, kind: $kind) {
|
||||||
|
id filename ext kind mime fileSize folderId
|
||||||
|
folder { id slug name }
|
||||||
|
authorId authorName createdAt updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
variables = {}
|
||||||
|
if folder_id is not None:
|
||||||
|
variables["folderId"] = folder_id
|
||||||
|
if kind is not None:
|
||||||
|
variables["kind"] = kind.upper()
|
||||||
|
|
||||||
|
response = await self._post(
|
||||||
|
"/graphql", json_data={"query": query, "variables": variables}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
assets_data = response.get("data", {}).get("assets", {}).get("list", [])
|
||||||
|
return [Asset(**self._normalize_asset_data(a)) for a in assets_data]
|
||||||
|
|
||||||
|
async def get(self, asset_id: int) -> Asset:
|
||||||
|
"""Get a specific asset by ID asynchronously."""
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query ($id: Int!) {
|
||||||
|
assets {
|
||||||
|
single(id: $id) {
|
||||||
|
id filename ext kind mime fileSize folderId
|
||||||
|
folder { id slug name }
|
||||||
|
authorId authorName createdAt updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = await self._post(
|
||||||
|
"/graphql", json_data={"query": query, "variables": {"id": asset_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
asset_data = response.get("data", {}).get("assets", {}).get("single")
|
||||||
|
|
||||||
|
if not asset_data:
|
||||||
|
raise APIError(f"Asset with ID {asset_id} not found")
|
||||||
|
|
||||||
|
return Asset(**self._normalize_asset_data(asset_data))
|
||||||
|
|
||||||
|
async def rename(self, asset_id: int, new_filename: str) -> Asset:
|
||||||
|
"""Rename an asset asynchronously."""
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
if not new_filename or not new_filename.strip():
|
||||||
|
raise ValidationError("new_filename cannot be empty")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!, $filename: String!) {
|
||||||
|
assets {
|
||||||
|
renameAsset(id: $id, filename: $filename) {
|
||||||
|
responseResult { succeeded errorCode slug message }
|
||||||
|
asset {
|
||||||
|
id filename ext kind mime fileSize folderId
|
||||||
|
authorId authorName createdAt updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = await self._post(
|
||||||
|
"/graphql",
|
||||||
|
json_data={
|
||||||
|
"query": mutation,
|
||||||
|
"variables": {"id": asset_id, "filename": new_filename.strip()},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("renameAsset", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to rename asset: {error_msg}")
|
||||||
|
|
||||||
|
asset_data = result.get("asset")
|
||||||
|
if not asset_data:
|
||||||
|
raise APIError("Asset renamed but no data returned")
|
||||||
|
|
||||||
|
return Asset(**self._normalize_asset_data(asset_data))
|
||||||
|
|
||||||
|
async def move(self, asset_id: int, folder_id: int) -> Asset:
|
||||||
|
"""Move an asset to a different folder asynchronously."""
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
if not isinstance(folder_id, int) or folder_id < 0:
|
||||||
|
raise ValidationError("folder_id must be non-negative")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!, $folderId: Int!) {
|
||||||
|
assets {
|
||||||
|
moveAsset(id: $id, folderId: $folderId) {
|
||||||
|
responseResult { succeeded errorCode slug message }
|
||||||
|
asset {
|
||||||
|
id filename ext kind mime fileSize folderId
|
||||||
|
folder { id slug name }
|
||||||
|
authorId authorName createdAt updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = await self._post(
|
||||||
|
"/graphql",
|
||||||
|
json_data={
|
||||||
|
"query": mutation,
|
||||||
|
"variables": {"id": asset_id, "folderId": folder_id},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("moveAsset", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to move asset: {error_msg}")
|
||||||
|
|
||||||
|
asset_data = result.get("asset")
|
||||||
|
if not asset_data:
|
||||||
|
raise APIError("Asset moved but no data returned")
|
||||||
|
|
||||||
|
return Asset(**self._normalize_asset_data(asset_data))
|
||||||
|
|
||||||
|
async def delete(self, asset_id: int) -> bool:
|
||||||
|
"""Delete an asset asynchronously."""
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!) {
|
||||||
|
assets {
|
||||||
|
deleteAsset(id: $id) {
|
||||||
|
responseResult { succeeded errorCode slug message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = await self._post(
|
||||||
|
"/graphql", json_data={"query": mutation, "variables": {"id": asset_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("deleteAsset", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to delete asset: {error_msg}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def list_folders(self) -> List[AssetFolder]:
|
||||||
|
"""List all asset folders asynchronously."""
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
assets {
|
||||||
|
folders {
|
||||||
|
id slug name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = await self._post("/graphql", json_data={"query": query})
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
folders_data = response.get("data", {}).get("assets", {}).get("folders", [])
|
||||||
|
return [AssetFolder(**folder) for folder in folders_data]
|
||||||
|
|
||||||
|
async def create_folder(self, slug: str, name: Optional[str] = None) -> AssetFolder:
|
||||||
|
"""Create a new asset folder asynchronously."""
|
||||||
|
if not slug or not slug.strip():
|
||||||
|
raise ValidationError("slug cannot be empty")
|
||||||
|
|
||||||
|
slug = slug.strip().strip("/")
|
||||||
|
if not slug:
|
||||||
|
raise ValidationError("slug cannot be just slashes")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($slug: String!, $name: String) {
|
||||||
|
assets {
|
||||||
|
createFolder(slug: $slug, name: $name) {
|
||||||
|
responseResult { succeeded errorCode slug message }
|
||||||
|
folder { id slug name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
variables = {"slug": slug}
|
||||||
|
if name:
|
||||||
|
variables["name"] = name
|
||||||
|
|
||||||
|
response = await self._post(
|
||||||
|
"/graphql", json_data={"query": mutation, "variables": variables}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("createFolder", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to create folder: {error_msg}")
|
||||||
|
|
||||||
|
folder_data = result.get("folder")
|
||||||
|
if not folder_data:
|
||||||
|
raise APIError("Folder created but no data returned")
|
||||||
|
|
||||||
|
return AssetFolder(**folder_data)
|
||||||
|
|
||||||
|
async def delete_folder(self, folder_id: int) -> bool:
|
||||||
|
"""Delete an asset folder asynchronously."""
|
||||||
|
if not isinstance(folder_id, int) or folder_id <= 0:
|
||||||
|
raise ValidationError("folder_id must be a positive integer")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!) {
|
||||||
|
assets {
|
||||||
|
deleteFolder(id: $id) {
|
||||||
|
responseResult { succeeded errorCode slug message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = await self._post(
|
||||||
|
"/graphql", json_data={"query": mutation, "variables": {"id": folder_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("deleteFolder", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to delete folder: {error_msg}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _normalize_asset_data(self, data: Dict) -> Dict:
|
||||||
|
"""Normalize asset data from API response."""
|
||||||
|
return {
|
||||||
|
"id": data.get("id"),
|
||||||
|
"filename": data.get("filename"),
|
||||||
|
"ext": data.get("ext"),
|
||||||
|
"kind": data.get("kind"),
|
||||||
|
"mime": data.get("mime"),
|
||||||
|
"file_size": data.get("fileSize"),
|
||||||
|
"folder_id": data.get("folderId"),
|
||||||
|
"folder": data.get("folder"),
|
||||||
|
"author_id": data.get("authorId"),
|
||||||
|
"author_name": data.get("authorName"),
|
||||||
|
"created_at": data.get("createdAt"),
|
||||||
|
"updated_at": data.get("updatedAt"),
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ from requests.adapters import HTTPAdapter
|
|||||||
from urllib3.util.retry import Retry
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
from .auth import APIKeyAuth, AuthHandler
|
from .auth import APIKeyAuth, AuthHandler
|
||||||
from .endpoints import GroupsEndpoint, PagesEndpoint, UsersEndpoint
|
from .endpoints import AssetsEndpoint, GroupsEndpoint, PagesEndpoint, UsersEndpoint
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
APIError,
|
APIError,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
@@ -92,8 +92,7 @@ class WikiJSClient:
|
|||||||
self.pages = PagesEndpoint(self)
|
self.pages = PagesEndpoint(self)
|
||||||
self.users = UsersEndpoint(self)
|
self.users = UsersEndpoint(self)
|
||||||
self.groups = GroupsEndpoint(self)
|
self.groups = GroupsEndpoint(self)
|
||||||
# Future endpoints:
|
self.assets = AssetsEndpoint(self)
|
||||||
# self.assets = AssetsEndpoint(self)
|
|
||||||
|
|
||||||
def _create_session(self) -> requests.Session:
|
def _create_session(self) -> requests.Session:
|
||||||
"""Create configured HTTP session with retry strategy.
|
"""Create configured HTTP session with retry strategy.
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ Implemented:
|
|||||||
- Pages API (CRUD operations) ✅
|
- Pages API (CRUD operations) ✅
|
||||||
- Users API (user management) ✅
|
- Users API (user management) ✅
|
||||||
- Groups API (group management) ✅
|
- Groups API (group management) ✅
|
||||||
|
- Assets API (file/asset management) ✅
|
||||||
|
|
||||||
Future implementations:
|
Future implementations:
|
||||||
- Assets API (file management)
|
|
||||||
- System API (system information)
|
- System API (system information)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .assets import AssetsEndpoint
|
||||||
from .base import BaseEndpoint
|
from .base import BaseEndpoint
|
||||||
from .groups import GroupsEndpoint
|
from .groups import GroupsEndpoint
|
||||||
from .pages import PagesEndpoint
|
from .pages import PagesEndpoint
|
||||||
from .users import UsersEndpoint
|
from .users import UsersEndpoint
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"AssetsEndpoint",
|
||||||
"BaseEndpoint",
|
"BaseEndpoint",
|
||||||
"GroupsEndpoint",
|
"GroupsEndpoint",
|
||||||
"PagesEndpoint",
|
"PagesEndpoint",
|
||||||
|
|||||||
667
wikijs/endpoints/assets.py
Normal file
667
wikijs/endpoints/assets.py
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
"""Assets endpoint for Wiki.js API."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import BinaryIO, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from ..exceptions import APIError, ValidationError
|
||||||
|
from ..models import Asset, AssetFolder, AssetMove, AssetRename, FolderCreate
|
||||||
|
from .base import BaseEndpoint
|
||||||
|
|
||||||
|
|
||||||
|
class AssetsEndpoint(BaseEndpoint):
|
||||||
|
"""Endpoint for managing Wiki.js assets.
|
||||||
|
|
||||||
|
Provides methods to:
|
||||||
|
- List assets
|
||||||
|
- Get asset details
|
||||||
|
- Upload files
|
||||||
|
- Download files
|
||||||
|
- Rename assets
|
||||||
|
- Move assets between folders
|
||||||
|
- Delete assets
|
||||||
|
- Manage folders
|
||||||
|
"""
|
||||||
|
|
||||||
|
def list(
|
||||||
|
self, folder_id: Optional[int] = None, kind: Optional[str] = None
|
||||||
|
) -> List[Asset]:
|
||||||
|
"""List all assets, optionally filtered by folder or kind.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_id: Filter by folder ID (None for all folders)
|
||||||
|
kind: Filter by asset kind (image, binary, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Asset objects
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If parameters are invalid
|
||||||
|
APIError: If the API request fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> assets = client.assets.list()
|
||||||
|
>>> images = client.assets.list(kind="image")
|
||||||
|
>>> folder_assets = client.assets.list(folder_id=1)
|
||||||
|
"""
|
||||||
|
# Validate folder_id
|
||||||
|
if folder_id is not None and folder_id < 0:
|
||||||
|
raise ValidationError("folder_id must be non-negative")
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query ($folderId: Int, $kind: AssetKind) {
|
||||||
|
assets {
|
||||||
|
list(folderId: $folderId, kind: $kind) {
|
||||||
|
id
|
||||||
|
filename
|
||||||
|
ext
|
||||||
|
kind
|
||||||
|
mime
|
||||||
|
fileSize
|
||||||
|
folderId
|
||||||
|
folder {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
authorId
|
||||||
|
authorName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
variables = {}
|
||||||
|
if folder_id is not None:
|
||||||
|
variables["folderId"] = folder_id
|
||||||
|
if kind is not None:
|
||||||
|
variables["kind"] = kind.upper()
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/graphql", json_data={"query": query, "variables": variables}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Extract and normalize assets
|
||||||
|
assets_data = response.get("data", {}).get("assets", {}).get("list", [])
|
||||||
|
return [Asset(**self._normalize_asset_data(a)) for a in assets_data]
|
||||||
|
|
||||||
|
def get(self, asset_id: int) -> Asset:
|
||||||
|
"""Get a specific asset by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: The asset ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Asset object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If asset_id is invalid
|
||||||
|
APIError: If the asset is not found or API request fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> asset = client.assets.get(123)
|
||||||
|
>>> print(f"{asset.filename}: {asset.size_mb:.2f} MB")
|
||||||
|
"""
|
||||||
|
# Validate asset_id
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
query = """
|
||||||
|
query ($id: Int!) {
|
||||||
|
assets {
|
||||||
|
single(id: $id) {
|
||||||
|
id
|
||||||
|
filename
|
||||||
|
ext
|
||||||
|
kind
|
||||||
|
mime
|
||||||
|
fileSize
|
||||||
|
folderId
|
||||||
|
folder {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
authorId
|
||||||
|
authorName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/graphql", json_data={"query": query, "variables": {"id": asset_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Extract asset data
|
||||||
|
asset_data = response.get("data", {}).get("assets", {}).get("single")
|
||||||
|
|
||||||
|
if not asset_data:
|
||||||
|
raise APIError(f"Asset with ID {asset_id} not found")
|
||||||
|
|
||||||
|
return Asset(**self._normalize_asset_data(asset_data))
|
||||||
|
|
||||||
|
def upload(
|
||||||
|
self,
|
||||||
|
file_path: str,
|
||||||
|
folder_id: int = 0,
|
||||||
|
filename: Optional[str] = None,
|
||||||
|
) -> Asset:
|
||||||
|
"""Upload a file as an asset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to local file to upload
|
||||||
|
folder_id: Target folder ID (default: 0 for root)
|
||||||
|
filename: Optional custom filename (uses original if not provided)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created Asset object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If file_path is invalid
|
||||||
|
APIError: If the upload fails
|
||||||
|
FileNotFoundError: If file doesn't exist
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> asset = client.assets.upload("/path/to/image.png", folder_id=1)
|
||||||
|
>>> print(f"Uploaded: {asset.filename}")
|
||||||
|
"""
|
||||||
|
# Validate file path
|
||||||
|
if not file_path or not file_path.strip():
|
||||||
|
raise ValidationError("file_path cannot be empty")
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise FileNotFoundError(f"File not found: {file_path}")
|
||||||
|
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
raise ValidationError(f"Path is not a file: {file_path}")
|
||||||
|
|
||||||
|
# Validate folder_id
|
||||||
|
if folder_id < 0:
|
||||||
|
raise ValidationError("folder_id must be non-negative")
|
||||||
|
|
||||||
|
# Get filename
|
||||||
|
if filename is None:
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
|
||||||
|
# For now, use GraphQL mutation
|
||||||
|
# Note: Wiki.js may require multipart form upload which would need special handling
|
||||||
|
mutation = """
|
||||||
|
mutation ($folderId: Int!, $file: Upload!) {
|
||||||
|
assets {
|
||||||
|
createFile(folderId: $folderId, file: $file) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
asset {
|
||||||
|
id
|
||||||
|
filename
|
||||||
|
ext
|
||||||
|
kind
|
||||||
|
mime
|
||||||
|
fileSize
|
||||||
|
folderId
|
||||||
|
authorId
|
||||||
|
authorName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Note: Actual file upload would require multipart/form-data
|
||||||
|
# This is a simplified version
|
||||||
|
raise NotImplementedError(
|
||||||
|
"File upload requires multipart form support. "
|
||||||
|
"Use the Wiki.js web interface or REST API directly for file uploads."
|
||||||
|
)
|
||||||
|
|
||||||
|
def download(self, asset_id: int, output_path: str) -> bool:
|
||||||
|
"""Download an asset to a local file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: The asset ID
|
||||||
|
output_path: Local path to save the file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if download successful
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If parameters are invalid
|
||||||
|
APIError: If the download fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> client.assets.download(123, "/path/to/save/file.png")
|
||||||
|
"""
|
||||||
|
# Validate asset_id
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
if not output_path or not output_path.strip():
|
||||||
|
raise ValidationError("output_path cannot be empty")
|
||||||
|
|
||||||
|
# Note: Downloading requires REST API endpoint, not GraphQL
|
||||||
|
raise NotImplementedError(
|
||||||
|
"File download requires REST API support. "
|
||||||
|
"Use the Wiki.js REST API directly: GET /a/{assetId}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def rename(self, asset_id: int, new_filename: str) -> Asset:
|
||||||
|
"""Rename an asset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: The asset ID
|
||||||
|
new_filename: New filename
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated Asset object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If parameters are invalid
|
||||||
|
APIError: If the rename fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> asset = client.assets.rename(123, "new-name.png")
|
||||||
|
"""
|
||||||
|
# Validate
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
if not new_filename or not new_filename.strip():
|
||||||
|
raise ValidationError("new_filename cannot be empty")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!, $filename: String!) {
|
||||||
|
assets {
|
||||||
|
renameAsset(id: $id, filename: $filename) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
asset {
|
||||||
|
id
|
||||||
|
filename
|
||||||
|
ext
|
||||||
|
kind
|
||||||
|
mime
|
||||||
|
fileSize
|
||||||
|
folderId
|
||||||
|
authorId
|
||||||
|
authorName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/graphql",
|
||||||
|
json_data={
|
||||||
|
"query": mutation,
|
||||||
|
"variables": {"id": asset_id, "filename": new_filename.strip()},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Check response result
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("renameAsset", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to rename asset: {error_msg}")
|
||||||
|
|
||||||
|
# Extract and return updated asset
|
||||||
|
asset_data = result.get("asset")
|
||||||
|
if not asset_data:
|
||||||
|
raise APIError("Asset renamed but no data returned")
|
||||||
|
|
||||||
|
return Asset(**self._normalize_asset_data(asset_data))
|
||||||
|
|
||||||
|
def move(self, asset_id: int, folder_id: int) -> Asset:
|
||||||
|
"""Move an asset to a different folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: The asset ID
|
||||||
|
folder_id: Target folder ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated Asset object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If parameters are invalid
|
||||||
|
APIError: If the move fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> asset = client.assets.move(123, folder_id=2)
|
||||||
|
"""
|
||||||
|
# Validate
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
if not isinstance(folder_id, int) or folder_id < 0:
|
||||||
|
raise ValidationError("folder_id must be non-negative")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!, $folderId: Int!) {
|
||||||
|
assets {
|
||||||
|
moveAsset(id: $id, folderId: $folderId) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
asset {
|
||||||
|
id
|
||||||
|
filename
|
||||||
|
ext
|
||||||
|
kind
|
||||||
|
mime
|
||||||
|
fileSize
|
||||||
|
folderId
|
||||||
|
folder {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
authorId
|
||||||
|
authorName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/graphql",
|
||||||
|
json_data={
|
||||||
|
"query": mutation,
|
||||||
|
"variables": {"id": asset_id, "folderId": folder_id},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Check response result
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("moveAsset", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to move asset: {error_msg}")
|
||||||
|
|
||||||
|
# Extract and return updated asset
|
||||||
|
asset_data = result.get("asset")
|
||||||
|
if not asset_data:
|
||||||
|
raise APIError("Asset moved but no data returned")
|
||||||
|
|
||||||
|
return Asset(**self._normalize_asset_data(asset_data))
|
||||||
|
|
||||||
|
def delete(self, asset_id: int) -> bool:
|
||||||
|
"""Delete an asset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: The asset ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deletion was successful
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If asset_id is invalid
|
||||||
|
APIError: If the deletion fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> success = client.assets.delete(123)
|
||||||
|
"""
|
||||||
|
# Validate asset_id
|
||||||
|
if not isinstance(asset_id, int) or asset_id <= 0:
|
||||||
|
raise ValidationError("asset_id must be a positive integer")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!) {
|
||||||
|
assets {
|
||||||
|
deleteAsset(id: $id) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/graphql", json_data={"query": mutation, "variables": {"id": asset_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Check response result
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("deleteAsset", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to delete asset: {error_msg}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def list_folders(self) -> List[AssetFolder]:
|
||||||
|
"""List all asset folders.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of AssetFolder objects
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
APIError: If the API request fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> folders = client.assets.list_folders()
|
||||||
|
>>> for folder in folders:
|
||||||
|
... print(f"{folder.name}: {folder.slug}")
|
||||||
|
"""
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
assets {
|
||||||
|
folders {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self._post("/graphql", json_data={"query": query})
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Extract folders
|
||||||
|
folders_data = response.get("data", {}).get("assets", {}).get("folders", [])
|
||||||
|
return [AssetFolder(**folder) for folder in folders_data]
|
||||||
|
|
||||||
|
def create_folder(self, slug: str, name: Optional[str] = None) -> AssetFolder:
|
||||||
|
"""Create a new asset folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slug: Folder slug/path
|
||||||
|
name: Optional folder name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Created AssetFolder object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If slug is invalid
|
||||||
|
APIError: If folder creation fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> folder = client.assets.create_folder("documents", "Documents")
|
||||||
|
"""
|
||||||
|
# Validate
|
||||||
|
if not slug or not slug.strip():
|
||||||
|
raise ValidationError("slug cannot be empty")
|
||||||
|
|
||||||
|
# Clean slug
|
||||||
|
slug = slug.strip().strip("/")
|
||||||
|
if not slug:
|
||||||
|
raise ValidationError("slug cannot be just slashes")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($slug: String!, $name: String) {
|
||||||
|
assets {
|
||||||
|
createFolder(slug: $slug, name: $name) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
folder {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
variables = {"slug": slug}
|
||||||
|
if name:
|
||||||
|
variables["name"] = name
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/graphql", json_data={"query": mutation, "variables": variables}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Check response result
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("createFolder", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to create folder: {error_msg}")
|
||||||
|
|
||||||
|
# Extract and return folder
|
||||||
|
folder_data = result.get("folder")
|
||||||
|
if not folder_data:
|
||||||
|
raise APIError("Folder created but no data returned")
|
||||||
|
|
||||||
|
return AssetFolder(**folder_data)
|
||||||
|
|
||||||
|
def delete_folder(self, folder_id: int) -> bool:
|
||||||
|
"""Delete an asset folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_id: The folder ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deletion was successful
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If folder_id is invalid
|
||||||
|
APIError: If the deletion fails
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> success = client.assets.delete_folder(5)
|
||||||
|
"""
|
||||||
|
# Validate folder_id
|
||||||
|
if not isinstance(folder_id, int) or folder_id <= 0:
|
||||||
|
raise ValidationError("folder_id must be a positive integer")
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation ($id: Int!) {
|
||||||
|
assets {
|
||||||
|
deleteFolder(id: $id) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"/graphql", json_data={"query": mutation, "variables": {"id": folder_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for GraphQL errors
|
||||||
|
if "errors" in response:
|
||||||
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
||||||
|
|
||||||
|
# Check response result
|
||||||
|
result = response.get("data", {}).get("assets", {}).get("deleteFolder", {})
|
||||||
|
response_result = result.get("responseResult", {})
|
||||||
|
|
||||||
|
if not response_result.get("succeeded"):
|
||||||
|
error_msg = response_result.get("message", "Unknown error")
|
||||||
|
raise APIError(f"Failed to delete folder: {error_msg}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _normalize_asset_data(self, data: Dict) -> Dict:
|
||||||
|
"""Normalize asset data from API response to Python naming convention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw asset data from API
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Normalized asset data with snake_case field names
|
||||||
|
"""
|
||||||
|
normalized = {
|
||||||
|
"id": data.get("id"),
|
||||||
|
"filename": data.get("filename"),
|
||||||
|
"ext": data.get("ext"),
|
||||||
|
"kind": data.get("kind"),
|
||||||
|
"mime": data.get("mime"),
|
||||||
|
"file_size": data.get("fileSize"),
|
||||||
|
"folder_id": data.get("folderId"),
|
||||||
|
"folder": data.get("folder"),
|
||||||
|
"author_id": data.get("authorId"),
|
||||||
|
"author_name": data.get("authorName"),
|
||||||
|
"created_at": data.get("createdAt"),
|
||||||
|
"updated_at": data.get("updatedAt"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
"""Data models for wikijs-python-sdk."""
|
"""Data models for wikijs-python-sdk."""
|
||||||
|
|
||||||
|
from .asset import (
|
||||||
|
Asset,
|
||||||
|
AssetFolder,
|
||||||
|
AssetMove,
|
||||||
|
AssetRename,
|
||||||
|
AssetUpload,
|
||||||
|
FolderCreate,
|
||||||
|
)
|
||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
from .group import (
|
from .group import (
|
||||||
Group,
|
Group,
|
||||||
@@ -15,7 +23,13 @@ from .page import Page, PageCreate, PageUpdate
|
|||||||
from .user import User, UserCreate, UserGroup, UserUpdate
|
from .user import User, UserCreate, UserGroup, UserUpdate
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Asset",
|
||||||
|
"AssetFolder",
|
||||||
|
"AssetMove",
|
||||||
|
"AssetRename",
|
||||||
|
"AssetUpload",
|
||||||
"BaseModel",
|
"BaseModel",
|
||||||
|
"FolderCreate",
|
||||||
"Group",
|
"Group",
|
||||||
"GroupAssignUser",
|
"GroupAssignUser",
|
||||||
"GroupCreate",
|
"GroupCreate",
|
||||||
|
|||||||
205
wikijs/models/asset.py
Normal file
205
wikijs/models/asset.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"""Data models for Wiki.js assets."""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
|
from .base import BaseModel, TimestampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class AssetFolder(BaseModel):
|
||||||
|
"""Asset folder model."""
|
||||||
|
|
||||||
|
id: int = Field(..., description="Folder ID")
|
||||||
|
slug: str = Field(..., description="Folder slug/path")
|
||||||
|
name: Optional[str] = Field(None, description="Folder name")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic configuration."""
|
||||||
|
|
||||||
|
populate_by_name = True
|
||||||
|
|
||||||
|
|
||||||
|
class Asset(TimestampedModel):
|
||||||
|
"""Wiki.js asset model.
|
||||||
|
|
||||||
|
Represents a file asset (image, document, etc.) in Wiki.js.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Asset ID
|
||||||
|
filename: Original filename
|
||||||
|
ext: File extension
|
||||||
|
kind: Asset kind (image, binary, etc.)
|
||||||
|
mime: MIME type
|
||||||
|
file_size: File size in bytes
|
||||||
|
folder_id: Parent folder ID
|
||||||
|
folder: Parent folder information
|
||||||
|
author_id: ID of user who uploaded
|
||||||
|
author_name: Name of user who uploaded
|
||||||
|
created_at: Upload timestamp
|
||||||
|
updated_at: Last update timestamp
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: int = Field(..., description="Asset ID")
|
||||||
|
filename: str = Field(..., min_length=1, description="Original filename")
|
||||||
|
ext: str = Field(..., description="File extension")
|
||||||
|
kind: str = Field(..., description="Asset kind (image, binary, etc.)")
|
||||||
|
mime: str = Field(..., description="MIME type")
|
||||||
|
file_size: int = Field(
|
||||||
|
..., alias="fileSize", ge=0, description="File size in bytes"
|
||||||
|
)
|
||||||
|
folder_id: Optional[int] = Field(
|
||||||
|
None, alias="folderId", description="Parent folder ID"
|
||||||
|
)
|
||||||
|
folder: Optional[AssetFolder] = Field(None, description="Parent folder")
|
||||||
|
author_id: Optional[int] = Field(None, alias="authorId", description="Author ID")
|
||||||
|
author_name: Optional[str] = Field(
|
||||||
|
None, alias="authorName", description="Author name"
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("filename")
|
||||||
|
@classmethod
|
||||||
|
def validate_filename(cls, v: str) -> str:
|
||||||
|
"""Validate filename."""
|
||||||
|
if not v or not v.strip():
|
||||||
|
raise ValueError("Filename cannot be empty")
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_mb(self) -> float:
|
||||||
|
"""Get file size in megabytes."""
|
||||||
|
return self.file_size / (1024 * 1024)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_kb(self) -> float:
|
||||||
|
"""Get file size in kilobytes."""
|
||||||
|
return self.file_size / 1024
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic configuration."""
|
||||||
|
|
||||||
|
populate_by_name = True
|
||||||
|
|
||||||
|
|
||||||
|
class AssetUpload(BaseModel):
|
||||||
|
"""Model for uploading a new asset.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
file_path: Local path to file to upload
|
||||||
|
folder_id: Target folder ID (default: 0 for root)
|
||||||
|
filename: Optional custom filename (uses file_path name if not provided)
|
||||||
|
"""
|
||||||
|
|
||||||
|
file_path: str = Field(..., alias="filePath", description="Local file path")
|
||||||
|
folder_id: int = Field(default=0, alias="folderId", description="Target folder ID")
|
||||||
|
filename: Optional[str] = Field(None, description="Custom filename")
|
||||||
|
|
||||||
|
@field_validator("file_path")
|
||||||
|
@classmethod
|
||||||
|
def validate_file_path(cls, v: str) -> str:
|
||||||
|
"""Validate file path."""
|
||||||
|
if not v or not v.strip():
|
||||||
|
raise ValueError("File path cannot be empty")
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic configuration."""
|
||||||
|
|
||||||
|
populate_by_name = True
|
||||||
|
|
||||||
|
|
||||||
|
class AssetRename(BaseModel):
|
||||||
|
"""Model for renaming an asset.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
asset_id: Asset ID to rename
|
||||||
|
new_filename: New filename
|
||||||
|
"""
|
||||||
|
|
||||||
|
asset_id: int = Field(..., alias="assetId", description="Asset ID")
|
||||||
|
new_filename: str = Field(
|
||||||
|
..., alias="newFilename", min_length=1, description="New filename"
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("asset_id")
|
||||||
|
@classmethod
|
||||||
|
def validate_asset_id(cls, v: int) -> int:
|
||||||
|
"""Validate asset ID."""
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Asset ID must be positive")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("new_filename")
|
||||||
|
@classmethod
|
||||||
|
def validate_filename(cls, v: str) -> str:
|
||||||
|
"""Validate filename."""
|
||||||
|
if not v or not v.strip():
|
||||||
|
raise ValueError("Filename cannot be empty")
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic configuration."""
|
||||||
|
|
||||||
|
populate_by_name = True
|
||||||
|
|
||||||
|
|
||||||
|
class AssetMove(BaseModel):
|
||||||
|
"""Model for moving an asset to a different folder.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
asset_id: Asset ID to move
|
||||||
|
folder_id: Target folder ID
|
||||||
|
"""
|
||||||
|
|
||||||
|
asset_id: int = Field(..., alias="assetId", description="Asset ID")
|
||||||
|
folder_id: int = Field(..., alias="folderId", description="Target folder ID")
|
||||||
|
|
||||||
|
@field_validator("asset_id")
|
||||||
|
@classmethod
|
||||||
|
def validate_asset_id(cls, v: int) -> int:
|
||||||
|
"""Validate asset ID."""
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Asset ID must be positive")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("folder_id")
|
||||||
|
@classmethod
|
||||||
|
def validate_folder_id(cls, v: int) -> int:
|
||||||
|
"""Validate folder ID."""
|
||||||
|
if v < 0:
|
||||||
|
raise ValueError("Folder ID must be non-negative")
|
||||||
|
return v
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic configuration."""
|
||||||
|
|
||||||
|
populate_by_name = True
|
||||||
|
|
||||||
|
|
||||||
|
class FolderCreate(BaseModel):
|
||||||
|
"""Model for creating a new folder.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
slug: Folder slug/path
|
||||||
|
name: Optional folder name
|
||||||
|
"""
|
||||||
|
|
||||||
|
slug: str = Field(..., min_length=1, description="Folder slug/path")
|
||||||
|
name: Optional[str] = Field(None, description="Folder name")
|
||||||
|
|
||||||
|
@field_validator("slug")
|
||||||
|
@classmethod
|
||||||
|
def validate_slug(cls, v: str) -> str:
|
||||||
|
"""Validate slug."""
|
||||||
|
if not v or not v.strip():
|
||||||
|
raise ValueError("Slug cannot be empty")
|
||||||
|
# Remove leading/trailing slashes
|
||||||
|
v = v.strip().strip("/")
|
||||||
|
if not v:
|
||||||
|
raise ValueError("Slug cannot be just slashes")
|
||||||
|
return v
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic configuration."""
|
||||||
|
|
||||||
|
populate_by_name = True
|
||||||
Reference in New Issue
Block a user