owui-cli 0.5.1__tar.gz → 0.5.3__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.
- {owui_cli-0.5.1 → owui_cli-0.5.3}/PKG-INFO +2 -2
- {owui_cli-0.5.1 → owui_cli-0.5.3}/pyproject.toml +1 -1
- owui_cli-0.5.3/src/owui_cli/__init__.py +1 -0
- {owui_cli-0.5.1 → owui_cli-0.5.3}/src/owui_cli/cli.py +126 -2
- {owui_cli-0.5.1 → owui_cli-0.5.3}/uv.lock +1 -1
- owui_cli-0.5.1/src/owui_cli/__init__.py +0 -1
- {owui_cli-0.5.1 → owui_cli-0.5.3}/.github/workflows/publish.yml +0 -0
- {owui_cli-0.5.1 → owui_cli-0.5.3}/.gitignore +0 -0
- {owui_cli-0.5.1 → owui_cli-0.5.3}/AGENTS.md +0 -0
- {owui_cli-0.5.1 → owui_cli-0.5.3}/LICENSE +0 -0
- {owui_cli-0.5.1 → owui_cli-0.5.3}/README.md +0 -0
- {owui_cli-0.5.1 → owui_cli-0.5.3}/src/owui_cli/data/api-schema.json +0 -0
- {owui_cli-0.5.1 → owui_cli-0.5.3}/update-schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: owui-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Admin CLI for Open WebUI instances
|
|
5
5
|
Project-URL: Homepage, https://github.com/rndmcnlly/owui-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/rndmcnlly/owui-cli
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.3"
|
|
@@ -15,6 +15,7 @@ import json
|
|
|
15
15
|
import os
|
|
16
16
|
import re
|
|
17
17
|
import sys
|
|
18
|
+
import time
|
|
18
19
|
from importlib.resources import files
|
|
19
20
|
|
|
20
21
|
import httpx
|
|
@@ -602,20 +603,56 @@ def models_show(url, token, model_id):
|
|
|
602
603
|
("grants", str(len(m.get("access_grants") or [])))]
|
|
603
604
|
out_kv(pairs)
|
|
604
605
|
|
|
606
|
+
_MIME_BY_EXT = {
|
|
607
|
+
"png": "image/png",
|
|
608
|
+
"jpg": "image/jpeg",
|
|
609
|
+
"jpeg": "image/jpeg",
|
|
610
|
+
"gif": "image/gif",
|
|
611
|
+
"webp": "image/webp",
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _inline_sibling_images(json_path: str, payload: dict) -> list[str]:
|
|
616
|
+
"""Replace bare sibling-image references in meta.profile_image_url with data URIs.
|
|
617
|
+
|
|
618
|
+
The repo convention stores the icon as a sibling file referenced by bare
|
|
619
|
+
filename (e.g. "profile.png"). OWUI's stored-model validator (utils/validate.py)
|
|
620
|
+
rejects bare filenames and silently clears them to null, so push must inline
|
|
621
|
+
the sibling image instead — the mirror of models_pull_all's extraction.
|
|
622
|
+
Returns the list of inlined filenames.
|
|
623
|
+
"""
|
|
624
|
+
inlined = []
|
|
625
|
+
meta = payload.get("meta") if isinstance(payload, dict) else None
|
|
626
|
+
ref = (meta or {}).get("profile_image_url")
|
|
627
|
+
if not isinstance(ref, str) or not ref or ref.startswith(("data:", "http://", "https://", "/")):
|
|
628
|
+
return inlined
|
|
629
|
+
image_path = os.path.join(os.path.dirname(os.path.abspath(json_path)), ref)
|
|
630
|
+
ext = os.path.splitext(ref)[1].lstrip(".").lower()
|
|
631
|
+
mime = _MIME_BY_EXT.get(ext)
|
|
632
|
+
if mime and os.path.isfile(image_path):
|
|
633
|
+
with open(image_path, "rb") as f:
|
|
634
|
+
b64 = base64.b64encode(f.read()).decode()
|
|
635
|
+
payload["meta"]["profile_image_url"] = f"data:{mime};base64,{b64}"
|
|
636
|
+
inlined.append(ref)
|
|
637
|
+
return inlined
|
|
638
|
+
|
|
639
|
+
|
|
605
640
|
def models_create(url, token, json_path):
|
|
606
641
|
with open(json_path) as f:
|
|
607
642
|
payload = json.load(f)
|
|
643
|
+
inlined = _inline_sibling_images(json_path, payload)
|
|
608
644
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
609
645
|
r = _post(c, url, "/api/v1/models/create", token, payload)
|
|
610
646
|
m = r.json()
|
|
611
|
-
out(f"created {m.get('id')}")
|
|
647
|
+
out(f"created {m.get('id')}" + (f" (inlined {', '.join(inlined)})" if inlined else ""))
|
|
612
648
|
|
|
613
649
|
def models_update(url, token, json_path):
|
|
614
650
|
with open(json_path) as f:
|
|
615
651
|
payload = json.load(f)
|
|
652
|
+
inlined = _inline_sibling_images(json_path, payload)
|
|
616
653
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
617
654
|
r = _post(c, url, "/api/v1/models/model/update", token, payload)
|
|
618
|
-
out(f"updated {r.json().get('id')}")
|
|
655
|
+
out(f"updated {r.json().get('id')}" + (f" (inlined {', '.join(inlined)})" if inlined else ""))
|
|
619
656
|
|
|
620
657
|
def models_delete(url, token, model_id):
|
|
621
658
|
with httpx.Client(timeout=TIMEOUT) as c:
|
|
@@ -1038,6 +1075,90 @@ def chats_delete(url, token, chat_id):
|
|
|
1038
1075
|
out(f"deleted {chat_id}")
|
|
1039
1076
|
|
|
1040
1077
|
|
|
1078
|
+
_STREAM_TIMEOUT = httpx.Timeout(TIMEOUT, read=3600.0)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def _iter_chats_all(url, token):
|
|
1082
|
+
"""Stream GET /api/v1/chats/all as NDJSON, yielding one chat per line.
|
|
1083
|
+
|
|
1084
|
+
On v0.11.x the endpoint returns an application/x-ndjson stream whose
|
|
1085
|
+
export can run to hundreds of MB — never buffered server-side. Parse
|
|
1086
|
+
incrementally so clients (and pull-all) don't need the whole thing in
|
|
1087
|
+
memory.
|
|
1088
|
+
|
|
1089
|
+
Decode-buffer manually over raw bytes: httpx's iter_lines can truncate a
|
|
1090
|
+
long line at a decode-chunk boundary (observed with model_dump_json
|
|
1091
|
+
exports whose message trees run large), so split b'\n' ourselves and
|
|
1092
|
+
decode each complete line.
|
|
1093
|
+
"""
|
|
1094
|
+
with httpx.Client(timeout=_STREAM_TIMEOUT) as c:
|
|
1095
|
+
with c.stream("GET", _api(url, "/api/v1/chats/all"), headers=_headers(token)) as r:
|
|
1096
|
+
r.raise_for_status()
|
|
1097
|
+
buf = b""
|
|
1098
|
+
for chunk in r.iter_raw():
|
|
1099
|
+
buf += chunk
|
|
1100
|
+
while True:
|
|
1101
|
+
idx = buf.find(b"\n")
|
|
1102
|
+
if idx < 0:
|
|
1103
|
+
break
|
|
1104
|
+
line = buf[:idx]
|
|
1105
|
+
buf = buf[idx + 1:]
|
|
1106
|
+
if line.strip():
|
|
1107
|
+
yield json.loads(line.decode("utf-8"))
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def chats_all(url, token):
|
|
1111
|
+
count = 0
|
|
1112
|
+
chats = []
|
|
1113
|
+
for chat in _iter_chats_all(url, token):
|
|
1114
|
+
count += 1
|
|
1115
|
+
if JSON_OUTPUT:
|
|
1116
|
+
chats.append(chat)
|
|
1117
|
+
else:
|
|
1118
|
+
title = chat.get("chat", {}).get("title") or chat.get("title") or "(untitled)"
|
|
1119
|
+
print(f"{count}\t{chat.get('id','')}\t{str(title)[:60]}")
|
|
1120
|
+
if JSON_OUTPUT:
|
|
1121
|
+
out(chats)
|
|
1122
|
+
else:
|
|
1123
|
+
print(f"-- {count} chats", file=sys.stderr)
|
|
1124
|
+
|
|
1125
|
+
def chats_pull_all(url, token, out_dir="chats"):
|
|
1126
|
+
count = 0
|
|
1127
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
1128
|
+
for chat in _iter_chats_all(url, token):
|
|
1129
|
+
chat_id = chat.get("id", "")
|
|
1130
|
+
if not chat_id:
|
|
1131
|
+
continue
|
|
1132
|
+
_write_json(os.path.join(out_dir, f"{chat_id}.json"), chat)
|
|
1133
|
+
count += 1
|
|
1134
|
+
if count % 100 == 0:
|
|
1135
|
+
print(f"... {count}", file=sys.stderr)
|
|
1136
|
+
out(f"pulled {count} chats into {out_dir}")
|
|
1137
|
+
|
|
1138
|
+
def chats_stats(url, token, page="1"):
|
|
1139
|
+
with httpx.Client(timeout=_STREAM_TIMEOUT) as c:
|
|
1140
|
+
r = _get(c, url, f"/api/v1/chats/stats/export?page={page}", token)
|
|
1141
|
+
data = r.json()
|
|
1142
|
+
if JSON_OUTPUT:
|
|
1143
|
+
out(data)
|
|
1144
|
+
return
|
|
1145
|
+
items = data.get("items", data) if isinstance(data, dict) else data
|
|
1146
|
+
total = data.get("total") if isinstance(data, dict) else None
|
|
1147
|
+
rows = []
|
|
1148
|
+
for s in items:
|
|
1149
|
+
st = s.get("stats") or {}
|
|
1150
|
+
models = st.get("models") or st.get("history_models") or {}
|
|
1151
|
+
rows.append({
|
|
1152
|
+
"id": s.get("id", s.get("chat_id", "")),
|
|
1153
|
+
"msgs": st.get("message_count", st.get("history_message_count", "?")),
|
|
1154
|
+
"models": ", ".join(models.keys())[:30],
|
|
1155
|
+
"updated": time.strftime("%Y-%m-%d", time.gmtime(s.get("updated_at"))) if s.get("updated_at") else "",
|
|
1156
|
+
})
|
|
1157
|
+
out_table(rows, [("CHAT_ID","id",36), ("MSGS","msgs",6), ("MODELS","models",30), ("UPDATED","updated",10)])
|
|
1158
|
+
if total is not None:
|
|
1159
|
+
print(f"-- page {page}: {len(items)} chats, total {total}")
|
|
1160
|
+
|
|
1161
|
+
|
|
1041
1162
|
# ── configs ──────────────────────────────────────────────────────────
|
|
1042
1163
|
|
|
1043
1164
|
def configs_show(url, token):
|
|
@@ -1219,6 +1340,9 @@ COMMANDS.update({
|
|
|
1219
1340
|
("chats", "list"): (chats_list, "[page]", (0, 1)),
|
|
1220
1341
|
("chats", "search"): (chats_search, "<query> [page]", (1, 2)),
|
|
1221
1342
|
("chats", "show"): (chats_show, "<id>", (1, 1)),
|
|
1343
|
+
("chats", "all"): (chats_all, "", (0, 0)),
|
|
1344
|
+
("chats", "pull-all"): (chats_pull_all, "[dir]", (0, 1)),
|
|
1345
|
+
("chats", "stats"): (chats_stats, "[page]", (0, 1)),
|
|
1222
1346
|
("chats", "delete"): (chats_delete, "<id>", (1, 1)),
|
|
1223
1347
|
("users", "list"): (users_list, "", (0, 0)),
|
|
1224
1348
|
("users", "find"): (users_find, "<query>", (1, 1)),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|