artdam-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- artdam_cli-0.1.0/PKG-INFO +7 -0
- artdam_cli-0.1.0/artdam_cli/__init__.py +1 -0
- artdam_cli-0.1.0/artdam_cli/client.py +50 -0
- artdam_cli-0.1.0/artdam_cli/commands/__init__.py +0 -0
- artdam_cli-0.1.0/artdam_cli/commands/download.py +28 -0
- artdam_cli-0.1.0/artdam_cli/commands/folders.py +37 -0
- artdam_cli-0.1.0/artdam_cli/commands/get.py +39 -0
- artdam_cli-0.1.0/artdam_cli/commands/login.py +31 -0
- artdam_cli-0.1.0/artdam_cli/commands/search.py +61 -0
- artdam_cli-0.1.0/artdam_cli/commands/tags.py +46 -0
- artdam_cli-0.1.0/artdam_cli/config.py +28 -0
- artdam_cli-0.1.0/artdam_cli/main.py +22 -0
- artdam_cli-0.1.0/artdam_cli.egg-info/PKG-INFO +7 -0
- artdam_cli-0.1.0/artdam_cli.egg-info/SOURCES.txt +18 -0
- artdam_cli-0.1.0/artdam_cli.egg-info/dependency_links.txt +1 -0
- artdam_cli-0.1.0/artdam_cli.egg-info/entry_points.txt +2 -0
- artdam_cli-0.1.0/artdam_cli.egg-info/requires.txt +3 -0
- artdam_cli-0.1.0/artdam_cli.egg-info/top_level.txt +1 -0
- artdam_cli-0.1.0/pyproject.toml +20 -0
- artdam_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from artdam_cli import config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _make_client() -> httpx.Client:
|
|
9
|
+
base_url = config.require("base_url")
|
|
10
|
+
token = config.require("token")
|
|
11
|
+
return httpx.Client(
|
|
12
|
+
base_url=base_url,
|
|
13
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
14
|
+
timeout=30,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get(path: str, params: dict | None = None) -> dict:
|
|
19
|
+
with _make_client() as c:
|
|
20
|
+
r = c.get(path, params=params)
|
|
21
|
+
_raise(r)
|
|
22
|
+
return r.json()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def download_file(path: str, dest: str) -> int:
|
|
26
|
+
base_url = config.require("base_url")
|
|
27
|
+
token = config.require("token")
|
|
28
|
+
with httpx.stream(
|
|
29
|
+
"GET",
|
|
30
|
+
f"{base_url}{path}",
|
|
31
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
32
|
+
timeout=120,
|
|
33
|
+
follow_redirects=True,
|
|
34
|
+
) as r:
|
|
35
|
+
_raise(r)
|
|
36
|
+
total = 0
|
|
37
|
+
with open(dest, "wb") as f:
|
|
38
|
+
for chunk in r.iter_bytes(65536):
|
|
39
|
+
f.write(chunk)
|
|
40
|
+
total += len(chunk)
|
|
41
|
+
return total
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _raise(r: httpx.Response) -> None:
|
|
45
|
+
if r.status_code == 401:
|
|
46
|
+
raise SystemExit("Token 无效或已过期,请重新运行 artdam login")
|
|
47
|
+
if r.status_code == 404:
|
|
48
|
+
raise SystemExit("资源不存在")
|
|
49
|
+
if r.is_error:
|
|
50
|
+
raise SystemExit(f"请求失败 {r.status_code}: {r.text[:200]}")
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.progress import Progress
|
|
6
|
+
|
|
7
|
+
from artdam_cli import client
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.argument("asset_id", type=int)
|
|
14
|
+
@click.option("--out", "-o", default=".", help="下载目录(默认当前目录)")
|
|
15
|
+
def download(asset_id: int, out: str) -> None:
|
|
16
|
+
"""下载资产文件。"""
|
|
17
|
+
data = client.get(f"/api/assets/{asset_id}")
|
|
18
|
+
filename = data.get("filename", f"asset_{asset_id}")
|
|
19
|
+
dest = os.path.join(out, filename)
|
|
20
|
+
os.makedirs(out, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
with Progress() as progress:
|
|
23
|
+
task = progress.add_task(f"下载 {filename}...", total=None)
|
|
24
|
+
total = client.download_file(f"/api/assets/{asset_id}/file", dest)
|
|
25
|
+
progress.update(task, completed=total, total=total)
|
|
26
|
+
|
|
27
|
+
size_str = f"{total / 1024 / 1024:.1f} MB"
|
|
28
|
+
console.print(f"✓ 已下载到 [green]{dest}[/green]({size_str})")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.tree import Tree
|
|
6
|
+
|
|
7
|
+
from artdam_cli import client
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _build_tree(node: Tree, items: list[dict], parent_id: int | None) -> None:
|
|
13
|
+
children = [i for i in items if i.get("parent_id") == parent_id]
|
|
14
|
+
for child in children:
|
|
15
|
+
branch = node.add(f"[cyan]{child['name']}[/cyan] [dim](id={child['id']})[/dim]")
|
|
16
|
+
_build_tree(branch, items, child["id"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command()
|
|
20
|
+
@click.option("--project", "-p", required=True, type=int, help="项目 ID")
|
|
21
|
+
@click.option("--json", "as_json", is_flag=True, help="输出原始 JSON")
|
|
22
|
+
def folders(project: int, as_json: bool) -> None:
|
|
23
|
+
"""查看文件夹结构。"""
|
|
24
|
+
data = client.get("/api/collections/", params={"project_id": project})
|
|
25
|
+
items = data if isinstance(data, list) else data.get("items", [])
|
|
26
|
+
|
|
27
|
+
if as_json:
|
|
28
|
+
click.echo(json.dumps(items, ensure_ascii=False, indent=2))
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if not items:
|
|
32
|
+
console.print("[yellow]暂无文件夹[/yellow]")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
tree = Tree(f"[bold]项目 {project} 文件夹[/bold]")
|
|
36
|
+
_build_tree(tree, items, None)
|
|
37
|
+
console.print(tree)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from artdam_cli import client
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command()
|
|
14
|
+
@click.argument("asset_id", type=int)
|
|
15
|
+
@click.option("--json", "as_json", is_flag=True, help="输出原始 JSON(AI Agent 用)")
|
|
16
|
+
def get(asset_id: int, as_json: bool) -> None:
|
|
17
|
+
"""查看资产详情。"""
|
|
18
|
+
data = client.get(f"/api/assets/{asset_id}")
|
|
19
|
+
|
|
20
|
+
if as_json:
|
|
21
|
+
click.echo(json.dumps(data, ensure_ascii=False, indent=2))
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
tags = ", ".join(t["name"] for t in data.get("tags", []))
|
|
25
|
+
size = data.get("file_size") or 0
|
|
26
|
+
size_str = f"{size / 1024 / 1024:.1f} MB" if size > 0 else "—"
|
|
27
|
+
|
|
28
|
+
lines = [
|
|
29
|
+
f"[bold]ID[/bold] {data['id']}",
|
|
30
|
+
f"[bold]文件名[/bold] {data.get('filename', '—')}",
|
|
31
|
+
f"[bold]类型[/bold] {data.get('mime_type', '—')}",
|
|
32
|
+
f"[bold]大小[/bold] {size_str}",
|
|
33
|
+
f"[bold]标签[/bold] {tags or '—'}",
|
|
34
|
+
f"[bold]AI 描述[/bold] {data.get('ai_description') or '—'}",
|
|
35
|
+
f"[bold]上传时间[/bold] {data.get('created_at', '—')}",
|
|
36
|
+
f"[bold]路径[/bold] {data.get('original_path', '—')}",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
console.print(Panel("\n".join(lines), title=f"资产 #{asset_id}"))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import httpx
|
|
3
|
+
|
|
4
|
+
from artdam_cli import config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.command()
|
|
8
|
+
@click.option("--url", prompt="ArtDAM 地址", help="例: http://artdam.dsworks.cn")
|
|
9
|
+
@click.option("--username", prompt="用户名")
|
|
10
|
+
@click.option("--password", prompt="密码", hide_input=True)
|
|
11
|
+
def login(url: str, username: str, password: str) -> None:
|
|
12
|
+
"""登录 ArtDAM,保存 token 到本地。"""
|
|
13
|
+
url = url.rstrip("/")
|
|
14
|
+
try:
|
|
15
|
+
r = httpx.post(
|
|
16
|
+
f"{url}/api/auth/login",
|
|
17
|
+
json={"username": username, "password": password},
|
|
18
|
+
timeout=10,
|
|
19
|
+
)
|
|
20
|
+
except httpx.ConnectError:
|
|
21
|
+
raise SystemExit(f"无法连接到 {url},请检查地址")
|
|
22
|
+
|
|
23
|
+
if r.status_code != 200:
|
|
24
|
+
raise SystemExit(f"登录失败: {r.json().get('detail', r.text)}")
|
|
25
|
+
|
|
26
|
+
token = r.json().get("access_token")
|
|
27
|
+
if not token:
|
|
28
|
+
raise SystemExit("登录响应中未找到 token")
|
|
29
|
+
|
|
30
|
+
config.save({"base_url": url, "token": token})
|
|
31
|
+
click.echo(f"✓ 登录成功,配置已保存到 ~/.artdam/config.json")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from artdam_cli import client
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.argument("keyword")
|
|
14
|
+
@click.option("--project", "-p", required=True, type=int, help="项目 ID")
|
|
15
|
+
@click.option("--limit", "-n", default=20, show_default=True, help="返回数量上限")
|
|
16
|
+
@click.option("--type", "mime", default=None, help="文件类型,例: image/png")
|
|
17
|
+
@click.option("--folder", default=None, type=int, help="限定文件夹 ID")
|
|
18
|
+
@click.option("--json", "as_json", is_flag=True, help="输出原始 JSON(AI Agent 用)")
|
|
19
|
+
def search(keyword: str, project: int, limit: int, mime: str | None, folder: int | None, as_json: bool) -> None:
|
|
20
|
+
"""搜索资产。"""
|
|
21
|
+
params: dict = {
|
|
22
|
+
"project_id": project,
|
|
23
|
+
"q": keyword,
|
|
24
|
+
"page_size": limit,
|
|
25
|
+
}
|
|
26
|
+
if mime:
|
|
27
|
+
params["mime_type"] = mime
|
|
28
|
+
if folder:
|
|
29
|
+
params["folder_id"] = folder
|
|
30
|
+
|
|
31
|
+
data = client.get("/api/assets/", params=params)
|
|
32
|
+
items = data.get("items", [])
|
|
33
|
+
|
|
34
|
+
if as_json:
|
|
35
|
+
click.echo(json.dumps(items, ensure_ascii=False, indent=2))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if not items:
|
|
39
|
+
console.print("[yellow]未找到匹配资产[/yellow]")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
table = Table(title=f"搜索「{keyword}」— 共 {data.get('total', len(items))} 条")
|
|
43
|
+
table.add_column("ID", style="dim", width=6)
|
|
44
|
+
table.add_column("文件名", min_width=20)
|
|
45
|
+
table.add_column("类型", width=10)
|
|
46
|
+
table.add_column("大小", width=10)
|
|
47
|
+
table.add_column("标签", min_width=20)
|
|
48
|
+
|
|
49
|
+
for item in items:
|
|
50
|
+
tags = ", ".join(t["name"] for t in item.get("tags", []))
|
|
51
|
+
size = item.get("file_size") or 0
|
|
52
|
+
size_str = f"{size / 1024 / 1024:.1f} MB" if size > 0 else "—"
|
|
53
|
+
table.add_row(
|
|
54
|
+
str(item["id"]),
|
|
55
|
+
item.get("filename", ""),
|
|
56
|
+
(item.get("mime_type") or "").split("/")[-1],
|
|
57
|
+
size_str,
|
|
58
|
+
tags or "—",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
console.print(table)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from artdam_cli import client
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.option("--project", "-p", required=True, type=int, help="项目 ID")
|
|
14
|
+
@click.option("--search", "-s", default=None, help="按名称过滤")
|
|
15
|
+
@click.option("--json", "as_json", is_flag=True, help="输出原始 JSON")
|
|
16
|
+
def tags(project: int, search: str | None, as_json: bool) -> None:
|
|
17
|
+
"""列出项目标签。"""
|
|
18
|
+
params: dict = {"project_id": project}
|
|
19
|
+
if search:
|
|
20
|
+
data = client.get("/api/tags/search", params={"project_id": project, "q": search})
|
|
21
|
+
else:
|
|
22
|
+
data = client.get("/api/tags/", params=params)
|
|
23
|
+
|
|
24
|
+
items = data if isinstance(data, list) else data.get("items", [])
|
|
25
|
+
|
|
26
|
+
if as_json:
|
|
27
|
+
click.echo(json.dumps(items, ensure_ascii=False, indent=2))
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if not items:
|
|
31
|
+
console.print("[yellow]暂无标签[/yellow]")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
table = Table(title=f"项目 {project} 标签列表({len(items)} 个)")
|
|
35
|
+
table.add_column("ID", style="dim", width=6)
|
|
36
|
+
table.add_column("标签名", min_width=20)
|
|
37
|
+
table.add_column("资产数", width=8)
|
|
38
|
+
|
|
39
|
+
for item in items:
|
|
40
|
+
table.add_row(
|
|
41
|
+
str(item["id"]),
|
|
42
|
+
item.get("name", ""),
|
|
43
|
+
str(item.get("asset_count", "—")),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
console.print(table)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_CONFIG_PATH = Path.home() / ".artdam" / "config.json"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load() -> dict:
|
|
10
|
+
if not _CONFIG_PATH.exists():
|
|
11
|
+
return {}
|
|
12
|
+
return json.loads(_CONFIG_PATH.read_text(encoding="utf-8"))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def save(data: dict) -> None:
|
|
16
|
+
_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
_CONFIG_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get(key: str) -> str | None:
|
|
21
|
+
return load().get(key)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def require(key: str) -> str:
|
|
25
|
+
value = get(key)
|
|
26
|
+
if not value:
|
|
27
|
+
raise SystemExit(f"未配置 {key},请先运行: artdam login")
|
|
28
|
+
return value
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from artdam_cli.commands.login import login
|
|
4
|
+
from artdam_cli.commands.search import search
|
|
5
|
+
from artdam_cli.commands.get import get
|
|
6
|
+
from artdam_cli.commands.tags import tags
|
|
7
|
+
from artdam_cli.commands.folders import folders
|
|
8
|
+
from artdam_cli.commands.download import download
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
@click.version_option(package_name="artdam-cli")
|
|
13
|
+
def cli() -> None:
|
|
14
|
+
"""ArtDAM 命令行工具 — 搜索、查看、下载数字资产。"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
cli.add_command(login)
|
|
18
|
+
cli.add_command(search)
|
|
19
|
+
cli.add_command(get)
|
|
20
|
+
cli.add_command(tags)
|
|
21
|
+
cli.add_command(folders)
|
|
22
|
+
cli.add_command(download)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
artdam_cli/__init__.py
|
|
3
|
+
artdam_cli/client.py
|
|
4
|
+
artdam_cli/config.py
|
|
5
|
+
artdam_cli/main.py
|
|
6
|
+
artdam_cli.egg-info/PKG-INFO
|
|
7
|
+
artdam_cli.egg-info/SOURCES.txt
|
|
8
|
+
artdam_cli.egg-info/dependency_links.txt
|
|
9
|
+
artdam_cli.egg-info/entry_points.txt
|
|
10
|
+
artdam_cli.egg-info/requires.txt
|
|
11
|
+
artdam_cli.egg-info/top_level.txt
|
|
12
|
+
artdam_cli/commands/__init__.py
|
|
13
|
+
artdam_cli/commands/download.py
|
|
14
|
+
artdam_cli/commands/folders.py
|
|
15
|
+
artdam_cli/commands/get.py
|
|
16
|
+
artdam_cli/commands/login.py
|
|
17
|
+
artdam_cli/commands/search.py
|
|
18
|
+
artdam_cli/commands/tags.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
artdam_cli
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "artdam-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"click>=8.1",
|
|
11
|
+
"httpx>=0.27",
|
|
12
|
+
"rich>=13.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
artdam = "artdam_cli.main:cli"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["."]
|
|
20
|
+
include = ["artdam_cli*"]
|