Files
Andrei Strongin 6db56e5d2e SDK resolution, arg parser, client builders, exports (#10824)
<!-- Raise an issue to propose your change
(https://github.com/cvat-ai/cvat/issues).
It helps to avoid duplication of efforts from multiple independent
contributors.
Discuss your ideas with maintainers to be sure that changes will be
approved and merged.
Read the [Contribution guide](https://docs.cvat.ai/docs/contributing/).
-->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
<!-- Why is this change required? What problem does it solve? If it
fixes an open
issue, please link to the issue here. Describe your changes in detail,
add
screenshots. -->

### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply.
If an item isn't applicable for some reason, then ~~explicitly
strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the
pull request.
If you're unsure about any of these, don't hesitate to ask. We're here
to help! -->
- [x] I submit my changes into the `develop` branch
- [x] I have created a changelog fragment <!-- see top comment in
CHANGELOG.md -->
- [ ] I have updated the documentation accordingly
- [x] I have added tests to cover my changes
- [x] I have linked related issues (see [GitHub docs](

https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))

### License

- [x] I submit _my code changes_ under the same [MIT License](
https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the
project.
  Feel free to contact the maintainers if that's a concern.

---------

Co-authored-by: Maxim Zhiltsov <maxim@cvat.ai>
2026-07-10 15:30:32 +04:00

241 lines
8.2 KiB
Python

# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import os
from datetime import timedelta
from io import BytesIO
from pathlib import Path
from unittest import mock
import packaging.version as pv
import pytest
from cvat_cli._internal.agent import (
_Event,
_NewReconnectionDelay,
_parse_event_stream,
_TaskCacheLimiter,
)
from cvat_sdk import Client
from cvat_sdk.api_client import models
from cvat_sdk.core.proxies.tasks import ResourceType
from sdk.util import https_reverse_proxy
from .util import TestCliBase, generate_images, run_cli
class TestCliMisc(TestCliBase):
def test_can_warn_on_mismatching_server_version(self, monkeypatch, caplog):
def mocked_version(_):
return pv.Version("0")
# We don't actually run a separate process in the tests here, so it works
monkeypatch.setattr(Client, "get_server_version", mocked_version)
self.run_cli("task", "ls")
assert "Server version '0' is not compatible with SDK version" in caplog.text
@pytest.mark.parametrize("verify", [True, False])
def test_can_control_ssl_verification_with_arg(self, verify: bool):
with https_reverse_proxy() as proxy_url:
if verify:
insecure_args = []
else:
insecure_args = ["--insecure"]
run_cli(
self,
f"--auth={self.user}:{self.password}",
f"--server-host={proxy_url}",
*insecure_args,
"task",
"ls",
expected_code=1 if verify else 0,
)
stdout = self.stdout.getvalue()
if not verify:
for line in stdout.splitlines():
int(line)
def test_can_control_organization_context(self):
org = "cli-test-org"
self.client.organizations.create(models.OrganizationWriteRequest(org))
files = generate_images(self.tmp_path, 1)
stdout = self.run_cli(
"task",
"create",
"personal_task",
ResourceType.LOCAL.name,
*map(os.fspath, files),
"--completion_verification_period=0.01",
organization="",
)
personal_task_id = int(stdout.split()[-1])
stdout = self.run_cli(
"task",
"create",
"org_task",
ResourceType.LOCAL.name,
*map(os.fspath, files),
"--completion_verification_period=0.01",
organization=org,
)
org_task_id = int(stdout.split()[-1])
personal_task_ids = list(map(int, self.run_cli("task", "ls", organization="").split()))
assert personal_task_id in personal_task_ids
assert org_task_id not in personal_task_ids
org_task_ids = list(map(int, self.run_cli("task", "ls", organization=org).split()))
assert personal_task_id not in org_task_ids
assert org_task_id in org_task_ids
all_task_ids = list(map(int, self.run_cli("task", "ls").split()))
assert personal_task_id in all_task_ids
assert org_task_id in all_task_ids
def test_can_use_access_token_env_variable(
self, monkeypatch: pytest.MonkeyPatch, access_tokens
):
token = next(t for t in access_tokens)["private_key"]
from cvat_sdk.api_client.rest import RESTClientObject
original_request = RESTClientObject.request
calls = 0
def patched_request(self, *args, **kwargs):
nonlocal calls
calls += 1
assert kwargs["headers"].get("Authorization") == f"Bearer {token}"
return original_request(self, *args, **kwargs)
monkeypatch.setenv("CVAT_ACCESS_TOKEN", token)
monkeypatch.setattr(RESTClientObject, "request", patched_request)
self.run_cli("task", "ls", authenticate=False)
assert calls
def test_can_use_current_user_env_variable(self, monkeypatch: pytest.MonkeyPatch):
# set all user env vars supported by getuser()
for env_var in ("LOGNAME", "USER", "LNAME", "USERNAME"):
monkeypatch.setenv(env_var, self.user)
from getpass import getuser as original_getuser
from cvat_sdk.core.auth import default_auth_factory
with (
mock.patch(
"cvat_sdk.core.auth.default_auth_factory", wraps=default_auth_factory
) as mock_auth_factory,
mock.patch("getpass.getuser", wraps=original_getuser) as mock_getuser,
mock.patch("getpass.getpass", return_value=self.password) as mock_getpass,
):
self.run_cli("task", "ls", authenticate=False)
mock_auth_factory.assert_called_once()
mock_getuser.assert_called()
mock_getpass.assert_called_once()
def test_can_use_pass_env_variable(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("PASS", self.password)
from cvat_sdk.core.auth import get_auth_factory
with (
mock.patch(
"cvat_sdk.core.auth.get_auth_factory", wraps=get_auth_factory
) as mock_auth_factory,
mock.patch("getpass.getpass") as mock_getpass,
):
self.run_cli(f"--auth={self.user}", "task", "ls", authenticate=False)
mock_auth_factory.assert_called_once()
mock_getpass.assert_not_called()
@pytest.mark.parametrize(
["lines", "messages"],
[
# empty
([], []),
([""], [_Event("", "")]),
# event only
(["event: test", ""], [_Event("test", "")]),
(["event: foo", "event: bar", ""], [_Event("bar", "")]),
# data only
(["data: test", ""], [_Event("", "test")]),
(["data: foo", "data: bar", ""], [_Event("", "foo\nbar")]),
# event and data
(["event: test", "data: foo", "data: bar", ""], [_Event("test", "foo\nbar")]),
(["data: foo", "event: test", "data: bar", ""], [_Event("test", "foo\nbar")]),
(["data: foo", "data: bar", "event: test", ""], [_Event("test", "foo\nbar")]),
# fields without values
(["event: test", "event", ""], [_Event("", "")]),
(["data: test", "data", ""], [_Event("", "test\n")]),
# incomplete event
(["event: test", "data: foo"], []),
# multiple events
(
["event: test1", "data: foo", "", "event: test2", "data: bar", ""],
[_Event("test1", "foo"), _Event("test2", "bar")],
),
# comments
([":"], []),
([":1", "event: test", ":2", "data: foo", ":3", ""], [_Event("test", "foo")]),
# retry
(["retry: 1234"], [_NewReconnectionDelay(timedelta(milliseconds=1234))]),
(["retry", "retry:", "retry: a"], []),
# no space
(["event:test", "data:foo", ""], [_Event("test", "foo")]),
# two spaces
(["event: test", "data: foo", ""], [_Event(" test", " foo")]),
# carriage return
(["event: test\r", "data: foo\r", "\r"], [_Event("test", "foo")]),
],
)
def test_parse_event_stream(lines, messages):
stream = BytesIO(b"".join(line.encode() + b"\n" for line in lines))
assert list(_parse_event_stream(stream)) == messages
def test_task_cache_limiter_keeps_last_10_tasks(
tmp_path: Path,
fxt_login: tuple[Client, str],
fxt_logger,
):
client = fxt_login[0]
client.logger = fxt_logger[0]
client.config.cache_dir = tmp_path / "cache"
limiter = _TaskCacheLimiter(client)
for task_id in range(1, 13):
limiter._cache_manager.task_dir(task_id).mkdir(parents=True)
with limiter.using_cache_for_task(task_id):
pass
if task_id <= 10:
for cached_task_id in range(1, task_id + 1):
assert limiter._cache_manager.task_dir(cached_task_id).exists()
elif task_id == 11:
assert not limiter._cache_manager.task_dir(1).exists()
for cached_task_id in range(2, 12):
assert limiter._cache_manager.task_dir(cached_task_id).exists()
elif task_id == 12:
assert not limiter._cache_manager.task_dir(1).exists()
assert not limiter._cache_manager.task_dir(2).exists()
for cached_task_id in range(3, 13):
assert limiter._cache_manager.task_dir(cached_task_id).exists()