chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"user": {
|
||||
"admin1": [],
|
||||
"admin2": [
|
||||
{
|
||||
"created_date": "2025-07-17T16:17:35.436000Z",
|
||||
"expiry_date": null,
|
||||
"id": 3,
|
||||
"last_used_date": null,
|
||||
"name": "test key",
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 18,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/18",
|
||||
"username": "admin2"
|
||||
},
|
||||
"read_only": false,
|
||||
"updated_date": "2025-07-17T16:17:35.436000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2025-08-12T10:30:10.206000Z",
|
||||
"expiry_date": null,
|
||||
"id": 7,
|
||||
"last_used_date": null,
|
||||
"name": "test readonly key",
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 18,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/18",
|
||||
"username": "admin2"
|
||||
},
|
||||
"read_only": true,
|
||||
"updated_date": "2025-08-12T10:30:10.206000Z"
|
||||
}
|
||||
],
|
||||
"lonely_user": [],
|
||||
"user1": [
|
||||
{
|
||||
"created_date": "2025-07-17T16:19:18.229000Z",
|
||||
"expiry_date": null,
|
||||
"id": 4,
|
||||
"last_used_date": null,
|
||||
"name": "test readonly key",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"read_only": true,
|
||||
"updated_date": "2025-07-17T16:19:18.229000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2025-07-17T16:19:46.576000Z",
|
||||
"expiry_date": null,
|
||||
"id": 5,
|
||||
"last_used_date": null,
|
||||
"name": "test readwrite key",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"read_only": false,
|
||||
"updated_date": "2025-07-17T16:19:46.576000Z"
|
||||
}
|
||||
],
|
||||
"user10": [],
|
||||
"user2": [],
|
||||
"user3": [],
|
||||
"user4": [],
|
||||
"user5": [],
|
||||
"user6": [],
|
||||
"user7": [],
|
||||
"user8": [],
|
||||
"user9": [],
|
||||
"worker1": [],
|
||||
"worker2": [],
|
||||
"worker3": [],
|
||||
"worker4": []
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"count": 5,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"created_date": "2026-02-11T16:29:48Z",
|
||||
"credentials_type": "KEY_SECRET_KEY_PAIR",
|
||||
"description": "Bucket to be used as backing cloud storage for tasks",
|
||||
"display_name": "Backing CS",
|
||||
"id": 5,
|
||||
"manifests": [],
|
||||
"organization": null,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"provider_type": "AWS_S3_BUCKET",
|
||||
"resource": "backingcs",
|
||||
"specific_attributes": "endpoint_url=http%3A%2F%2Fminio%3A9000",
|
||||
"updated_date": "2026-02-11T16:29:48Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-06-29T12:56:18.257000Z",
|
||||
"credentials_type": "KEY_SECRET_KEY_PAIR",
|
||||
"description": "Bucket with wrong endpoint for testing purposes",
|
||||
"display_name": "Import/Export bucket with wrong endpoint",
|
||||
"id": 4,
|
||||
"manifests": [],
|
||||
"organization": 2,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"provider_type": "AWS_S3_BUCKET",
|
||||
"resource": "importexportbucketwrongendpoint",
|
||||
"specific_attributes": "endpoint_url=http%3A%2F%2Fminio%3A9777",
|
||||
"updated_date": "2022-06-29T12:56:18.264000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-06-29T12:56:18.257000Z",
|
||||
"credentials_type": "KEY_SECRET_KEY_PAIR",
|
||||
"description": "Bucket for importing and exporting annotations and backups",
|
||||
"display_name": "Import/Export bucket",
|
||||
"id": 3,
|
||||
"manifests": [
|
||||
"images_with_manifest/manifest.jsonl"
|
||||
],
|
||||
"organization": 2,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"provider_type": "AWS_S3_BUCKET",
|
||||
"resource": "importexportbucket",
|
||||
"specific_attributes": "endpoint_url=http%3A%2F%2Fminio%3A9000",
|
||||
"updated_date": "2022-06-29T12:56:18.264000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-03-17T07:23:59.305000Z",
|
||||
"credentials_type": "KEY_SECRET_KEY_PAIR",
|
||||
"description": "",
|
||||
"display_name": "Bucket 2",
|
||||
"id": 2,
|
||||
"manifests": [
|
||||
"sub/images_with_manifest/manifest.jsonl"
|
||||
],
|
||||
"organization": 2,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
},
|
||||
"provider_type": "AWS_S3_BUCKET",
|
||||
"resource": "private",
|
||||
"specific_attributes": "endpoint_url=http%3A%2F%2Fminio%3A9000",
|
||||
"updated_date": "2022-07-13T12:46:45.587000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-03-17T07:22:49.519000Z",
|
||||
"credentials_type": "ANONYMOUS_ACCESS",
|
||||
"description": "",
|
||||
"display_name": "Bucket 1",
|
||||
"id": 1,
|
||||
"manifests": [
|
||||
"images_with_manifest/manifest.jsonl"
|
||||
],
|
||||
"organization": null,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"provider_type": "AWS_S3_BUCKET",
|
||||
"resource": "public",
|
||||
"specific_attributes": "endpoint_url=http%3A%2F%2Fminio%3A9000",
|
||||
"updated_date": "2022-03-17T07:22:49.529000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"count": 6,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"created_date": "2022-03-16T12:49:29.372000Z",
|
||||
"id": 6,
|
||||
"issue": 5,
|
||||
"message": "Wrong position",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 20,
|
||||
"last_name": "Sixth",
|
||||
"url": "http://localhost:8080/api/users/20",
|
||||
"username": "user6"
|
||||
},
|
||||
"updated_date": "2022-03-16T12:49:29.372000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-03-16T12:40:00.767000Z",
|
||||
"id": 5,
|
||||
"issue": 4,
|
||||
"message": "Issue with empty frame",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"updated_date": "2022-03-16T12:40:00.767000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-03-16T11:08:18.370000Z",
|
||||
"id": 4,
|
||||
"issue": 3,
|
||||
"message": "Another one issue",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
},
|
||||
"updated_date": "2022-03-16T11:08:18.370000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-03-16T11:07:22.173000Z",
|
||||
"id": 3,
|
||||
"issue": 2,
|
||||
"message": "Something should be here",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
},
|
||||
"updated_date": "2022-03-16T11:07:22.173000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-03-16T11:04:49.821000Z",
|
||||
"id": 2,
|
||||
"issue": 1,
|
||||
"message": "Just to suffer?",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"updated_date": "2022-03-16T11:04:49.821000Z"
|
||||
},
|
||||
{
|
||||
"created_date": "2022-03-16T11:04:39.447000Z",
|
||||
"id": 1,
|
||||
"issue": 1,
|
||||
"message": "Why are we still here?",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"updated_date": "2022-03-16T11:04:39.447000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"iou_threshold": 0.4,
|
||||
"task_id": 30
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"iou_threshold": 0.4,
|
||||
"task_id": 31
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
SELECT :'from' = 'cvat' AS fromcvat \gset
|
||||
|
||||
\if :fromcvat
|
||||
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE pg_stat_activity.datname = 'cvat' AND pid <> pg_backend_pid();
|
||||
\endif
|
||||
|
||||
DROP DATABASE IF EXISTS :to WITH (FORCE);
|
||||
|
||||
CREATE DATABASE :to WITH TEMPLATE :from;
|
||||
@@ -0,0 +1,345 @@
|
||||
{
|
||||
"count": 13,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2023-09-15T07:53:52.116000Z",
|
||||
"expired": true,
|
||||
"key": "q8GWTPiR1Vz9DDO6MQo1B6pUBzW9GjDb6AUQPziAV62jD7OpCLZji0GS66C48wRX",
|
||||
"organization": 1,
|
||||
"organization_info": {
|
||||
"id": 1,
|
||||
"slug": "org1"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2023-09-15T07:53:52.115000Z",
|
||||
"expired": true,
|
||||
"key": "d2Zaawf81uImG1nmWA0Va0Bv5EPERt1edJDTgTgMZiefZ2QmC1IdPld9LIPnkiWR",
|
||||
"organization": 1,
|
||||
"organization_info": {
|
||||
"id": 1,
|
||||
"slug": "org1"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 5,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/5",
|
||||
"username": "user4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2022-09-28T13:11:37.839000Z",
|
||||
"expired": true,
|
||||
"key": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9",
|
||||
"organization": 1,
|
||||
"organization_info": {
|
||||
"id": 1,
|
||||
"slug": "org1"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 19,
|
||||
"last_name": "Fifth",
|
||||
"url": "http://localhost:8080/api/users/19",
|
||||
"username": "user5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2022-02-24T21:29:21.978000Z",
|
||||
"expired": true,
|
||||
"key": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql",
|
||||
"organization": 2,
|
||||
"organization_info": {
|
||||
"id": 2,
|
||||
"slug": "org2"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 19,
|
||||
"last_name": "Fifth",
|
||||
"url": "http://localhost:8080/api/users/19",
|
||||
"username": "user5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2022-01-19T13:54:42.015000Z",
|
||||
"expired": true,
|
||||
"key": "BrwoDmMNQQ1v9WXOukp9DwQVuqB3RDPjpUECCEq6QcAuG0Pi8k1IYtQ9uz9jg0Bv",
|
||||
"organization": 2,
|
||||
"organization_info": {
|
||||
"id": 2,
|
||||
"slug": "org2"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 5,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/5",
|
||||
"username": "user4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2022-01-19T13:54:42.005000Z",
|
||||
"expired": true,
|
||||
"key": "5FjIXya6fTGvlRpauFvi2QN1wDOqo1V9REB5rJinDR8FZO9gr0qmtWpghsCte8Y1",
|
||||
"organization": 2,
|
||||
"organization_info": {
|
||||
"id": 2,
|
||||
"slug": "org2"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 4,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/4",
|
||||
"username": "user3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2021-12-14T19:55:13.745000Z",
|
||||
"expired": true,
|
||||
"key": "h43G28di7vfs4Jv5VrKZ26xvGAfm6Yc2FFv14z9EKhiuIEDQ22pEnzmSCab8MnK1",
|
||||
"organization": 2,
|
||||
"organization_info": {
|
||||
"id": 2,
|
||||
"slug": "org2"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 3,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/3",
|
||||
"username": "user2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2021-12-14T19:54:56.431000Z",
|
||||
"expired": true,
|
||||
"key": "mFpVV2Yh39uUdU8IpigSxvuPegqi8sjxFi6P9Jdy6fBE8Ky9Juzi1KjeGDQsizSS",
|
||||
"organization": 2,
|
||||
"organization_info": {
|
||||
"id": 2,
|
||||
"slug": "org2"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 8,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/8",
|
||||
"username": "worker3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2021-12-14T19:54:46.172000Z",
|
||||
"expired": true,
|
||||
"key": "62HplmGPJuzpTXSyzPWiAlREkq8smCjK30GdtYze3q03J9X5ghQe3oMhlAyQ0WBH",
|
||||
"organization": 2,
|
||||
"organization_info": {
|
||||
"id": 2,
|
||||
"slug": "org2"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 7,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/7",
|
||||
"username": "worker2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2021-12-14T19:54:33.591000Z",
|
||||
"expired": true,
|
||||
"key": "Y1I4FFU27WRqq2rWQLtKjDztMqpvqW7gJgg7q73F7oE4H5kukvXugWjiTLHclPDu",
|
||||
"organization": 2,
|
||||
"organization_info": {
|
||||
"id": 2,
|
||||
"slug": "org2"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2021-12-14T18:48:46.579000Z",
|
||||
"expired": true,
|
||||
"key": "cbmm587Z05WQUYvesIZUCtbTl7CEL4thv1Au6Nr51psflITn9X6BsvNFXcNEkoYn",
|
||||
"organization": 1,
|
||||
"organization_info": {
|
||||
"id": 1,
|
||||
"slug": "org1"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2021-12-14T18:47:49.322000Z",
|
||||
"expired": true,
|
||||
"key": "aViZkw9TaieLoZaswEnkMy8tTet1yYDRof3eKZDtZaHf1BItgCNNM6y6fnjrkrej",
|
||||
"organization": 1,
|
||||
"organization_info": {
|
||||
"id": 1,
|
||||
"slug": "org1"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 7,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/7",
|
||||
"username": "worker2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"accepted": true,
|
||||
"created_date": "2021-12-14T18:47:39.935000Z",
|
||||
"expired": true,
|
||||
"key": "Lzyzgo161I7Fej1vC5RXPdyUgCBfbuxsEEhHYeYOJqvbeJe5clPDnqCm7pKOC9tr",
|
||||
"organization": 1,
|
||||
"organization_info": {
|
||||
"id": 1,
|
||||
"slug": "org1"
|
||||
},
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 6,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/6",
|
||||
"username": "worker1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"count": 5,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"assignee": null,
|
||||
"comments": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/comments?issue_id=5"
|
||||
},
|
||||
"created_date": "2022-03-16T12:49:29.369000Z",
|
||||
"frame": 0,
|
||||
"id": 5,
|
||||
"job": 11,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 20,
|
||||
"last_name": "Sixth",
|
||||
"url": "http://localhost:8080/api/users/20",
|
||||
"username": "user6"
|
||||
},
|
||||
"position": [
|
||||
65.6189987163034,
|
||||
100.96585365853753,
|
||||
142.12734274711147,
|
||||
362.6243902439037
|
||||
],
|
||||
"resolved": false,
|
||||
"updated_date": "2022-03-16T12:49:29.369000Z"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"comments": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/comments?issue_id=4"
|
||||
},
|
||||
"created_date": "2022-03-16T12:40:00.764000Z",
|
||||
"frame": 5,
|
||||
"id": 4,
|
||||
"job": 10,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"position": [
|
||||
295.36328125,
|
||||
243.6044921875,
|
||||
932.23046875,
|
||||
561.4921875
|
||||
],
|
||||
"resolved": false,
|
||||
"updated_date": "2022-03-16T12:40:00.764000Z"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"comments": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/comments?issue_id=3"
|
||||
},
|
||||
"created_date": "2022-03-16T11:08:18.367000Z",
|
||||
"frame": 5,
|
||||
"id": 3,
|
||||
"job": 16,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
},
|
||||
"position": [
|
||||
108.1845703125,
|
||||
235.0,
|
||||
720.0087890625,
|
||||
703.3505859375
|
||||
],
|
||||
"resolved": false,
|
||||
"updated_date": "2022-03-16T11:08:18.367000Z"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"comments": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/comments?issue_id=2"
|
||||
},
|
||||
"created_date": "2022-03-16T11:07:22.170000Z",
|
||||
"frame": 0,
|
||||
"id": 2,
|
||||
"job": 9,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
},
|
||||
"position": [
|
||||
98.48046875,
|
||||
696.72265625,
|
||||
326.1220703125,
|
||||
841.5859375
|
||||
],
|
||||
"resolved": false,
|
||||
"updated_date": "2022-03-16T11:07:22.170000Z"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"comments": {
|
||||
"count": 2,
|
||||
"url": "http://localhost:8080/api/comments?issue_id=1"
|
||||
},
|
||||
"created_date": "2022-03-16T11:04:39.444000Z",
|
||||
"frame": 0,
|
||||
"id": 1,
|
||||
"job": 7,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"position": [
|
||||
244.58581235698148,
|
||||
319.63386727689067,
|
||||
326.9656750572103,
|
||||
192.76887871853796,
|
||||
543.6247139588122,
|
||||
175.4691075514893,
|
||||
835.2494279176244,
|
||||
360.0000000000018,
|
||||
609.5286041189956,
|
||||
586.544622425632,
|
||||
364.0361328125,
|
||||
528.87890625,
|
||||
244.58581235698148,
|
||||
319.63386727689067
|
||||
],
|
||||
"resolved": true,
|
||||
"updated_date": "2022-03-16T11:04:39.444000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"count": 16,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 16,
|
||||
"invitation": null,
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T18:45:40.172000Z",
|
||||
"organization": 3,
|
||||
"role": "owner",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"invitation": "q8GWTPiR1Vz9DDO6MQo1B6pUBzW9GjDb6AUQPziAV62jD7OpCLZji0GS66C48wRX",
|
||||
"is_active": true,
|
||||
"joined_date": "2023-09-15T07:53:52.116000Z",
|
||||
"organization": 1,
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"invitation": "d2Zaawf81uImG1nmWA0Va0Bv5EPERt1edJDTgTgMZiefZ2QmC1IdPld9LIPnkiWR",
|
||||
"is_active": true,
|
||||
"joined_date": "2023-09-15T07:53:52.115000Z",
|
||||
"organization": 1,
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 5,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/5",
|
||||
"username": "user4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"invitation": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9",
|
||||
"is_active": true,
|
||||
"joined_date": "2022-09-28T13:11:37.839000Z",
|
||||
"organization": 1,
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 19,
|
||||
"last_name": "Fifth",
|
||||
"url": "http://localhost:8080/api/users/19",
|
||||
"username": "user5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"invitation": "Fi3WRUhFxTWpMiVpdwNR2CGyhgcIXSCUYgPCugPq72QUOgHz9NSMOGiKS3PfJ7Ql",
|
||||
"is_active": true,
|
||||
"joined_date": "2022-02-24T21:29:21.978000Z",
|
||||
"organization": 2,
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 19,
|
||||
"last_name": "Fifth",
|
||||
"url": "http://localhost:8080/api/users/19",
|
||||
"username": "user5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"invitation": "BrwoDmMNQQ1v9WXOukp9DwQVuqB3RDPjpUECCEq6QcAuG0Pi8k1IYtQ9uz9jg0Bv",
|
||||
"is_active": true,
|
||||
"joined_date": "2022-01-19T13:54:42.015000Z",
|
||||
"organization": 2,
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 5,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/5",
|
||||
"username": "user4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"invitation": "5FjIXya6fTGvlRpauFvi2QN1wDOqo1V9REB5rJinDR8FZO9gr0qmtWpghsCte8Y1",
|
||||
"is_active": true,
|
||||
"joined_date": "2022-01-19T13:54:42.005000Z",
|
||||
"organization": 2,
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 4,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/4",
|
||||
"username": "user3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"invitation": "h43G28di7vfs4Jv5VrKZ26xvGAfm6Yc2FFv14z9EKhiuIEDQ22pEnzmSCab8MnK1",
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T19:55:13.745000Z",
|
||||
"organization": 2,
|
||||
"role": "supervisor",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 3,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/3",
|
||||
"username": "user2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"invitation": "mFpVV2Yh39uUdU8IpigSxvuPegqi8sjxFi6P9Jdy6fBE8Ky9Juzi1KjeGDQsizSS",
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T19:54:56.431000Z",
|
||||
"organization": 2,
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 8,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/8",
|
||||
"username": "worker3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"invitation": "62HplmGPJuzpTXSyzPWiAlREkq8smCjK30GdtYze3q03J9X5ghQe3oMhlAyQ0WBH",
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T19:54:46.172000Z",
|
||||
"organization": 2,
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 7,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/7",
|
||||
"username": "worker2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"invitation": "Y1I4FFU27WRqq2rWQLtKjDztMqpvqW7gJgg7q73F7oE4H5kukvXugWjiTLHclPDu",
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T19:54:33.591000Z",
|
||||
"organization": 2,
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 11,
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"invitation": null,
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T19:51:38.667000Z",
|
||||
"organization": 2,
|
||||
"role": "owner",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"invitation": "cbmm587Z05WQUYvesIZUCtbTl7CEL4thv1Au6Nr51psflITn9X6BsvNFXcNEkoYn",
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T18:48:46.579000Z",
|
||||
"organization": 1,
|
||||
"role": "maintainer",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"invitation": "aViZkw9TaieLoZaswEnkMy8tTet1yYDRof3eKZDtZaHf1BItgCNNM6y6fnjrkrej",
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T18:47:49.322000Z",
|
||||
"organization": 1,
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 7,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/7",
|
||||
"username": "worker2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"invitation": "Lzyzgo161I7Fej1vC5RXPdyUgCBfbuxsEEhHYeYOJqvbeJe5clPDnqCm7pKOC9tr",
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T18:47:39.935000Z",
|
||||
"organization": 1,
|
||||
"role": "worker",
|
||||
"user": {
|
||||
"first_name": "Worker",
|
||||
"id": 6,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/6",
|
||||
"username": "worker1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"invitation": null,
|
||||
"is_active": true,
|
||||
"joined_date": "2021-12-14T18:45:40.172000Z",
|
||||
"organization": 1,
|
||||
"role": "owner",
|
||||
"user": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"count": 3,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"contact": {
|
||||
"email": "org3@cvat.org"
|
||||
},
|
||||
"created_date": "2021-12-14T19:51:38.667000Z",
|
||||
"description": "",
|
||||
"id": 3,
|
||||
"name": "Organization #3",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"slug": "org3",
|
||||
"updated_date": "2021-12-14T19:51:38.667000Z"
|
||||
},
|
||||
{
|
||||
"contact": {
|
||||
"email": "org2@cvat.org"
|
||||
},
|
||||
"created_date": "2021-12-14T19:51:38.667000Z",
|
||||
"description": "",
|
||||
"id": 2,
|
||||
"name": "Organization #2",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"slug": "org2",
|
||||
"updated_date": "2021-12-14T19:51:38.667000Z"
|
||||
},
|
||||
{
|
||||
"contact": {
|
||||
"email": "org1@cvat.org"
|
||||
},
|
||||
"created_date": "2021-12-14T18:45:40.172000Z",
|
||||
"description": "",
|
||||
"id": 1,
|
||||
"name": "organization #1",
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"slug": "org1",
|
||||
"updated_date": "2021-12-14T18:45:40.172000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
{
|
||||
"count": 17,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2025-11-13T11:32:07.581000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 18,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=18"
|
||||
},
|
||||
"name": "project with tags",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 57,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 58,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=18"
|
||||
},
|
||||
"updated_date": "2025-11-13T11:32:48.144000Z",
|
||||
"url": "http://localhost:8080/api/projects/18"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2025-05-05T15:35:00.036000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 17,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=17"
|
||||
},
|
||||
"name": "project 4",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 3,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/3",
|
||||
"username": "user2"
|
||||
},
|
||||
"source_storage": null,
|
||||
"status": "annotation",
|
||||
"target_storage": null,
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 2,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=17"
|
||||
},
|
||||
"updated_date": "2025-05-05T15:35:00.542000Z",
|
||||
"url": "http://localhost:8080/api/projects/17"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"assignee_updated_date": "2024-09-23T08:09:45.461000Z",
|
||||
"bug_tracker": "",
|
||||
"created_date": "2024-09-23T08:09:42.827000Z",
|
||||
"dimension": null,
|
||||
"guide_id": null,
|
||||
"id": 16,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=16"
|
||||
},
|
||||
"name": "project assigned to org owner",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 43,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 44,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 0,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=16"
|
||||
},
|
||||
"updated_date": "2024-09-23T08:09:45.474000Z",
|
||||
"url": "http://localhost:8080/api/projects/16"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "User",
|
||||
"id": 5,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/5",
|
||||
"username": "user4"
|
||||
},
|
||||
"assignee_updated_date": "2024-09-22T20:40:23.452000Z",
|
||||
"bug_tracker": "",
|
||||
"created_date": "2024-09-22T20:40:15.423000Z",
|
||||
"dimension": null,
|
||||
"guide_id": null,
|
||||
"id": 15,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=15"
|
||||
},
|
||||
"name": "project assigned to user4",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 41,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 42,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 0,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=15"
|
||||
},
|
||||
"updated_date": "2024-09-22T20:40:23.462000Z",
|
||||
"url": "http://localhost:8080/api/projects/15"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2024-07-15T15:29:04.426000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 14,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=14"
|
||||
},
|
||||
"name": "project with subsets",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 35,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 36,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [
|
||||
"Train",
|
||||
"Validation"
|
||||
],
|
||||
"tasks": {
|
||||
"count": 2,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=14"
|
||||
},
|
||||
"updated_date": "2024-07-15T15:34:53.175000Z",
|
||||
"url": "http://localhost:8080/api/projects/14"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2023-03-10T11:58:04.216000Z",
|
||||
"dimension": null,
|
||||
"guide_id": null,
|
||||
"id": 13,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=13"
|
||||
},
|
||||
"name": "project with attributes 2",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 31,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 32,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 0,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=13"
|
||||
},
|
||||
"updated_date": "2023-03-10T11:58:04.216000Z",
|
||||
"url": "http://localhost:8080/api/projects/13"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2023-03-10T11:57:14.944000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 12,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=12"
|
||||
},
|
||||
"name": "project with attributes",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 27,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 28,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=12"
|
||||
},
|
||||
"updated_date": "2023-03-10T11:57:48.747000Z",
|
||||
"url": "http://localhost:8080/api/projects/12"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2023-03-01T15:36:11.840000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 11,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=11"
|
||||
},
|
||||
"name": "project7",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 21,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 22,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=11"
|
||||
},
|
||||
"updated_date": "2023-03-01T15:36:37.812000Z",
|
||||
"url": "http://localhost:8080/api/projects/11"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "Worker",
|
||||
"id": 8,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/8",
|
||||
"username": "worker3"
|
||||
},
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2023-02-10T11:42:43.192000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 10,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=10"
|
||||
},
|
||||
"name": "project 6",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 4,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/4",
|
||||
"username": "user3"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 19,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 20,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=10"
|
||||
},
|
||||
"updated_date": "2024-09-23T21:42:22.688000Z",
|
||||
"url": "http://localhost:8080/api/projects/10"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2022-12-01T12:52:42.454000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 8,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=8"
|
||||
},
|
||||
"name": "project with video data",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 13,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 14,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=8"
|
||||
},
|
||||
"updated_date": "2022-12-01T12:53:34.917000Z",
|
||||
"url": "http://localhost:8080/api/projects/8"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "Worker",
|
||||
"id": 9,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/9",
|
||||
"username": "worker4"
|
||||
},
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2022-09-28T12:26:25.296000Z",
|
||||
"dimension": null,
|
||||
"guide_id": null,
|
||||
"id": 7,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=7"
|
||||
},
|
||||
"name": "admin1_project",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 11,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 12,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 0,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=7"
|
||||
},
|
||||
"updated_date": "2022-09-28T12:26:29.285000Z",
|
||||
"url": "http://localhost:8080/api/projects/7"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "User",
|
||||
"id": 13,
|
||||
"last_name": "Tenth",
|
||||
"url": "http://localhost:8080/api/users/13",
|
||||
"username": "user10"
|
||||
},
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2022-09-28T12:15:50.768000Z",
|
||||
"dimension": null,
|
||||
"guide_id": null,
|
||||
"id": 6,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=6"
|
||||
},
|
||||
"name": "user1_project",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 9,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 10,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 0,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=6"
|
||||
},
|
||||
"updated_date": "2022-09-28T12:25:54.563000Z",
|
||||
"url": "http://localhost:8080/api/projects/6"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2022-09-22T14:21:53.791000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 5,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=5"
|
||||
},
|
||||
"name": "project5",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 5,
|
||||
"location": "local"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": null,
|
||||
"id": 6,
|
||||
"location": "local"
|
||||
},
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=5"
|
||||
},
|
||||
"updated_date": "2022-09-28T12:26:49.493000Z",
|
||||
"url": "http://localhost:8080/api/projects/5"
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2022-06-08T08:32:45.521000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 4,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=4"
|
||||
},
|
||||
"name": "project4",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"source_storage": null,
|
||||
"status": "annotation",
|
||||
"target_storage": null,
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=4"
|
||||
},
|
||||
"updated_date": "2023-02-10T11:50:18.436000Z",
|
||||
"url": "http://localhost:8080/api/projects/4"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "User",
|
||||
"id": 19,
|
||||
"last_name": "Fifth",
|
||||
"url": "http://localhost:8080/api/users/19",
|
||||
"username": "user5"
|
||||
},
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2022-03-28T13:05:24.659000Z",
|
||||
"dimension": null,
|
||||
"guide_id": null,
|
||||
"id": 3,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=3"
|
||||
},
|
||||
"name": "project 3",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 3,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/3",
|
||||
"username": "user2"
|
||||
},
|
||||
"source_storage": null,
|
||||
"status": "annotation",
|
||||
"target_storage": null,
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 0,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=3"
|
||||
},
|
||||
"updated_date": "2022-03-28T13:06:09.283000Z",
|
||||
"url": "http://localhost:8080/api/projects/3"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "User",
|
||||
"id": 3,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/3",
|
||||
"username": "user2"
|
||||
},
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2021-12-14T19:52:37.278000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 2,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=2"
|
||||
},
|
||||
"name": "project2",
|
||||
"organization": 2,
|
||||
"organization_id": 2,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"source_storage": {
|
||||
"cloud_storage_id": 3,
|
||||
"id": 3,
|
||||
"location": "cloud_storage"
|
||||
},
|
||||
"status": "annotation",
|
||||
"target_storage": {
|
||||
"cloud_storage_id": 3,
|
||||
"id": 1,
|
||||
"location": "cloud_storage"
|
||||
},
|
||||
"task_subsets": [
|
||||
"Train"
|
||||
],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=2"
|
||||
},
|
||||
"updated_date": "2022-06-30T08:56:45.601000Z",
|
||||
"url": "http://localhost:8080/api/projects/2"
|
||||
},
|
||||
{
|
||||
"assignee": {
|
||||
"first_name": "User",
|
||||
"id": 20,
|
||||
"last_name": "Sixth",
|
||||
"url": "http://localhost:8080/api/users/20",
|
||||
"username": "user6"
|
||||
},
|
||||
"assignee_updated_date": null,
|
||||
"bug_tracker": "",
|
||||
"created_date": "2021-12-14T19:46:37.969000Z",
|
||||
"dimension": "2d",
|
||||
"guide_id": null,
|
||||
"id": 1,
|
||||
"labels": {
|
||||
"url": "http://localhost:8080/api/labels?project_id=1"
|
||||
},
|
||||
"name": "project1",
|
||||
"organization": null,
|
||||
"organization_id": null,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"source_storage": null,
|
||||
"status": "annotation",
|
||||
"target_storage": null,
|
||||
"task_subsets": [],
|
||||
"tasks": {
|
||||
"count": 1,
|
||||
"url": "http://localhost:8080/api/tasks?project_id=1"
|
||||
},
|
||||
"updated_date": "2022-11-03T13:57:25.895000Z",
|
||||
"url": "http://localhost:8080/api/projects/1"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,679 @@
|
||||
{
|
||||
"count": 18,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-05-06T16:27:52.046000Z",
|
||||
"gt_last_updated": null,
|
||||
"id": 18,
|
||||
"job_id": null,
|
||||
"parent_id": null,
|
||||
"project_id": 17,
|
||||
"summary": {
|
||||
"accuracy": 0.2727272727272727,
|
||||
"conflict_count": 10,
|
||||
"conflicts_by_type": {
|
||||
"extra_annotation": 1,
|
||||
"low_overlap": 2,
|
||||
"missing_annotation": 7
|
||||
},
|
||||
"ds_count": 8,
|
||||
"error_count": 8,
|
||||
"frame_count": 6,
|
||||
"frame_share": 0.5,
|
||||
"gt_count": 20,
|
||||
"jobs": {
|
||||
"excluded": 0,
|
||||
"included": 3,
|
||||
"not_checkable": 0,
|
||||
"total": 3
|
||||
},
|
||||
"precision": 0.75,
|
||||
"recall": 0.3,
|
||||
"tasks": {
|
||||
"custom": 0,
|
||||
"excluded": 0,
|
||||
"included": 2,
|
||||
"not_configured": 0,
|
||||
"total": 2
|
||||
},
|
||||
"total_count": 22,
|
||||
"total_frames": 12,
|
||||
"valid_count": 6,
|
||||
"validation_frame_share": 0.5,
|
||||
"validation_frames": 6,
|
||||
"warning_count": 2
|
||||
},
|
||||
"target": "project",
|
||||
"target_last_updated": "2025-05-05T15:35:00.542000Z",
|
||||
"task_id": null
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-05-06T16:27:52.036000Z",
|
||||
"gt_last_updated": "2025-05-05T15:38:37.188000Z",
|
||||
"id": 17,
|
||||
"job_id": 57,
|
||||
"parent_id": 15,
|
||||
"project_id": 17,
|
||||
"summary": {
|
||||
"accuracy": 0.25,
|
||||
"conflict_count": 3,
|
||||
"conflicts_by_type": {
|
||||
"extra_annotation": 1,
|
||||
"missing_annotation": 2
|
||||
},
|
||||
"ds_count": 2,
|
||||
"error_count": 3,
|
||||
"frame_count": 2,
|
||||
"frame_share": 0.6666666666666666,
|
||||
"gt_count": 3,
|
||||
"precision": 0.5,
|
||||
"recall": 0.3333333333333333,
|
||||
"total_count": 4,
|
||||
"total_frames": 3,
|
||||
"valid_count": 1,
|
||||
"validation_frame_share": 0.6666666666666666,
|
||||
"validation_frames": 2,
|
||||
"warning_count": 0
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2025-05-05T15:35:00.515000Z",
|
||||
"task_id": 33
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-05-06T16:27:52.036000Z",
|
||||
"gt_last_updated": "2025-05-05T15:38:37.188000Z",
|
||||
"id": 16,
|
||||
"job_id": 56,
|
||||
"parent_id": 15,
|
||||
"project_id": 17,
|
||||
"summary": {
|
||||
"accuracy": 0.3333333333333333,
|
||||
"conflict_count": 3,
|
||||
"conflicts_by_type": {
|
||||
"low_overlap": 1,
|
||||
"missing_annotation": 2
|
||||
},
|
||||
"ds_count": 1,
|
||||
"error_count": 2,
|
||||
"frame_count": 2,
|
||||
"frame_share": 0.4,
|
||||
"gt_count": 3,
|
||||
"precision": 1.0,
|
||||
"recall": 0.3333333333333333,
|
||||
"total_count": 3,
|
||||
"total_frames": 5,
|
||||
"valid_count": 1,
|
||||
"validation_frame_share": 0.4,
|
||||
"validation_frames": 2,
|
||||
"warning_count": 1
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2025-05-05T15:35:00.493000Z",
|
||||
"task_id": 33
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-05-06T16:27:52.034000Z",
|
||||
"gt_last_updated": "2025-05-05T15:38:37.188000Z",
|
||||
"id": 15,
|
||||
"job_id": null,
|
||||
"parent_id": 18,
|
||||
"project_id": 17,
|
||||
"summary": {
|
||||
"accuracy": 0.2857142857142857,
|
||||
"conflict_count": 6,
|
||||
"conflicts_by_type": {
|
||||
"extra_annotation": 1,
|
||||
"low_overlap": 1,
|
||||
"missing_annotation": 4
|
||||
},
|
||||
"ds_count": 3,
|
||||
"error_count": 5,
|
||||
"frame_count": 4,
|
||||
"frame_share": 0.5,
|
||||
"gt_count": 6,
|
||||
"jobs": {
|
||||
"excluded": 0,
|
||||
"included": 2,
|
||||
"not_checkable": 0,
|
||||
"total": 2
|
||||
},
|
||||
"precision": 0.6666666666666666,
|
||||
"recall": 0.3333333333333333,
|
||||
"total_count": 7,
|
||||
"total_frames": 8,
|
||||
"valid_count": 2,
|
||||
"validation_frame_share": 0.5,
|
||||
"validation_frames": 4,
|
||||
"warning_count": 1
|
||||
},
|
||||
"target": "task",
|
||||
"target_last_updated": "2025-05-05T15:36:34.341000Z",
|
||||
"task_id": 33
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-05-06T16:27:51.967000Z",
|
||||
"gt_last_updated": "2025-05-05T15:38:29.597000Z",
|
||||
"id": 14,
|
||||
"job_id": 54,
|
||||
"parent_id": 13,
|
||||
"project_id": 17,
|
||||
"summary": {
|
||||
"accuracy": 0.25,
|
||||
"conflict_count": 4,
|
||||
"conflicts_by_type": {
|
||||
"low_overlap": 1,
|
||||
"missing_annotation": 3
|
||||
},
|
||||
"ds_count": 1,
|
||||
"error_count": 3,
|
||||
"frame_count": 2,
|
||||
"frame_share": 0.5,
|
||||
"gt_count": 4,
|
||||
"precision": 1.0,
|
||||
"recall": 0.25,
|
||||
"total_count": 4,
|
||||
"total_frames": 4,
|
||||
"valid_count": 1,
|
||||
"validation_frame_share": 0.5,
|
||||
"validation_frames": 2,
|
||||
"warning_count": 1
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2025-05-05T15:35:00.294000Z",
|
||||
"task_id": 32
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-05-06T16:27:51.965000Z",
|
||||
"gt_last_updated": "2025-05-05T15:38:29.597000Z",
|
||||
"id": 13,
|
||||
"job_id": null,
|
||||
"parent_id": 18,
|
||||
"project_id": 17,
|
||||
"summary": {
|
||||
"accuracy": 0.25,
|
||||
"conflict_count": 4,
|
||||
"conflicts_by_type": {
|
||||
"low_overlap": 1,
|
||||
"missing_annotation": 3
|
||||
},
|
||||
"ds_count": 1,
|
||||
"error_count": 3,
|
||||
"frame_count": 2,
|
||||
"frame_share": 0.5,
|
||||
"gt_count": 4,
|
||||
"jobs": {
|
||||
"excluded": 0,
|
||||
"included": 1,
|
||||
"not_checkable": 0,
|
||||
"total": 1
|
||||
},
|
||||
"precision": 1.0,
|
||||
"recall": 0.25,
|
||||
"total_count": 4,
|
||||
"total_frames": 4,
|
||||
"valid_count": 1,
|
||||
"validation_frame_share": 0.5,
|
||||
"validation_frames": 2,
|
||||
"warning_count": 1
|
||||
},
|
||||
"target": "task",
|
||||
"target_last_updated": "2025-05-05T15:38:46.662000Z",
|
||||
"task_id": 32
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-03-31T13:38:00.464000Z",
|
||||
"gt_last_updated": "2023-11-24T15:18:55.216000Z",
|
||||
"id": 12,
|
||||
"job_id": 27,
|
||||
"parent_id": 11,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4583333333333333,
|
||||
"conflict_count": 42,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 11,
|
||||
"low_overlap": 7,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 12
|
||||
},
|
||||
"ds_count": 36,
|
||||
"error_count": 26,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 37,
|
||||
"precision": 0.6111111111111112,
|
||||
"recall": 0.5945945945945946,
|
||||
"total_count": 48,
|
||||
"total_frames": 11,
|
||||
"valid_count": 22,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 16
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2023-11-24T15:23:30.269000Z",
|
||||
"task_id": 22
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2025-03-31T13:38:00.460000Z",
|
||||
"gt_last_updated": "2023-11-24T15:18:55.216000Z",
|
||||
"id": 11,
|
||||
"job_id": null,
|
||||
"parent_id": null,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4583333333333333,
|
||||
"conflict_count": 42,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 11,
|
||||
"low_overlap": 7,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 12
|
||||
},
|
||||
"ds_count": 36,
|
||||
"error_count": 26,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 37,
|
||||
"jobs": {
|
||||
"excluded": 0,
|
||||
"included": 1,
|
||||
"not_checkable": 0,
|
||||
"total": 1
|
||||
},
|
||||
"precision": 0.6111111111111112,
|
||||
"recall": 0.5945945945945946,
|
||||
"total_count": 48,
|
||||
"total_frames": 11,
|
||||
"valid_count": 22,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 16
|
||||
},
|
||||
"target": "task",
|
||||
"target_last_updated": "2023-11-24T15:23:30.045000Z",
|
||||
"task_id": 22
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2024-03-21T20:52:03.552000Z",
|
||||
"gt_last_updated": "2024-03-21T20:50:20.020000Z",
|
||||
"id": 10,
|
||||
"job_id": 29,
|
||||
"parent_id": 7,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.0,
|
||||
"conflict_count": 2,
|
||||
"conflicts_by_type": {
|
||||
"extra_annotation": 1,
|
||||
"missing_annotation": 1
|
||||
},
|
||||
"ds_count": 1,
|
||||
"error_count": 2,
|
||||
"frame_count": 1,
|
||||
"frame_share": 0.2,
|
||||
"gt_count": 1,
|
||||
"precision": 0.0,
|
||||
"recall": 0.0,
|
||||
"total_count": 2,
|
||||
"total_frames": 5,
|
||||
"valid_count": 0,
|
||||
"validation_frame_share": 0.2,
|
||||
"validation_frames": 1,
|
||||
"warning_count": 0
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2024-03-21T20:50:39.585000Z",
|
||||
"task_id": 23
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2024-03-21T20:52:03.552000Z",
|
||||
"gt_last_updated": "2024-03-21T20:50:20.020000Z",
|
||||
"id": 9,
|
||||
"job_id": 30,
|
||||
"parent_id": 7,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 1.0,
|
||||
"conflict_count": 1,
|
||||
"conflicts_by_type": {
|
||||
"low_overlap": 1
|
||||
},
|
||||
"ds_count": 2,
|
||||
"error_count": 0,
|
||||
"frame_count": 2,
|
||||
"frame_share": 0.4,
|
||||
"gt_count": 2,
|
||||
"precision": 1.0,
|
||||
"recall": 1.0,
|
||||
"total_count": 2,
|
||||
"total_frames": 5,
|
||||
"valid_count": 2,
|
||||
"validation_frame_share": 0.4,
|
||||
"validation_frames": 2,
|
||||
"warning_count": 1
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2024-03-21T20:50:33.610000Z",
|
||||
"task_id": 23
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2024-03-21T20:52:03.552000Z",
|
||||
"gt_last_updated": "2024-03-21T20:50:20.020000Z",
|
||||
"id": 8,
|
||||
"job_id": 31,
|
||||
"parent_id": 7,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.0,
|
||||
"conflict_count": 0,
|
||||
"conflicts_by_type": {},
|
||||
"ds_count": 0,
|
||||
"error_count": 0,
|
||||
"frame_count": 0,
|
||||
"frame_share": 0.0,
|
||||
"gt_count": 0,
|
||||
"precision": 0.0,
|
||||
"recall": 0.0,
|
||||
"total_count": 0,
|
||||
"total_frames": 0,
|
||||
"valid_count": 0,
|
||||
"validation_frame_share": 0.0,
|
||||
"validation_frames": 0,
|
||||
"warning_count": 0
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2024-03-21T20:50:27.594000Z",
|
||||
"task_id": 23
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2024-03-21T20:52:03.542000Z",
|
||||
"gt_last_updated": "2024-03-21T20:50:20.020000Z",
|
||||
"id": 7,
|
||||
"job_id": null,
|
||||
"parent_id": null,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.5,
|
||||
"conflict_count": 3,
|
||||
"conflicts_by_type": {
|
||||
"extra_annotation": 1,
|
||||
"low_overlap": 1,
|
||||
"missing_annotation": 1
|
||||
},
|
||||
"ds_count": 3,
|
||||
"error_count": 2,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 3,
|
||||
"precision": 0.6666666666666666,
|
||||
"recall": 0.6666666666666666,
|
||||
"total_count": 4,
|
||||
"total_frames": 11,
|
||||
"valid_count": 2,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 1
|
||||
},
|
||||
"target": "task",
|
||||
"target_last_updated": "2024-03-21T20:50:05.947000Z",
|
||||
"task_id": 23
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2024-03-21T11:16:21.847000Z",
|
||||
"gt_last_updated": "2023-11-24T15:18:55.216000Z",
|
||||
"id": 6,
|
||||
"job_id": 27,
|
||||
"parent_id": 5,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4782608695652174,
|
||||
"conflict_count": 40,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 11,
|
||||
"low_overlap": 7,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 10
|
||||
},
|
||||
"ds_count": 36,
|
||||
"error_count": 24,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 35,
|
||||
"precision": 0.6111111111111112,
|
||||
"recall": 0.6285714285714286,
|
||||
"total_count": 46,
|
||||
"total_frames": 11,
|
||||
"valid_count": 22,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 16
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2023-11-24T15:23:30.269000Z",
|
||||
"task_id": 22
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2024-03-21T11:16:21.845000Z",
|
||||
"gt_last_updated": "2023-11-24T15:18:55.216000Z",
|
||||
"id": 5,
|
||||
"job_id": null,
|
||||
"parent_id": null,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4782608695652174,
|
||||
"conflict_count": 40,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 11,
|
||||
"low_overlap": 7,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 10
|
||||
},
|
||||
"ds_count": 36,
|
||||
"error_count": 24,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 35,
|
||||
"precision": 0.6111111111111112,
|
||||
"recall": 0.6285714285714286,
|
||||
"total_count": 46,
|
||||
"total_frames": 11,
|
||||
"valid_count": 22,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 16
|
||||
},
|
||||
"target": "task",
|
||||
"target_last_updated": "2023-11-24T15:23:30.045000Z",
|
||||
"task_id": 22
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2023-11-24T15:24:25.357000Z",
|
||||
"gt_last_updated": "2023-11-24T15:18:55.216000Z",
|
||||
"id": 4,
|
||||
"job_id": 27,
|
||||
"parent_id": 3,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4782608695652174,
|
||||
"conflict_count": 40,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 11,
|
||||
"low_overlap": 7,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 10
|
||||
},
|
||||
"ds_count": 36,
|
||||
"error_count": 24,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 35,
|
||||
"precision": 0.6111111111111112,
|
||||
"recall": 0.6285714285714286,
|
||||
"total_count": 46,
|
||||
"total_frames": 11,
|
||||
"valid_count": 22,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 16
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2023-11-24T15:23:30.269000Z",
|
||||
"task_id": 22
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2023-11-24T15:24:25.355000Z",
|
||||
"gt_last_updated": "2023-11-24T15:18:55.216000Z",
|
||||
"id": 3,
|
||||
"job_id": null,
|
||||
"parent_id": null,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4782608695652174,
|
||||
"conflict_count": 40,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 11,
|
||||
"low_overlap": 7,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 10
|
||||
},
|
||||
"ds_count": 36,
|
||||
"error_count": 24,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 35,
|
||||
"precision": 0.6111111111111112,
|
||||
"recall": 0.6285714285714286,
|
||||
"total_count": 46,
|
||||
"total_frames": 11,
|
||||
"valid_count": 22,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 16
|
||||
},
|
||||
"target": "task",
|
||||
"target_last_updated": "2023-11-24T15:23:30.045000Z",
|
||||
"task_id": 22
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2023-05-26T16:25:36.616000Z",
|
||||
"gt_last_updated": "2023-05-26T16:16:50.630000Z",
|
||||
"id": 2,
|
||||
"job_id": 27,
|
||||
"parent_id": 1,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4883720930232558,
|
||||
"conflict_count": 37,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 10,
|
||||
"low_overlap": 6,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 9
|
||||
},
|
||||
"ds_count": 34,
|
||||
"error_count": 22,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 33,
|
||||
"precision": 0.6176470588235294,
|
||||
"recall": 0.6363636363636364,
|
||||
"total_count": 43,
|
||||
"total_frames": 11,
|
||||
"valid_count": 21,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 15
|
||||
},
|
||||
"target": "job",
|
||||
"target_last_updated": "2023-05-26T16:11:24.294000Z",
|
||||
"task_id": 22
|
||||
},
|
||||
{
|
||||
"assignee": null,
|
||||
"created_date": "2023-05-26T16:25:36.613000Z",
|
||||
"gt_last_updated": "2023-05-26T16:16:50.630000Z",
|
||||
"id": 1,
|
||||
"job_id": null,
|
||||
"parent_id": null,
|
||||
"project_id": null,
|
||||
"summary": {
|
||||
"accuracy": 0.4883720930232558,
|
||||
"conflict_count": 37,
|
||||
"conflicts_by_type": {
|
||||
"covered_annotation": 1,
|
||||
"extra_annotation": 10,
|
||||
"low_overlap": 6,
|
||||
"mismatching_attributes": 1,
|
||||
"mismatching_direction": 1,
|
||||
"mismatching_groups": 6,
|
||||
"mismatching_label": 3,
|
||||
"missing_annotation": 9
|
||||
},
|
||||
"ds_count": 34,
|
||||
"error_count": 22,
|
||||
"frame_count": 3,
|
||||
"frame_share": 0.2727272727272727,
|
||||
"gt_count": 33,
|
||||
"precision": 0.6176470588235294,
|
||||
"recall": 0.6363636363636364,
|
||||
"total_count": 43,
|
||||
"total_frames": 11,
|
||||
"valid_count": 21,
|
||||
"validation_frame_share": 0.2727272727272727,
|
||||
"validation_frames": 3,
|
||||
"warning_count": 15
|
||||
},
|
||||
"target": "task",
|
||||
"target_last_updated": "2023-05-26T16:17:02.635000Z",
|
||||
"task_id": 22
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,356 @@
|
||||
{
|
||||
"count": 21,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"date_joined": "2023-03-30T09:37:31.615000Z",
|
||||
"email": "lonely_user@cvat.org",
|
||||
"first_name": "Lonely",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 21,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": "2023-03-30T09:37:31.708000Z",
|
||||
"last_name": "User",
|
||||
"url": "http://localhost:8080/api/users/21",
|
||||
"username": "lonely_user"
|
||||
},
|
||||
{
|
||||
"date_joined": "2022-02-24T20:45:19Z",
|
||||
"email": "user6@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 20,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Sixth",
|
||||
"url": "http://localhost:8080/api/users/20",
|
||||
"username": "user6"
|
||||
},
|
||||
{
|
||||
"date_joined": "2022-02-24T20:45:07Z",
|
||||
"email": "user5@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 19,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Fifth",
|
||||
"url": "http://localhost:8080/api/users/19",
|
||||
"username": "user5"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:38:46Z",
|
||||
"email": "admin2@cvat.org",
|
||||
"first_name": "Admin",
|
||||
"groups": [
|
||||
"admin"
|
||||
],
|
||||
"has_analytics_access": true,
|
||||
"id": 18,
|
||||
"is_active": true,
|
||||
"is_staff": true,
|
||||
"is_superuser": true,
|
||||
"last_login": "2022-03-05T09:37:01.636000Z",
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/18",
|
||||
"username": "admin2"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:37:41Z",
|
||||
"email": "dummy4@cvat.org",
|
||||
"first_name": "Dummy",
|
||||
"groups": [],
|
||||
"has_analytics_access": false,
|
||||
"id": 17,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/17",
|
||||
"username": "dummy4"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:37:09Z",
|
||||
"email": "dummy3@cvat.org",
|
||||
"first_name": "Dummy",
|
||||
"groups": [],
|
||||
"has_analytics_access": false,
|
||||
"id": 16,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/16",
|
||||
"username": "dummy3"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:36:31Z",
|
||||
"email": "dummy2@cvat.org",
|
||||
"first_name": "Dummy",
|
||||
"groups": [],
|
||||
"has_analytics_access": false,
|
||||
"id": 15,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/15",
|
||||
"username": "dummy2"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:36:00Z",
|
||||
"email": "dummy1@cvat.org",
|
||||
"first_name": "Dummy",
|
||||
"groups": [],
|
||||
"has_analytics_access": false,
|
||||
"id": 14,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/14",
|
||||
"username": "dummy1"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:35:15Z",
|
||||
"email": "user10@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 13,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Tenth",
|
||||
"url": "http://localhost:8080/api/users/13",
|
||||
"username": "user10"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:34:34Z",
|
||||
"email": "user9@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 12,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Ninth",
|
||||
"url": "http://localhost:8080/api/users/12",
|
||||
"username": "user9"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:34:01Z",
|
||||
"email": "user8@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 11,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": "2022-03-17T07:22:55.930000Z",
|
||||
"last_name": "Eighth",
|
||||
"url": "http://localhost:8080/api/users/11",
|
||||
"username": "user8"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:33:06Z",
|
||||
"email": "user7@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 10,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": "2022-09-28T12:17:51.373000Z",
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:32:01Z",
|
||||
"email": "worker4@cvat.org",
|
||||
"first_name": "Worker",
|
||||
"groups": [
|
||||
"worker"
|
||||
],
|
||||
"has_analytics_access": true,
|
||||
"id": 9,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/9",
|
||||
"username": "worker4"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:31:25Z",
|
||||
"email": "worker3@cvat.org",
|
||||
"first_name": "Worker",
|
||||
"groups": [
|
||||
"worker"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 8,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/8",
|
||||
"username": "worker3"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:30:43Z",
|
||||
"email": "worker2@cvat.org",
|
||||
"first_name": "Worker",
|
||||
"groups": [
|
||||
"worker"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 7,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/7",
|
||||
"username": "worker2"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:30:00Z",
|
||||
"email": "worker1@cvat.org",
|
||||
"first_name": "Worker",
|
||||
"groups": [
|
||||
"worker"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 6,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": "2022-09-06T07:57:19.879000Z",
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/6",
|
||||
"username": "worker1"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:25:10Z",
|
||||
"email": "user4@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": true,
|
||||
"id": 5,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Fourth",
|
||||
"url": "http://localhost:8080/api/users/5",
|
||||
"username": "user4"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:24:39Z",
|
||||
"email": "user3@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 4,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": null,
|
||||
"last_name": "Third",
|
||||
"url": "http://localhost:8080/api/users/4",
|
||||
"username": "user3"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:24:12Z",
|
||||
"email": "user2@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 3,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": "2022-09-28T12:19:33.698000Z",
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/3",
|
||||
"username": "user2"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:21:09Z",
|
||||
"email": "user1@cvat.org",
|
||||
"first_name": "User",
|
||||
"groups": [
|
||||
"user"
|
||||
],
|
||||
"has_analytics_access": false,
|
||||
"id": 2,
|
||||
"is_active": true,
|
||||
"is_staff": false,
|
||||
"is_superuser": false,
|
||||
"last_login": "2023-09-15T07:52:46.964000Z",
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
{
|
||||
"date_joined": "2021-12-14T18:04:57Z",
|
||||
"email": "admin1@cvat.org",
|
||||
"first_name": "Admin",
|
||||
"groups": [
|
||||
"admin"
|
||||
],
|
||||
"has_analytics_access": true,
|
||||
"id": 1,
|
||||
"is_active": true,
|
||||
"is_staff": true,
|
||||
"is_superuser": true,
|
||||
"last_login": "2026-01-15T08:22:10.292000Z",
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"count": 4,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"content_type": "application/json",
|
||||
"created_date": "2022-09-29T08:00:48.440000Z",
|
||||
"description": "",
|
||||
"enable_ssl": true,
|
||||
"events": [
|
||||
"create:comment",
|
||||
"create:invitation",
|
||||
"create:issue",
|
||||
"create:project",
|
||||
"create:task",
|
||||
"delete:comment",
|
||||
"delete:invitation",
|
||||
"delete:issue",
|
||||
"delete:membership",
|
||||
"delete:project",
|
||||
"delete:task",
|
||||
"update:comment",
|
||||
"update:issue",
|
||||
"update:job",
|
||||
"update:membership",
|
||||
"update:organization",
|
||||
"update:project",
|
||||
"update:task"
|
||||
],
|
||||
"id": 6,
|
||||
"is_active": true,
|
||||
"last_delivery_date": "2023-09-15T07:53:53.135000Z",
|
||||
"last_status": 200,
|
||||
"organization": 1,
|
||||
"owner": {
|
||||
"first_name": "Admin",
|
||||
"id": 1,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/1",
|
||||
"username": "admin1"
|
||||
},
|
||||
"project_id": null,
|
||||
"target_url": "http://webhooks.internal/",
|
||||
"type": "organization",
|
||||
"updated_date": "2022-09-29T08:00:48.441000Z",
|
||||
"url": "http://localhost:8080/api/webhooks/6"
|
||||
},
|
||||
{
|
||||
"content_type": "application/json",
|
||||
"created_date": "2022-09-28T12:19:49.744000Z",
|
||||
"description": "",
|
||||
"enable_ssl": true,
|
||||
"events": [
|
||||
"create:issue",
|
||||
"delete:issue",
|
||||
"update:issue",
|
||||
"update:project"
|
||||
],
|
||||
"id": 3,
|
||||
"is_active": true,
|
||||
"organization": 2,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 3,
|
||||
"last_name": "Second",
|
||||
"url": "http://localhost:8080/api/users/3",
|
||||
"username": "user2"
|
||||
},
|
||||
"project_id": 3,
|
||||
"target_url": "http://webhooks.internal",
|
||||
"type": "project",
|
||||
"updated_date": "2022-09-28T12:19:49.744000Z",
|
||||
"url": "http://localhost:8080/api/webhooks/3"
|
||||
},
|
||||
{
|
||||
"content_type": "application/json",
|
||||
"created_date": "2022-09-28T12:18:12.412000Z",
|
||||
"description": "",
|
||||
"enable_ssl": true,
|
||||
"events": [
|
||||
"create:comment",
|
||||
"create:issue",
|
||||
"create:task",
|
||||
"delete:comment",
|
||||
"delete:issue",
|
||||
"delete:task",
|
||||
"update:comment",
|
||||
"update:issue",
|
||||
"update:job",
|
||||
"update:project",
|
||||
"update:task"
|
||||
],
|
||||
"id": 2,
|
||||
"is_active": true,
|
||||
"organization": null,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 10,
|
||||
"last_name": "Seventh",
|
||||
"url": "http://localhost:8080/api/users/10",
|
||||
"username": "user7"
|
||||
},
|
||||
"project_id": 1,
|
||||
"target_url": "http://webhooks.internal/",
|
||||
"type": "project",
|
||||
"updated_date": "2022-09-28T12:18:12.412000Z",
|
||||
"url": "http://localhost:8080/api/webhooks/2"
|
||||
},
|
||||
{
|
||||
"content_type": "application/json",
|
||||
"created_date": "2022-09-28T12:16:28.311000Z",
|
||||
"description": "",
|
||||
"enable_ssl": true,
|
||||
"events": [
|
||||
"create:task",
|
||||
"delete:task",
|
||||
"update:job",
|
||||
"update:task"
|
||||
],
|
||||
"id": 1,
|
||||
"is_active": true,
|
||||
"organization": null,
|
||||
"owner": {
|
||||
"first_name": "User",
|
||||
"id": 2,
|
||||
"last_name": "First",
|
||||
"url": "http://localhost:8080/api/users/2",
|
||||
"username": "user1"
|
||||
},
|
||||
"project_id": 6,
|
||||
"target_url": "http://webhooks.internal/",
|
||||
"type": "project",
|
||||
"updated_date": "2022-09-28T12:16:28.311000Z",
|
||||
"url": "http://localhost:8080/api/webhooks/1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import json
|
||||
import operator
|
||||
from collections import defaultdict
|
||||
from collections.abc import Generator, Iterable
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from shared.utils.config import ASSETS_DIR, SHARE_DIR
|
||||
|
||||
|
||||
class Container:
|
||||
def __init__(self, data, key="id"):
|
||||
self.raw_data = data
|
||||
self.map_data = {obj[key]: obj for obj in data}
|
||||
|
||||
@property
|
||||
def raw(self):
|
||||
return self.raw_data
|
||||
|
||||
@property
|
||||
def map(self):
|
||||
return self.map_data
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.raw_data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.raw_data)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, slice):
|
||||
return self.raw_data[key]
|
||||
return self.map_data[key]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def users():
|
||||
with open(ASSETS_DIR / "users.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def organizations():
|
||||
with open(ASSETS_DIR / "organizations.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def memberships():
|
||||
with open(ASSETS_DIR / "memberships.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tasks():
|
||||
with open(ASSETS_DIR / "tasks.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
def filter_assets(resources: Iterable, **kwargs):
|
||||
filtered_resources = []
|
||||
exclude_prefix = "exclude_"
|
||||
filter_operator = lambda arg, func: bool(func(arg))
|
||||
|
||||
for resource in resources:
|
||||
is_matched = True
|
||||
for key, value in kwargs.items():
|
||||
if not is_matched:
|
||||
break
|
||||
|
||||
op = operator.eq
|
||||
if key.startswith(exclude_prefix):
|
||||
key = key[len(exclude_prefix) :]
|
||||
op = operator.ne
|
||||
elif callable(value):
|
||||
op = filter_operator
|
||||
|
||||
cur_value, rest = resource, key
|
||||
while rest:
|
||||
field_and_rest = rest.split("__", maxsplit=1)
|
||||
if 2 == len(field_and_rest):
|
||||
field, rest = field_and_rest
|
||||
else:
|
||||
field, rest = field_and_rest[0], None
|
||||
cur_value = cur_value.get(field, None)
|
||||
# e.g. task has null target_storage
|
||||
# or there are mutexed project_id, task_id
|
||||
if not cur_value:
|
||||
break
|
||||
|
||||
if not (not rest and op(cur_value, value) or rest and op == operator.ne):
|
||||
is_matched = False
|
||||
|
||||
if is_matched:
|
||||
filtered_resources.append(resource)
|
||||
|
||||
return filtered_resources
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def filter_projects(projects):
|
||||
def filter_(**kwargs):
|
||||
return filter_assets(projects, **kwargs)
|
||||
|
||||
return filter_
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def filter_tasks(tasks):
|
||||
def filter_(**kwargs):
|
||||
return filter_assets(tasks, **kwargs)
|
||||
|
||||
return filter_
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tasks_wlc(labels, tasks): # tasks with labels count
|
||||
tasks = deepcopy(tasks)
|
||||
tasks_by_project = defaultdict(list)
|
||||
for task in tasks:
|
||||
tasks_by_project[task["project_id"]].append(task)
|
||||
task["labels"]["count"] = 0
|
||||
|
||||
for label in labels:
|
||||
task_id = label.get("task_id")
|
||||
project_id = label.get("project_id")
|
||||
if not label["parent_id"]:
|
||||
if task_id:
|
||||
tasks[task_id]["labels"]["count"] += 1
|
||||
elif project_id:
|
||||
for task in tasks_by_project[project_id]:
|
||||
task["labels"]["count"] += 1
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def projects():
|
||||
with open(ASSETS_DIR / "projects.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def projects_wlc(projects, labels): # projects with labels count
|
||||
projects = deepcopy(projects)
|
||||
for project in projects:
|
||||
project["labels"]["count"] = 0
|
||||
|
||||
for label in labels:
|
||||
project_id = label.get("project_id")
|
||||
if not label["parent_id"] and project_id:
|
||||
projects[project_id]["labels"]["count"] += 1
|
||||
|
||||
return projects
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def jobs():
|
||||
with open(ASSETS_DIR / "jobs.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def jobs_wlc(jobs, tasks_wlc): # jobs with labels count
|
||||
jobs = deepcopy(jobs)
|
||||
for job in jobs:
|
||||
tid = job["task_id"]
|
||||
job["labels"]["count"] = tasks_wlc[tid]["labels"]["count"]
|
||||
return jobs
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def invitations():
|
||||
with open(ASSETS_DIR / "invitations.json") as f:
|
||||
return Container(json.load(f)["results"], key="key")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def annotations():
|
||||
with open(ASSETS_DIR / "annotations.json") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
CloudStorageAssets = Container
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def cloud_storages() -> CloudStorageAssets:
|
||||
with open(ASSETS_DIR / "cloudstorages.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def issues():
|
||||
with open(ASSETS_DIR / "issues.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def comments():
|
||||
with open(ASSETS_DIR / "comments.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def webhooks():
|
||||
with open(ASSETS_DIR / "webhooks.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def labels():
|
||||
with open(ASSETS_DIR / "labels.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def quality_reports():
|
||||
with open(ASSETS_DIR / "quality_reports.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def quality_conflicts():
|
||||
with open(ASSETS_DIR / "quality_conflicts.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def quality_settings():
|
||||
with open(ASSETS_DIR / "quality_settings.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def consensus_settings():
|
||||
with open(ASSETS_DIR / "consensus_settings.json") as f:
|
||||
return Container(json.load(f)["results"])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def users_by_name(users):
|
||||
return {user["username"]: user for user in users}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def jobs_by_org(tasks, jobs):
|
||||
# FUTURE-FIXME: should be based on organizations to include orgs without jobs too
|
||||
data = {}
|
||||
for job in jobs:
|
||||
data.setdefault(tasks[job["task_id"]]["organization"], []).append(job)
|
||||
data[""] = data.pop(None, [])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def projects_by_org(projects):
|
||||
data = {}
|
||||
for project in projects:
|
||||
data.setdefault(project["organization"], []).append(project)
|
||||
data[""] = data.pop(None, [])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tasks_by_org(tasks):
|
||||
data = {}
|
||||
for task in tasks:
|
||||
data.setdefault(task["organization"], []).append(task)
|
||||
data[""] = data.pop(None, [])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def issues_by_org(tasks, jobs, issues):
|
||||
data = {}
|
||||
for issue in issues:
|
||||
data.setdefault(tasks[jobs[issue["job"]]["task_id"]]["organization"], []).append(issue)
|
||||
data[""] = data.pop(None, [])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def assignee_id():
|
||||
def get_id(data):
|
||||
if data.get("assignee") is not None:
|
||||
return data["assignee"]["id"]
|
||||
|
||||
return get_id
|
||||
|
||||
|
||||
def ownership(func):
|
||||
def wrap(user_id, resource_id):
|
||||
if resource_id is None:
|
||||
return False
|
||||
return func(user_id, resource_id)
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def is_project_staff(projects, assignee_id):
|
||||
@ownership
|
||||
def check(user_id, pid):
|
||||
return user_id == projects[pid]["owner"]["id"] or user_id == assignee_id(projects[pid])
|
||||
|
||||
return check
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def is_task_staff(tasks, is_project_staff, assignee_id):
|
||||
@ownership
|
||||
def check(user_id, tid):
|
||||
return (
|
||||
user_id == tasks[tid]["owner"]["id"]
|
||||
or user_id == assignee_id(tasks[tid])
|
||||
or is_project_staff(user_id, tasks[tid]["project_id"])
|
||||
)
|
||||
|
||||
return check
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def is_job_staff(jobs, is_task_staff, assignee_id):
|
||||
@ownership
|
||||
def check(user_id, jid):
|
||||
return user_id == assignee_id(jobs[jid]) or is_task_staff(user_id, jobs[jid]["task_id"])
|
||||
|
||||
return check
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def is_issue_staff(issues, jobs, assignee_id):
|
||||
@ownership
|
||||
def check(user_id, issue_id):
|
||||
return (
|
||||
user_id == issues[issue_id]["owner"]["id"]
|
||||
or user_id == assignee_id(issues[issue_id])
|
||||
or user_id == assignee_id(jobs[issues[issue_id]["job"]])
|
||||
)
|
||||
|
||||
return check
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def is_issue_admin(issues, jobs, is_task_staff):
|
||||
@ownership
|
||||
def check(user_id, issue_id):
|
||||
return is_task_staff(user_id, jobs[issues[issue_id]["job"]]["task_id"])
|
||||
|
||||
return check
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def find_users(test_db):
|
||||
def find(**kwargs):
|
||||
assert len(kwargs) > 0
|
||||
|
||||
data = test_db
|
||||
for field, value in kwargs.items():
|
||||
if field.startswith("exclude_"):
|
||||
field = field.split("_", maxsplit=1)[1]
|
||||
exclude_rows = set(v["id"] for v in filter(lambda a: a[field] == value, test_db))
|
||||
data = list(filter(lambda a: a["id"] not in exclude_rows, data))
|
||||
else:
|
||||
data = list(filter(lambda a: a[field] == value, data))
|
||||
|
||||
return data
|
||||
|
||||
return find
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_db(users, users_by_name, memberships):
|
||||
data = []
|
||||
fields = [
|
||||
"username",
|
||||
"id",
|
||||
"privilege",
|
||||
"role",
|
||||
"org",
|
||||
"membership_id",
|
||||
"is_superuser",
|
||||
"has_analytics_access",
|
||||
]
|
||||
|
||||
def add_row(**kwargs):
|
||||
data.append({field: kwargs.get(field) for field in fields})
|
||||
|
||||
for user in users:
|
||||
for group in user["groups"]:
|
||||
add_row(
|
||||
username=user["username"],
|
||||
id=user["id"],
|
||||
privilege=group,
|
||||
has_analytics_access=user["has_analytics_access"],
|
||||
is_superuser=user["is_superuser"],
|
||||
)
|
||||
|
||||
for membership in memberships:
|
||||
username = membership["user"]["username"]
|
||||
for group in users_by_name[username]["groups"]:
|
||||
add_row(
|
||||
username=username,
|
||||
role=membership["role"],
|
||||
privilege=group,
|
||||
id=membership["user"]["id"],
|
||||
org=membership["organization"],
|
||||
membership_id=membership["id"],
|
||||
has_analytics_access=users_by_name[username]["has_analytics_access"],
|
||||
is_superuser=users_by_name[username]["is_superuser"],
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def org_staff(memberships):
|
||||
def find(org_id):
|
||||
if org_id in ["", None]:
|
||||
return set()
|
||||
else:
|
||||
return set(
|
||||
m["user"]["id"]
|
||||
for m in memberships
|
||||
if m["role"] in ["maintainer", "owner"]
|
||||
and m["user"] is not None
|
||||
and m["organization"] == org_id
|
||||
)
|
||||
|
||||
return find
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def is_org_member(memberships):
|
||||
def check(user_id, org_id, *, role=None):
|
||||
if org_id in ["", None]:
|
||||
return True
|
||||
else:
|
||||
return user_id in set(
|
||||
m["user"]["id"]
|
||||
for m in memberships
|
||||
if m["user"] is not None
|
||||
if m["organization"] == org_id
|
||||
if not role or m["role"] == role
|
||||
)
|
||||
|
||||
return check
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def find_job_staff_user(is_job_staff):
|
||||
def find(jobs, users, is_staff, wo_jobs=None):
|
||||
for job in jobs:
|
||||
if wo_jobs is not None and job["id"] in wo_jobs:
|
||||
continue
|
||||
for user in users:
|
||||
if is_staff == is_job_staff(user["id"], job["id"]):
|
||||
return user["username"], job["id"]
|
||||
return None, None
|
||||
|
||||
return find
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def find_task_staff_user(is_task_staff):
|
||||
def find(tasks, users, is_staff, wo_tasks=None):
|
||||
for task in tasks:
|
||||
if wo_tasks is not None and task["id"] in wo_tasks:
|
||||
continue
|
||||
for user in users:
|
||||
if is_staff == is_task_staff(user["id"], task["id"]):
|
||||
return user["username"], task["id"]
|
||||
return None, None
|
||||
|
||||
return find
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def find_issue_staff_user(is_issue_staff, is_issue_admin):
|
||||
def find(issues, users, is_staff, is_admin):
|
||||
for issue in issues:
|
||||
for user in users:
|
||||
i_admin, i_staff = (
|
||||
is_issue_admin(user["id"], issue["id"]),
|
||||
is_issue_staff(user["id"], issue["id"]),
|
||||
)
|
||||
if (is_admin is None and (i_staff or i_admin) == is_staff) or (
|
||||
is_admin == i_admin and is_staff == i_staff
|
||||
):
|
||||
return user["username"], issue["id"]
|
||||
return None, None
|
||||
|
||||
return find
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def filter_jobs(jobs):
|
||||
def filter_(**kwargs):
|
||||
return filter_assets(jobs, **kwargs)
|
||||
|
||||
return filter_
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def filter_labels(labels):
|
||||
def filter_(**kwargs):
|
||||
return filter_assets(labels, **kwargs)
|
||||
|
||||
return filter_
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def filter_jobs_with_shapes(annotations):
|
||||
def find(jobs):
|
||||
return list(filter(lambda j: annotations["job"].get(str(j["id"]), {}).get("shapes"), jobs))
|
||||
|
||||
return find
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def filter_tasks_with_shapes(annotations):
|
||||
def find(tasks):
|
||||
return list(
|
||||
filter(lambda t: annotations["task"].get(str(t["id"]), {}).get("shapes"), tasks)
|
||||
)
|
||||
|
||||
return find
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def jobs_with_shapes(jobs, filter_jobs_with_shapes):
|
||||
return filter_jobs_with_shapes(jobs)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tasks_with_shapes(tasks, filter_tasks_with_shapes):
|
||||
return filter_tasks_with_shapes(tasks)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def admin_user(users):
|
||||
for user in users:
|
||||
if user["is_superuser"] and user["is_active"]:
|
||||
return user["username"]
|
||||
raise Exception("Can't find any admin user in the test DB")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def regular_user(users):
|
||||
for user in users:
|
||||
if not user["is_superuser"] and user["is_active"]:
|
||||
return user["username"]
|
||||
raise Exception("Can't find any regular user in the test DB")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def regular_lonely_user(users):
|
||||
for user in users:
|
||||
if user["username"] == "lonely_user":
|
||||
return user["username"]
|
||||
raise Exception("Can't find the lonely user in the test DB")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def job_has_annotations(annotations) -> bool:
|
||||
def check_has_annotations(job_id: int) -> bool:
|
||||
job_annotations = annotations["job"][str(job_id)]
|
||||
return bool(
|
||||
job_annotations["tags"] or job_annotations["shapes"] or job_annotations["tracks"]
|
||||
)
|
||||
|
||||
return check_has_annotations
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def access_tokens(access_tokens_by_username):
|
||||
"Private keys are available in the 'private_key' field."
|
||||
|
||||
return sorted(
|
||||
(t for user_tokens in access_tokens_by_username.values() for t in user_tokens),
|
||||
key=lambda t: t["id"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def raw_access_tokens_by_username():
|
||||
with open(ASSETS_DIR / "access_tokens.json") as f:
|
||||
return json.load(f)["user"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def access_tokens_by_username(raw_access_tokens_by_username):
|
||||
"Private keys are available in the 'private_key' field."
|
||||
|
||||
private_keys = {
|
||||
3: "XQRwNl8D.N5EYCzdyWdroeVVfJylkquAmBqgt9Kw2", # nosec
|
||||
4: "waUchCLi.wWxJTdYBt6R8auMse86bwHobMomjQvEB", # nosec
|
||||
5: "2HVbBoWR.ZJqJtm3TEKEkjqZwyoL7Ig71LVvKRj79", # nosec
|
||||
7: "gIUANJCa.W4Y101GNS8wOyFcncvxMZjTEnU7dzAUF", # nosec
|
||||
}
|
||||
|
||||
data = {}
|
||||
for username, user_tokens in raw_access_tokens_by_username.items():
|
||||
if not user_tokens:
|
||||
continue
|
||||
|
||||
extended_user_tokens = []
|
||||
|
||||
for access_token in user_tokens:
|
||||
access_token = access_token.copy()
|
||||
access_token["private_key"] = private_keys[access_token["id"]]
|
||||
extended_user_tokens.append(access_token)
|
||||
|
||||
data[username] = extended_user_tokens
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def fxt_local_audio_file_path() -> Generator[Path, None, None]:
|
||||
yield SHARE_DIR / "audio" / "sample1.mp3"
|
||||
@@ -0,0 +1,707 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, CalledProcessError, run
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
from shared.utils.config import ASSETS_DIR, get_server_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CVAT_ROOT_DIR = next(dir.parent for dir in Path(__file__).parents if dir.name == "tests")
|
||||
CVAT_DB_DIR = ASSETS_DIR / "cvat_db"
|
||||
CLICKHOUSE_INIT_SCRIPT = "components/analytics/clickhouse/init.py"
|
||||
PREFIX = "test"
|
||||
|
||||
CONTAINER_NAME_FILES = ["docker-compose.tests.yml"]
|
||||
|
||||
DC_FILES = CONTAINER_NAME_FILES + [
|
||||
"docker-compose.dev.yml",
|
||||
"tests/docker-compose.file_share.yml",
|
||||
"tests/docker-compose.minio.yml",
|
||||
"tests/docker-compose.pat_settings.yml",
|
||||
"tests/docker-compose.test_servers.yml",
|
||||
]
|
||||
|
||||
|
||||
class Container(str, Enum):
|
||||
DB = "cvat_db"
|
||||
SERVER = "cvat_server"
|
||||
WORKER_ANNOTATION = "cvat_worker_annotation"
|
||||
WORKER_IMPORT = "cvat_worker_import"
|
||||
WORKER_EXPORT = "cvat_worker_export"
|
||||
WORKER_QUALITY_REPORTS = "cvat_worker_quality_reports"
|
||||
WORKER_WEBHOOKS = "cvat_worker_webhooks"
|
||||
WORKER_UTILS = "cvat_worker_utils"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def covered(cls):
|
||||
return [item.value for item in cls if item != cls.DB]
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("CVAT REST API testing options")
|
||||
group._addoption(
|
||||
"--start-services",
|
||||
action="store_true",
|
||||
help="Start all necessary CVAT containers without running tests. (default: %(default)s)",
|
||||
)
|
||||
|
||||
group._addoption(
|
||||
"--stop-services",
|
||||
action="store_true",
|
||||
help="Stop all testing containers without running tests. (default: %(default)s)",
|
||||
)
|
||||
|
||||
group._addoption(
|
||||
"--rebuild",
|
||||
action="store_true",
|
||||
help="Rebuild CVAT images and then start containers. (default: %(default)s)",
|
||||
)
|
||||
|
||||
group._addoption(
|
||||
"--cleanup",
|
||||
action="store_true",
|
||||
help="Delete files that was create by tests without running tests. (default: %(default)s)",
|
||||
)
|
||||
|
||||
group._addoption(
|
||||
"--dumpdb",
|
||||
action="store_true",
|
||||
help="Update data.json without running tests. (default: %(default)s)",
|
||||
)
|
||||
group._addoption(
|
||||
"--keep-data",
|
||||
action="store_true",
|
||||
help="Do not reset volumes and database. (default: %(default)s)",
|
||||
)
|
||||
|
||||
group._addoption(
|
||||
"--no-services",
|
||||
action="store_true",
|
||||
help=("""Don't start, stop, or seed any containers — assume the user runs
|
||||
the test stack externally"""),
|
||||
)
|
||||
|
||||
group._addoption(
|
||||
"--platform",
|
||||
action="store",
|
||||
default="local",
|
||||
choices=("kube", "local"),
|
||||
help="Platform identifier - 'kube' or 'local'. (default: %(default)s)",
|
||||
)
|
||||
|
||||
|
||||
def _run(command, capture_output=True):
|
||||
_command = command.split() if isinstance(command, str) else command
|
||||
logger.debug(f"Executing a command: {_command}")
|
||||
|
||||
if capture_output:
|
||||
proc = run(_command, check=True, stdout=PIPE) # nosec
|
||||
stdout = proc.stdout.decode()
|
||||
else:
|
||||
proc = run(_command, check=True) # nosec
|
||||
stdout = ""
|
||||
|
||||
if stdout:
|
||||
logger.debug(f"Output (stdout): {stdout}")
|
||||
|
||||
return stdout
|
||||
|
||||
|
||||
def _kube_get_pod_name(label_filter):
|
||||
return _run(f"kubectl get pods -l {label_filter} -o jsonpath={{.items[0].metadata.name}}")
|
||||
|
||||
|
||||
def _kube_get_server_pod_name():
|
||||
return _kube_get_pod_name("component=server")
|
||||
|
||||
|
||||
def _kube_get_db_pod_name():
|
||||
return _kube_get_pod_name("app.kubernetes.io/name=postgresql")
|
||||
|
||||
|
||||
def _kube_get_clichouse_pod_name():
|
||||
return _kube_get_pod_name("app.kubernetes.io/name=clickhouse")
|
||||
|
||||
|
||||
def _kube_get_redis_inmem_pod_name():
|
||||
return _kube_get_pod_name("app.kubernetes.io/name=redis")
|
||||
|
||||
|
||||
def _kube_get_redis_ondisk_pod_name():
|
||||
return _kube_get_pod_name("app.kubernetes.io/name=cvat,tier=kvrocks")
|
||||
|
||||
|
||||
def docker_cp(source, target):
|
||||
_run(f"docker container cp {source} {target}")
|
||||
|
||||
|
||||
def kube_cp(source, target):
|
||||
_run(f"kubectl cp {source} {target}")
|
||||
|
||||
|
||||
def docker_exec(container, command, capture_output=True):
|
||||
return _run(f"docker exec -u root {PREFIX}_{container}_1 {command}", capture_output)
|
||||
|
||||
|
||||
def docker_exec_cvat(command: list[str] | str):
|
||||
base = f"docker exec {PREFIX}_cvat_server_1"
|
||||
_command = f"{base} {command}" if isinstance(command, str) else base.split() + command
|
||||
return _run(_command)
|
||||
|
||||
|
||||
def kube_exec_cvat(command: list[str] | str):
|
||||
pod_name = _kube_get_server_pod_name()
|
||||
base = f"kubectl exec {pod_name} --"
|
||||
_command = f"{base} {command}" if isinstance(command, str) else base.split() + command
|
||||
return _run(_command)
|
||||
|
||||
|
||||
def container_exec_cvat(request: pytest.FixtureRequest, command: list[str] | str):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
return docker_exec_cvat(command)
|
||||
elif platform == "kube":
|
||||
return kube_exec_cvat(command)
|
||||
else:
|
||||
assert False, "unknown platform"
|
||||
|
||||
|
||||
def kube_exec_cvat_db(command):
|
||||
pod_name = _kube_get_db_pod_name()
|
||||
_run(["kubectl", "exec", pod_name, "--"] + command)
|
||||
|
||||
|
||||
def docker_exec_clickhouse_db(command):
|
||||
_run(["docker", "exec", f"{PREFIX}_cvat_clickhouse_1"] + command)
|
||||
|
||||
|
||||
def kube_exec_clickhouse_db(command):
|
||||
pod_name = _kube_get_clichouse_pod_name()
|
||||
_run(["kubectl", "exec", pod_name, "--"] + command)
|
||||
|
||||
|
||||
def docker_exec_redis_inmem(command):
|
||||
return _run(["docker", "exec", f"{PREFIX}_cvat_redis_inmem_1"] + command)
|
||||
|
||||
|
||||
def kube_exec_redis_inmem(command):
|
||||
pod_name = _kube_get_redis_inmem_pod_name()
|
||||
return _run(["kubectl", "exec", pod_name, "--"] + command)
|
||||
|
||||
|
||||
def docker_exec_redis_ondisk(command):
|
||||
_run(["docker", "exec", f"{PREFIX}_cvat_redis_ondisk_1"] + command)
|
||||
|
||||
|
||||
def kube_exec_redis_ondisk(command):
|
||||
pod_name = _kube_get_redis_ondisk_pod_name()
|
||||
_run(["kubectl", "exec", pod_name, "--"] + command)
|
||||
|
||||
|
||||
def docker_restore_db():
|
||||
docker_exec(
|
||||
Container.DB, "psql -U root -d postgres -v from=test_db -v to=cvat -f /tmp/restore.sql"
|
||||
)
|
||||
|
||||
|
||||
def kube_restore_db():
|
||||
kube_exec_cvat_db(
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"PGPASSWORD=cvat_postgresql_postgres psql -U postgres -d postgres -v from=test_db -v to=cvat -f /tmp/restore.sql",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def docker_restore_clickhouse_db():
|
||||
docker_exec_cvat(
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
f'python "{CLICKHOUSE_INIT_SCRIPT}" --clear',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def kube_restore_clickhouse_db():
|
||||
kube_exec_cvat(
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
f'python "{CLICKHOUSE_INIT_SCRIPT}" --clear',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _get_redis_inmem_keys_to_keep():
|
||||
return (
|
||||
"rq:worker:",
|
||||
"rq:workers",
|
||||
"rq:scheduler_instance:",
|
||||
"rq:queues:",
|
||||
"cvat:applied_migrations",
|
||||
"cvat:applied_migration:",
|
||||
)
|
||||
|
||||
|
||||
def docker_restore_redis_inmem():
|
||||
docker_exec_redis_inmem(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
'redis-cli -e --scan --pattern "*" |'
|
||||
'grep -v "' + r"\|".join(_get_redis_inmem_keys_to_keep()) + '" |'
|
||||
"xargs -r redis-cli -e del",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def kube_restore_redis_inmem():
|
||||
kube_exec_redis_inmem(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
'export REDISCLI_AUTH="${REDIS_PASSWORD}" && '
|
||||
'redis-cli -e --scan --pattern "*" | '
|
||||
'grep -v "' + r"\|".join(_get_redis_inmem_keys_to_keep()) + '" | '
|
||||
"xargs -r redis-cli -e del",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def docker_restore_redis_ondisk():
|
||||
docker_exec_redis_ondisk(["redis-cli", "-e", "-p", "6666", "flushall"])
|
||||
|
||||
|
||||
def kube_restore_redis_ondisk():
|
||||
kube_exec_redis_ondisk(
|
||||
["sh", "-c", 'REDISCLI_AUTH="${CVAT_REDIS_ONDISK_PASSWORD}" redis-cli -e -p 6666 flushall']
|
||||
)
|
||||
|
||||
|
||||
def running_containers():
|
||||
return [cn for cn in _run("docker ps --format {{.Names}}").split("\n") if cn]
|
||||
|
||||
|
||||
def dump_db():
|
||||
if "test_cvat_server_1" not in running_containers():
|
||||
pytest.exit("CVAT is not running")
|
||||
try:
|
||||
run( # nosec
|
||||
[
|
||||
sys.executable,
|
||||
str(Path(__file__).resolve().parents[1] / "utils" / "dump_test_db.py"),
|
||||
"--output",
|
||||
str(CVAT_DB_DIR / "data.json"),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
except CalledProcessError:
|
||||
pytest.exit("Database dump failed.\n")
|
||||
|
||||
|
||||
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,
|
||||
):
|
||||
dc_config = yaml.safe_load(dcf)
|
||||
|
||||
for service_name, service_config in dc_config["services"].items():
|
||||
service_config.pop("container_name", None)
|
||||
if service_name in (Container.SERVER, Container.WORKER_UTILS):
|
||||
service_env = service_config["environment"]
|
||||
service_env["DJANGO_SETTINGS_MODULE"] = "cvat.settings.testing_rest"
|
||||
|
||||
if service_name in Container.covered():
|
||||
service_env = service_config["environment"]
|
||||
service_env["COVERAGE_PROCESS_START"] = ".coveragerc"
|
||||
service_config["volumes"].append(
|
||||
"./tests/python/.coveragerc:/home/django/.coveragerc"
|
||||
)
|
||||
|
||||
yaml.dump(dc_config, ndcf)
|
||||
|
||||
|
||||
def delete_compose_files(container_name_files):
|
||||
for filename in container_name_files:
|
||||
filename.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def wait_for_services(num_secs: int = 300) -> None:
|
||||
for i in range(num_secs):
|
||||
logger.debug(f"waiting for the server to load ... ({i})")
|
||||
|
||||
try:
|
||||
response = requests.get(get_server_url("api/server/health/", format="json"))
|
||||
|
||||
statuses = response.json()
|
||||
logger.debug(f"server status: \n{statuses}")
|
||||
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
logger.debug("the server has finished loading!")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"an error occurred during the server status checking: {e}")
|
||||
|
||||
sleep(1)
|
||||
|
||||
raise Exception(
|
||||
f"Failed to reach the server during {num_secs} seconds. Please check the configuration."
|
||||
)
|
||||
|
||||
|
||||
def docker_restore_data_volumes():
|
||||
docker_cp(
|
||||
CVAT_DB_DIR / "cvat_data.tar.bz2",
|
||||
f"{PREFIX}_cvat_server_1:/tmp/cvat_data.tar.bz2",
|
||||
)
|
||||
docker_exec_cvat("tar --strip 3 -xjf /tmp/cvat_data.tar.bz2 -C /home/django/data/")
|
||||
|
||||
|
||||
def kube_restore_data_volumes():
|
||||
pod_name = _kube_get_server_pod_name()
|
||||
kube_cp(
|
||||
CVAT_DB_DIR / "cvat_data.tar.bz2",
|
||||
f"{pod_name}:/tmp/cvat_data.tar.bz2",
|
||||
)
|
||||
kube_exec_cvat("tar --strip 3 -xjf /tmp/cvat_data.tar.bz2 -C /home/django/data/")
|
||||
|
||||
|
||||
def get_server_image_tag():
|
||||
return f"cvat/server:{os.environ.get('CVAT_VERSION', 'dev')}"
|
||||
|
||||
|
||||
def docker_compose(dc_files, cvat_root_dir):
|
||||
return [
|
||||
"docker",
|
||||
"compose",
|
||||
f"--project-name={PREFIX}",
|
||||
# use compatibility mode to have fixed names for containers (with underscores)
|
||||
# https://github.com/docker/compose#about-update-and-backward-compatibility
|
||||
"--compatibility",
|
||||
f"--env-file={cvat_root_dir / 'tests/python/webhook_receiver/.env'}",
|
||||
*(f"--file={f}" for f in dc_files),
|
||||
]
|
||||
|
||||
|
||||
def start_services(dc_files, rebuild=False, cvat_root_dir=CVAT_ROOT_DIR):
|
||||
if any([cn in ["cvat_server", "cvat_db"] for cn in running_containers()]):
|
||||
pytest.exit(
|
||||
"It's looks like you already have running cvat containers. Stop them and try again. "
|
||||
f"List of running containers: {', '.join(running_containers())}"
|
||||
)
|
||||
|
||||
_run(
|
||||
docker_compose(dc_files, cvat_root_dir) + ["up", "-d", *["--build"] * rebuild],
|
||||
capture_output=False,
|
||||
)
|
||||
|
||||
|
||||
def stop_services(dc_files, cvat_root_dir=CVAT_ROOT_DIR):
|
||||
run(docker_compose(dc_files, cvat_root_dir) + ["down", "-v"], capture_output=False)
|
||||
|
||||
|
||||
def session_start(
|
||||
session,
|
||||
cvat_root_dir=CVAT_ROOT_DIR,
|
||||
cvat_db_dir=CVAT_DB_DIR,
|
||||
extra_dc_files=None,
|
||||
waiting_time=300,
|
||||
):
|
||||
stop = session.config.getoption("--stop-services")
|
||||
start = session.config.getoption("--start-services")
|
||||
rebuild = session.config.getoption("--rebuild")
|
||||
keep_data = session.config.getoption("--keep-data")
|
||||
cleanup = session.config.getoption("--cleanup")
|
||||
dumpdb = session.config.getoption("--dumpdb")
|
||||
|
||||
if session.config.getoption("--collect-only"):
|
||||
if any((stop, start, rebuild, cleanup, dumpdb)):
|
||||
raise Exception("""--collect-only is not compatible with any of the other options:
|
||||
--stop-services --start-services --rebuild --cleanup --dumpdb""")
|
||||
return # don't need to start the services to collect tests
|
||||
|
||||
if session.config.getoption("--no-services"):
|
||||
# User manages the stack and the server externally — pytest does
|
||||
# nothing on session start. Per-test restore fixtures still docker-exec
|
||||
# into the user's containers (which must be named test_<service>_1).
|
||||
return
|
||||
|
||||
platform = session.config.getoption("--platform")
|
||||
|
||||
if platform == "kube" and any((stop, start, rebuild, cleanup, dumpdb)):
|
||||
raise Exception("""--platform=kube is not compatible with any of the other options
|
||||
--stop-services --start-services --rebuild --cleanup --dumpdb""")
|
||||
|
||||
if platform == "local":
|
||||
local_start(
|
||||
start,
|
||||
stop,
|
||||
dumpdb,
|
||||
cleanup,
|
||||
rebuild,
|
||||
keep_data,
|
||||
cvat_root_dir,
|
||||
cvat_db_dir,
|
||||
extra_dc_files,
|
||||
waiting_time,
|
||||
)
|
||||
|
||||
elif platform == "kube":
|
||||
kube_start(cvat_db_dir, keep_data)
|
||||
|
||||
|
||||
def local_start(
|
||||
start,
|
||||
stop,
|
||||
dumpdb,
|
||||
cleanup,
|
||||
rebuild,
|
||||
keep_data,
|
||||
cvat_root_dir,
|
||||
cvat_db_dir,
|
||||
extra_dc_files,
|
||||
waiting_time,
|
||||
):
|
||||
if start and stop:
|
||||
raise Exception("--start-services and --stop-services are incompatible")
|
||||
|
||||
if dumpdb:
|
||||
dump_db()
|
||||
pytest.exit("data.json has been updated", returncode=0)
|
||||
|
||||
dc_files = [cvat_root_dir / f for f in DC_FILES]
|
||||
if extra_dc_files is not None:
|
||||
dc_files += extra_dc_files
|
||||
|
||||
container_name_files = [cvat_root_dir / f for f in CONTAINER_NAME_FILES]
|
||||
|
||||
if cleanup:
|
||||
delete_compose_files(container_name_files)
|
||||
pytest.exit("All generated test files have been deleted", returncode=0)
|
||||
|
||||
if not all([f.exists() for f in container_name_files]) or rebuild:
|
||||
delete_compose_files(container_name_files)
|
||||
create_compose_files(container_name_files)
|
||||
|
||||
if stop:
|
||||
stop_services(dc_files, cvat_root_dir)
|
||||
pytest.exit("All testing containers are stopped", returncode=0)
|
||||
|
||||
start_services(dc_files, rebuild, cvat_root_dir)
|
||||
|
||||
if not keep_data:
|
||||
docker_restore_data_volumes()
|
||||
docker_cp(cvat_db_dir / "restore.sql", f"{PREFIX}_cvat_db_1:/tmp/restore.sql")
|
||||
docker_cp(cvat_db_dir / "data.json", f"{PREFIX}_cvat_server_1:/tmp/data.json")
|
||||
|
||||
wait_for_services(waiting_time)
|
||||
|
||||
docker_exec_cvat(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
"./manage.py flush --no-input && ./manage.py loaddata_sorted /tmp/data.json",
|
||||
]
|
||||
)
|
||||
docker_exec(
|
||||
Container.DB, "psql -U root -d postgres -v from=cvat -v to=test_db -f /tmp/restore.sql"
|
||||
)
|
||||
|
||||
if start:
|
||||
pytest.exit("All necessary containers have been created and started.", returncode=0)
|
||||
|
||||
|
||||
def kube_start(cvat_db_dir, keep_data):
|
||||
if keep_data:
|
||||
wait_for_services()
|
||||
return
|
||||
|
||||
kube_restore_data_volumes()
|
||||
server_pod_name = _kube_get_server_pod_name()
|
||||
db_pod_name = _kube_get_db_pod_name()
|
||||
kube_cp(cvat_db_dir / "restore.sql", f"{db_pod_name}:/tmp/restore.sql")
|
||||
kube_cp(cvat_db_dir / "data.json", f"{server_pod_name}:/tmp/data.json")
|
||||
|
||||
wait_for_services()
|
||||
|
||||
kube_exec_cvat(
|
||||
["sh", "-c", "./manage.py flush --no-input && ./manage.py loaddata_sorted /tmp/data.json"]
|
||||
)
|
||||
|
||||
kube_exec_cvat_db(
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"PGPASSWORD=cvat_postgresql_postgres psql -U postgres -d postgres -v from=cvat -v to=test_db -f /tmp/restore.sql",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def pytest_sessionstart(session: pytest.Session) -> None:
|
||||
session_start(session)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
|
||||
session_finish(session)
|
||||
|
||||
|
||||
def session_finish(session):
|
||||
if session.config.getoption("--collect-only"):
|
||||
return
|
||||
|
||||
platform = session.config.getoption("--platform")
|
||||
|
||||
if platform == "local":
|
||||
if os.environ.get("COVERAGE_PROCESS_START"):
|
||||
collect_code_coverage_from_containers()
|
||||
|
||||
|
||||
def collect_code_coverage_from_containers():
|
||||
for container in Container.covered():
|
||||
process_command = "python3"
|
||||
|
||||
# find process with code coverage
|
||||
pid = docker_exec(container, f"pidof {process_command} -o 1")
|
||||
|
||||
# stop process with code coverage
|
||||
docker_exec(container, f"kill -15 {pid}")
|
||||
sleep(3)
|
||||
|
||||
# get code coverage report
|
||||
docker_exec(container, "coverage combine", capture_output=False)
|
||||
docker_exec(container, "coverage json", capture_output=False)
|
||||
docker_cp(
|
||||
f"{PREFIX}_{container}_1:home/django/coverage.json",
|
||||
f"coverage_{container}.json",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def restore_db_per_function(request):
|
||||
# Note that autouse fixtures are executed first within their scope, so be aware of the order
|
||||
# Pre-test DB setups (eg. with class-declared autouse setup() method) may be cleaned.
|
||||
# https://docs.pytest.org/en/stable/reference/fixtures.html#autouse-fixtures-are-executed-first-within-their-scope
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_db()
|
||||
else:
|
||||
kube_restore_db()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def restore_db_per_class(request):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_db()
|
||||
else:
|
||||
kube_restore_db()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def restore_cvat_data_per_function(request):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_data_volumes()
|
||||
else:
|
||||
kube_restore_data_volumes()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def restore_cvat_data_per_class(request):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_data_volumes()
|
||||
else:
|
||||
kube_restore_data_volumes()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def restore_clickhouse_db_per_function(request):
|
||||
# Note that autouse fixtures are executed first within their scope, so be aware of the order
|
||||
# Pre-test DB setups (eg. with class-declared autouse setup() method) may be cleaned.
|
||||
# https://docs.pytest.org/en/stable/reference/fixtures.html#autouse-fixtures-are-executed-first-within-their-scope
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_clickhouse_db()
|
||||
else:
|
||||
kube_restore_clickhouse_db()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def restore_clickhouse_db_per_class(request):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_clickhouse_db()
|
||||
else:
|
||||
kube_restore_clickhouse_db()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def restore_redis_inmem_per_function(request):
|
||||
# Note that autouse fixtures are executed first within their scope, so be aware of the order
|
||||
# Pre-test DB setups (eg. with class-declared autouse setup() method) may be cleaned.
|
||||
# https://docs.pytest.org/en/stable/reference/fixtures.html#autouse-fixtures-are-executed-first-within-their-scope
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_redis_inmem()
|
||||
else:
|
||||
kube_restore_redis_inmem()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def restore_redis_inmem_per_class(request):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_redis_inmem()
|
||||
else:
|
||||
kube_restore_redis_inmem()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def restore_redis_ondisk_per_function(request):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_redis_ondisk()
|
||||
else:
|
||||
kube_restore_redis_ondisk()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def restore_redis_ondisk_per_class(request):
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_redis_ondisk()
|
||||
else:
|
||||
kube_restore_redis_ondisk()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def restore_redis_ondisk_after_class(request):
|
||||
yield
|
||||
|
||||
platform = request.config.getoption("--platform")
|
||||
if platform == "local":
|
||||
docker_restore_redis_ondisk()
|
||||
else:
|
||||
kube_restore_redis_ondisk()
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from functools import partial
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
import shared.utils.s3 as s3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fxt_uploaded_s3_file(request: pytest.FixtureRequest):
|
||||
def upload_file(s3_client: s3.S3Client, *, path: PurePosixPath | str, data: bytes):
|
||||
s3_client.create_file(
|
||||
data=data,
|
||||
filename=str(path),
|
||||
)
|
||||
request.addfinalizer(
|
||||
partial(
|
||||
s3_client.remove_file,
|
||||
filename=str(path),
|
||||
)
|
||||
)
|
||||
|
||||
return upload_file
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import io
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fxt_stdout(capsys):
|
||||
class IoProxy(io.IOBase):
|
||||
def __init__(self, capsys):
|
||||
self.capsys = capsys
|
||||
|
||||
def getvalue(self) -> str:
|
||||
capture = self.capsys.readouterr()
|
||||
return capture.out
|
||||
|
||||
yield IoProxy(capsys)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fxt_logger():
|
||||
logger_stream = io.StringIO()
|
||||
logger = logging.Logger("test", level=logging.INFO)
|
||||
logger.propagate = False
|
||||
logger.addHandler(logging.StreamHandler(logger_stream))
|
||||
yield logger, logger_stream
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fxt_test_name(request: pytest.FixtureRequest):
|
||||
name = request.node.name
|
||||
if request.fixturename and request.fixturename != "fxt_test_name":
|
||||
name += f"[{request.fixturename}]"
|
||||
|
||||
yield name
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
@@ -0,0 +1,36 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from typing import Any
|
||||
|
||||
import attrs
|
||||
from cvat_sdk.api_client import models
|
||||
|
||||
from shared.tasks.interface import ITaskSpec
|
||||
from shared.tasks.utils import parse_frame_step
|
||||
|
||||
|
||||
@attrs.define
|
||||
class TaskSpecBase(ITaskSpec):
|
||||
_params: dict | models.TaskWriteRequest
|
||||
_data_params: dict | models.DataRequest
|
||||
size: int = attrs.field(kw_only=True)
|
||||
|
||||
@property
|
||||
def frame_step(self) -> int:
|
||||
return parse_frame_step(getattr(self, "frame_filter", ""))
|
||||
|
||||
def __getattr__(self, k: str) -> Any:
|
||||
notfound = object()
|
||||
|
||||
for params in [self._params, self._data_params]:
|
||||
if isinstance(params, dict):
|
||||
v = params.get(k, notfound)
|
||||
else:
|
||||
v = getattr(params, k, notfound)
|
||||
|
||||
if v is not notfound:
|
||||
return v
|
||||
|
||||
raise AttributeError(k)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SourceDataType(Enum):
|
||||
images = "images"
|
||||
video = "video"
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from cvat_sdk.api_client import models
|
||||
from PIL import Image
|
||||
|
||||
from shared.tasks.enums import SourceDataType
|
||||
|
||||
|
||||
class ITaskSpec(models.ITaskWriteRequest, models.IDataRequest, metaclass=ABCMeta):
|
||||
size: int
|
||||
frame_step: int
|
||||
source_data_type: SourceDataType
|
||||
|
||||
@abstractmethod
|
||||
def read_frame(self, i: int) -> Image.Image: ...
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import io
|
||||
from collections.abc import Callable
|
||||
from contextlib import closing
|
||||
from typing import ClassVar
|
||||
|
||||
import attrs
|
||||
from PIL import Image
|
||||
|
||||
from shared.tasks.base import TaskSpecBase
|
||||
from shared.tasks.enums import SourceDataType
|
||||
from shared.utils.helpers import read_video_file
|
||||
|
||||
|
||||
@attrs.define
|
||||
class VideoTaskSpec(TaskSpecBase):
|
||||
source_data_type: ClassVar[SourceDataType] = SourceDataType.video
|
||||
chapters: list[dict]
|
||||
|
||||
_get_video_file: Callable[[], io.IOBase] = attrs.field(kw_only=True)
|
||||
|
||||
def read_frame(self, i: int) -> Image.Image:
|
||||
with closing(read_video_file(self._get_video_file())) as reader:
|
||||
for _ in range(i + 1):
|
||||
frame = next(reader)
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
@attrs.define
|
||||
class ImagesTaskSpec(TaskSpecBase):
|
||||
source_data_type: ClassVar[SourceDataType] = SourceDataType.images
|
||||
|
||||
_get_frame: Callable[[int], bytes] = attrs.field(kw_only=True)
|
||||
get_related_files: Callable[[int], dict[str, bytes]] | None = attrs.field(
|
||||
kw_only=True, default=None
|
||||
)
|
||||
|
||||
def read_frame(self, i: int) -> Image.Image:
|
||||
return Image.open(io.BytesIO(self._get_frame(i)))
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
def parse_frame_step(frame_filter: str) -> int:
|
||||
return int((frame_filter or "step=1").split("=")[1])
|
||||
|
||||
|
||||
def to_rel_frame(abs_frame: int, *, frame_step: int, task_start_frame: int) -> int:
|
||||
return (abs_frame - task_start_frame) // frame_step
|
||||
|
||||
|
||||
def to_rel_frames(
|
||||
abs_frames: Iterable[int], *, frame_step: int, task_start_frame: int
|
||||
) -> list[int]:
|
||||
return [
|
||||
to_rel_frame(f, frame_step=frame_step, task_start_frame=task_start_frame)
|
||||
for f in abs_frames
|
||||
]
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from cvat_sdk.api_client import ApiClient, Configuration
|
||||
from cvat_sdk.core.client import Client, Config
|
||||
|
||||
ROOT_DIR = next(dir.parent for dir in Path(__file__).parents if dir.name == "utils")
|
||||
ASSETS_DIR = (ROOT_DIR / "assets").resolve()
|
||||
SHARE_DIR = (ROOT_DIR.parents[1] / "mounted_file_share").resolve()
|
||||
# Suppress the warning from Bandit about hardcoded passwords
|
||||
USER_PASS = "!Q@W#E$R" # nosec
|
||||
BASE_URL = "http://localhost:8080"
|
||||
API_URL = BASE_URL + "/api/"
|
||||
|
||||
# MiniIO settings
|
||||
MINIO_KEY = "minio_access_key"
|
||||
MINIO_SECRET_KEY = "minio_secret_key" # nosec
|
||||
MINIO_ENDPOINT_URL = "http://localhost:9000"
|
||||
IMPORT_EXPORT_BUCKET_ID = 3
|
||||
|
||||
|
||||
def _to_query_params(**kwargs):
|
||||
return "&".join([f"{k}={v}" for k, v in kwargs.items()])
|
||||
|
||||
|
||||
def get_server_url(endpoint, **kwargs):
|
||||
return BASE_URL + "/" + endpoint + "?" + _to_query_params(**kwargs)
|
||||
|
||||
|
||||
def get_api_url(endpoint, **kwargs):
|
||||
return API_URL + endpoint + "?" + _to_query_params(**kwargs)
|
||||
|
||||
|
||||
def get_method(username, endpoint, **kwargs):
|
||||
return requests.get(get_api_url(endpoint, **kwargs), auth=(username, USER_PASS))
|
||||
|
||||
|
||||
def options_method(username, endpoint, **kwargs):
|
||||
return requests.options(get_api_url(endpoint, **kwargs), auth=(username, USER_PASS))
|
||||
|
||||
|
||||
def delete_method(username, endpoint, **kwargs):
|
||||
return requests.delete(get_api_url(endpoint, **kwargs), auth=(username, USER_PASS))
|
||||
|
||||
|
||||
def patch_method(username, endpoint, data, **kwargs):
|
||||
return requests.patch(get_api_url(endpoint, **kwargs), json=data, auth=(username, USER_PASS))
|
||||
|
||||
|
||||
def post_method(username, endpoint, data, **kwargs):
|
||||
return requests.post(get_api_url(endpoint, **kwargs), json=data, auth=(username, USER_PASS))
|
||||
|
||||
|
||||
def post_files_method(username, endpoint, data, files, **kwargs):
|
||||
return requests.post(
|
||||
get_api_url(endpoint, **kwargs), data=data, files=files, auth=(username, USER_PASS)
|
||||
)
|
||||
|
||||
|
||||
def put_method(username, endpoint, data, **kwargs):
|
||||
return requests.put(get_api_url(endpoint, **kwargs), json=data, auth=(username, USER_PASS))
|
||||
|
||||
|
||||
def server_get(username, endpoint, **kwargs):
|
||||
return requests.get(get_server_url(endpoint, **kwargs), auth=(username, USER_PASS))
|
||||
|
||||
|
||||
def make_api_client(
|
||||
user: str | None = None,
|
||||
*,
|
||||
password: str | None = None,
|
||||
access_token: str | None = None,
|
||||
) -> ApiClient:
|
||||
assert (
|
||||
sum([bool(access_token), bool(user)]) <= 1
|
||||
), "Expected only one of the 'access_token' and 'user' args"
|
||||
|
||||
configuration = Configuration(host=BASE_URL)
|
||||
if access_token:
|
||||
configuration.access_token = access_token
|
||||
else:
|
||||
configuration.username = user
|
||||
configuration.password = password or USER_PASS
|
||||
|
||||
return ApiClient(configuration=configuration)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def make_sdk_client(user: str, *, password: str = None) -> Generator[Client, None, None]:
|
||||
with Client(BASE_URL, config=Config(status_check_period=0.01)) as client:
|
||||
client.login((user, password or USER_PASS))
|
||||
yield client
|
||||
@@ -0,0 +1,119 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
This script lists resources on the CVAT server
|
||||
and saves them to JSON files in the `assets` directory.
|
||||
Before running it, start the test instance by running:
|
||||
|
||||
pytest tests/python --start-services
|
||||
|
||||
The script determines which endpoints to query by looking at the set of existing JSON files.
|
||||
For example, if `tasks.json` exists, the script will overwrite it with output of `GET /api/tasks`.
|
||||
Underscores in the file name are replaced with slashes in the URL path. If the default path for an
|
||||
asset file is different, it can be overridden via the --asset-path argument.
|
||||
In addition, `annotations.json` is always saved.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import timezone
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from config import ASSETS_DIR, get_method
|
||||
from dateutil.parser import ParserError, parse
|
||||
|
||||
|
||||
def clean_list_response(data: dict[str, Any]) -> dict[str, Any]:
|
||||
# truncate milliseconds to 3 digit precision to align with data.json
|
||||
# "2023-03-30T09:37:31.615123Z" ->
|
||||
# "2023-03-30T09:37:31.615000Z"
|
||||
|
||||
for result in data["results"]:
|
||||
for k, v in result.items():
|
||||
if not isinstance(v, str):
|
||||
continue
|
||||
|
||||
try:
|
||||
parsed_date = parse(v)
|
||||
except ParserError:
|
||||
continue
|
||||
|
||||
parsed_date = parsed_date.replace(
|
||||
microsecond=parsed_date.microsecond - (parsed_date.microsecond % 1000)
|
||||
)
|
||||
result[k] = parsed_date.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _parse_asset_url_path(s: str) -> tuple[str, str]:
|
||||
asset_filename, url_path = s.lower().rsplit(":", maxsplit=1)
|
||||
return asset_filename, url_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--assets-dir", type=Path, default=ASSETS_DIR)
|
||||
parser.add_argument(
|
||||
"--asset-path",
|
||||
dest="asset_url_paths",
|
||||
default=[],
|
||||
action="append",
|
||||
type=_parse_asset_url_path,
|
||||
help="Repeatable, an override for the default inferred URL path for an asset. "
|
||||
"Format: '<asset filename without extension>:<url path after api/>'",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
assets_dir: Path = args.assets_dir
|
||||
|
||||
asset_url_paths: dict[str, str] = dict(args.asset_url_paths)
|
||||
|
||||
annotations = {}
|
||||
access_tokens = {"user": {}}
|
||||
|
||||
for dump_path in assets_dir.glob("*.json"):
|
||||
asset_name = dump_path.stem
|
||||
endpoint = asset_url_paths.get(asset_name, asset_name.replace("_", "/"))
|
||||
|
||||
if asset_name in ("annotations", "access_tokens"):
|
||||
continue # this will be handled at the end
|
||||
|
||||
response = get_method("admin1", endpoint, page_size="all")
|
||||
|
||||
with open(dump_path, "w") as f:
|
||||
json.dump(clean_list_response(response.json()), f, indent=2, sort_keys=True)
|
||||
|
||||
if endpoint in ["jobs", "tasks"]:
|
||||
obj_type = endpoint.removesuffix("s")
|
||||
annotations[obj_type] = {}
|
||||
for obj in response.json()["results"]:
|
||||
oid = obj["id"]
|
||||
|
||||
response = get_method("admin1", f"{endpoint}/{oid}/annotations")
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
annotations[obj_type][oid] = response.json()
|
||||
|
||||
if endpoint == "users":
|
||||
obj_type = endpoint.removesuffix("s")
|
||||
for user in response.json()["results"]:
|
||||
response = get_method(
|
||||
user["username"], "auth/access_tokens", page_size=100, sort="id"
|
||||
)
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
access_tokens[obj_type][user["username"]] = response.json()["results"]
|
||||
|
||||
for filename, data in [
|
||||
("annotations.json", annotations),
|
||||
("access_tokens.json", access_tokens),
|
||||
]:
|
||||
with open(assets_dir / filename, "w") as f:
|
||||
json.dump(data, f, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+458
@@ -0,0 +1,458 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Dump the test CVAT database to JSON with stable ordering.
|
||||
|
||||
The default ``manage.py dumpdata`` output is not deterministic: record order
|
||||
within a model depends on the underlying ``SELECT`` order, which can differ
|
||||
between runs even when DB contents are identical. This causes spurious
|
||||
diffs in ``tests/python/shared/assets/cvat_db/data.json`` after each dump,
|
||||
which then need to be reviewed and reverted by hand.
|
||||
|
||||
This script wraps ``dumpdata`` and post-processes its output to make the
|
||||
result stable:
|
||||
|
||||
* records are sorted alphabetically by ``(model, pk)`` (the
|
||||
``loaddata_sorted`` management command reorders by FK dependency at
|
||||
load time, so the on-disk order can be arbitrary);
|
||||
* field keys within each record are sorted alphabetically;
|
||||
* known unordered many-to-many fields (e.g. ``auth.user.groups``) are
|
||||
sorted as well.
|
||||
|
||||
It can also preserve volatile timestamp fields (``last_login``,
|
||||
``*_updated_date``, ``last_used_date`` and similar) from the existing
|
||||
output file. When preservation is enabled (the default), an unchanged
|
||||
record keeps its previous timestamp values, so re-dumping after a
|
||||
no-op test run produces zero diff. Genuinely new records get whatever
|
||||
timestamp the server wrote.
|
||||
|
||||
Usage:
|
||||
|
||||
# default: run dumpdata in the test container and write the
|
||||
# normalized output to tests/python/shared/assets/cvat_db/data.json,
|
||||
# preserving volatile fields from the existing file.
|
||||
python tests/python/shared/utils/dump_test_db.py
|
||||
|
||||
# read pre-existing dump from a file (or stdin via "-") instead of
|
||||
# running docker exec
|
||||
python tests/python/shared/utils/dump_test_db.py --from-file dump.json
|
||||
|
||||
# also refresh volatile fields (don't preserve from existing output)
|
||||
python tests/python/shared/utils/dump_test_db.py --refresh-volatile
|
||||
|
||||
# write to a custom location / stdout
|
||||
python tests/python/shared/utils/dump_test_db.py -o /tmp/data.json
|
||||
python tests/python/shared/utils/dump_test_db.py -o -
|
||||
|
||||
# extend the built-in volatile/sortable maps and dumpdata excludes
|
||||
# from a JSON file (e.g. when running against a Django project that
|
||||
# adds models beyond the OSS apps covered by this script or you want
|
||||
# to customize the dump).
|
||||
python tests/python/shared/utils/dump_test_db.py \\
|
||||
--extra-config path/to/dump_extra_config.json
|
||||
|
||||
The extra config file has up to three optional sections:
|
||||
|
||||
{
|
||||
"extra_volatile_fields": { "<model>": ["<field>", ...], ... },
|
||||
"extra_sortable_list_fields": { "<model>": ["<field>", ...], ... },
|
||||
"extra_excludes": ["<app_or_app.model>", ...]
|
||||
}
|
||||
|
||||
Field maps union with the built-in entries per model;
|
||||
``extra_excludes`` append to ``DUMPDATA_EXCLUDES`` (and are ignored
|
||||
when ``--from-file`` is used, since the dump is already produced).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, run
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_CONTAINER_NAME = "test_cvat_server_1"
|
||||
|
||||
DEFAULT_OUTPUT = Path(__file__).resolve().parents[2] / "shared" / "assets" / "cvat_db" / "data.json"
|
||||
|
||||
STDOUT_SENTINEL = "-"
|
||||
|
||||
DUMPDATA_EXCLUDES = (
|
||||
"admin",
|
||||
"auth.permission",
|
||||
"authtoken",
|
||||
"contenttypes",
|
||||
"django_rq",
|
||||
"sessions",
|
||||
)
|
||||
|
||||
# Fields whose value changes on every dump even when no test-relevant state
|
||||
# has changed (auto_now timestamps, last-login markers, etc.). When
|
||||
# preservation is enabled the script copies these values from the existing
|
||||
# data.json so that re-dumping an unchanged DB produces no diff.
|
||||
VOLATILE_FIELDS: dict[str, frozenset[str]] = {
|
||||
"auth.user": frozenset({"last_login"}),
|
||||
"engine.profile": frozenset({"last_activity_date"}),
|
||||
"quality_control.qualityreport": frozenset(
|
||||
{"assignee_last_updated", "gt_last_updated", "target_last_updated"}
|
||||
),
|
||||
"access_tokens.accesstoken": frozenset({"last_used_date"}),
|
||||
}
|
||||
|
||||
|
||||
# Any field whose name is exactly ``updated_date`` or ends with
|
||||
# ``_updated_date`` is treated as volatile for every model. ``auto_now``
|
||||
# timestamps drift on every save and the exact value is not relevant for
|
||||
# tests, so we don't track the per-model list of such fields explicitly.
|
||||
def _is_blanket_volatile(field_name: str) -> bool:
|
||||
return field_name == "updated_date" or field_name.endswith("_updated_date")
|
||||
|
||||
|
||||
# Fields holding many-to-many references whose element order is not
|
||||
# semantically meaningful. Sorting them stabilizes the dump even if the
|
||||
# m2m join table returns rows in a different order between runs.
|
||||
SORTABLE_LIST_FIELDS: dict[str, frozenset[str]] = {
|
||||
"auth.user": frozenset({"groups", "user_permissions"}),
|
||||
"auth.group": frozenset({"permissions"}),
|
||||
}
|
||||
|
||||
|
||||
# ``DjangoJSONEncoder`` truncates microseconds to milliseconds: a value
|
||||
# with ``microsecond`` in [1, 999] is encoded as ``.000Z``. After
|
||||
# ``loaddata`` parses that back to ``microsecond=0``, the next ``dumpdata``
|
||||
# omits the fraction entirely and writes a bare ``Z`` instead, producing a
|
||||
# spurious diff. Drop the redundant zero milliseconds up-front so the
|
||||
# round-trip is idempotent.
|
||||
ZERO_MS_PATTERN = r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\.000(Z|[+-]\d{2}:\d{2})$"
|
||||
|
||||
_DEFAULT = object()
|
||||
|
||||
|
||||
def _strip_zero_milliseconds(value: Any) -> Any:
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
return re.sub(ZERO_MS_PATTERN, r"\1\2", value)
|
||||
|
||||
|
||||
def _pk_sort_key(pk: Any) -> tuple:
|
||||
# `pk` is usually an int but can be a list of natural-key parts.
|
||||
# Wrap into a tuple of (type_tag, value) so heterogeneous pks order
|
||||
# consistently without TypeError.
|
||||
if isinstance(pk, (int, float)):
|
||||
return (0, pk)
|
||||
if isinstance(pk, list):
|
||||
return (1, tuple(_pk_sort_key(p) for p in pk))
|
||||
return (2, str(pk))
|
||||
|
||||
|
||||
def _normalize_record(
|
||||
record: dict[str, Any],
|
||||
*,
|
||||
sortable_list_fields: dict[str, frozenset[str]] | Any = _DEFAULT,
|
||||
) -> dict[str, Any]:
|
||||
if sortable_list_fields is _DEFAULT:
|
||||
sortable_list_fields = SORTABLE_LIST_FIELDS
|
||||
|
||||
model = record.get("model", "")
|
||||
fields = record.get("fields", {})
|
||||
sortable = sortable_list_fields.get(model, frozenset())
|
||||
|
||||
new_fields: dict[str, Any] = {}
|
||||
for key in sorted(fields):
|
||||
value = fields[key]
|
||||
if key in sortable and isinstance(value, list):
|
||||
try:
|
||||
value = sorted(value, key=lambda v: json.dumps(v, sort_keys=True))
|
||||
except TypeError:
|
||||
pass
|
||||
value = _strip_zero_milliseconds(value)
|
||||
new_fields[key] = value
|
||||
|
||||
return {"model": model, "pk": record.get("pk"), "fields": new_fields}
|
||||
|
||||
|
||||
def _preserve_volatile(
|
||||
records: list[dict[str, Any]],
|
||||
reference: list[dict[str, Any]],
|
||||
*,
|
||||
volatile_fields: dict[str, frozenset[str]] | Any = _DEFAULT,
|
||||
) -> list[dict[str, Any]]:
|
||||
if volatile_fields is _DEFAULT:
|
||||
volatile_fields = VOLATILE_FIELDS
|
||||
|
||||
ref_index = {
|
||||
(r.get("model", ""), _pk_sort_key(r.get("pk"))): r.get("fields", {}) for r in reference
|
||||
}
|
||||
|
||||
for record in records:
|
||||
model = record.get("model", "")
|
||||
ref_fields = ref_index.get((model, _pk_sort_key(record.get("pk"))))
|
||||
if ref_fields is None:
|
||||
continue
|
||||
|
||||
explicit = volatile_fields.get(model, frozenset())
|
||||
fields = record.get("fields", {})
|
||||
for name in fields:
|
||||
if name not in ref_fields:
|
||||
continue
|
||||
if name in explicit or _is_blanket_volatile(name):
|
||||
fields[name] = ref_fields[name]
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def normalize(
|
||||
records: list[dict[str, Any]],
|
||||
*,
|
||||
reference: list[dict[str, Any]] | None = None,
|
||||
volatile_fields: dict[str, frozenset[str]] | Any = _DEFAULT,
|
||||
sortable_list_fields: dict[str, frozenset[str]] | Any = _DEFAULT,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return ``records`` sorted and field-normalized, with volatile fields
|
||||
optionally preserved from ``reference``."""
|
||||
|
||||
if volatile_fields is _DEFAULT:
|
||||
volatile_fields = VOLATILE_FIELDS
|
||||
|
||||
if sortable_list_fields is _DEFAULT:
|
||||
sortable_list_fields = _DEFAULT
|
||||
|
||||
normalized = [_normalize_record(r, sortable_list_fields=sortable_list_fields) for r in records]
|
||||
|
||||
if reference is not None:
|
||||
_preserve_volatile(normalized, reference=reference, volatile_fields=volatile_fields)
|
||||
|
||||
# Sort alphabetically by model name (then by pk) so the committed file
|
||||
# has a layout that doesn't depend on Django's dependency sort. The
|
||||
# ``loaddata_sorted`` management command reorders records by dependency
|
||||
# at load time, so any total ordering is fine here.
|
||||
normalized.sort(key=lambda r: (r.get("model", ""), _pk_sort_key(r.get("pk"))))
|
||||
return normalized
|
||||
|
||||
|
||||
def dump_from_container(
|
||||
container: str = DEFAULT_CONTAINER_NAME,
|
||||
*,
|
||||
excludes: tuple[str, ...] | list[str] = DUMPDATA_EXCLUDES,
|
||||
) -> list[dict[str, Any]]:
|
||||
cmd = [
|
||||
"docker",
|
||||
"exec",
|
||||
container,
|
||||
"python",
|
||||
"manage.py",
|
||||
"dumpdata",
|
||||
"--indent",
|
||||
"2",
|
||||
"--natural-foreign",
|
||||
]
|
||||
for ex in excludes:
|
||||
cmd.extend(["--exclude", ex])
|
||||
|
||||
proc = run(cmd, check=True, stdout=PIPE) # nosec
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
EXTRA_CONFIG_KEYS = ("extra_volatile_fields", "extra_sortable_list_fields", "extra_excludes")
|
||||
|
||||
# Conventional Django ``app_label.modelname`` shape: lowercase identifier on
|
||||
# each side, joined by a single dot. Mismatch is a warning (not an error)
|
||||
# because the script doesn't load Django and can't authoritatively validate
|
||||
# model names — but the convention catches typos like ``engine.Task``.
|
||||
_MODEL_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$")
|
||||
|
||||
|
||||
class _ConfigError(ValueError):
|
||||
"""Raised when ``--extra-config`` content is malformed."""
|
||||
|
||||
|
||||
def _require_non_empty_str(value: Any, where: str) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise _ConfigError(f"{where}: expected string, got {type(value).__name__}")
|
||||
if not value:
|
||||
raise _ConfigError(f"{where}: expected non-empty string")
|
||||
|
||||
|
||||
def _validate_field_map(section: Any, where: str) -> None:
|
||||
if not isinstance(section, dict):
|
||||
raise _ConfigError(f"{where}: expected object, got {type(section).__name__}")
|
||||
for model, fields in section.items():
|
||||
_require_non_empty_str(model, f"{where} key")
|
||||
if not _MODEL_NAME_RE.match(model):
|
||||
print(
|
||||
f"warning: {where}.{model!r} does not match the conventional "
|
||||
"<app_label>.<model_lowercase> shape",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if not isinstance(fields, list):
|
||||
raise _ConfigError(f"{where}.{model}: expected list, got {type(fields).__name__}")
|
||||
seen: set[str] = set()
|
||||
for i, field in enumerate(fields):
|
||||
_require_non_empty_str(field, f"{where}.{model}[{i}]")
|
||||
if field in seen:
|
||||
raise _ConfigError(f"{where}.{model}[{i}]: duplicate field {field!r}")
|
||||
seen.add(field)
|
||||
|
||||
|
||||
def _validate_excludes(section: Any, where: str) -> None:
|
||||
if not isinstance(section, list):
|
||||
raise _ConfigError(f"{where}: expected list, got {type(section).__name__}")
|
||||
seen: set[str] = set()
|
||||
for i, item in enumerate(section):
|
||||
_require_non_empty_str(item, f"{where}[{i}]")
|
||||
if item in seen:
|
||||
raise _ConfigError(f"{where}[{i}]: duplicate exclude {item!r}")
|
||||
seen.add(item)
|
||||
|
||||
|
||||
def _load_extra_config(path: Path) -> dict[str, Any]:
|
||||
"""Read and validate an extension config file.
|
||||
|
||||
See ``EXTRA_CONFIG_KEYS`` for the allowed top-level sections; missing
|
||||
sections default to empty. Malformed content raises ``_ConfigError``
|
||||
with a JSON-pointer-style path so the offending entry is easy to find.
|
||||
"""
|
||||
raw = json.loads(path.read_text())
|
||||
if not isinstance(raw, dict):
|
||||
raise _ConfigError(f"{path}: top-level must be an object, got {type(raw).__name__}")
|
||||
unknown = sorted(set(raw) - set(EXTRA_CONFIG_KEYS))
|
||||
if unknown:
|
||||
raise _ConfigError(
|
||||
f"{path}: unknown top-level keys: {unknown}. " f"valid keys: {list(EXTRA_CONFIG_KEYS)}"
|
||||
)
|
||||
|
||||
extra_volatile = raw.get("extra_volatile_fields", {})
|
||||
extra_sortable = raw.get("extra_sortable_list_fields", {})
|
||||
extra_excludes = raw.get("extra_excludes", [])
|
||||
|
||||
_validate_field_map(extra_volatile, "extra_volatile_fields")
|
||||
_validate_field_map(extra_sortable, "extra_sortable_list_fields")
|
||||
_validate_excludes(extra_excludes, "extra_excludes")
|
||||
|
||||
return {
|
||||
"extra_volatile_fields": extra_volatile,
|
||||
"extra_sortable_list_fields": extra_sortable,
|
||||
"extra_excludes": extra_excludes,
|
||||
}
|
||||
|
||||
|
||||
def _merge_field_map(
|
||||
base: dict[str, frozenset[str]],
|
||||
extra: dict[str, list[str]],
|
||||
) -> dict[str, frozenset[str]]:
|
||||
merged = dict(base)
|
||||
for model, fields in extra.items():
|
||||
merged[model] = merged.get(model, frozenset()) | frozenset(fields)
|
||||
return merged
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=str,
|
||||
default=str(DEFAULT_OUTPUT),
|
||||
help=f'Output file (default: %(default)s). Use "{STDOUT_SENTINEL}" for stdout.',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--from-file",
|
||||
type=str,
|
||||
help="Read pre-existing dumpdata JSON from this file instead of running docker exec. "
|
||||
'Use "-" to read from stdin.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--container",
|
||||
default=DEFAULT_CONTAINER_NAME,
|
||||
help="Docker container to dump from (default: %(default)s).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--refresh-volatile",
|
||||
action="store_true",
|
||||
help="Take volatile timestamp fields from the new dump instead of "
|
||||
"preserving them from the existing output file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extra-config",
|
||||
type=Path,
|
||||
help="Path to a JSON file extending the built-in volatile/sortable "
|
||||
"maps and dumpdata excludes. Sections (all optional): "
|
||||
"extra_volatile_fields, extra_sortable_list_fields, extra_excludes. "
|
||||
"Field maps union with the built-in entries per model; "
|
||||
"extra_excludes append to the built-in DUMPDATA_EXCLUDES (and are "
|
||||
"ignored when --from-file is used).",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parsed_args = _build_parser().parse_args(argv)
|
||||
|
||||
if parsed_args.extra_config:
|
||||
try:
|
||||
extra = _load_extra_config(parsed_args.extra_config)
|
||||
except _ConfigError as exc:
|
||||
print(f"error: invalid --extra-config: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
else:
|
||||
extra = {
|
||||
"extra_volatile_fields": {},
|
||||
"extra_sortable_list_fields": {},
|
||||
"extra_excludes": [],
|
||||
}
|
||||
volatile_fields = _merge_field_map(VOLATILE_FIELDS, extra["extra_volatile_fields"])
|
||||
sortable_list_fields = _merge_field_map(
|
||||
SORTABLE_LIST_FIELDS, extra["extra_sortable_list_fields"]
|
||||
)
|
||||
excludes = list(DUMPDATA_EXCLUDES) + [
|
||||
ex for ex in extra["extra_excludes"] if ex not in DUMPDATA_EXCLUDES
|
||||
]
|
||||
if extra["extra_excludes"] and parsed_args.from_file:
|
||||
print(
|
||||
"warning: extra_excludes is ignored when reading the dump from "
|
||||
"--from-file (the dump is already produced).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if parsed_args.from_file == "-":
|
||||
records = json.loads(sys.stdin.read())
|
||||
elif parsed_args.from_file:
|
||||
records = json.loads(Path(parsed_args.from_file).read_text())
|
||||
else:
|
||||
records = dump_from_container(parsed_args.container, excludes=excludes)
|
||||
|
||||
reference_dump: list[dict[str, Any]] | None = None
|
||||
output_path: Path | None = (
|
||||
None if parsed_args.output == STDOUT_SENTINEL else Path(parsed_args.output)
|
||||
)
|
||||
if not parsed_args.refresh_volatile and output_path is not None and output_path.exists():
|
||||
reference_dump = json.loads(output_path.read_text())
|
||||
|
||||
normalized = normalize(
|
||||
records,
|
||||
reference=reference_dump,
|
||||
volatile_fields=volatile_fields,
|
||||
sortable_list_fields=sortable_list_fields,
|
||||
)
|
||||
serialized = json.dumps(normalized, indent=2, ensure_ascii=False)
|
||||
|
||||
if output_path is None:
|
||||
print(serialized)
|
||||
else:
|
||||
output_path.write_text(serialized + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,131 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import io
|
||||
import subprocess
|
||||
from collections.abc import Generator
|
||||
from fractions import Fraction
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import av
|
||||
import av.video.reformatter
|
||||
import librosa
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from shared.fixtures.init import get_server_image_tag
|
||||
|
||||
|
||||
def generate_image_file(filename="image.png", size=(100, 50), color=(0, 0, 0)):
|
||||
f = BytesIO()
|
||||
f.name = filename
|
||||
image = Image.new("RGB", size=size, color=color)
|
||||
image.save(f)
|
||||
f.seek(0)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def generate_image_files(
|
||||
count: int,
|
||||
*,
|
||||
prefixes: list[str] | None = None,
|
||||
filenames: list[str] | None = None,
|
||||
sizes: list[tuple[int, int]] | None = None,
|
||||
) -> list[BytesIO]:
|
||||
assert not (prefixes and filenames), "prefixes cannot be used together with filenames"
|
||||
assert not prefixes or len(prefixes) == count
|
||||
assert not filenames or len(filenames) == count
|
||||
|
||||
images = []
|
||||
for i in range(count):
|
||||
prefix = prefixes[i] if prefixes else ""
|
||||
filename = f"{prefix}{i}.jpeg" if not filenames else filenames[i]
|
||||
image = generate_image_file(
|
||||
filename, color=(i, i, i), **({"size": sizes[i]}) if sizes else {}
|
||||
)
|
||||
images.append(image)
|
||||
|
||||
return images
|
||||
|
||||
|
||||
def generate_video_file(
|
||||
num_frames: int,
|
||||
*,
|
||||
size: tuple[int, int] = (100, 50),
|
||||
invalid_keyframes: bool = False,
|
||||
) -> BytesIO:
|
||||
f = BytesIO()
|
||||
f.name = "video.mkv"
|
||||
chapters = [
|
||||
{
|
||||
"id": 0,
|
||||
"start": 0,
|
||||
"end": 100,
|
||||
"time_base": Fraction(1, 1000),
|
||||
"metadata": {"title": "Intro"},
|
||||
}
|
||||
]
|
||||
|
||||
with av.open(f, "w") as container:
|
||||
container.set_chapters(chapters)
|
||||
stream = container.add_stream("mjpeg", rate=60)
|
||||
stream.width = size[0]
|
||||
stream.height = size[1]
|
||||
stream.color_range = av.video.reformatter.ColorRange.JPEG
|
||||
|
||||
for i in range(num_frames):
|
||||
frame = av.VideoFrame.from_image(Image.new("RGB", size=size, color=(i, i, i)))
|
||||
for packet in stream.encode(frame):
|
||||
if invalid_keyframes:
|
||||
# Specify pts/dts values that result in 0 valid keyframes
|
||||
packet.pts = 10
|
||||
packet.dts = 10
|
||||
|
||||
container.mux(packet)
|
||||
|
||||
f.seek(0)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def read_video_file(file: BytesIO) -> Generator[Image.Image, None, None]:
|
||||
file.seek(0)
|
||||
|
||||
with av.open(file) as container:
|
||||
video_stream = container.streams.video[0]
|
||||
|
||||
for frame in container.decode(video_stream):
|
||||
yield frame.to_image()
|
||||
|
||||
|
||||
def generate_manifest(path: str) -> None:
|
||||
command = [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"-u",
|
||||
"root:root",
|
||||
"-v",
|
||||
f"{path}:/local",
|
||||
"--entrypoint",
|
||||
"python3",
|
||||
get_server_image_tag(),
|
||||
"utils/dataset_manifest/create.py",
|
||||
"--output-dir",
|
||||
"/local",
|
||||
"/local",
|
||||
]
|
||||
try:
|
||||
subprocess.check_output(command, stderr=subprocess.PIPE)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.stderr.decode("utf-8"))
|
||||
raise
|
||||
|
||||
|
||||
def read_audio_pcm(
|
||||
f: Path | io.IOBase, *, offset_ms: int = 0, rate: int = 8000
|
||||
) -> tuple[np.ndarray, float]:
|
||||
return librosa.load(f, mono=True, sr=rate, offset=offset_ms / 1000)
|
||||
@@ -0,0 +1,220 @@
|
||||
import functools
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import ExitStack
|
||||
from http import HTTPStatus
|
||||
from time import sleep
|
||||
from typing import Any, TypeVar
|
||||
|
||||
import pytest
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
from shared.utils.config import get_method, post_method
|
||||
|
||||
FILENAME_TEMPLATE = "cvat/{}/{}.zip"
|
||||
EXPORT_FORMAT = "CVAT for images 1.1"
|
||||
IMPORT_FORMAT = "CVAT 1.1"
|
||||
|
||||
|
||||
def _make_custom_resource_params(resource: str, obj: str, cloud_storage_id: int) -> dict[str, Any]:
|
||||
return {
|
||||
"filename": FILENAME_TEMPLATE.format(obj, resource),
|
||||
"location": "cloud_storage",
|
||||
"cloud_storage_id": cloud_storage_id,
|
||||
}
|
||||
|
||||
|
||||
def _make_default_resource_params(resource: str, obj: str) -> dict[str, Any]:
|
||||
return {
|
||||
"filename": FILENAME_TEMPLATE.format(obj, resource),
|
||||
}
|
||||
|
||||
|
||||
def _make_export_resource_params(
|
||||
resource: str, is_default: bool = True, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
func = _make_default_resource_params if is_default else _make_custom_resource_params
|
||||
params = func(resource, **kwargs)
|
||||
if resource != "backup":
|
||||
params["format"] = EXPORT_FORMAT
|
||||
params["save_images"] = resource == "dataset"
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def _make_import_resource_params(
|
||||
resource: str, is_default: bool = True, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
func = _make_default_resource_params if is_default else _make_custom_resource_params
|
||||
params = func(resource, **kwargs)
|
||||
if resource != "backup":
|
||||
params["format"] = IMPORT_FORMAT
|
||||
return params
|
||||
|
||||
|
||||
# FUTURE-TODO: reuse common logic from rest_api/utils
|
||||
def _wait_request(
|
||||
user: str,
|
||||
request_id: str,
|
||||
*,
|
||||
sleep_interval: float = 0.1,
|
||||
number_of_checks: int = 100,
|
||||
):
|
||||
for _ in range(number_of_checks):
|
||||
sleep(sleep_interval)
|
||||
response = get_method(user, f"requests/{request_id}")
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
request_details = json.loads(response.content)
|
||||
status = request_details["status"]
|
||||
assert status in {"started", "queued", "finished", "failed"}
|
||||
if status in {"finished", "failed"}:
|
||||
return
|
||||
|
||||
|
||||
class _CloudStorageResourceTest(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _make_client():
|
||||
pass
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, admin_user: str):
|
||||
self.user = admin_user
|
||||
self.client = self._make_client()
|
||||
self.exit_stack = ExitStack()
|
||||
with self.exit_stack:
|
||||
yield
|
||||
|
||||
def _ensure_file_created(self, func: T, storage: dict[str, Any]) -> T:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
filename = kwargs["filename"]
|
||||
bucket = storage["resource"]
|
||||
|
||||
# check that file doesn't exist on the bucket
|
||||
assert not self.client.file_exists(bucket=bucket, filename=filename)
|
||||
|
||||
func(*args, **kwargs)
|
||||
|
||||
# check that file exists on the bucket
|
||||
assert self.client.file_exists(bucket=bucket, filename=filename)
|
||||
|
||||
return wrapper
|
||||
|
||||
def _export_resource_to_cloud_storage(
|
||||
self,
|
||||
obj_id: int,
|
||||
obj: str,
|
||||
resource: str,
|
||||
*,
|
||||
user: str,
|
||||
_expect_status: HTTPStatus = HTTPStatus.ACCEPTED,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
# initialize the export process
|
||||
response = post_method(
|
||||
user,
|
||||
f"{obj}/{obj_id}/{resource if resource != 'annotations' else 'dataset'}/export",
|
||||
data=None,
|
||||
**kwargs,
|
||||
)
|
||||
assert response.status_code == _expect_status
|
||||
|
||||
if _expect_status == HTTPStatus.FORBIDDEN:
|
||||
return
|
||||
|
||||
rq_id = json.loads(response.content).get("rq_id")
|
||||
assert rq_id, "The rq_id was not found in server request"
|
||||
|
||||
_wait_request(user, rq_id)
|
||||
|
||||
def _import_resource_from_cloud_storage(
|
||||
self, url: str, *, user: str, _expect_status: HTTPStatus = HTTPStatus.ACCEPTED, **kwargs
|
||||
) -> None:
|
||||
response = post_method(user, url, data=None, **kwargs)
|
||||
status = response.status_code
|
||||
|
||||
assert status == _expect_status, status
|
||||
if status == HTTPStatus.FORBIDDEN:
|
||||
return
|
||||
|
||||
rq_id = response.json().get("rq_id")
|
||||
assert rq_id, "The rq_id parameter was not found in the server response"
|
||||
|
||||
_wait_request(user, rq_id)
|
||||
|
||||
def _import_annotations_from_cloud_storage(
|
||||
self,
|
||||
obj_id,
|
||||
obj,
|
||||
*,
|
||||
user,
|
||||
_expect_status: HTTPStatus = HTTPStatus.ACCEPTED,
|
||||
_check_uploaded: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
url = f"{obj}/{obj_id}/annotations"
|
||||
self._import_resource_from_cloud_storage(
|
||||
url, user=user, _expect_status=_expect_status, **kwargs
|
||||
)
|
||||
|
||||
if _expect_status == HTTPStatus.FORBIDDEN:
|
||||
return
|
||||
|
||||
if _check_uploaded:
|
||||
response = get_method(user, url)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
annotations = response.json()
|
||||
|
||||
assert len(annotations["shapes"])
|
||||
|
||||
def _import_backup_from_cloud_storage(
|
||||
self, obj, *, user, _expect_status: HTTPStatus = HTTPStatus.ACCEPTED, **kwargs
|
||||
):
|
||||
self._import_resource_from_cloud_storage(
|
||||
f"{obj}/backup", user=user, _expect_status=_expect_status, **kwargs
|
||||
)
|
||||
|
||||
def _import_dataset_from_cloud_storage(
|
||||
self, obj_id, obj, *, user, _expect_status: HTTPStatus = HTTPStatus.ACCEPTED, **kwargs
|
||||
):
|
||||
self._import_resource_from_cloud_storage(
|
||||
f"{obj}/{obj_id}/dataset", user=user, _expect_status=_expect_status, **kwargs
|
||||
)
|
||||
|
||||
def _import_resource(self, cloud_storage: dict[str, Any], resource_type: str, *args, **kwargs):
|
||||
methods = {
|
||||
"annotations": self._import_annotations_from_cloud_storage,
|
||||
"dataset": self._import_dataset_from_cloud_storage,
|
||||
"backup": self._import_backup_from_cloud_storage,
|
||||
}
|
||||
|
||||
org_id = cloud_storage["organization"]
|
||||
if org_id:
|
||||
kwargs.setdefault("org_id", org_id)
|
||||
|
||||
kwargs.setdefault("user", self.user)
|
||||
|
||||
return methods[resource_type](*args, **kwargs)
|
||||
|
||||
def _export_resource(self, cloud_storage: dict[str, Any], *args, **kwargs):
|
||||
org_id = cloud_storage["organization"]
|
||||
if org_id:
|
||||
kwargs.setdefault("org_id", org_id)
|
||||
|
||||
kwargs.setdefault("user", self.user)
|
||||
|
||||
export_callback = self._ensure_file_created(
|
||||
self._export_resource_to_cloud_storage, storage=cloud_storage
|
||||
)
|
||||
export_callback(*args, **kwargs)
|
||||
|
||||
self.exit_stack.callback(
|
||||
self.client.remove_file,
|
||||
bucket=cloud_storage["resource"],
|
||||
filename=kwargs["filename"],
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from shared.utils.config import MINIO_ENDPOINT_URL, MINIO_KEY, MINIO_SECRET_KEY
|
||||
|
||||
|
||||
class S3Client:
|
||||
def __init__(
|
||||
self, endpoint_url: str, *, access_key: str, secret_key: str, bucket: str | None = None
|
||||
) -> None:
|
||||
self.client = self._make_boto_client(
|
||||
endpoint_url=endpoint_url, access_key=access_key, secret_key=secret_key
|
||||
)
|
||||
self.bucket = bucket
|
||||
|
||||
@staticmethod
|
||||
def _make_boto_client(endpoint_url: str, *, access_key: str, secret_key: str):
|
||||
s3 = boto3.resource(
|
||||
"s3",
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
endpoint_url=endpoint_url,
|
||||
)
|
||||
return s3.meta.client
|
||||
|
||||
def create_file(self, filename: str, data: bytes = b"", *, bucket: str | None = None):
|
||||
bucket = bucket or self.bucket
|
||||
assert bucket
|
||||
self.client.put_object(Body=data, Bucket=bucket, Key=filename)
|
||||
|
||||
def remove_file(self, filename: str, *, bucket: str | None = None):
|
||||
bucket = bucket or self.bucket
|
||||
assert bucket
|
||||
self.client.delete_object(Bucket=bucket, Key=filename)
|
||||
|
||||
def file_exists(self, filename: str, *, bucket: str | None = None) -> bool:
|
||||
bucket = bucket or self.bucket
|
||||
assert bucket
|
||||
try:
|
||||
self.client.head_object(Bucket=bucket, Key=filename)
|
||||
return True
|
||||
except ClientError as e:
|
||||
if e.response["Error"]["Code"] == "404":
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
def download_fileobj(self, key: str, *, bucket: str | None = None) -> bytes:
|
||||
bucket = bucket or self.bucket
|
||||
assert bucket
|
||||
with BytesIO() as data:
|
||||
self.client.download_fileobj(Bucket=bucket, Key=key, Fileobj=data)
|
||||
return data.getvalue()
|
||||
|
||||
|
||||
def make_client(*, bucket: str | None = None) -> S3Client:
|
||||
return S3Client(
|
||||
endpoint_url=MINIO_ENDPOINT_URL,
|
||||
access_key=MINIO_KEY,
|
||||
secret_key=MINIO_SECRET_KEY,
|
||||
bucket=bucket,
|
||||
)
|
||||
Reference in New Issue
Block a user