uno-cli 1.0.4__tar.gz → 1.0.5__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.
- {uno_cli-1.0.4 → uno_cli-1.0.5}/.gitignore +4 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/PKG-INFO +1 -1
- {uno_cli-1.0.4 → uno_cli-1.0.5}/pyproject.toml +1 -1
- uno_cli-1.0.5/tests/test_reference_upload.py +165 -0
- uno_cli-1.0.5/uno_cli/_version.py +1 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/uno_cli/cli.py +44 -5
- uno_cli-1.0.5/uno_cli/references.py +205 -0
- uno_cli-1.0.4/uno_cli/_version.py +0 -1
- {uno_cli-1.0.4 → uno_cli-1.0.5}/LICENSE +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/README.md +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/__init__.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/test_auth.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/test_cli.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/test_connections_logs.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/test_dry_run.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/test_register.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/test_scope.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/tests/test_token_masking.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/uno_cli/__init__.py +0 -0
- {uno_cli-1.0.4 → uno_cli-1.0.5}/uno_cli/__main__.py +0 -0
|
@@ -8,6 +8,7 @@ __pycache__/
|
|
|
8
8
|
node_modules/
|
|
9
9
|
.next/
|
|
10
10
|
frontend/.next/
|
|
11
|
+
frontend/.next-releases/
|
|
11
12
|
frontend/node_modules/
|
|
12
13
|
.DS_Store
|
|
13
14
|
.idea/
|
|
@@ -19,6 +20,9 @@ deploy.sh
|
|
|
19
20
|
data_export/
|
|
20
21
|
data_export_prod/
|
|
21
22
|
scripts/backup_prod*.json
|
|
23
|
+
scripts/probe_all.json
|
|
24
|
+
scripts/probe_200.json
|
|
25
|
+
var/mcp-url-secret-backups/
|
|
22
26
|
.mcp_auth/
|
|
23
27
|
.cursor/
|
|
24
28
|
.worktrees/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: uno-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Uno CLI — Agent tool gateway by ClawdChat. Access 2000+ real-world tools from the command line.
|
|
5
5
|
Project-URL: Homepage, https://agentools.uno
|
|
6
6
|
Project-URL: Documentation, https://agentools.uno/docs
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Reference images are uploaded before they enter ``--args``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from uno_cli import cli
|
|
12
|
+
from uno_cli.references import split_reference_list
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _args(raw):
|
|
16
|
+
return argparse.Namespace(
|
|
17
|
+
tool="gpt-image-2.gpt_image2_submit",
|
|
18
|
+
args=raw,
|
|
19
|
+
timeout=60,
|
|
20
|
+
dry_run=False,
|
|
21
|
+
base_url="https://agentools.uno",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _API:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.uploads = []
|
|
28
|
+
self.call = None
|
|
29
|
+
|
|
30
|
+
def post_bytes(self, path, data, content_type, timeout=120):
|
|
31
|
+
self.uploads.append({"path": path, "data": data, "content_type": content_type})
|
|
32
|
+
file_id = "f_" + f"{len(self.uploads):02x}" + ("ab" * 15)
|
|
33
|
+
url = f"https://agentools.uno/v1/files/{file_id}"
|
|
34
|
+
assert "," not in file_id
|
|
35
|
+
assert "," not in url
|
|
36
|
+
return {"file_id": file_id, "url": url}
|
|
37
|
+
|
|
38
|
+
def post(self, path, data, timeout=60):
|
|
39
|
+
self.call = {"path": path, "data": data}
|
|
40
|
+
return {"data": {"status": "pending", "job_id": "job1"}, "meta": {}}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_oversized_data_uri_does_not_enter_call_args(monkeypatch, capsys):
|
|
44
|
+
image = b"\xff\xd8\xff" + (b"x" * 80000)
|
|
45
|
+
encoded = base64.b64encode(image).decode()
|
|
46
|
+
data_uri = f"data:image/jpeg;base64,{encoded}"
|
|
47
|
+
raw = json.dumps({"prompt": "cat", "reference_image_urls": data_uri})
|
|
48
|
+
assert len(raw) > 102400
|
|
49
|
+
assert "," in data_uri
|
|
50
|
+
|
|
51
|
+
api = _API()
|
|
52
|
+
monkeypatch.delenv("UNO_ALLOWED_TOOLS", raising=False)
|
|
53
|
+
monkeypatch.setattr(cli, "_get_api", lambda args: api)
|
|
54
|
+
|
|
55
|
+
cli.cmd_call(_args(raw))
|
|
56
|
+
|
|
57
|
+
assert api.call["path"] == "/v1/call"
|
|
58
|
+
sent = api.call["data"]["arguments"]
|
|
59
|
+
ref = sent["reference_image_urls"]
|
|
60
|
+
assert encoded not in json.dumps(sent)
|
|
61
|
+
assert "base64" not in ref
|
|
62
|
+
assert data_uri not in json.dumps(sent)
|
|
63
|
+
assert "," not in ref
|
|
64
|
+
assert ref.startswith("https://agentools.uno/v1/files/f_")
|
|
65
|
+
assert len(json.dumps(sent, ensure_ascii=False)) <= 102400
|
|
66
|
+
assert api.uploads[0]["data"] == image
|
|
67
|
+
assert api.uploads[0]["content_type"] == "image/jpeg"
|
|
68
|
+
out = json.loads(capsys.readouterr().out)
|
|
69
|
+
assert out["success"] is True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_local_paths_upload_separately(monkeypatch, capsys, tmp_path):
|
|
73
|
+
first = tmp_path / "a.jpg"
|
|
74
|
+
second = tmp_path / "b.png"
|
|
75
|
+
first.write_bytes(b"\xff\xd8\xff\xd9")
|
|
76
|
+
second.write_bytes(b"\x89PNG\r\n")
|
|
77
|
+
raw = json.dumps({"prompt": "keep the subject", "reference_image_urls": f"{first},{second}"})
|
|
78
|
+
|
|
79
|
+
api = _API()
|
|
80
|
+
monkeypatch.delenv("UNO_ALLOWED_TOOLS", raising=False)
|
|
81
|
+
monkeypatch.setattr(cli, "_get_api", lambda args: api)
|
|
82
|
+
cli.cmd_call(_args(raw))
|
|
83
|
+
|
|
84
|
+
assert len(api.uploads) == 2
|
|
85
|
+
refs = api.call["data"]["arguments"]["reference_image_urls"].split(",")
|
|
86
|
+
assert len(refs) == 2
|
|
87
|
+
assert all("," not in item for item in refs)
|
|
88
|
+
assert all(item.startswith("https://agentools.uno/v1/files/f_") for item in refs)
|
|
89
|
+
assert str(first) not in json.dumps(api.call["data"]["arguments"])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_data_uri_comma_is_not_a_second_image():
|
|
93
|
+
raw = "data:image/jpeg;base64," + ("QUJD" * 30)
|
|
94
|
+
assert split_reference_list(raw) == [raw]
|
|
95
|
+
mixed = raw + ",https://cdn.example/b.png"
|
|
96
|
+
parts = split_reference_list(mixed)
|
|
97
|
+
assert parts == [raw, "https://cdn.example/b.png"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_plain_prompt_comma_is_not_uploaded(monkeypatch, capsys):
|
|
101
|
+
api = _API()
|
|
102
|
+
monkeypatch.delenv("UNO_ALLOWED_TOOLS", raising=False)
|
|
103
|
+
monkeypatch.setattr(cli, "_get_api", lambda args: api)
|
|
104
|
+
cli.cmd_call(_args(json.dumps({"prompt": "a cat, a dog"})))
|
|
105
|
+
assert api.uploads == []
|
|
106
|
+
assert api.call["data"]["arguments"]["prompt"] == "a cat, a dog"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_base64_fields_keep_the_data_uri(monkeypatch, capsys):
|
|
110
|
+
encoded = base64.b64encode(b"\xff\xd8\xff\xd9").decode()
|
|
111
|
+
data_uri = f"data:image/jpeg;base64,{encoded}"
|
|
112
|
+
raw = json.dumps({
|
|
113
|
+
"prompt": "cat",
|
|
114
|
+
"reference_image_base64": data_uri,
|
|
115
|
+
"reference_images_base64": [data_uri],
|
|
116
|
+
"image_base64": data_uri,
|
|
117
|
+
"reference_image_urls": data_uri,
|
|
118
|
+
})
|
|
119
|
+
api = _API()
|
|
120
|
+
monkeypatch.delenv("UNO_ALLOWED_TOOLS", raising=False)
|
|
121
|
+
monkeypatch.setattr(cli, "_get_api", lambda args: api)
|
|
122
|
+
cli.cmd_call(_args(raw))
|
|
123
|
+
sent = api.call["data"]["arguments"]
|
|
124
|
+
assert sent["reference_image_base64"] == data_uri
|
|
125
|
+
assert sent["reference_images_base64"] == [data_uri]
|
|
126
|
+
assert sent["image_base64"] == data_uri
|
|
127
|
+
assert data_uri not in sent["reference_image_urls"]
|
|
128
|
+
assert sent["reference_image_urls"].startswith("https://agentools.uno/v1/files/f_")
|
|
129
|
+
assert len(api.uploads) == 1
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_non_whitelist_tool_does_not_upload(monkeypatch, capsys):
|
|
133
|
+
encoded = base64.b64encode(b"\xff\xd8\xff\xd9").decode()
|
|
134
|
+
data_uri = f"data:image/jpeg;base64,{encoded}"
|
|
135
|
+
raw = json.dumps({"prompt": "cat", "reference_image_urls": data_uri})
|
|
136
|
+
api = _API()
|
|
137
|
+
monkeypatch.delenv("UNO_ALLOWED_TOOLS", raising=False)
|
|
138
|
+
monkeypatch.setattr(cli, "_get_api", lambda args: api)
|
|
139
|
+
args = _args(raw)
|
|
140
|
+
args.tool = "seedream.generate"
|
|
141
|
+
cli.cmd_call(args)
|
|
142
|
+
assert api.uploads == []
|
|
143
|
+
assert api.call["data"]["arguments"]["reference_image_urls"] == data_uri
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_prompt_path_and_data_uri_are_not_uploaded(monkeypatch, capsys, tmp_path):
|
|
147
|
+
image = tmp_path / "cat.jpg"
|
|
148
|
+
image.write_bytes(b"\xff\xd8\xff\xd9")
|
|
149
|
+
data_uri = "data:image/jpeg;base64," + base64.b64encode(b"\xff\xd8\xff\xd9").decode()
|
|
150
|
+
prompt = f"use {image} and {data_uri} as the subject"
|
|
151
|
+
raw = json.dumps({"prompt": prompt})
|
|
152
|
+
api = _API()
|
|
153
|
+
monkeypatch.delenv("UNO_ALLOWED_TOOLS", raising=False)
|
|
154
|
+
monkeypatch.setattr(cli, "_get_api", lambda args: api)
|
|
155
|
+
cli.cmd_call(_args(raw))
|
|
156
|
+
assert api.uploads == []
|
|
157
|
+
assert api.call["data"]["arguments"]["prompt"] == prompt
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_upload_locator_rejects_comma():
|
|
161
|
+
from uno_cli.references import ReferenceUploadError, locator_from_upload
|
|
162
|
+
|
|
163
|
+
with pytest.raises(ReferenceUploadError):
|
|
164
|
+
locator_from_upload({"url": "https://example.com/a,b.png", "file_id": "f_ab,cd"})
|
|
165
|
+
assert "," not in locator_from_upload({"file_id": "f_" + "ab" * 16, "url": ""})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.5"
|
|
@@ -22,6 +22,13 @@ import urllib.parse
|
|
|
22
22
|
import urllib.request
|
|
23
23
|
|
|
24
24
|
from uno_cli._version import __version__
|
|
25
|
+
from uno_cli.references import (
|
|
26
|
+
CALL_ARGS_MAX_CHARS,
|
|
27
|
+
RAW_ARGS_MAX_CHARS,
|
|
28
|
+
ReferenceUploadError,
|
|
29
|
+
locator_from_upload,
|
|
30
|
+
rewrite_call_arguments,
|
|
31
|
+
)
|
|
25
32
|
|
|
26
33
|
DEFAULT_BASE = "https://agentools.uno"
|
|
27
34
|
LEGACY_BASE = "https://clawdtools.uno"
|
|
@@ -134,14 +141,22 @@ class UnoAPI:
|
|
|
134
141
|
h["Authorization"] = f"Bearer {self.api_key}"
|
|
135
142
|
return h
|
|
136
143
|
|
|
137
|
-
def _request(self, method, path, data=None, params=None, timeout=60):
|
|
144
|
+
def _request(self, method, path, data=None, params=None, timeout=60, *, raw=None, content_type=None):
|
|
138
145
|
url = f"{self.base}{path}"
|
|
139
146
|
if params:
|
|
140
147
|
url += "?" + urllib.parse.urlencode(
|
|
141
148
|
{k: v for k, v in params.items() if v is not None}
|
|
142
149
|
)
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
headers = self._headers()
|
|
151
|
+
if content_type:
|
|
152
|
+
headers["Content-Type"] = content_type
|
|
153
|
+
if raw is not None:
|
|
154
|
+
body = raw
|
|
155
|
+
elif data is not None:
|
|
156
|
+
body = json.dumps(data).encode()
|
|
157
|
+
else:
|
|
158
|
+
body = None
|
|
159
|
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
145
160
|
try:
|
|
146
161
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
147
162
|
raw = resp.read().decode()
|
|
@@ -170,6 +185,12 @@ class UnoAPI:
|
|
|
170
185
|
def post(self, path, data=None, timeout=60):
|
|
171
186
|
return self._request("POST", path, data=data, timeout=timeout)
|
|
172
187
|
|
|
188
|
+
def post_bytes(self, path, data, content_type, timeout=120):
|
|
189
|
+
"""POST raw bytes (reference image upload). Not the JSON ``--args`` body."""
|
|
190
|
+
return self._request(
|
|
191
|
+
"POST", path, raw=data, content_type=content_type, timeout=timeout,
|
|
192
|
+
)
|
|
193
|
+
|
|
173
194
|
def delete(self, path):
|
|
174
195
|
return self._request("DELETE", path)
|
|
175
196
|
|
|
@@ -700,8 +721,8 @@ def cmd_call(args):
|
|
|
700
721
|
raw_args = getattr(args, "args", None)
|
|
701
722
|
if raw_args:
|
|
702
723
|
raw_args = raw_args.replace("\x00", "")
|
|
703
|
-
if len(raw_args) >
|
|
704
|
-
_fail("Arguments too large
|
|
724
|
+
if len(raw_args) > RAW_ARGS_MAX_CHARS:
|
|
725
|
+
_fail("Arguments too large to upload")
|
|
705
726
|
try:
|
|
706
727
|
arguments = json.loads(raw_args)
|
|
707
728
|
if not isinstance(arguments, dict):
|
|
@@ -735,6 +756,11 @@ def cmd_call(args):
|
|
|
735
756
|
|
|
736
757
|
api = _get_api(args)
|
|
737
758
|
try:
|
|
759
|
+
arguments = rewrite_call_arguments(
|
|
760
|
+
arguments, _reference_uploader(api), tool_slug,
|
|
761
|
+
)
|
|
762
|
+
if len(json.dumps(arguments, ensure_ascii=False)) > CALL_ARGS_MAX_CHARS:
|
|
763
|
+
_fail("Arguments too large (max 100KB)")
|
|
738
764
|
result = api.post("/v1/call", {"tool": tool_slug, "arguments": arguments}, timeout=timeout)
|
|
739
765
|
if result.get("error"):
|
|
740
766
|
out = {"success": False, "error": result["error"]}
|
|
@@ -778,6 +804,19 @@ def cmd_call(args):
|
|
|
778
804
|
except APIError as e:
|
|
779
805
|
_out(_format_api_error(e.status, e.body))
|
|
780
806
|
sys.exit(1)
|
|
807
|
+
except ReferenceUploadError as e:
|
|
808
|
+
_fail(str(e))
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _reference_uploader(api):
|
|
812
|
+
"""Upload one image and return a comma-free URL or file_id."""
|
|
813
|
+
|
|
814
|
+
def upload(data, filename, media_type):
|
|
815
|
+
del filename
|
|
816
|
+
result = api.post_bytes("/v1/files", data, media_type, timeout=120)
|
|
817
|
+
return locator_from_upload(result)
|
|
818
|
+
|
|
819
|
+
return upload
|
|
781
820
|
|
|
782
821
|
|
|
783
822
|
def cmd_rate(args):
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Upload local paths and data URIs before they land in ``--args``.
|
|
2
|
+
|
|
3
|
+
The call payload stays under 100KB. Each upload locator is a public URL or
|
|
4
|
+
``file_id`` and does not contain a comma, so a later comma-split stays intact.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
CALL_ARGS_MAX_CHARS = 102400
|
|
13
|
+
RAW_ARGS_MAX_CHARS = 32 * 1024 * 1024
|
|
14
|
+
MAX_IMAGE_BYTES = 8 * 1024 * 1024
|
|
15
|
+
|
|
16
|
+
# Link fields only. A name containing "base64" is never uploaded, even if it
|
|
17
|
+
# is later added here. Tools absent from this map are left untouched.
|
|
18
|
+
_LINK_FIELDS = frozenset({
|
|
19
|
+
"reference_image_urls",
|
|
20
|
+
"reference_image_url",
|
|
21
|
+
"image_url",
|
|
22
|
+
"image_urls",
|
|
23
|
+
})
|
|
24
|
+
_UPLOAD_FIELDS = {
|
|
25
|
+
"gpt-image-2.gpt_image2_submit": _LINK_FIELDS,
|
|
26
|
+
}
|
|
27
|
+
_IMAGE_EXT = {
|
|
28
|
+
".jpg": "image/jpeg",
|
|
29
|
+
".jpeg": "image/jpeg",
|
|
30
|
+
".png": "image/png",
|
|
31
|
+
".webp": "image/webp",
|
|
32
|
+
".gif": "image/gif",
|
|
33
|
+
}
|
|
34
|
+
_NEXT_REF = ("http://", "https://", "data:")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ReferenceUploadError(Exception):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def split_reference_list(raw: str) -> list:
|
|
42
|
+
"""Split comma-separated refs without breaking a data URI on its comma."""
|
|
43
|
+
text = raw.strip()
|
|
44
|
+
if not text:
|
|
45
|
+
return []
|
|
46
|
+
parts = []
|
|
47
|
+
index = 0
|
|
48
|
+
length = len(text)
|
|
49
|
+
while index < length:
|
|
50
|
+
while index < length and text[index] in ", \t\r\n":
|
|
51
|
+
index += 1
|
|
52
|
+
if index >= length:
|
|
53
|
+
break
|
|
54
|
+
if text.startswith("data:", index):
|
|
55
|
+
comma = text.find(",", index)
|
|
56
|
+
if comma == -1:
|
|
57
|
+
token = text[index:].strip()
|
|
58
|
+
if token:
|
|
59
|
+
parts.append(token)
|
|
60
|
+
break
|
|
61
|
+
cursor = comma + 1
|
|
62
|
+
end = length
|
|
63
|
+
while True:
|
|
64
|
+
nxt = text.find(",", cursor)
|
|
65
|
+
if nxt == -1:
|
|
66
|
+
break
|
|
67
|
+
rest = text[nxt + 1:].lstrip()
|
|
68
|
+
if rest.startswith(_NEXT_REF):
|
|
69
|
+
end = nxt
|
|
70
|
+
break
|
|
71
|
+
cursor = nxt + 1
|
|
72
|
+
token = text[index:end].strip()
|
|
73
|
+
if token:
|
|
74
|
+
parts.append(token)
|
|
75
|
+
index = end
|
|
76
|
+
continue
|
|
77
|
+
comma = text.find(",", index)
|
|
78
|
+
if comma == -1:
|
|
79
|
+
token = text[index:].strip()
|
|
80
|
+
if token:
|
|
81
|
+
parts.append(token)
|
|
82
|
+
break
|
|
83
|
+
token = text[index:comma].strip()
|
|
84
|
+
if token:
|
|
85
|
+
parts.append(token)
|
|
86
|
+
index = comma + 1
|
|
87
|
+
return parts
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def locator_from_upload(payload) -> str:
|
|
91
|
+
"""Pick the public URL, else file_id. Either must be free of commas."""
|
|
92
|
+
if not isinstance(payload, dict):
|
|
93
|
+
raise ReferenceUploadError("reference upload returned no locator")
|
|
94
|
+
url = payload.get("url") or ""
|
|
95
|
+
file_id = payload.get("file_id") or ""
|
|
96
|
+
if isinstance(url, str) and url and "," not in url:
|
|
97
|
+
return url
|
|
98
|
+
if isinstance(file_id, str) and file_id and "," not in file_id:
|
|
99
|
+
return file_id
|
|
100
|
+
raise ReferenceUploadError("upload locator must not contain a comma")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decode_data_uri(value: str):
|
|
104
|
+
if not value.startswith("data:") or "," not in value:
|
|
105
|
+
raise ReferenceUploadError("invalid data uri")
|
|
106
|
+
header, payload = value.split(",", 1)
|
|
107
|
+
if ";base64" not in header.lower():
|
|
108
|
+
raise ReferenceUploadError("data uri must be base64")
|
|
109
|
+
media = header[5:].split(";", 1)[0].strip().lower() or "application/octet-stream"
|
|
110
|
+
if media not in _IMAGE_EXT.values():
|
|
111
|
+
raise ReferenceUploadError("unsupported image type")
|
|
112
|
+
try:
|
|
113
|
+
data = base64.b64decode(payload, validate=True)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
raise ReferenceUploadError("invalid base64") from exc
|
|
116
|
+
return data, media
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_data_uri(value: str) -> bool:
|
|
120
|
+
return value.startswith("data:image/") and "," in value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _local_image(value: str):
|
|
124
|
+
if value.startswith(("http://", "https://", "data:")):
|
|
125
|
+
return None
|
|
126
|
+
path = os.path.expanduser(value)
|
|
127
|
+
ext = os.path.splitext(path)[1].lower()
|
|
128
|
+
media = _IMAGE_EXT.get(ext)
|
|
129
|
+
if not media or not os.path.isfile(path):
|
|
130
|
+
return None
|
|
131
|
+
return path, media
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _should_upload(value: str) -> bool:
|
|
135
|
+
if _is_data_uri(value):
|
|
136
|
+
return True
|
|
137
|
+
return _local_image(value) is not None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _field_allows_upload(field, allowed) -> bool:
|
|
141
|
+
if not isinstance(field, str) or "base64" in field.lower():
|
|
142
|
+
return False
|
|
143
|
+
return field in allowed
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _upload_one(value: str, upload) -> str:
|
|
147
|
+
if _is_data_uri(value):
|
|
148
|
+
data, media = decode_data_uri(value)
|
|
149
|
+
filename = "reference." + next(ext.lstrip(".") for ext, kind in _IMAGE_EXT.items() if kind == media)
|
|
150
|
+
else:
|
|
151
|
+
found = _local_image(value)
|
|
152
|
+
if not found:
|
|
153
|
+
raise ReferenceUploadError("not a reference image")
|
|
154
|
+
path, media = found
|
|
155
|
+
filename = os.path.basename(path)
|
|
156
|
+
with open(path, "rb") as handle:
|
|
157
|
+
data = handle.read()
|
|
158
|
+
if not data:
|
|
159
|
+
raise ReferenceUploadError("reference image is empty")
|
|
160
|
+
if len(data) > MAX_IMAGE_BYTES:
|
|
161
|
+
raise ReferenceUploadError("reference image exceeds 8MB")
|
|
162
|
+
locator = upload(data, filename, media)
|
|
163
|
+
if not isinstance(locator, str) or not locator or "," in locator:
|
|
164
|
+
raise ReferenceUploadError("upload locator must not contain a comma")
|
|
165
|
+
return locator
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _rewrite_string(value: str, upload) -> str:
|
|
169
|
+
if "," not in value and not _is_data_uri(value):
|
|
170
|
+
if _should_upload(value):
|
|
171
|
+
return _upload_one(value, upload)
|
|
172
|
+
return value
|
|
173
|
+
parts = split_reference_list(value)
|
|
174
|
+
if len(parts) == 1 and _should_upload(parts[0]):
|
|
175
|
+
return _upload_one(parts[0], upload)
|
|
176
|
+
if not any(_should_upload(part) for part in parts):
|
|
177
|
+
return value
|
|
178
|
+
rewritten = []
|
|
179
|
+
for part in parts:
|
|
180
|
+
if _should_upload(part):
|
|
181
|
+
rewritten.append(_upload_one(part, upload))
|
|
182
|
+
else:
|
|
183
|
+
rewritten.append(part)
|
|
184
|
+
return ",".join(rewritten)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _rewrite(obj, upload, allowed, field):
|
|
188
|
+
if isinstance(obj, dict):
|
|
189
|
+
return {key: _rewrite(val, upload, allowed, key) for key, val in obj.items()}
|
|
190
|
+
if isinstance(obj, list):
|
|
191
|
+
return [_rewrite(item, upload, allowed, field) for item in obj]
|
|
192
|
+
if isinstance(obj, str) and _field_allows_upload(field, allowed):
|
|
193
|
+
return _rewrite_string(obj, upload)
|
|
194
|
+
return obj
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def rewrite_call_arguments(obj, upload, tool_slug=None, field=None):
|
|
198
|
+
"""Replace inline images on whitelisted link fields.
|
|
199
|
+
|
|
200
|
+
``upload(data, filename, media) -> str``. Fields whose names contain
|
|
201
|
+
``base64``, every other field (including ``prompt``), and tools that are
|
|
202
|
+
not listed are returned unchanged.
|
|
203
|
+
"""
|
|
204
|
+
allowed = _UPLOAD_FIELDS.get(tool_slug or "", frozenset())
|
|
205
|
+
return _rewrite(obj, upload, allowed, field)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|