chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:23 +08:00
commit 10df941303
2913 changed files with 455804 additions and 0 deletions
@@ -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
@@ -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"
}
]
}
+91
View File
@@ -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;
+345
View File
@@ -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"
}
}
]
}
+153
View File
@@ -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
+247
View File
@@ -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"
}
]
}
+708
View File
@@ -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
+356
View File
@@ -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"
}
]
}
+138
View File
@@ -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"
}
]
}
+628
View File
@@ -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"
+707
View File
@@ -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()
+27
View File
@@ -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
+39
View 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
+3
View File
@@ -0,0 +1,3 @@
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
+36
View File
@@ -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)
+10
View File
@@ -0,0 +1,10 @@
# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
from enum import Enum
class SourceDataType(Enum):
images = "images"
video = "video"
+19
View File
@@ -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: ...
+43
View File
@@ -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)))
+23
View File
@@ -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
]
+98
View File
@@ -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
+119
View File
@@ -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()
+458
View File
@@ -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())
+131
View File
@@ -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"],
)
+68
View File
@@ -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,
)