{frameTags
- .filter((tag: any) => tag.source !== core.enums.Source.GT)
- .map((tag: any) => (
+ .filter((tag: ObjectState) => !tag.isGroundTruth)
+ .map((tag: ObjectState) => (
conflict.serverID === tag.serverID).length !== 0 ? 'cvat-frame-tag-highlighted' : 'cvat-frame-tag'
- }
+ className={tagClassName(tag)}
color={tag.label.color}
onClose={() => {
onRemoveState(tag);
@@ -88,14 +62,12 @@ function FrameTags(props: StateToProps & DispatchToProps): JSX.Element {
))}
+
{frameTags
- .filter((tag: any) => tag.source === core.enums.Source.GT)
- .map((tag: any) => (
+ .filter((tag: ObjectState) => tag.isGroundTruth)
+ .map((tag: ObjectState) => (
conflict.serverID === tag.serverID).length !== 0 ? 'cvat-frame-tag-highlighted' : 'cvat-frame-tag'
- }
+ className={tagClassName(tag)}
color={tag.label.color}
onClose={() => {
onRemoveState(tag);
@@ -112,4 +84,4 @@ function FrameTags(props: StateToProps & DispatchToProps): JSX.Element {
);
}
-export default connect(mapStateToProps, mapDispatchToProps)(FrameTags);
+export default React.memo(FrameTags);
diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss
index 8ac9962e01e8..0740e3c9046d 100644
--- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss
+++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss
@@ -96,3 +96,7 @@
transform: scale(1.1);
}
+
+.cvat-canvas-annotation-frame-tags {
+ margin-bottom: $grid-unit-size;
+}
diff --git a/cvat-ui/src/components/requests-page/request-card.tsx b/cvat-ui/src/components/requests-page/request-card.tsx
index 980af114563a..52c109e3822c 100644
--- a/cvat-ui/src/components/requests-page/request-card.tsx
+++ b/cvat-ui/src/components/requests-page/request-card.tsx
@@ -100,11 +100,15 @@ function constructTimestamps(request: Request): JSX.Element {
);
}
case RQStatus.FAILED: {
- return (
+ return (request.startedDate ? (
{`Started by ${request.owner.username} on ${started}`}
- );
+ ) : (
+
+ {`Enqueued by ${request.owner.username} on ${created}`}
+
+ ));
}
case RQStatus.STARTED: {
return (
diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts
index 275cedcc8ab9..94b70373a1c7 100644
--- a/cvat-ui/src/cvat-core-wrapper.ts
+++ b/cvat-ui/src/cvat-core-wrapper.ts
@@ -17,6 +17,7 @@ import {
import {
SerializedAttribute, SerializedLabel, SerializedAPISchema,
} from 'cvat-core/src/server-response-types';
+import { UpdateStatusData } from 'cvat-core/src/core-types';
import { Job, Task } from 'cvat-core/src/session';
import Project from 'cvat-core/src/project';
import QualityReport, { QualitySummary } from 'cvat-core/src/quality-report';
@@ -41,7 +42,7 @@ import { Dumper } from 'cvat-core/src/annotation-formats';
import { Event } from 'cvat-core/src/event';
import { APIWrapperEnterOptions } from 'cvat-core/src/plugins';
import BaseSingleFrameAction, { ActionParameterType, FrameSelectionType } from 'cvat-core/src/annotations-actions';
-import { Request } from 'cvat-core/src/request';
+import { Request, RequestOperation } from 'cvat-core/src/request';
const cvat: CVATCore = _cvat;
@@ -120,4 +121,6 @@ export type {
CVATCore,
SerializedAPISchema,
ProjectOrTaskOrJob,
+ RequestOperation,
+ UpdateStatusData,
};
diff --git a/cvat-ui/src/utils/is-able-to-change-frame.ts b/cvat-ui/src/utils/is-able-to-change-frame.ts
index b3029c4b2670..3cbc127a8a86 100644
--- a/cvat-ui/src/utils/is-able-to-change-frame.ts
+++ b/cvat-ui/src/utils/is-able-to-change-frame.ts
@@ -17,15 +17,15 @@ export default function isAbleToChangeFrame(frame?: number): boolean {
return false;
}
- const frameInTheJob = true;
+ let frameInTheJob = true;
if (typeof frame === 'number') {
if (meta.includedFrames) {
// frame argument comes in job coordinates
// hovewer includedFrames contains absolute data values
- return meta.includedFrames.includes(meta.getDataFrameNumber(frame - job.startFrame));
+ frameInTheJob = meta.includedFrames.includes(meta.getDataFrameNumber(frame - job.startFrame));
}
- return frame >= job.startFrame && frame <= job.stopFrame;
+ frameInTheJob = frame >= job.startFrame && frame <= job.stopFrame;
}
return canvas.isAbleToChangeFrame() && frameInTheJob && !state.annotation.player.navigationBlocked;
diff --git a/cvat/__init__.py b/cvat/__init__.py
index 33ddf9c436cf..ac47316ad0a1 100644
--- a/cvat/__init__.py
+++ b/cvat/__init__.py
@@ -4,6 +4,6 @@
from cvat.utils.version import get_version
-VERSION = (2, 21, 2, 'final', 0)
+VERSION = (2, 21, 3, 'final', 0)
__version__ = get_version(VERSION)
diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py
index a64637359ff5..c923083b18b3 100644
--- a/cvat/apps/engine/media_extractors.py
+++ b/cvat/apps/engine/media_extractors.py
@@ -539,7 +539,9 @@ def extract(self):
class _AvVideoReading:
@contextmanager
- def read_av_container(self, source: Union[str, io.BytesIO]) -> av.container.InputContainer:
+ def read_av_container(
+ self, source: Union[str, io.BytesIO]
+ ) -> Generator[av.container.InputContainer, None, None]:
if isinstance(source, io.BytesIO):
source.seek(0) # required for re-reading
diff --git a/cvat/schema.yml b/cvat/schema.yml
index b05b2989d53b..3af7944889b4 100644
--- a/cvat/schema.yml
+++ b/cvat/schema.yml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: CVAT REST API
- version: 2.21.2
+ version: 2.21.3
description: REST API for Computer Vision Annotation Tool (CVAT)
termsOfService: https://www.google.com/policies/terms/
contact:
diff --git a/docker-compose.yml b/docker-compose.yml
index 45399e0c39ea..b51e38fce7d5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -79,7 +79,7 @@ services:
cvat_server:
container_name: cvat_server
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on:
<<: *backend-deps
@@ -113,7 +113,7 @@ services:
cvat_utils:
container_name: cvat_utils
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on: *backend-deps
environment:
@@ -130,7 +130,7 @@ services:
cvat_worker_import:
container_name: cvat_worker_import
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on: *backend-deps
environment:
@@ -146,7 +146,7 @@ services:
cvat_worker_export:
container_name: cvat_worker_export
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on: *backend-deps
environment:
@@ -162,7 +162,7 @@ services:
cvat_worker_annotation:
container_name: cvat_worker_annotation
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on: *backend-deps
environment:
@@ -178,7 +178,7 @@ services:
cvat_worker_webhooks:
container_name: cvat_worker_webhooks
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on: *backend-deps
environment:
@@ -194,7 +194,7 @@ services:
cvat_worker_quality_reports:
container_name: cvat_worker_quality_reports
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on: *backend-deps
environment:
@@ -210,7 +210,7 @@ services:
cvat_worker_analytics_reports:
container_name: cvat_worker_analytics_reports
- image: cvat/server:${CVAT_VERSION:-v2.21.2}
+ image: cvat/server:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on: *backend-deps
environment:
@@ -226,7 +226,7 @@ services:
cvat_ui:
container_name: cvat_ui
- image: cvat/ui:${CVAT_VERSION:-v2.21.2}
+ image: cvat/ui:${CVAT_VERSION:-v2.21.3}
restart: always
depends_on:
- cvat_server
diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml
index fc1e841bec43..d0906952f96d 100644
--- a/helm-chart/values.yaml
+++ b/helm-chart/values.yaml
@@ -129,7 +129,7 @@ cvat:
additionalVolumeMounts: []
replicas: 1
image: cvat/server
- tag: v2.21.2
+ tag: v2.21.3
imagePullPolicy: Always
permissionFix:
enabled: true
@@ -153,7 +153,7 @@ cvat:
frontend:
replicas: 1
image: cvat/ui
- tag: v2.21.2
+ tag: v2.21.3
imagePullPolicy: Always
labels: {}
# test: test
diff --git a/pyproject.toml b/pyproject.toml
index 581552a67ebc..6d0772451578 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,4 +6,4 @@ skip_gitignore = true # align tool behavior with Black
[tool.black]
line-length = 100
-target-version = ['py38']
+target-version = ['py39']
diff --git a/site/content/en/docs/administration/advanced/analytics.md b/site/content/en/docs/administration/advanced/analytics.md
index 36425c341a32..445c212f9687 100644
--- a/site/content/en/docs/administration/advanced/analytics.md
+++ b/site/content/en/docs/administration/advanced/analytics.md
@@ -18,6 +18,10 @@ and enhance user satisfaction.
CVAT analytics are available from the top menu.
+Superusers and users with administrator role have access to analytics.
+Permission to access analytics can also be granted when editing a user
+on admin page by `Has access to analytics` checkbox.
+
![CVAT Analytics](/images/analytics_menu.jpg)
> Note: CVAT analytics and monitoring are available only for on-prem solution.
diff --git a/site/content/en/docs/api_sdk/cli/_index.md b/site/content/en/docs/api_sdk/cli/_index.md
index f17d712717b2..541e29f30611 100644
--- a/site/content/en/docs/api_sdk/cli/_index.md
+++ b/site/content/en/docs/api_sdk/cli/_index.md
@@ -30,7 +30,7 @@ To install an [official release of CVAT CLI](https://pypi.org/project/cvat-cli/)
pip install cvat-cli
```
-We support Python versions 3.8 and higher.
+We support Python versions 3.9 and higher.
## Usage
diff --git a/site/content/en/docs/api_sdk/sdk/_index.md b/site/content/en/docs/api_sdk/sdk/_index.md
index 4c133a7b0231..e9683583ab0e 100644
--- a/site/content/en/docs/api_sdk/sdk/_index.md
+++ b/site/content/en/docs/api_sdk/sdk/_index.md
@@ -48,7 +48,7 @@ To use the PyTorch adapter, request the `pytorch` extra:
pip install "cvat-sdk[pytorch]"
```
-We support Python versions 3.8 and higher.
+We support Python versions 3.9 and higher.
## Usage
diff --git a/tests/cypress.config.js b/tests/cypress.config.js
index da1f20d8dee1..17f157c59c8d 100644
--- a/tests/cypress.config.js
+++ b/tests/cypress.config.js
@@ -1,3 +1,7 @@
+// Copyright (C) 2024 CVAT.ai Corporation
+//
+// SPDX-License-Identifier: MIT
+
const { defineConfig } = require('cypress');
const baseConfig = require('./cypress.base.config');
diff --git a/tests/cypress/e2e/actions_objects/regression_tests.js b/tests/cypress/e2e/actions_objects/regression_tests.js
index 8cf00b90e0c1..7bf11c7b0d7b 100644
--- a/tests/cypress/e2e/actions_objects/regression_tests.js
+++ b/tests/cypress/e2e/actions_objects/regression_tests.js
@@ -9,9 +9,9 @@ context('Regression tests', () => {
let jobID = null;
const taskPayload = {
- name: 'Test annotations actions',
+ name: 'Regression tests',
labels: [{
- name: 'label 1',
+ name: 'car',
attributes: [],
type: 'any',
}],
@@ -29,12 +29,9 @@ context('Regression tests', () => {
};
const rectanglePayload = {
- frame: 99,
- objectType: 'shape',
shapeType: 'rectangle',
- points: [250, 64, 491, 228],
occluded: false,
- labelName: 'label 1',
+ labelName: taskPayload.labels[0].name,
};
before(() => {
@@ -45,41 +42,65 @@ context('Regression tests', () => {
taskID = response.taskID;
[jobID] = response.jobIDs;
- cy.headlessCreateObjects([rectanglePayload], jobID);
- cy.visit(`/tasks/${taskID}/jobs/${jobID}`);
+ cy.headlessCreateObjects([
+ {
+ ...rectanglePayload, frame: 99, points: [250, 64, 491, 228], objectType: 'shape',
+ },
+ {
+ ...rectanglePayload, frame: 0, points: [10, 10, 30, 30], objectType: 'track',
+ },
+ ], jobID);
});
});
- describe('Regression tests', () => {
+ describe('UI does not crash', () => {
+ beforeEach(() => {
+ cy.visit(`/tasks/${taskID}/jobs/${jobID}`);
+ cy.get('.cvat-canvas-container').should('not.exist');
+ cy.get('.cvat-canvas-container').should('exist').and('be.visible');
+ });
+
it('UI does not crash if to activate an object while frame fetching', () => {
- cy.reload();
cy.intercept('GET', '/api/jobs/**/data?**', (req) => {
req.continue((res) => {
- res.setDelay(1000);
+ res.setDelay(3000);
});
}).as('delayedRequest');
+
cy.get('.cvat-player-last-button').click();
- cy.get('#cvat_canvas_shape_1').trigger('mousemove');
- cy.get('#cvat_canvas_shape_1').should('not.have.class', 'cvat_canvas_shape_activated');
+ cy.get('#cvat-objects-sidebar-state-item-1').trigger('mousemove');
+ cy.get('#cvat-objects-sidebar-state-item-1').should('not.have.class', 'cvat-objects-sidebar-state-active-item');
cy.wait('@delayedRequest');
cy.get('#cvat_canvas_shape_1').trigger('mousemove');
cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated');
});
+
+ it('UI does not crash if to navigate during an element resizing (issue 1922)', { scrollBehavior: false }, () => {
+ cy.get('#cvat_canvas_shape_2').then(([el]) => {
+ const rect = el.getBoundingClientRect();
+
+ cy.get('body').trigger('mousemove', rect.x + rect.width / 2, rect.y + rect.height / 2);
+ cy.get('#cvat_canvas_shape_2').should('have.class', 'cvat_canvas_shape_activated');
+
+ cy.get('body').trigger('mousedown', rect.right, rect.bottom, { button: 0 });
+ cy.get('body').trigger('mousemove', rect.right + 100, rect.bottom + 100);
+
+ cy.get('body').type('f'); // go to next frame
+ cy.get('body').trigger('mouseup');
+
+ // Page with the error is missing
+ cy.get('.cvat-global-boundary').should('not.exist');
+ cy.checkFrameNum(0);
+ });
+ });
});
after(() => {
+ if (taskID !== null) {
+ cy.headlessDeleteTask(taskID);
+ }
cy.logout();
- cy.getAuthKey().then((response) => {
- const authKey = response.body.key;
- cy.request({
- method: 'DELETE',
- url: `/api/tasks/${taskID}`,
- headers: {
- Authorization: `Token ${authKey}`,
- },
- });
- });
});
});
diff --git a/tests/cypress/e2e/features/masks_basics.js b/tests/cypress/e2e/features/masks_basics.js
index b39d6ea769d7..3e119e97f039 100644
--- a/tests/cypress/e2e/features/masks_basics.js
+++ b/tests/cypress/e2e/features/masks_basics.js
@@ -156,6 +156,12 @@ context('Manipulations with masks', { scrollBehavior: false }, () => {
cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit');
cy.drawMask(editingActions);
+
+ // Check issue fixed in https://github.com/cvat-ai/cvat/pull/8598
+ // Frames navigation should not work during editing
+ cy.get('.cvat-player-next-button').click();
+ cy.checkFrameNum(0);
+
cy.finishMaskDrawing();
});
diff --git a/tests/cypress/e2e/features/requests_page.js b/tests/cypress/e2e/features/requests_page.js
index 3cdf187a9825..8622092b89d8 100644
--- a/tests/cypress/e2e/features/requests_page.js
+++ b/tests/cypress/e2e/features/requests_page.js
@@ -357,5 +357,24 @@ context('Requests page', () => {
});
});
});
+
+ it('Export task. Request for status fails, UI is not crushing', () => {
+ cy.intercept('GET', '/api/requests/**', {
+ statusCode: 500,
+ body: 'Network error',
+ });
+
+ cy.exportTask({
+ type: 'annotations',
+ format: exportFormat,
+ archiveCustomName: annotationsArchiveNameLocal,
+ });
+
+ cy.contains('Could not export dataset').should('be.visible');
+ cy.closeNotification('.ant-notification-notice-error');
+
+ cy.contains('.cvat-header-button', 'Requests').should('be.visible').click();
+ cy.get('.cvat-requests-page').should('be.visible');
+ });
});
});
diff --git a/tests/cypress/e2e/issues_prs/issue_1922_error_canvas_is_busy_at_resize_element.js b/tests/cypress/e2e/issues_prs/issue_1922_error_canvas_is_busy_at_resize_element.js
deleted file mode 100644
index 53b5606f457b..000000000000
--- a/tests/cypress/e2e/issues_prs/issue_1922_error_canvas_is_busy_at_resize_element.js
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2020-2022 Intel Corporation
-// Copyright (C) 2023 CVAT.ai Corporation
-//
-// SPDX-License-Identifier: MIT
-
-///
-
-import { taskName, labelName } from '../../support/const';
-
-context('Check error canvas is busy at resize element', () => {
- const issueId = '1922';
- const createRectangleShape2Points = {
- points: 'By 2 Points',
- type: 'Shape',
- labelName,
- firstX: 100,
- firstY: 100,
- secondX: 300,
- secondY: 300,
- };
-
- before(() => {
- cy.openTaskJob(taskName);
- });
-
- describe(`Testing issue "${issueId}"`, () => {
- it('Create an object in first frame', () => {
- cy.createRectangle(createRectangleShape2Points);
- });
-
- it('Go to next frame and create an object in second frame', () => {
- cy.get('.cvat-player-next-button').click();
- cy.createRectangle(createRectangleShape2Points);
- });
-
- it('Switching mode of button on "back with a filter"', () => {
- cy.get('.cvat-player-previous-button').rightclick();
- cy.get('.cvat-player-previous-filtered-inlined-button').click();
- });
-
- it('Resize element on second frame and go to previous frame at resizing element', () => {
- const { secondX, secondY } = createRectangleShape2Points;
- cy.get('.cvat-canvas-container').trigger('mousemove', secondX - 10, secondY - 10); // activate second shape
- cy.get('.cvat-canvas-container').trigger('mousedown', secondX, secondY, { button: 0 });
- cy.get('.cvat-canvas-container').trigger('mousemove', secondX + 100, secondY + 100);
- cy.get('body').type('d'); // go to previous frame
- cy.get('body').trigger('mouseup');
- });
-
- it('Page with the error is missing', () => {
- cy.get('.cvat-global-boundary').should('not.exist');
- });
- });
-});
diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js
index 76f7ffe2640c..91322e9c3695 100644
--- a/tests/cypress/support/commands.js
+++ b/tests/cypress/support/commands.js
@@ -1272,7 +1272,7 @@ Cypress.Commands.add('exportTask', ({
cy.get('.cvat-cloud-storage-select-provider').click();
}
}
- cy.contains('button', 'OK').click();
+ cy.contains('.cvat-modal-export-task button', 'OK').click();
cy.get('.cvat-notification-notice-export-task-start').should('be.visible');
cy.closeNotification('.cvat-notification-notice-export-task-start');
});
diff --git a/tests/cypress_canvas3d.config.js b/tests/cypress_canvas3d.config.js
index e1cd5ede69f2..f542fe78bde9 100644
--- a/tests/cypress_canvas3d.config.js
+++ b/tests/cypress_canvas3d.config.js
@@ -1,3 +1,7 @@
+// Copyright (C) 2024 CVAT.ai Corporation
+//
+// SPDX-License-Identifier: MIT
+
const { defineConfig } = require('cypress');
const baseConfig = require('./cypress.base.config');
diff --git a/tests/python/cli/self-signed.crt b/tests/python/cli/self-signed.crt
new file mode 100644
index 000000000000..815373bf286c
--- /dev/null
+++ b/tests/python/cli/self-signed.crt
@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBPDCB76ADAgECAhQksQwFGcyVwF0+gIOPMPBB+/NjNTAFBgMrZXAwFDESMBAG
+A1UEAwwJbG9jYWxob3N0MB4XDTI0MTAyODEyMTkyNFoXDTI0MTAyOTEyMTkyNFow
+FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAzGOv96vkrHr0GPcWL7vN
+8mgR4XMg9ItNpJ2nbMmjYCKjUzBRMB0GA1UdDgQWBBR6Hn0aG/ZGAJjY9HIUK7El
+84qAgzAfBgNVHSMEGDAWgBR6Hn0aG/ZGAJjY9HIUK7El84qAgzAPBgNVHRMBAf8E
+BTADAQH/MAUGAytlcANBAMj2zWdIa8oOiEtUWFMv+KYf1kyP1lUnlcC2xUpOj8d3
+kRYtlRX4E7F5zzzgKgNpbanRAg72qnqPiFAFCGVAhgY=
+-----END CERTIFICATE-----
diff --git a/tests/python/cli/self-signed.key b/tests/python/cli/self-signed.key
new file mode 100644
index 000000000000..f81d8519bcac
--- /dev/null
+++ b/tests/python/cli/self-signed.key
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIKe5zj/UrVJ/LySjKm9BBVHXziqFIwJ6w+HuTHnldCLo
+-----END PRIVATE KEY-----
diff --git a/tests/python/cli/test_cli.py b/tests/python/cli/test_cli.py
index 364c7011e7ca..d6b19cfe0a3c 100644
--- a/tests/python/cli/test_cli.py
+++ b/tests/python/cli/test_cli.py
@@ -10,7 +10,6 @@
import packaging.version as pv
import pytest
-from cvat_cli.cli import CLI
from cvat_sdk import Client, make_client
from cvat_sdk.api_client import exceptions, models
from cvat_sdk.core.proxies.tasks import ResourceType, Task
@@ -20,7 +19,7 @@
from shared.utils.config import BASE_URL, USER_PASS
from shared.utils.helpers import generate_image_file
-from .util import generate_images, run_cli
+from .util import generate_images, https_reverse_proxy, run_cli
class TestCLI:
@@ -243,23 +242,26 @@ def mocked_version(_):
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, monkeypatch, verify: bool):
- # TODO: Very hacky implementation, improve it, if possible
- class MyException(Exception):
- pass
-
- normal_init = CLI.__init__
-
- def my_init(self, *args, **kwargs):
- normal_init(self, *args, **kwargs)
- raise MyException(self.client.api_client.configuration.verify_ssl)
-
- monkeypatch.setattr(CLI, "__init__", my_init)
-
- with pytest.raises(MyException) as capture:
- self.run_cli(*(["--insecure"] if not verify else []), "ls")
-
- assert capture.value.args[0] == verify
+ 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,
+ "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"
diff --git a/tests/python/cli/util.py b/tests/python/cli/util.py
index 034d5d073ace..ff1173fa4a8d 100644
--- a/tests/python/cli/util.py
+++ b/tests/python/cli/util.py
@@ -3,10 +3,17 @@
# SPDX-License-Identifier: MIT
+import contextlib
+import http.server
+import ssl
+import threading
import unittest
from pathlib import Path
-from typing import Any, List, Union
+from typing import Any, Dict, Generator, List, Union
+import requests
+
+from shared.utils.config import BASE_URL
from shared.utils.helpers import generate_image_file
@@ -29,3 +36,50 @@ def generate_images(dst_dir: Path, count: int) -> List[Path]:
filename.write_bytes(generate_image_file(filename.name).getvalue())
filenames.append(filename)
return filenames
+
+
+@contextlib.contextmanager
+def https_reverse_proxy() -> Generator[str, None, None]:
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
+ cert_dir = Path(__file__).parent
+ ssl_context.load_cert_chain(cert_dir / "self-signed.crt", cert_dir / "self-signed.key")
+
+ with http.server.HTTPServer(("localhost", 0), _ProxyHttpRequestHandler) as proxy_server:
+ proxy_server.socket = ssl_context.wrap_socket(
+ proxy_server.socket,
+ server_side=True,
+ )
+ server_thread = threading.Thread(target=proxy_server.serve_forever)
+ server_thread.start()
+ try:
+ yield f"https://localhost:{proxy_server.server_port}"
+ finally:
+ proxy_server.shutdown()
+ server_thread.join()
+
+
+class _ProxyHttpRequestHandler(http.server.BaseHTTPRequestHandler):
+ def do_GET(self):
+ response = requests.get(**self._shared_request_args())
+ self._translate_response(response)
+
+ def do_POST(self):
+ body_length = int(self.headers["Content-Length"])
+
+ response = requests.post(data=self.rfile.read(body_length), **self._shared_request_args())
+ self._translate_response(response)
+
+ def _shared_request_args(self) -> Dict[str, Any]:
+ headers = {k.lower(): v for k, v in self.headers.items()}
+ del headers["host"]
+
+ return {"url": BASE_URL + self.path, "headers": headers, "timeout": 60, "stream": True}
+
+ def _translate_response(self, response: requests.Response) -> None:
+ self.send_response(response.status_code)
+ for key, value in response.headers.items():
+ self.send_header(key, value)
+ self.end_headers()
+ # Need to use raw here to prevent requests from handling Content-Encoding.
+ self.wfile.write(response.raw.read())
diff --git a/tests/python/shared/fixtures/init.py b/tests/python/shared/fixtures/init.py
index aa1192a0acf5..b29c5c30528b 100644
--- a/tests/python/shared/fixtures/init.py
+++ b/tests/python/shared/fixtures/init.py
@@ -300,9 +300,10 @@ def dump_db():
def create_compose_files(container_name_files):
for filename in container_name_files:
- with open(filename.with_name(filename.name.replace(".tests", "")), "r") as dcf, open(
- filename, "w"
- ) as ndcf:
+ with (
+ open(filename.with_name(filename.name.replace(".tests", "")), "r") as dcf,
+ open(filename, "w") as ndcf,
+ ):
dc_config = yaml.safe_load(dcf)
for service_name, service_config in dc_config["services"].items():
diff --git a/yarn.lock b/yarn.lock
index 111a2b86f925..1f42b7b8dccc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6294,9 +6294,9 @@ http-proxy-agent@^5.0.0:
debug "4"
http-proxy-middleware@^2.0.3:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
- integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6"
+ integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==
dependencies:
"@types/http-proxy" "^1.17.8"
http-proxy "^1.18.1"