outo-models-cli 0.2.1__tar.gz → 0.2.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.
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/PKG-INFO +1 -1
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/pyproject.toml +1 -1
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/api/lfs_batch.py +2 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/api/lfs_upload.py +14 -3
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/api/upload.py +2 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/errors.py +12 -4
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/http.py +13 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_errors.py +23 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_lfs_upload.py +35 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/uv.lock +1 -1
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/.gitignore +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/LICENSE +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/README.md +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/__init__.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/__main__.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/api/__init__.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/api/auth.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/api/lfs.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/api/repos.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/__init__.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/_shared.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/_upload_commit.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/auth.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/download.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/ls.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/repo.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/upload.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/config.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/downloader/__init__.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/downloader/transfer.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/downloader/walk.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/main.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/matchers.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/conftest.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_api_crud.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_api_upload.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_cli_auth.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_cli_errors.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_cli_help.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_cli_repo.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_cli_upload_lfs.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_cli_xfer.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_config.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_download_lfs.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_download_repo.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_download_transfer.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_download_walk.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_lfs.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_matchers.py +0 -0
- {outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/tests/test_upload_commit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: outo-models-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Command-line client for self-hosted outo-models servers — auth, repo, download, and upload.
|
|
5
5
|
Project-URL: Homepage, https://github.com/outo-models/outo-models
|
|
6
6
|
Project-URL: Documentation, https://github.com/outo-models/outo-models/blob/main/omc/README.md
|
|
@@ -23,6 +23,7 @@ import httpx
|
|
|
23
23
|
from outo_models_cli.api import send, unwrap
|
|
24
24
|
from outo_models_cli.api.lfs import LFS_CONTENT_TYPE
|
|
25
25
|
from outo_models_cli.errors import BadResponseError, map_response_error
|
|
26
|
+
from outo_models_cli.http import TIMEOUT_BATCH
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
@dataclass(frozen=True, slots=True)
|
|
@@ -72,6 +73,7 @@ def batch_upload(
|
|
|
72
73
|
f"/{owner}/{name}.git/info/lfs/objects/batch",
|
|
73
74
|
headers={"Accept": LFS_CONTENT_TYPE, "Content-Type": LFS_CONTENT_TYPE},
|
|
74
75
|
json=body,
|
|
76
|
+
timeout=TIMEOUT_BATCH,
|
|
75
77
|
)
|
|
76
78
|
if response.status_code == 404:
|
|
77
79
|
raise BadResponseError(
|
|
@@ -37,6 +37,7 @@ from outo_models_cli.errors import (
|
|
|
37
37
|
FileTooLargeError,
|
|
38
38
|
map_response_error,
|
|
39
39
|
)
|
|
40
|
+
from outo_models_cli.http import TIMEOUT_PUT
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
class _ProgressLike(Protocol):
|
|
@@ -84,10 +85,11 @@ def _put_stream(
|
|
|
84
85
|
chunk = fp.read(PUT_CHUNK_BYTES)
|
|
85
86
|
if not chunk:
|
|
86
87
|
break
|
|
87
|
-
progress
|
|
88
|
+
if progress is not None and task_id is not None:
|
|
89
|
+
progress.update(task_id, advance=len(chunk))
|
|
88
90
|
yield chunk
|
|
89
91
|
|
|
90
|
-
response = client.put(url, content=_chunk_iter(), headers=headers)
|
|
92
|
+
response = client.put(url, content=_chunk_iter(), headers=headers, timeout=TIMEOUT_PUT)
|
|
91
93
|
if response.status_code == 404:
|
|
92
94
|
raise BadResponseError("LFS object endpoint returned 404.")
|
|
93
95
|
if response.status_code == 401:
|
|
@@ -96,8 +98,17 @@ def _put_stream(
|
|
|
96
98
|
"Check that the configured PAT matches the account owner.",
|
|
97
99
|
)
|
|
98
100
|
if response.status_code == 413:
|
|
101
|
+
# Prefer the server's reason: it names the configured limit and the
|
|
102
|
+
# env var that raises it (field failure: the generic line gave the
|
|
103
|
+
# operator nothing actionable).
|
|
104
|
+
detail = ""
|
|
105
|
+
try:
|
|
106
|
+
detail = str(response.json().get("error", ""))
|
|
107
|
+
except ValueError:
|
|
108
|
+
detail = response.text[:200]
|
|
109
|
+
suffix = f" ({detail})" if detail else ""
|
|
99
110
|
raise FileTooLargeError(
|
|
100
|
-
f"
|
|
111
|
+
f"{path.name} was rejected by the server as too large.{suffix}",
|
|
101
112
|
)
|
|
102
113
|
if response.status_code >= 400:
|
|
103
114
|
raise map_response_error(response)
|
|
@@ -15,6 +15,7 @@ from outo_models_cli.api import (
|
|
|
15
15
|
unwrap,
|
|
16
16
|
)
|
|
17
17
|
from outo_models_cli.errors import BadResponseError, map_response_error
|
|
18
|
+
from outo_models_cli.http import TIMEOUT_COMMIT
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def upload(
|
|
@@ -84,6 +85,7 @@ def upload(
|
|
|
84
85
|
f"/api/repos/{owner}/{name}/upload",
|
|
85
86
|
files=multipart_files,
|
|
86
87
|
data=fields,
|
|
88
|
+
timeout=TIMEOUT_COMMIT,
|
|
87
89
|
)
|
|
88
90
|
if response.status_code >= 400:
|
|
89
91
|
raise map_response_error(response)
|
|
@@ -130,12 +130,20 @@ def map_response_error(response: httpx.Response, *, fallback: str | None = None)
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def map_transport_error(exc: httpx.HTTPError) -> OmcError:
|
|
133
|
-
"""Wrap an `httpx.HTTPError` as
|
|
133
|
+
"""Wrap an `httpx.HTTPError` as a stable, actionable CLI error.
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
Timeouts get their own message (field failure: a slow-but-working
|
|
136
|
+
server was reported as "cannot reach", sending the operator hunting
|
|
137
|
+
for outages). A timeout often means the server is STILL processing —
|
|
138
|
+
retrying a commit can duplicate work, so tell the user to check
|
|
139
|
+
first.
|
|
138
140
|
"""
|
|
141
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
142
|
+
return ServerUnreachableError(
|
|
143
|
+
"The server did not respond in time. It may still be processing — "
|
|
144
|
+
"verify with `omc ls` before retrying, then use a longer timeout "
|
|
145
|
+
"if it keeps happening.",
|
|
146
|
+
)
|
|
139
147
|
return ServerUnreachableError(
|
|
140
148
|
"Cannot reach the server. Check the URL, network, and that the server is running.",
|
|
141
149
|
)
|
|
@@ -19,6 +19,16 @@ import httpx
|
|
|
19
19
|
# the connection for a few seconds. Anything longer is genuinely hung.
|
|
20
20
|
_TIMEOUT = httpx.Timeout(30.0, connect=5.0)
|
|
21
21
|
|
|
22
|
+
# Long operations need long read timeouts. A multipart commit after a
|
|
23
|
+
# multi-hundred-GiB LFS session does real server-side work (worktree
|
|
24
|
+
# clone, fetch, quota, audit) that can exceed 30 s — the field failure
|
|
25
|
+
# surfaced as "Cannot reach the server" while the server was alive and
|
|
26
|
+
# working. Writes stay unlimited: the transport buffers, so a slow disk
|
|
27
|
+
# on either side must not abort a 3 GiB PUT mid-stream.
|
|
28
|
+
TIMEOUT_BATCH = httpx.Timeout(120.0, connect=5.0)
|
|
29
|
+
TIMEOUT_COMMIT = httpx.Timeout(600.0, connect=5.0)
|
|
30
|
+
TIMEOUT_PUT = httpx.Timeout(None, connect=10.0, read=900.0)
|
|
31
|
+
|
|
22
32
|
|
|
23
33
|
def build_client(
|
|
24
34
|
base_url: str,
|
|
@@ -138,6 +148,9 @@ def build_basic_async_client(
|
|
|
138
148
|
|
|
139
149
|
|
|
140
150
|
__all__ = [
|
|
151
|
+
"TIMEOUT_BATCH",
|
|
152
|
+
"TIMEOUT_COMMIT",
|
|
153
|
+
"TIMEOUT_PUT",
|
|
141
154
|
"basic_auth_header",
|
|
142
155
|
"build_async_client",
|
|
143
156
|
"build_basic_async_client",
|
|
@@ -105,3 +105,26 @@ def test_every_omc_error_has_stable_code() -> None:
|
|
|
105
105
|
assert ValidationFailedError.code == "validation_failed"
|
|
106
106
|
assert ServerUnreachableError.code == "server_unreachable"
|
|
107
107
|
assert BadResponseError.code == "bad_response"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestTimeoutMessageSplit:
|
|
111
|
+
"""A working-but-slow server is NOT 'unreachable' (field failure)."""
|
|
112
|
+
|
|
113
|
+
def test_timeout_gets_its_own_message(self):
|
|
114
|
+
import httpx
|
|
115
|
+
|
|
116
|
+
from outo_models_cli.errors import map_transport_error
|
|
117
|
+
|
|
118
|
+
exc = httpx.ReadTimeout("took too long")
|
|
119
|
+
err = map_transport_error(exc)
|
|
120
|
+
assert "did not respond in time" in str(err)
|
|
121
|
+
assert "Cannot reach" not in str(err)
|
|
122
|
+
|
|
123
|
+
def test_connect_error_keeps_unreachable_message(self):
|
|
124
|
+
import httpx
|
|
125
|
+
|
|
126
|
+
from outo_models_cli.errors import map_transport_error
|
|
127
|
+
|
|
128
|
+
exc = httpx.ConnectError("refused")
|
|
129
|
+
err = map_transport_error(exc)
|
|
130
|
+
assert "Cannot reach the server" in str(err)
|
|
@@ -64,3 +64,38 @@ class TestPartitionHashingProgress:
|
|
|
64
64
|
partition = partition_files([big, small], cap_bytes=512, show_progress=True)
|
|
65
65
|
assert [f.name for f in partition.small] == ["small.txt"]
|
|
66
66
|
assert partition.large and partition.large[0].path.name == "big.bin"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Test413SurfacesServerReason:
|
|
70
|
+
def test_put_413_includes_server_detail(self, tmp_path):
|
|
71
|
+
import httpx
|
|
72
|
+
import pytest
|
|
73
|
+
|
|
74
|
+
from outo_models_cli.api.lfs_upload import _put_stream
|
|
75
|
+
from outo_models_cli.errors import FileTooLargeError
|
|
76
|
+
|
|
77
|
+
big = tmp_path / "shard-00001.safetensors"
|
|
78
|
+
big.write_bytes(b"z" * 32)
|
|
79
|
+
transport = httpx.MockTransport(
|
|
80
|
+
lambda req: httpx.Response(
|
|
81
|
+
413,
|
|
82
|
+
json={
|
|
83
|
+
"error": (
|
|
84
|
+
"object exceeds per-object limit 107374182400 bytes "
|
|
85
|
+
"(raise OUTO_LFS_MAX_OBJECT_BYTES ...)"
|
|
86
|
+
)
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
client = httpx.Client(transport=transport)
|
|
91
|
+
with pytest.raises(FileTooLargeError) as excinfo:
|
|
92
|
+
_put_stream(
|
|
93
|
+
client,
|
|
94
|
+
url="http://srv/put",
|
|
95
|
+
path=big,
|
|
96
|
+
extra_headers={},
|
|
97
|
+
progress=None,
|
|
98
|
+
task_id=None,
|
|
99
|
+
)
|
|
100
|
+
assert "shard-00001.safetensors" in str(excinfo.value)
|
|
101
|
+
assert "OUTO_LFS_MAX_OBJECT_BYTES" in str(excinfo.value)
|
|
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
|
{outo_models_cli-0.2.1 → outo_models_cli-0.2.3}/src/outo_models_cli/commands/_upload_commit.py
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|