From c52e0bb29cdf30dccfe5ab513f1007dfa1353492 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 15 May 2025 19:06:34 +0300 Subject: [PATCH 1/6] Fix possible no assignments error when there are --- .../cvat/exchange-oracle/src/services/cvat.py | 28 ++- .../exchange-oracle/src/services/exchange.py | 58 +++--- .../integration/services/test_exchange.py | 176 ++++++++---------- .../exchange-oracle/tests/utils/constants.py | 3 + 4 files changed, 130 insertions(+), 135 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/services/cvat.py b/packages/examples/cvat/exchange-oracle/src/services/cvat.py index 4ba663917d..a456d8ae3a 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/cvat.py +++ b/packages/examples/cvat/exchange-oracle/src/services/cvat.py @@ -794,7 +794,8 @@ def count_jobs_by_escrow_address( def get_free_job( session: Session, - cvat_projects: list[int], + escrow_address: str, + chain_id: int, *, user_wallet_address: str, for_update: bool | ForUpdateParams = False, @@ -805,7 +806,11 @@ def get_free_job( return ( _maybe_for_update(session.query(Job), enable=for_update) .where( - Job.cvat_project_id.in_(cvat_projects), + Job.project.has( + (Project.escrow_address == escrow_address) + & (Project.chain_id == chain_id) + & (Project.status == ProjectStatuses.annotation) + ), Job.status == JobStatuses.new, ~Job.assignments.any( ( @@ -984,22 +989,27 @@ def get_user_assignments_in_cvat_projects( ) -def count_active_user_assignments( +def has_active_user_assignments( session: Session, wallet_address: int, - cvat_projects: list[int], -) -> int: - return ( + escrow_address: str, + chain_id: int, +) -> bool: + return session.query( session.query(Assignment) .where( - Assignment.job.has(Job.cvat_project_id.in_(cvat_projects)), + Assignment.job.has( + Job.project.has( + (Project.escrow_address == escrow_address) & (Project.chain_id == chain_id) + ) + ), Assignment.user_wallet_address == wallet_address, Assignment.status == AssignmentStatuses.created.value, Assignment.completed_at == None, utcnow() < Assignment.expires_at, ) - .count() - ) + .exists() + ).scalar() # Image diff --git a/packages/examples/cvat/exchange-oracle/src/services/exchange.py b/packages/examples/cvat/exchange-oracle/src/services/exchange.py index c1fec576f0..23a7b8e1cc 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/services/exchange.py @@ -18,7 +18,7 @@ def __str__(self) -> str: ) -def create_assignment(escrow_address: str, chain_id: Networks, wallet_address: str) -> str | None: # noqa: ARG001 (don't we want to use chain_id for filter?) +def create_assignment(escrow_address: str, chain_id: Networks, wallet_address: str) -> str | None: with SessionLocal.begin() as session: user = get_or_404( cvat_service.get_user_by_id(session, wallet_address, for_update=True), @@ -26,44 +26,37 @@ def create_assignment(escrow_address: str, chain_id: Networks, wallet_address: s "user", ) - # There can be several projects under one escrow, we need any - project = cvat_service.get_project_by_escrow_address( + if cvat_service.has_active_user_assignments( session, - escrow_address, - status_in=[ - ProjectStatuses.annotation - ], # avoid unnecessary locking on completed projects - for_update=True, - ) - - if not project: - # Retry without a lock to check if the project doesn't exist - get_or_404( - cvat_service.get_project_by_escrow_address( - session, escrow_address, status_in=[ProjectStatuses.annotation] - ), - escrow_address, - "job", - ) - return None - - has_active_assignments = ( - cvat_service.count_active_user_assignments( - session, wallet_address=wallet_address, cvat_projects=[project.cvat_id] - ) - > 0 - ) - if has_active_assignments: + wallet_address=wallet_address, + escrow_address=escrow_address, + chain_id=chain_id.value, + ): raise UserHasUnfinishedAssignmentError( "The user already has an unfinished assignment in this project" ) + # TODO: Try to put into 1 request. SQLAlchemy generates 2 queries with simple + # .options(selectinload(Job.project)) + project = get_or_404( + cvat_service.get_project_by_escrow_address( + session, escrow_address, status_in=[ProjectStatuses.annotation] + ), + escrow_address, + "job", + ) + unassigned_job = cvat_service.get_free_job( session, - cvat_projects=[project.cvat_id], + escrow_address=escrow_address, + chain_id=chain_id.value, user_wallet_address=wallet_address, for_update=True, + # lock the job to be able to make a rollback if CVAT requests fail + # can potentially be optimized to make less DB requests + # and rely only on assignment expiration ) + if not unassigned_job: return None @@ -72,7 +65,12 @@ def create_assignment(escrow_address: str, chain_id: Networks, wallet_address: s wallet_address=user.wallet_address, cvat_job_id=unassigned_job.cvat_id, expires_at=utcnow() - + timedelta(seconds=get_default_assignment_timeout(TaskTypes(project.job_type))), + + timedelta( + seconds=get_default_assignment_timeout( + TaskTypes(project.job_type) + # TODO: need to update this if we have multiple job types per escrow + ) + ), ) cvat_service.touch(session, Job, [unassigned_job.id]) diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py index 25d1e6f81b..7d074d12e9 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py @@ -8,13 +8,14 @@ from fastapi import HTTPException from pydantic import ValidationError -from src.core.types import AssignmentStatuses, JobStatuses, Networks +from src.core.types import AssignmentStatuses, JobStatuses, Networks, TaskTypes from src.db import SessionLocal from src.endpoints.serializers import serialize_job from src.models.cvat import Assignment, User from src.schemas import exchange as service_api from src.services.exchange import create_assignment +from tests.utils.constants import ESCROW_ADDRESS, WALLET_ADDRESS1, WALLET_ADDRESS2 from tests.utils.db_helper import ( create_job, create_project, @@ -32,7 +33,7 @@ def tearDown(self): def test_serialize_job(self): cvat_id = 1 - escrow_address = "0x86e83d346041E8806e352681f3F14549C0d2BC67" + escrow_address = ESCROW_ADDRESS cvat_project = create_project(self.session, escrow_address, cvat_id) self.session.commit() @@ -64,7 +65,7 @@ def test_serialize_task_invalid_project(self): def test_serialize_task_invalid_manifest(self): cvat_id = 1 - escrow_address = "0x86e83d346041E8806e352681f3F14549C0d2BC67" + escrow_address = ESCROW_ADDRESS cvat_project = create_project(self.session, escrow_address, cvat_id) self.session.commit() @@ -75,10 +76,8 @@ def test_serialize_task_invalid_manifest(self): serialize_job(cvat_project) def test_create_assignment(self): - cvat_project_1, _, cvat_job_1 = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 - ) - user_address = "0x86e83d346041E8806e352681f3F14549C0d2BC69" + cvat_project_1, _, cvat_job_1 = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) + user_address = WALLET_ADDRESS1 user = User( wallet_address=user_address, cvat_email="test@hmt.ai", @@ -87,15 +86,9 @@ def test_create_assignment(self): self.session.add(user) self.session.commit() - with ( - open("tests/utils/manifest.json") as data, - patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, - patch("src.services.exchange.cvat_api"), - ): - manifest = json.load(data) - mock_get_manifest.return_value = manifest + with patch("src.services.exchange.cvat_api"): assignment_id = create_assignment( - cvat_project_1.escrow_address, cvat_project_1.chain_id, user_address + cvat_project_1.escrow_address, Networks(cvat_project_1.chain_id), user_address ) assignment = self.session.query(Assignment).filter_by(id=assignment_id).first() @@ -105,15 +98,13 @@ def test_create_assignment(self): assert assignment.status == AssignmentStatuses.created def test_create_assignment_many_jobs_1_completed(self): - cvat_project, _, cvat_job_1 = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 - ) + cvat_project, _, cvat_job_1 = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) cvat_job_1.status = JobStatuses.completed.value cvat_task_2 = create_task(self.session, 2, cvat_project.cvat_id) cvat_job_2 = create_job(self.session, 2, cvat_task_2.cvat_id, cvat_project.cvat_id) - user_address = "0x86e83d346041E8806e352681f3F14549C0d2BC69" + user_address = WALLET_ADDRESS1 user = User( wallet_address=user_address, cvat_email="test@hmt.ai", @@ -135,15 +126,9 @@ def test_create_assignment_many_jobs_1_completed(self): self.session.commit() - with ( - open("tests/utils/manifest.json") as data, - patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, - patch("src.services.exchange.cvat_api"), - ): - manifest = json.load(data) - mock_get_manifest.return_value = manifest + with patch("src.services.exchange.cvat_api"): assignment_id = create_assignment( - cvat_project.escrow_address, cvat_project.chain_id, user_address + cvat_project.escrow_address, Networks(cvat_project.chain_id), user_address ) assignment = self.session.query(Assignment).filter_by(id=assignment_id).first() @@ -153,20 +138,18 @@ def test_create_assignment_many_jobs_1_completed(self): assert assignment.status == AssignmentStatuses.created def test_create_assignment_invalid_user_address(self): - cvat_project_1, _, _ = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 - ) + cvat_project_1, _, _ = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) self.session.commit() - with pytest.raises(HTTPException): + with pytest.raises(HTTPException, match="Can't find user"): create_assignment( cvat_project_1.escrow_address, - cvat_project_1.chain_id, + Networks(cvat_project_1.chain_id), "invalid_address", ) def test_create_assignment_invalid_project(self): - user_address = "0x86e83d346041E8806e352681f3F14549C0d2BC69" + user_address = WALLET_ADDRESS1 user = User( wallet_address=user_address, cvat_email="test@hmt.ai", @@ -175,14 +158,12 @@ def test_create_assignment_invalid_project(self): self.session.add(user) self.session.commit() - with pytest.raises(HTTPException): - create_assignment("1", Networks.localhost.value, user_address) + with pytest.raises(HTTPException, match="Can't find job"): + create_assignment("1", Networks.localhost, user_address) def test_create_assignment_unfinished_assignment(self): - _, _, cvat_job = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 - ) - user_address = "0x86e83d346041E8806e352681f3F14549C0d2BC69" + _, _, cvat_job = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) + user_address = WALLET_ADDRESS1 user = User( wallet_address=user_address, cvat_email="test@hmt.ai", @@ -200,23 +181,52 @@ def test_create_assignment_unfinished_assignment(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, - patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), + pytest.raises(Exception, match="unfinished assignment"), ): - manifest = json.load(data) - mock_get_manifest.return_value = manifest + create_assignment(ESCROW_ADDRESS, Networks.localhost, user_address) - with pytest.raises(HTTPException): - create_assignment("1", Networks.localhost.value, user_address) + def test_create_assignment_has_expired_assignment_and_available_jobs(self): + escrow_address = ESCROW_ADDRESS + project1, _, cvat_job1 = create_project_task_and_job(self.session, escrow_address, 1) + project2, _, cvat_job2 = create_project_task_and_job(self.session, escrow_address, 2) + project1.job_type = TaskTypes.image_skeletons_from_boxes + project2.job_type = TaskTypes.image_skeletons_from_boxes + self.session.add_all([project1, project2]) - def test_create_assignment_no_available_jobs_completed_assignment(self): - cvat_project, _, cvat_job_1 = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 + user_address = WALLET_ADDRESS1 + user = User( + wallet_address=user_address, + cvat_email="test@hmt.ai", + cvat_id=1, ) + self.session.add(user) + + old_assignment = Assignment( + id=str(uuid.uuid4()), + user_wallet_address=user_address, + cvat_job_id=cvat_job1.cvat_id, + created_at=datetime.now() - timedelta(hours=1), + expires_at=datetime.now() - timedelta(minutes=1), + status=AssignmentStatuses.expired.value, + ) + self.session.add(old_assignment) + + self.session.commit() + + with patch("src.services.exchange.cvat_api"): + new_assignment_id = create_assignment(escrow_address, Networks.localhost, user_address) + + new_assignment = self.session.query(Assignment).filter_by(id=new_assignment_id).first() + assert new_assignment.cvat_job_id == cvat_job2.cvat_id # job1 was attempted already + assert new_assignment.user_wallet_address == user_address + assert new_assignment.status == AssignmentStatuses.created + + def test_create_assignment_no_available_jobs_completed_assignment(self): + cvat_project, _, cvat_job_1 = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) cvat_job_1.status = JobStatuses.completed.value - user_address1 = "0x86e83d346041E8806e352681f3F14549C0d2BC69" + user_address1 = WALLET_ADDRESS1 user1 = User( wallet_address=user_address1, cvat_email="test1@hmt.ai", @@ -224,7 +234,7 @@ def test_create_assignment_no_available_jobs_completed_assignment(self): ) self.session.add(user1) - user_address2 = "0x86e83d346041E8806e352681f3F14549C0d2BC70" + user_address2 = WALLET_ADDRESS2 user2 = User( wallet_address=user_address2, cvat_email="test2@hmt.ai", @@ -246,25 +256,17 @@ def test_create_assignment_no_available_jobs_completed_assignment(self): self.session.commit() - with ( - open("tests/utils/manifest.json") as data, - patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, - patch("src.services.exchange.cvat_api"), - ): - manifest = json.load(data) - mock_get_manifest.return_value = manifest + with patch("src.services.exchange.cvat_api"): assignment_id = create_assignment( - cvat_project.escrow_address, cvat_project.chain_id, user_address2 + cvat_project.escrow_address, Networks(cvat_project.chain_id), user_address2 ) assert assignment_id == None def test_create_assignment_no_available_jobs_active_foreign_assignment(self): - cvat_project, _, cvat_job_1 = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 - ) + cvat_project, _, cvat_job_1 = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) - user_address1 = "0x86e83d346041E8806e352681f3F14549C0d2BC69" + user_address1 = WALLET_ADDRESS1 user1 = User( wallet_address=user_address1, cvat_email="test1@hmt.ai", @@ -272,7 +274,7 @@ def test_create_assignment_no_available_jobs_active_foreign_assignment(self): ) self.session.add(user1) - user_address2 = "0x86e83d346041E8806e352681f3F14549C0d2BC70" + user_address2 = WALLET_ADDRESS2 user2 = User( wallet_address=user_address2, cvat_email="test2@hmt.ai", @@ -290,27 +292,19 @@ def test_create_assignment_no_available_jobs_active_foreign_assignment(self): self.session.commit() - with ( - open("tests/utils/manifest.json") as data, - patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, - patch("src.services.exchange.cvat_api"), - ): - manifest = json.load(data) - mock_get_manifest.return_value = manifest + with patch("src.services.exchange.cvat_api"): assignment_id = create_assignment( - cvat_project.escrow_address, cvat_project.chain_id, user_address2 + cvat_project.escrow_address, Networks(cvat_project.chain_id), user_address2 ) assert assignment_id == None def test_create_assignment_wont_reassign_job_to_previous_user(self): - cvat_project_1, _, cvat_job_1 = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 - ) + cvat_project_1, _, cvat_job_1 = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) cvat_job_1.status = JobStatuses.new.value # validated and rejected return to 'new' user = User( - wallet_address="0x86e83d346041E8806e352681f3F14549C0d2BC69", + wallet_address=WALLET_ADDRESS1, cvat_email="test@hmt.ai", cvat_id=1, ) @@ -330,32 +324,26 @@ def test_create_assignment_wont_reassign_job_to_previous_user(self): self.session.commit() - with ( - open("tests/utils/manifest.json") as data, - patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, - patch("src.services.exchange.cvat_api"), - ): - manifest = json.load(data) - mock_get_manifest.return_value = manifest + with patch("src.services.exchange.cvat_api"): assignment_id = create_assignment( - cvat_project_1.escrow_address, cvat_project_1.chain_id, user.wallet_address + cvat_project_1.escrow_address, + Networks(cvat_project_1.chain_id), + user.wallet_address, ) assert assignment_id is None def test_create_assignment_can_assign_job_to_new_user(self): - cvat_project_1, _, cvat_job_1 = create_project_task_and_job( - self.session, "0x86e83d346041E8806e352681f3F14549C0d2BC67", 1 - ) + cvat_project_1, _, cvat_job_1 = create_project_task_and_job(self.session, ESCROW_ADDRESS, 1) cvat_job_1.status = JobStatuses.new.value # validated and rejected return to 'new' previous_user = User( - wallet_address="0x86e83d346041E8806e352681f3F14549C0d2BC69", + wallet_address=WALLET_ADDRESS1, cvat_email="previous@hmt.ai", cvat_id=1, ) new_user = User( - wallet_address="0x69e83d346041E8806e352681f3F14549C0d2BC42", + wallet_address=WALLET_ADDRESS2, cvat_email="new@hmt.ai", cvat_id=2, ) @@ -376,15 +364,11 @@ def test_create_assignment_can_assign_job_to_new_user(self): self.session.commit() - with ( - open("tests/utils/manifest.json") as data, - patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, - patch("src.services.exchange.cvat_api"), - ): - manifest = json.load(data) - mock_get_manifest.return_value = manifest + with patch("src.services.exchange.cvat_api"): assignment_id = create_assignment( - cvat_project_1.escrow_address, cvat_project_1.chain_id, new_user.wallet_address + cvat_project_1.escrow_address, + Networks(cvat_project_1.chain_id), + new_user.wallet_address, ) assignment = self.session.get(Assignment, assignment_id) diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/constants.py b/packages/examples/cvat/exchange-oracle/tests/utils/constants.py index 2a5082c255..a1d535a814 100644 --- a/packages/examples/cvat/exchange-oracle/tests/utils/constants.py +++ b/packages/examples/cvat/exchange-oracle/tests/utils/constants.py @@ -20,6 +20,9 @@ TOKEN_ADDRESS = "0x976EA74026E726554dB657fA54763abd0C3a0aa9" FACTORY_ADDRESS = "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955" +WALLET_ADDRESS1 = "0x86e83d346041E8806e352681f3F14549C0d2BC69" +WALLET_ADDRESS2 = "0x86e83d346041E8806e352681f3F14549C0d2BC70" + DEFAULT_MANIFEST_URL = "http://host.docker.internal:9000/manifests/manifest.json" DEFAULT_HASH = "test" From acaef5a5b3602750ba8f78888eae8ed68014d5a5 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 15 May 2025 19:10:17 +0300 Subject: [PATCH 2/6] Update readme --- packages/examples/cvat/exchange-oracle/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/README.md b/packages/examples/cvat/exchange-oracle/README.md index 11ba79973b..7d70eeac82 100644 --- a/packages/examples/cvat/exchange-oracle/README.md +++ b/packages/examples/cvat/exchange-oracle/README.md @@ -19,14 +19,14 @@ For deployment it is required to have PostgreSQL(v14.4) ### Run the oracle locally: ``` -docker-compose -f docker-compose.dev.yml up -d +docker compose -f docker-compose.dev.yml up -d ./bin/start_dev.sh ``` or ``` -docker-compose -f docker-compose.dev.yml up -d +docker compose -f docker-compose.dev.yml up -d ./bin/start_debug.sh ``` @@ -73,5 +73,6 @@ Available at `/docs` route To run tests ``` -docker-compose -f docker-compose.test.yml up --build test --attach test --exit-code-from test +docker compose -f docker-compose.test.yml up --build test --attach test --exit-code-from test && \ + docker compose -f docker-compose.test.yml down ``` \ No newline at end of file From d778a3af9cd1e405a1bbf6c07e26635fa09d26a5 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 16 May 2025 19:14:51 +0300 Subject: [PATCH 3/6] Update cvat sdk versions --- packages/examples/cvat/exchange-oracle/poetry.lock | 8 ++++---- packages/examples/cvat/exchange-oracle/pyproject.toml | 2 +- packages/examples/cvat/recording-oracle/poetry.lock | 8 ++++---- packages/examples/cvat/recording-oracle/pyproject.toml | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/poetry.lock b/packages/examples/cvat/exchange-oracle/poetry.lock index f1970962d5..8af6c9bc2d 100644 --- a/packages/examples/cvat/exchange-oracle/poetry.lock +++ b/packages/examples/cvat/exchange-oracle/poetry.lock @@ -945,13 +945,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cvat-sdk" -version = "2.31.0" +version = "2.37.0" description = "CVAT REST API" optional = false python-versions = ">=3.9" files = [ - {file = "cvat_sdk-2.31.0-py3-none-any.whl", hash = "sha256:b33e8526dad8c481f82e445badfced5d69747eaf7e5660b0d176cf86d394a02e"}, - {file = "cvat_sdk-2.31.0.tar.gz", hash = "sha256:aaeff833c32bfe711f418c62bdab135e0746eff0e89757e8b61cfad14a42ef23"}, + {file = "cvat_sdk-2.37.0-py3-none-any.whl", hash = "sha256:faa94cfd6678089814179a8da828761dfa3daf08eb752490ee85551a1045dac5"}, + {file = "cvat_sdk-2.37.0.tar.gz", hash = "sha256:e990908a473c499eb6d7b84f7f2e640ea729ef027d4c4cc32a5a925752532689"}, ] [package.dependencies] @@ -5047,4 +5047,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "c643f28ae7113ae0b8051952c0adfcb74a0ae182ba5039133f57f52b78608007" +content-hash = "8bf7f09b99af5cd8b02a36fc0a1b5ad4af28d5d17d7c0275afa38edb281c3cfc" diff --git a/packages/examples/cvat/exchange-oracle/pyproject.toml b/packages/examples/cvat/exchange-oracle/pyproject.toml index 187d9c81e3..5dd04c1813 100644 --- a/packages/examples/cvat/exchange-oracle/pyproject.toml +++ b/packages/examples/cvat/exchange-oracle/pyproject.toml @@ -16,7 +16,7 @@ sqlalchemy-utils = "^0.41.1" alembic = "^1.11.1" httpx = "^0.24.1" pytest = "^7.2.2" -cvat-sdk = "2.31.0" +cvat-sdk = "2.37.0" sqlalchemy = "^2.0.16" apscheduler = "^3.10.1" xmltodict = "^0.13.0" diff --git a/packages/examples/cvat/recording-oracle/poetry.lock b/packages/examples/cvat/recording-oracle/poetry.lock index 4b99cada57..52dd95468a 100644 --- a/packages/examples/cvat/recording-oracle/poetry.lock +++ b/packages/examples/cvat/recording-oracle/poetry.lock @@ -914,13 +914,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cvat-sdk" -version = "2.31.0" +version = "2.37.0" description = "CVAT REST API" optional = false python-versions = ">=3.9" files = [ - {file = "cvat_sdk-2.31.0-py3-none-any.whl", hash = "sha256:b33e8526dad8c481f82e445badfced5d69747eaf7e5660b0d176cf86d394a02e"}, - {file = "cvat_sdk-2.31.0.tar.gz", hash = "sha256:aaeff833c32bfe711f418c62bdab135e0746eff0e89757e8b61cfad14a42ef23"}, + {file = "cvat_sdk-2.37.0-py3-none-any.whl", hash = "sha256:faa94cfd6678089814179a8da828761dfa3daf08eb752490ee85551a1045dac5"}, + {file = "cvat_sdk-2.37.0.tar.gz", hash = "sha256:e990908a473c499eb6d7b84f7f2e640ea729ef027d4c4cc32a5a925752532689"}, ] [package.dependencies] @@ -4732,4 +4732,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10, <3.13" -content-hash = "5f830a339a6f870a60e94be16dc742280e0ec9002fb4a51404fca9e18a6f399f" +content-hash = "3f4ce0cc7668a0c9ffaa02c1306404603d41215e39199c212dabef36ab112a7c" diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index fa03eb2769..194543a267 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -24,7 +24,7 @@ google-cloud-storage = "^2.14.0" datumaro = {git = "https://github.com/cvat-ai/datumaro.git", rev = "ff83c00c2c1bc4b8fdfcc55067fcab0a9b5b6b11"} hexbytes = ">=1.2.0" # required for to_0x_hex() function starlette = ">=0.40.0" # avoid the vulnerability with multipart/form-data -cvat-sdk = "2.31.0" +cvat-sdk = "2.37.0" cryptography = "<44.0.0" # human-protocol-sdk -> pgpy dep requires cryptography < 45 human-protocol-sdk = "^4.0.3" From 4f87e546bb7b6259394a9719de38f302b2c6618a Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 16 May 2025 19:15:43 +0300 Subject: [PATCH 4/6] Address api changes --- .../exchange-oracle/src/cvat/api_calls.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py index 27c869c863..5f8bb7578d 100644 --- a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py @@ -47,15 +47,23 @@ def _request_annotations(endpoint: Endpoint, cvat_id: int, format_name: str) -> _get_annotations(request_id, ...) """ - (_, response) = endpoint.call_with_http_info( - id=cvat_id, - format=format_name, - save_images=False, - _parse_response=False, - ) + try: + (_, response) = endpoint.call_with_http_info( + id=cvat_id, + format=format_name, + save_images=False, + _parse_response=False, + ) + + assert response.status in [HTTPStatus.ACCEPTED, HTTPStatus.CREATED] + rq_id = response.json()["rq_id"] + except exceptions.ApiException as e: + if e.status == HTTPStatus.CONFLICT: + rq_id = json.loads(e.body)["rq_id"] + else: + raise - assert response.status in [HTTPStatus.ACCEPTED, HTTPStatus.CREATED] - return response.json()["rq_id"] + return rq_id def _get_annotations( @@ -711,6 +719,7 @@ def update_quality_control_settings( logger = logging.getLogger("app") params = { + "inherit": False, "max_validations_per_job": max_validations_per_job, "target_metric": target_metric, "target_metric_threshold": target_metric_threshold, From 165f4428dcb78d0d7a9cbc04e5212e318dd60f56 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 16 May 2025 19:16:34 +0300 Subject: [PATCH 5/6] Improve performance of some cvat calls --- packages/examples/cvat/exchange-oracle/src/.env.template | 2 ++ packages/examples/cvat/exchange-oracle/src/core/config.py | 5 ++++- packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py | 2 ++ packages/examples/cvat/recording-oracle/src/.env.template | 1 + packages/examples/cvat/recording-oracle/src/core/config.py | 1 + .../examples/cvat/recording-oracle/src/cvat/api_calls.py | 5 ++++- 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/.env.template b/packages/examples/cvat/exchange-oracle/src/.env.template index b07c93515c..88a546cff2 100644 --- a/packages/examples/cvat/exchange-oracle/src/.env.template +++ b/packages/examples/cvat/exchange-oracle/src/.env.template @@ -77,6 +77,8 @@ CVAT_IOU_THRESHOLD= CVAT_OKS_SIGMA= CVAT_EXPORT_TIMEOUT= CVAT_IMPORT_TIMEOUT= +CVAT_PROJECTS_PAGE_SIZE= +CVAT_JOBS_PAGE_SIZE= # Storage Config (S3/GCS) diff --git a/packages/examples/cvat/exchange-oracle/src/core/config.py b/packages/examples/cvat/exchange-oracle/src/core/config.py index 9e40bb1265..245380cc58 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/config.py +++ b/packages/examples/cvat/exchange-oracle/src/core/config.py @@ -146,7 +146,7 @@ class CronConfig: "Maximum number of downloading attempts per job or project during results downloading" track_completed_escrows_jobs_downloading_batch_size = int( - getenv("TRACK_COMPLETED_ESCROWS_JOBS_DOWNLOADING_BATCH_SIZE", 500) + getenv("TRACK_COMPLETED_ESCROWS_JOBS_DOWNLOADING_BATCH_SIZE", 10) ) "Maximum number of parallel downloading requests during results downloading" @@ -183,6 +183,9 @@ class CvatConfig: incoming_webhooks_url = getenv("CVAT_INCOMING_WEBHOOKS_URL") webhook_secret = getenv("CVAT_WEBHOOK_SECRET", "thisisasamplesecret") + projects_page_size = int(getenv("CVAT_PROJECTS_PAGE_SIZE", 100)) + jobs_page_size = int(getenv("CVAT_JOBS_PAGE_SIZE", 100)) + class StorageConfig: provider: ClassVar[str] = os.environ["STORAGE_PROVIDER"].lower() diff --git a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py index 5f8bb7578d..06934225b4 100644 --- a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py @@ -470,6 +470,7 @@ def fetch_task_jobs(task_id: int) -> list[models.JobRead]: api_client.jobs_api.list_endpoint, task_id=task_id, type="annotation", + page_size=Config.cvat_config.jobs_page_size, ) except exceptions.ApiException as e: logger.exception(f"Exception when calling JobsApi.list: {e}\n") @@ -543,6 +544,7 @@ def fetch_projects(assignee: str = "") -> list[models.ProjectRead]: return get_paginated_collection( api_client.projects_api.list_endpoint, **({"assignee": assignee} if assignee else {}), + page_size=Config.cvat_config.projects_page_size, ) except exceptions.ApiException as e: logger.exception(f"Exception when calling ProjectsApi.list(): {e}\n") diff --git a/packages/examples/cvat/recording-oracle/src/.env.template b/packages/examples/cvat/recording-oracle/src/.env.template index 6abb5d245f..a5851b0fb6 100644 --- a/packages/examples/cvat/recording-oracle/src/.env.template +++ b/packages/examples/cvat/recording-oracle/src/.env.template @@ -61,6 +61,7 @@ CVAT_ADMIN_PASS= CVAT_ORG_SLUG= CVAT_QUALITY_RETRIEVAL_TIMEOUT= CVAT_QUALITY_CHECK_INTERVAL= +CVAT_QUALITY_REPORTS_PAGE_SIZE= # Localhost diff --git a/packages/examples/cvat/recording-oracle/src/core/config.py b/packages/examples/cvat/recording-oracle/src/core/config.py index fa262b8b0b..37f2afce18 100644 --- a/packages/examples/cvat/recording-oracle/src/core/config.py +++ b/packages/examples/cvat/recording-oracle/src/core/config.py @@ -234,6 +234,7 @@ class CvatConfig: quality_retrieval_timeout = int(getenv("CVAT_QUALITY_RETRIEVAL_TIMEOUT", 60 * 60)) quality_check_interval = int(getenv("CVAT_QUALITY_CHECK_INTERVAL", 5)) + quality_reports_page_size = int(getenv("CVAT_QUALITY_REPORTS_PAGE_SIZE", 100)) class Config: diff --git a/packages/examples/cvat/recording-oracle/src/cvat/api_calls.py b/packages/examples/cvat/recording-oracle/src/cvat/api_calls.py index 0b51e7986c..574e44c918 100644 --- a/packages/examples/cvat/recording-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/recording-oracle/src/cvat/api_calls.py @@ -134,7 +134,10 @@ def get_jobs_quality_reports(parent_id: int) -> list[models.QualityReport]: with get_api_client() as api_client: try: return get_paginated_collection( - api_client.quality_api.list_reports_endpoint, parent_id=parent_id, target="job" + api_client.quality_api.list_reports_endpoint, + parent_id=parent_id, + target="job", + page_size=Config.cvat_config.quality_reports_page_size, ) except exceptions.ApiException as e: From 48eb254879813ab7120818649719f64887f0c3bd Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 16 May 2025 19:59:59 +0300 Subject: [PATCH 6/6] Improve description --- packages/examples/cvat/recording-oracle/src/core/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/examples/cvat/recording-oracle/src/core/config.py b/packages/examples/cvat/recording-oracle/src/core/config.py index 37f2afce18..248f51cc6e 100644 --- a/packages/examples/cvat/recording-oracle/src/core/config.py +++ b/packages/examples/cvat/recording-oracle/src/core/config.py @@ -195,6 +195,7 @@ class ValidationConfig: warmup_iterations = int(getenv("WARMUP_ITERATIONS", "1")) """ The first escrow iterations where the annotation speed is checked to be big enough. + Set to 0 to disable. """ min_warmup_progress = float(getenv("MIN_WARMUP_PROGRESS", "10"))