contree-client 0.2.1.dev0__tar.gz → 0.2.2__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.
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/PKG-INFO +1 -1
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/base.py +156 -26
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/models.py +19 -8
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/operations.py +5 -4
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/runtime.py +13 -4
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/spec_info.py +15 -6
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/pyproject.toml +1 -1
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/pyproject.toml.orig +1 -1
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/stub_server.py +45 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_operation_helpers.py +49 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_runtime.py +14 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/LICENSE +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/README.md +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/__init__.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/aiohttp.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/asyncio.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/exceptions.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/http.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/httpx.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/profiles.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/py.typed +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/requests.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/sync.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/testing.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/types.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/contree_client/urllib3.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/conftest.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_aiohttp_payload_interruption.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_autodetect.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_clients.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_file_helpers.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_integration.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_interface.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_keepalive.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_logging.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_models.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_operations.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_pagination.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_profiles.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_retry.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_sse.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_stream_lifecycle.py +0 -0
- {contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_testing.py +0 -0
|
@@ -16,9 +16,9 @@ import platform
|
|
|
16
16
|
import sys
|
|
17
17
|
import time
|
|
18
18
|
from abc import ABC, abstractmethod
|
|
19
|
-
from collections.abc import AsyncGenerator, Iterable, Iterator
|
|
19
|
+
from collections.abc import AsyncGenerator, Iterable, Iterator, Sequence
|
|
20
20
|
from contextlib import aclosing
|
|
21
|
-
from datetime import datetime
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from types import EllipsisType, TracebackType
|
|
24
24
|
from typing import IO, Any, Literal, TypeVar
|
|
@@ -35,6 +35,7 @@ from .exceptions import (
|
|
|
35
35
|
from .models import (
|
|
36
36
|
ClosableStreamRepr,
|
|
37
37
|
DirectoryList,
|
|
38
|
+
EventDataCompletion,
|
|
38
39
|
File,
|
|
39
40
|
FileResponse,
|
|
40
41
|
FilesListResponse,
|
|
@@ -55,6 +56,7 @@ from .models import (
|
|
|
55
56
|
)
|
|
56
57
|
from .profiles import AUTH_TYPE_IAM, Profile, ProfileError, resolve_profile
|
|
57
58
|
from .runtime import (
|
|
59
|
+
EVENTS_UNAVAILABLE_STATUSES,
|
|
58
60
|
TIGHT_LOOP_FLOOR,
|
|
59
61
|
BodyFormatter,
|
|
60
62
|
HeaderFormatter,
|
|
@@ -370,10 +372,13 @@ class ContreeSyncClient(ContreeClientBase, ABC):
|
|
|
370
372
|
(410/425/5xx) reconnect from the last received event id, so no
|
|
371
373
|
event is delivered twice; before every reconnect the operation
|
|
372
374
|
status is probed and iteration ends if it already became
|
|
373
|
-
terminal.
|
|
375
|
+
terminal. A status meaning the events endpoint itself doesn't
|
|
376
|
+
exist for this operation/server (400/404/405/406) stops
|
|
377
|
+
reconnecting and instead polls :meth:`get_operation_status`
|
|
378
|
+
until the operation is terminal. Other API errors propagate.
|
|
374
379
|
Iteration also ends after the ``completion`` event. *timeout*
|
|
375
|
-
bounds the whole wait; it is enforced between events
|
|
376
|
-
|
|
380
|
+
bounds the whole wait; it is enforced between events, reconnect
|
|
381
|
+
cycles and polling, and raises :class:`TimeoutError`.
|
|
377
382
|
"""
|
|
378
383
|
last_id = last_event_id
|
|
379
384
|
delays = retry_generator()
|
|
@@ -410,6 +415,55 @@ class ContreeSyncClient(ContreeClientBase, ABC):
|
|
|
410
415
|
last_id = exc.last_event_id
|
|
411
416
|
self.log.warning("stream error (last_id=%s): %s", last_id, exc)
|
|
412
417
|
except ContreeAPIError as exc:
|
|
418
|
+
if exc.status in EVENTS_UNAVAILABLE_STATUSES:
|
|
419
|
+
# the endpoint is gone, not just failing: reconnecting
|
|
420
|
+
# the same request will never work, but the operation
|
|
421
|
+
# may still finish - stop touching /events and poll
|
|
422
|
+
# status instead for the rest of this wait
|
|
423
|
+
self.log.warning(
|
|
424
|
+
"events endpoint unavailable (%d), falling back to polling",
|
|
425
|
+
exc.status,
|
|
426
|
+
)
|
|
427
|
+
while True:
|
|
428
|
+
check_deadline()
|
|
429
|
+
try:
|
|
430
|
+
response = self.get_operation_status(operation_id)
|
|
431
|
+
except (ContreeError, *self.retryable_errors):
|
|
432
|
+
response = None
|
|
433
|
+
if (
|
|
434
|
+
response is not None
|
|
435
|
+
and not isinstance(response.status, EllipsisType)
|
|
436
|
+
and response.status.is_terminal()
|
|
437
|
+
):
|
|
438
|
+
# no event log to relay, but a caller of
|
|
439
|
+
# follow_operation_events must still observe a
|
|
440
|
+
# terminal completion rather than nothing at all
|
|
441
|
+
last_id = 0 if last_id is None else last_id + 1
|
|
442
|
+
duration = response.duration
|
|
443
|
+
image_size = response.image_size
|
|
444
|
+
yield OperationEvent(
|
|
445
|
+
id=last_id,
|
|
446
|
+
ts=datetime.now(timezone.utc),
|
|
447
|
+
type="completion",
|
|
448
|
+
data=EventDataCompletion(
|
|
449
|
+
status=response.status,
|
|
450
|
+
duration_ms=round(duration * 1000)
|
|
451
|
+
if isinstance(duration, (int, float))
|
|
452
|
+
else 0,
|
|
453
|
+
result_image_uuid=response.result_image_uuid,
|
|
454
|
+
error=response.error,
|
|
455
|
+
image_size_bytes=(
|
|
456
|
+
image_size
|
|
457
|
+
if isinstance(image_size, int)
|
|
458
|
+
else ...
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
return
|
|
463
|
+
delay = next(delays)
|
|
464
|
+
if deadline is not None:
|
|
465
|
+
delay = min(delay, max(0.0, deadline - time.monotonic()))
|
|
466
|
+
time.sleep(delay)
|
|
413
467
|
retryable = exc.status in (410, 425) or 500 <= exc.status < 600
|
|
414
468
|
if not retryable:
|
|
415
469
|
raise
|
|
@@ -925,9 +979,12 @@ class ContreeSyncClient(ContreeClientBase, ABC):
|
|
|
925
979
|
uid: User ID to run the process as. Example: ``0``.
|
|
926
980
|
gid: Group ID to run the process as. Example: ``0``.
|
|
927
981
|
timeout: Maximum execution time in seconds Example: ``60``.
|
|
928
|
-
truncate_output_at: Maximum number of bytes to keep
|
|
929
|
-
stdout
|
|
930
|
-
|
|
982
|
+
truncate_output_at: Maximum number of bytes to keep per stream
|
|
983
|
+
(stdout, stderr, and stdin) in the subprocess event log.
|
|
984
|
+
Does not affect stdin delivery to the child, which always
|
|
985
|
+
receives the complete stream regardless of this cap. Default
|
|
986
|
+
is ``1048576`` (1 MiB). The maximum value is ``10485760``
|
|
987
|
+
(10 MiB). Example: ``65535``.
|
|
931
988
|
files: Map of absolute file paths to file metadata Example:
|
|
932
989
|
``{"/root/hello.txt":{"uuid":"a9165a5d-5c86-4bd8-8ee4-ae46c...``.
|
|
933
990
|
"""
|
|
@@ -1335,9 +1392,11 @@ class ContreeSyncClient(ContreeClientBase, ABC):
|
|
|
1335
1392
|
main process and exec'd subprocesses. With ``close=false``
|
|
1336
1393
|
the pipe stays open and more data can be written via
|
|
1337
1394
|
``POST /operations/{operationId}/subprocesses/{spid}/stdin``.
|
|
1338
|
-
truncate_output_at: Maximum number of bytes
|
|
1339
|
-
|
|
1340
|
-
|
|
1395
|
+
truncate_output_at: Maximum number of bytes to keep per stream
|
|
1396
|
+
(stdout, stderr, and stdin) in the subprocess event log.
|
|
1397
|
+
Does not affect delivery to the child — stdin is always
|
|
1398
|
+
delivered in full regardless of this cap. Defaults to 1 MiB;
|
|
1399
|
+
capped at 10 MiB. Example: ``65535``.
|
|
1341
1400
|
"""
|
|
1342
1401
|
spec = operations.build_operation_subprocess_create(
|
|
1343
1402
|
operation_id=operation_id,
|
|
@@ -1618,10 +1677,10 @@ class ContreeSyncClient(ContreeClientBase, ABC):
|
|
|
1618
1677
|
def inspect_image_grep(
|
|
1619
1678
|
self,
|
|
1620
1679
|
image_uuid: str,
|
|
1621
|
-
pattern: str,
|
|
1680
|
+
pattern: str | Sequence[str],
|
|
1622
1681
|
*,
|
|
1623
|
-
path: str | None = None,
|
|
1624
|
-
glob: str | None = None,
|
|
1682
|
+
path: str | Sequence[str] | None = None,
|
|
1683
|
+
glob: str | Sequence[str] | None = None,
|
|
1625
1684
|
max_count: int | None = None,
|
|
1626
1685
|
max_total: int | None = None,
|
|
1627
1686
|
case: Literal["sensitive", "insensitive", "smart"] | None = None,
|
|
@@ -1863,10 +1922,13 @@ class ContreeAsyncClient(ContreeClientBase, ABC):
|
|
|
1863
1922
|
(410/425/5xx) reconnect from the last received event id, so no
|
|
1864
1923
|
event is delivered twice; before every reconnect the operation
|
|
1865
1924
|
status is probed and iteration ends if it already became
|
|
1866
|
-
terminal.
|
|
1925
|
+
terminal. A status meaning the events endpoint itself doesn't
|
|
1926
|
+
exist for this operation/server (400/404/405/406) stops
|
|
1927
|
+
reconnecting and instead polls :meth:`get_operation_status`
|
|
1928
|
+
until the operation is terminal. Other API errors propagate.
|
|
1867
1929
|
Iteration also ends after the ``completion`` event. *timeout*
|
|
1868
|
-
bounds the whole wait; it is enforced between events
|
|
1869
|
-
|
|
1930
|
+
bounds the whole wait; it is enforced between events, reconnect
|
|
1931
|
+
cycles and polling, and raises :class:`TimeoutError`.
|
|
1870
1932
|
"""
|
|
1871
1933
|
last_id = last_event_id
|
|
1872
1934
|
delays = retry_generator()
|
|
@@ -1894,6 +1956,24 @@ class ContreeAsyncClient(ContreeClientBase, ABC):
|
|
|
1894
1956
|
check_deadline()
|
|
1895
1957
|
raise
|
|
1896
1958
|
|
|
1959
|
+
async def operation_status_before_deadline() -> OperationResponse | None:
|
|
1960
|
+
try:
|
|
1961
|
+
if deadline is None:
|
|
1962
|
+
return await self.get_operation_status(operation_id)
|
|
1963
|
+
remaining = deadline - time.monotonic()
|
|
1964
|
+
if remaining <= 0:
|
|
1965
|
+
check_deadline()
|
|
1966
|
+
try:
|
|
1967
|
+
return await asyncio.wait_for(
|
|
1968
|
+
self.get_operation_status(operation_id),
|
|
1969
|
+
timeout=remaining,
|
|
1970
|
+
)
|
|
1971
|
+
except asyncio.TimeoutError:
|
|
1972
|
+
check_deadline()
|
|
1973
|
+
raise
|
|
1974
|
+
except (ContreeError, *self.retryable_errors):
|
|
1975
|
+
return None
|
|
1976
|
+
|
|
1897
1977
|
while True:
|
|
1898
1978
|
check_deadline()
|
|
1899
1979
|
events_before = last_id
|
|
@@ -1923,6 +2003,51 @@ class ContreeAsyncClient(ContreeClientBase, ABC):
|
|
|
1923
2003
|
last_id = exc.last_event_id
|
|
1924
2004
|
self.log.warning("stream error (last_id=%s): %s", last_id, exc)
|
|
1925
2005
|
except ContreeAPIError as exc:
|
|
2006
|
+
if exc.status in EVENTS_UNAVAILABLE_STATUSES:
|
|
2007
|
+
# the endpoint is gone, not just failing: reconnecting
|
|
2008
|
+
# the same request will never work, but the operation
|
|
2009
|
+
# may still finish - stop touching /events and poll
|
|
2010
|
+
# status instead for the rest of this wait
|
|
2011
|
+
self.log.warning(
|
|
2012
|
+
"events endpoint unavailable (%d), falling back to polling",
|
|
2013
|
+
exc.status,
|
|
2014
|
+
)
|
|
2015
|
+
while True:
|
|
2016
|
+
response = await operation_status_before_deadline()
|
|
2017
|
+
if (
|
|
2018
|
+
response is not None
|
|
2019
|
+
and not isinstance(response.status, EllipsisType)
|
|
2020
|
+
and response.status.is_terminal()
|
|
2021
|
+
):
|
|
2022
|
+
# no event log to relay, but a caller of
|
|
2023
|
+
# follow_operation_events must still observe a
|
|
2024
|
+
# terminal completion rather than nothing at all
|
|
2025
|
+
last_id = 0 if last_id is None else last_id + 1
|
|
2026
|
+
duration = response.duration
|
|
2027
|
+
image_size = response.image_size
|
|
2028
|
+
yield OperationEvent(
|
|
2029
|
+
id=last_id,
|
|
2030
|
+
ts=datetime.now(timezone.utc),
|
|
2031
|
+
type="completion",
|
|
2032
|
+
data=EventDataCompletion(
|
|
2033
|
+
status=response.status,
|
|
2034
|
+
duration_ms=round(duration * 1000)
|
|
2035
|
+
if isinstance(duration, (int, float))
|
|
2036
|
+
else 0,
|
|
2037
|
+
result_image_uuid=response.result_image_uuid,
|
|
2038
|
+
error=response.error,
|
|
2039
|
+
image_size_bytes=(
|
|
2040
|
+
image_size
|
|
2041
|
+
if isinstance(image_size, int)
|
|
2042
|
+
else ...
|
|
2043
|
+
),
|
|
2044
|
+
),
|
|
2045
|
+
)
|
|
2046
|
+
return
|
|
2047
|
+
delay = next(delays)
|
|
2048
|
+
if deadline is not None:
|
|
2049
|
+
delay = min(delay, max(0.0, deadline - time.monotonic()))
|
|
2050
|
+
await asyncio.sleep(delay)
|
|
1926
2051
|
retryable = exc.status in (410, 425) or 500 <= exc.status < 600
|
|
1927
2052
|
if not retryable:
|
|
1928
2053
|
raise
|
|
@@ -2450,9 +2575,12 @@ class ContreeAsyncClient(ContreeClientBase, ABC):
|
|
|
2450
2575
|
uid: User ID to run the process as. Example: ``0``.
|
|
2451
2576
|
gid: Group ID to run the process as. Example: ``0``.
|
|
2452
2577
|
timeout: Maximum execution time in seconds Example: ``60``.
|
|
2453
|
-
truncate_output_at: Maximum number of bytes to keep
|
|
2454
|
-
stdout
|
|
2455
|
-
|
|
2578
|
+
truncate_output_at: Maximum number of bytes to keep per stream
|
|
2579
|
+
(stdout, stderr, and stdin) in the subprocess event log.
|
|
2580
|
+
Does not affect stdin delivery to the child, which always
|
|
2581
|
+
receives the complete stream regardless of this cap. Default
|
|
2582
|
+
is ``1048576`` (1 MiB). The maximum value is ``10485760``
|
|
2583
|
+
(10 MiB). Example: ``65535``.
|
|
2456
2584
|
files: Map of absolute file paths to file metadata Example:
|
|
2457
2585
|
``{"/root/hello.txt":{"uuid":"a9165a5d-5c86-4bd8-8ee4-ae46c...``.
|
|
2458
2586
|
"""
|
|
@@ -2861,9 +2989,11 @@ class ContreeAsyncClient(ContreeClientBase, ABC):
|
|
|
2861
2989
|
main process and exec'd subprocesses. With ``close=false``
|
|
2862
2990
|
the pipe stays open and more data can be written via
|
|
2863
2991
|
``POST /operations/{operationId}/subprocesses/{spid}/stdin``.
|
|
2864
|
-
truncate_output_at: Maximum number of bytes
|
|
2865
|
-
|
|
2866
|
-
|
|
2992
|
+
truncate_output_at: Maximum number of bytes to keep per stream
|
|
2993
|
+
(stdout, stderr, and stdin) in the subprocess event log.
|
|
2994
|
+
Does not affect delivery to the child — stdin is always
|
|
2995
|
+
delivered in full regardless of this cap. Defaults to 1 MiB;
|
|
2996
|
+
capped at 10 MiB. Example: ``65535``.
|
|
2867
2997
|
"""
|
|
2868
2998
|
spec = operations.build_operation_subprocess_create(
|
|
2869
2999
|
operation_id=operation_id,
|
|
@@ -3149,10 +3279,10 @@ class ContreeAsyncClient(ContreeClientBase, ABC):
|
|
|
3149
3279
|
async def inspect_image_grep(
|
|
3150
3280
|
self,
|
|
3151
3281
|
image_uuid: str,
|
|
3152
|
-
pattern: str,
|
|
3282
|
+
pattern: str | Sequence[str],
|
|
3153
3283
|
*,
|
|
3154
|
-
path: str | None = None,
|
|
3155
|
-
glob: str | None = None,
|
|
3284
|
+
path: str | Sequence[str] | None = None,
|
|
3285
|
+
glob: str | Sequence[str] | None = None,
|
|
3156
3286
|
max_count: int | None = None,
|
|
3157
3287
|
max_total: int | None = None,
|
|
3158
3288
|
case: Literal["sensitive", "insensitive", "smart"] | None = None,
|
|
@@ -601,9 +601,12 @@ class StreamRepr(ContreeModel):
|
|
|
601
601
|
|
|
602
602
|
@dataclass
|
|
603
603
|
class ClosableStreamRepr(ContreeModel):
|
|
604
|
-
"""Stdin payload.
|
|
604
|
+
"""Stdin payload. Delivery to the child is always complete —.
|
|
605
605
|
|
|
606
|
-
|
|
606
|
+
stdin is never truncated on its way to the process. Only the
|
|
607
|
+
*recorded* ``stdin`` events in the subprocess event log are
|
|
608
|
+
capped by ``truncate_output_at``, same as stdout/stderr. This
|
|
609
|
+
schema also carries a ``close`` flag controlling the pipe's EOF.
|
|
607
610
|
"""
|
|
608
611
|
|
|
609
612
|
value: str = field(
|
|
@@ -732,8 +735,10 @@ class ExecSpec(ContreeModel):
|
|
|
732
735
|
default=...,
|
|
733
736
|
metadata={
|
|
734
737
|
"description": (
|
|
735
|
-
"Maximum number of bytes
|
|
736
|
-
"
|
|
738
|
+
"Maximum number of bytes to keep per stream (stdout,\nstderr, and "
|
|
739
|
+
"stdin) in the subprocess event log. Does not\naffect delivery to the "
|
|
740
|
+
"child — stdin is always delivered\nin full regardless of this cap. "
|
|
741
|
+
"Defaults to 1 MiB; capped\nat 10 MiB."
|
|
737
742
|
),
|
|
738
743
|
"example": 65535,
|
|
739
744
|
"default": 1048576,
|
|
@@ -904,8 +909,11 @@ class InstanceSpawnRequest(ContreeModel):
|
|
|
904
909
|
default=...,
|
|
905
910
|
metadata={
|
|
906
911
|
"description": (
|
|
907
|
-
"Maximum number of bytes to keep
|
|
908
|
-
"
|
|
912
|
+
"Maximum number of bytes to keep per stream (stdout, stderr, and "
|
|
913
|
+
"stdin) in the subprocess event log. Does not affect stdin delivery to "
|
|
914
|
+
"the child, which always receives the complete stream regardless of "
|
|
915
|
+
"this cap. Default is `1048576` (1 MiB). The maximum value is "
|
|
916
|
+
"`10485760` (10 MiB)."
|
|
909
917
|
),
|
|
910
918
|
"example": 65535,
|
|
911
919
|
"default": 1048576,
|
|
@@ -1357,8 +1365,11 @@ class OperationInstanceMetadata(ContreeModel):
|
|
|
1357
1365
|
default=...,
|
|
1358
1366
|
metadata={
|
|
1359
1367
|
"description": (
|
|
1360
|
-
"Maximum number of bytes to keep
|
|
1361
|
-
"
|
|
1368
|
+
"Maximum number of bytes to keep per stream (stdout, stderr, and "
|
|
1369
|
+
"stdin) in the subprocess event log. Does not affect stdin delivery to "
|
|
1370
|
+
"the child, which always receives the complete stream regardless of "
|
|
1371
|
+
"this cap. Default is `1048576` (1 MiB). The maximum value is "
|
|
1372
|
+
"`10485760` (10 MiB)."
|
|
1362
1373
|
),
|
|
1363
1374
|
"example": 65535,
|
|
1364
1375
|
"default": 1048576,
|
|
@@ -6,6 +6,7 @@ Generated by codegen from the OpenAPI spec - do not edit.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
from collections.abc import Sequence
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from types import EllipsisType
|
|
11
12
|
from typing import IO, Any, Literal
|
|
@@ -591,10 +592,10 @@ def parse_inspect_image_list(response: ResponseData) -> DirectoryList:
|
|
|
591
592
|
|
|
592
593
|
def build_inspect_image_grep(
|
|
593
594
|
image_uuid: str,
|
|
594
|
-
pattern: str,
|
|
595
|
+
pattern: str | Sequence[str],
|
|
595
596
|
*,
|
|
596
|
-
path: str | None = None,
|
|
597
|
-
glob: str | None = None,
|
|
597
|
+
path: str | Sequence[str] | None = None,
|
|
598
|
+
glob: str | Sequence[str] | None = None,
|
|
598
599
|
max_count: int | None = None,
|
|
599
600
|
max_total: int | None = None,
|
|
600
601
|
case: Literal["sensitive", "insensitive", "smart"] | None = None,
|
|
@@ -602,7 +603,7 @@ def build_inspect_image_grep(
|
|
|
602
603
|
after: int | None = None,
|
|
603
604
|
) -> RequestSpec:
|
|
604
605
|
"""Build the request for `GET /inspect/{image_uuid}/grep`."""
|
|
605
|
-
query: dict[str, str] = {}
|
|
606
|
+
query: dict[str, str | Sequence[str]] = {}
|
|
606
607
|
query["pattern"] = pattern
|
|
607
608
|
if path is not None:
|
|
608
609
|
query["path"] = path
|
|
@@ -9,7 +9,7 @@ import logging
|
|
|
9
9
|
import math
|
|
10
10
|
import re
|
|
11
11
|
import zlib
|
|
12
|
-
from collections.abc import AsyncIterator, Iterable, Iterator
|
|
12
|
+
from collections.abc import AsyncIterator, Iterable, Iterator, Mapping, Sequence
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from datetime import datetime, timezone
|
|
15
15
|
from email.utils import parsedate_to_datetime
|
|
@@ -146,7 +146,7 @@ class RequestSpec:
|
|
|
146
146
|
|
|
147
147
|
method: str
|
|
148
148
|
path: str
|
|
149
|
-
query:
|
|
149
|
+
query: Mapping[str, str | Sequence[str]] = field(default_factory=dict)
|
|
150
150
|
headers: dict[str, str] = field(default_factory=dict)
|
|
151
151
|
body: bytes | IO[bytes] | None = None
|
|
152
152
|
content_type: str | None = None
|
|
@@ -247,6 +247,15 @@ RETRY_DELAYS = (0.1, 0.2, 0.5, 1.0, 2.0, 5.0)
|
|
|
247
247
|
# returning immediate empty streams does not spin the client
|
|
248
248
|
TIGHT_LOOP_FLOOR = 0.5
|
|
249
249
|
|
|
250
|
+
# the events route itself doesn't exist for this operation/server
|
|
251
|
+
# (malformed request an older backend rejects, or a reverse proxy
|
|
252
|
+
# that never forwards it) rather than merely being down: reconnecting
|
|
253
|
+
# will never succeed, but the operation itself may still complete -
|
|
254
|
+
# degrade to polling instead of failing the whole wait. 401/403 are
|
|
255
|
+
# deliberately excluded: an auth/permission failure here likely means
|
|
256
|
+
# the whole client is broken, not just this route, so it still raises
|
|
257
|
+
EVENTS_UNAVAILABLE_STATUSES = frozenset({400, 404, 405, 406})
|
|
258
|
+
|
|
250
259
|
|
|
251
260
|
def retry_generator(delays: tuple[float, ...] = RETRY_DELAYS) -> Iterator[float]:
|
|
252
261
|
"""An endless ladder of backoff delays.
|
|
@@ -370,8 +379,8 @@ def quote_path(value: str | int) -> str:
|
|
|
370
379
|
return quote(str(value), safe="")
|
|
371
380
|
|
|
372
381
|
|
|
373
|
-
def encode_query(query:
|
|
374
|
-
return urlencode(query, safe="/", quote_via=quote)
|
|
382
|
+
def encode_query(query: Mapping[str, str | Sequence[str]]) -> str:
|
|
383
|
+
return urlencode(query, doseq=True, safe="/", quote_via=quote)
|
|
375
384
|
|
|
376
385
|
|
|
377
386
|
def format_time_param(value: str | int | float | datetime) -> str:
|
|
@@ -2146,8 +2146,11 @@ Generated by codegen from the OpenAPI spec - do not edit.
|
|
|
2146
2146
|
ClosableStreamRepr:
|
|
2147
2147
|
type: object
|
|
2148
2148
|
description: |
|
|
2149
|
-
Stdin payload.
|
|
2150
|
-
|
|
2149
|
+
Stdin payload. Delivery to the child is always complete —
|
|
2150
|
+
stdin is never truncated on its way to the process. Only the
|
|
2151
|
+
*recorded* `stdin` events in the subprocess event log are
|
|
2152
|
+
capped by `truncate_output_at`, same as stdout/stderr. This
|
|
2153
|
+
schema also carries a `close` flag controlling the pipe's EOF.
|
|
2151
2154
|
required:
|
|
2152
2155
|
- value
|
|
2153
2156
|
properties:
|
|
@@ -2251,8 +2254,11 @@ Generated by codegen from the OpenAPI spec - do not edit.
|
|
|
2251
2254
|
truncate_output_at:
|
|
2252
2255
|
type: integer
|
|
2253
2256
|
description: |
|
|
2254
|
-
Maximum number of bytes
|
|
2255
|
-
|
|
2257
|
+
Maximum number of bytes to keep per stream (stdout,
|
|
2258
|
+
stderr, and stdin) in the subprocess event log. Does not
|
|
2259
|
+
affect delivery to the child — stdin is always delivered
|
|
2260
|
+
in full regardless of this cap. Defaults to 1 MiB; capped
|
|
2261
|
+
at 10 MiB.
|
|
2256
2262
|
default: 1048576
|
|
2257
2263
|
example: 65535
|
|
2258
2264
|
|
|
@@ -2361,7 +2367,10 @@ Generated by codegen from the OpenAPI spec - do not edit.
|
|
|
2361
2367
|
truncate_output_at:
|
|
2362
2368
|
type: integer
|
|
2363
2369
|
description: >
|
|
2364
|
-
Maximum number of bytes to keep
|
|
2370
|
+
Maximum number of bytes to keep per stream (stdout,
|
|
2371
|
+
stderr, and stdin) in the subprocess event log.
|
|
2372
|
+
Does not affect stdin delivery to the child, which always
|
|
2373
|
+
receives the complete stream regardless of this cap.
|
|
2365
2374
|
Default is `1048576` (1 MiB).
|
|
2366
2375
|
The maximum value is `10485760` (10 MiB).
|
|
2367
2376
|
example: 65535
|
|
@@ -3446,4 +3455,4 @@ DEFAULT_BASE_URL = "https://api.tokenfactory.nebius.com/sandboxes"
|
|
|
3446
3455
|
|
|
3447
3456
|
# sha256 of the exact OpenAPI document this package was built from -
|
|
3448
3457
|
# the build input provenance
|
|
3449
|
-
SPEC_SHA256 = "
|
|
3458
|
+
SPEC_SHA256 = "23d748a2918b1124b3072d54d17d8eb43aa00d0b969bc1fa718f34ac884ee887"
|
|
@@ -41,6 +41,8 @@ CLOSING_OPERATION_UUID = "00000000-0000-0000-0000-0000000c105e"
|
|
|
41
41
|
DROPPING_OPERATION_UUID = "00000000-0000-0000-0000-00000000d40b"
|
|
42
42
|
KEEPALIVE_OPERATION_UUID = "00000000-0000-0000-0000-0000000cee9a"
|
|
43
43
|
RESET_OPERATION_UUID = "00000000-0000-0000-0000-000000000e5e"
|
|
44
|
+
EVENTS_UNAVAILABLE_OPERATION_UUID = "00000000-0000-0000-0000-00000000eee0"
|
|
45
|
+
EVENTS_UNAVAILABLE_STALLED_OPERATION_UUID = "00000000-0000-0000-0000-00000000eee1"
|
|
44
46
|
SSE_HANG_SECONDS = 10.0
|
|
45
47
|
STATUS_HANG_SECONDS = 2.0
|
|
46
48
|
FILE_UUID = "a9165a5d-5c86-4bd8-8ee4-ae46c19cf45d"
|
|
@@ -409,6 +411,49 @@ def route(request: Captured, attempts: collections.Counter[str]) -> Reply:
|
|
|
409
411
|
stream_chunks=chunks,
|
|
410
412
|
)
|
|
411
413
|
|
|
414
|
+
if (
|
|
415
|
+
path == f"/v1/operations/{EVENTS_UNAVAILABLE_OPERATION_UUID}"
|
|
416
|
+
and method == "GET"
|
|
417
|
+
):
|
|
418
|
+
# the operation itself progresses and finishes normally; only
|
|
419
|
+
# its /events route is missing
|
|
420
|
+
status = "EXECUTING" if attempt < 3 else "SUCCESS"
|
|
421
|
+
return json_reply(
|
|
422
|
+
200,
|
|
423
|
+
{
|
|
424
|
+
**OPERATION_RESPONSE,
|
|
425
|
+
"uuid": EVENTS_UNAVAILABLE_OPERATION_UUID,
|
|
426
|
+
"status": status,
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if path == f"/v1/operations/{EVENTS_UNAVAILABLE_OPERATION_UUID}/events":
|
|
431
|
+
# no /events route at all for this operation (older backend,
|
|
432
|
+
# or a reverse proxy that doesn't forward it): reconnecting
|
|
433
|
+
# this exact request will never succeed, but the operation
|
|
434
|
+
# itself is healthy - the client must fall back to polling
|
|
435
|
+
# get_operation_status instead of failing the whole wait
|
|
436
|
+
return json_reply(404, {"error": "not found", "status": 404})
|
|
437
|
+
|
|
438
|
+
if (
|
|
439
|
+
path == f"/v1/operations/{EVENTS_UNAVAILABLE_STALLED_OPERATION_UUID}"
|
|
440
|
+
and method == "GET"
|
|
441
|
+
):
|
|
442
|
+
# same missing-events situation, but the operation itself
|
|
443
|
+
# never finishes - the polling fallback must still honor the
|
|
444
|
+
# caller's deadline instead of looping forever
|
|
445
|
+
return json_reply(
|
|
446
|
+
200,
|
|
447
|
+
{
|
|
448
|
+
**OPERATION_RESPONSE,
|
|
449
|
+
"uuid": EVENTS_UNAVAILABLE_STALLED_OPERATION_UUID,
|
|
450
|
+
"status": "EXECUTING",
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
if path == f"/v1/operations/{EVENTS_UNAVAILABLE_STALLED_OPERATION_UUID}/events":
|
|
455
|
+
return json_reply(404, {"error": "not found", "status": 404})
|
|
456
|
+
|
|
412
457
|
if (
|
|
413
458
|
path == f"/v1/operations/{PAYLOAD_INTERRUPTION_OPERATION_UUID}"
|
|
414
459
|
and method == "GET"
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
+
from types import ModuleType
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
10
11
|
from tests.stub_server import (
|
|
12
|
+
EVENTS_UNAVAILABLE_OPERATION_UUID,
|
|
13
|
+
EVENTS_UNAVAILABLE_STALLED_OPERATION_UUID,
|
|
11
14
|
EXECUTING_OPERATION_UUID,
|
|
12
15
|
IMAGE_UUID,
|
|
13
16
|
PENDING_OPERATION_UUID,
|
|
@@ -62,6 +65,52 @@ def test_follow_operation_events_reconnects(
|
|
|
62
65
|
assert stream_requests[1].headers["last-event-id"] == "1"
|
|
63
66
|
|
|
64
67
|
|
|
68
|
+
def test_wait_operation_falls_back_to_polling_when_events_missing(
|
|
69
|
+
invoke: Callable[..., Any], stub_server: StubServer
|
|
70
|
+
) -> None:
|
|
71
|
+
# /events 404s outright (older backend, proxy that drops the
|
|
72
|
+
# route, ...); the operation itself still finishes
|
|
73
|
+
operation = invoke("wait_operation", EVENTS_UNAVAILABLE_OPERATION_UUID)
|
|
74
|
+
|
|
75
|
+
assert str(operation.status) == "SUCCESS"
|
|
76
|
+
events_path = f"/v1/operations/{EVENTS_UNAVAILABLE_OPERATION_UUID}/events"
|
|
77
|
+
events_requests = [c for c in stub_server.captured if c.path == events_path]
|
|
78
|
+
# a 404 means reconnecting will never work: touched exactly once
|
|
79
|
+
assert len(events_requests) == 1
|
|
80
|
+
status_path = f"/v1/operations/{EVENTS_UNAVAILABLE_OPERATION_UUID}"
|
|
81
|
+
status_requests = [c for c in stub_server.captured if c.path == status_path]
|
|
82
|
+
# polled until terminal, plus wait_operation's own final fetch
|
|
83
|
+
assert len(status_requests) >= 3
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_follow_operation_events_yields_completion_when_events_missing(
|
|
87
|
+
invoke: Callable[..., Any], stub_server: StubServer, models: ModuleType
|
|
88
|
+
) -> None:
|
|
89
|
+
# there is no event log to relay, but the caller must still see a
|
|
90
|
+
# terminal completion event, not an iterator that silently ends
|
|
91
|
+
events = invoke(
|
|
92
|
+
"follow_operation_events", EVENTS_UNAVAILABLE_OPERATION_UUID, collect=True
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert len(events) == 1
|
|
96
|
+
assert events[0].type == "completion"
|
|
97
|
+
assert isinstance(events[0].data, models.EventDataCompletion)
|
|
98
|
+
assert str(events[0].data.status) == "SUCCESS"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_wait_operation_polling_fallback_respects_timeout(
|
|
102
|
+
invoke: Callable[..., Any], stub_server: StubServer
|
|
103
|
+
) -> None:
|
|
104
|
+
# events unavailable and the operation never finishes: the polling
|
|
105
|
+
# fallback must still honor the deadline instead of spinning forever
|
|
106
|
+
with pytest.raises(TimeoutError, match=EVENTS_UNAVAILABLE_STALLED_OPERATION_UUID):
|
|
107
|
+
invoke(
|
|
108
|
+
"wait_operation",
|
|
109
|
+
EVENTS_UNAVAILABLE_STALLED_OPERATION_UUID,
|
|
110
|
+
timeout=0.3,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
65
114
|
def test_resolve_image_tag_prefix(
|
|
66
115
|
invoke: Callable[..., Any], stub_server: StubServer
|
|
67
116
|
) -> None:
|
|
@@ -26,6 +26,20 @@ def test_format_time_param(runtime: ModuleType) -> None:
|
|
|
26
26
|
assert runtime.format_time_param("1h") == "1h"
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def test_encode_query_preserves_repeated_values(runtime: ModuleType) -> None:
|
|
30
|
+
assert (
|
|
31
|
+
runtime.encode_query(
|
|
32
|
+
{
|
|
33
|
+
"pattern": ["^root:", "^bin:"],
|
|
34
|
+
"path": ("/etc/passwd", "/etc/group"),
|
|
35
|
+
"case": "sensitive",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
== "pattern=%5Eroot%3A&pattern=%5Ebin%3A"
|
|
39
|
+
"&path=/etc/passwd&path=/etc/group&case=sensitive"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
29
43
|
def test_json_object_rejects_array(
|
|
30
44
|
runtime: ModuleType,
|
|
31
45
|
exceptions: ModuleType,
|
|
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
|
{contree_client-0.2.1.dev0 → contree_client-0.2.2}/tests/test_aiohttp_payload_interruption.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
|