Skip to content

Commit ea83864

Browse files
committed
Refactor RQId / RQIdManager
* Merge these into one class. These classes clearly deal with the same concept, so it doesn't make sense to divide the logic into two classes. * Turn `build` into an instance method (`render`). That way, the validation logic can be reused between it and the `RQId` constructor. Adjust the fields so that the first three fields can be specified as positional arguments. * Make the class frozen (I don't see a compelling case to mutate it). * Change string fields into corresponding enums. This reduces the amount of hardcoded string literals everywhere. Note that I had to move the enums into the `models` module to avoid a circular import. * Rename `resource` to `target`, because that's the name of the enum and the corresponding field in the API.
1 parent 5314bda commit ea83864

File tree

10 files changed

+149
-120
lines changed

10 files changed

+149
-120
lines changed

cvat/apps/engine/background_operations.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
from cvat.apps.engine.cloud_provider import export_resource_to_cloud_storage
2929
from cvat.apps.engine.location import StorageType, get_location_configuration
3030
from cvat.apps.engine.log import ServerLogManager
31-
from cvat.apps.engine.models import Location, Project, Task
31+
from cvat.apps.engine.models import (
32+
Location, Project, Task, RequestAction, RequestTarget, RequestSubresource,
33+
)
3234
from cvat.apps.engine.permissions import get_cloud_storage_for_import_or_export
33-
from cvat.apps.engine.rq_job_handler import RQIdManager
35+
from cvat.apps.engine.rq_job_handler import RQId
3436
from cvat.apps.engine.serializers import RqIdSerializer
3537
from cvat.apps.engine.utils import (
3638
build_annotations_file_name,
@@ -342,14 +344,15 @@ def export(self) -> Response:
342344
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
343345

344346
queue: DjangoRQ = django_rq.get_queue(self.QUEUE_NAME)
345-
rq_id = RQIdManager.build(
346-
"export",
347-
self.resource,
347+
rq_id = RQId(
348+
RequestAction.EXPORT,
349+
RequestTarget(self.resource),
348350
self.db_instance.pk,
349-
subresource="dataset" if self.export_args.save_images else "annotations",
350-
anno_format=self.export_args.format,
351+
subresource=RequestSubresource.DATASET
352+
if self.export_args.save_images else RequestSubresource.ANNOTATIONS,
353+
format=self.export_args.format,
351354
user_id=self.request.user.id,
352-
)
355+
).render()
353356

354357
rq_job = queue.fetch_job(rq_id)
355358

@@ -580,13 +583,13 @@ def _handle_rq_job_v1(
580583

581584
def export(self) -> Response:
582585
queue: DjangoRQ = django_rq.get_queue(self.QUEUE_NAME)
583-
rq_id = RQIdManager.build(
584-
"export",
585-
self.resource,
586+
rq_id = RQId(
587+
RequestAction.EXPORT,
588+
RequestTarget(self.resource),
586589
self.db_instance.pk,
587-
subresource="backup",
590+
subresource=RequestSubresource.BACKUP,
588591
user_id=self.request.user.id,
589-
)
592+
).render()
590593
rq_job = queue.fetch_job(rq_id)
591594

592595
if rq_job:

cvat/apps/engine/backup.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@
3939
get_rq_job_meta, import_resource_with_clean_up_after,
4040
define_dependent_job, get_rq_lock_by_user,
4141
)
42-
from cvat.apps.engine.rq_job_handler import RQIdManager, RQJobMetaField
42+
from cvat.apps.engine.rq_job_handler import RQId, RQJobMetaField
4343
from cvat.apps.engine.models import (
44-
StorageChoice, StorageMethodChoice, DataChoice, Project, Location)
44+
StorageChoice, StorageMethodChoice, DataChoice, Project, Location,
45+
RequestAction, RequestTarget, RequestSubresource,
46+
)
4547
from cvat.apps.engine.task import JobFileMapping, _create_thread
4648
from cvat.apps.engine.cloud_provider import import_resource_from_cloud_storage
4749
from cvat.apps.engine.location import StorageType, get_location_configuration
@@ -1013,7 +1015,10 @@ def import_project(request, queue_name, filename=None):
10131015
if 'rq_id' in request.data:
10141016
rq_id = request.data['rq_id']
10151017
else:
1016-
rq_id = RQIdManager.build('import', 'project', uuid.uuid4(), subresource='backup')
1018+
rq_id = RQId(
1019+
RequestAction.IMPORT, RequestTarget.PROJECT, uuid.uuid4(),
1020+
subresource=RequestSubresource.BACKUP,
1021+
).render()
10171022
Serializer = ProjectFileSerializer
10181023
file_field_name = 'project_file'
10191024

@@ -1036,7 +1041,10 @@ def import_project(request, queue_name, filename=None):
10361041
)
10371042

10381043
def import_task(request, queue_name, filename=None):
1039-
rq_id = request.data.get('rq_id', RQIdManager.build('import', 'task', uuid.uuid4(), subresource='backup'))
1044+
rq_id = request.data.get('rq_id', RQId(
1045+
RequestAction.IMPORT, RequestTarget.TASK, uuid.uuid4(),
1046+
subresource=RequestSubresource.BACKUP,
1047+
).render())
10401048
Serializer = TaskFileSerializer
10411049
file_field_name = 'task_file'
10421050

cvat/apps/engine/mixins.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
from cvat.apps.engine.handlers import clear_import_cache
3434
from cvat.apps.engine.location import StorageType, get_location_configuration
3535
from cvat.apps.engine.log import ServerLogManager
36-
from cvat.apps.engine.models import Location
37-
from cvat.apps.engine.rq_job_handler import RQIdManager
36+
from cvat.apps.engine.models import Location, RequestAction, RequestTarget, RequestSubresource
37+
from cvat.apps.engine.rq_job_handler import RQId
3838
from cvat.apps.engine.serializers import DataSerializer, RqIdSerializer
3939
from cvat.apps.engine.utils import is_dataset_export
4040

@@ -275,7 +275,10 @@ def init_tus_upload(self, request):
275275
if file_exists:
276276
# check whether the rq_job is in progress or has been finished/failed
277277
object_class_name = self._object.__class__.__name__.lower()
278-
template = RQIdManager.build('import', object_class_name, self._object.pk, subresource=import_type)
278+
template = RQId(
279+
RequestAction.IMPORT, RequestTarget(object_class_name), self._object.pk,
280+
subresource=RequestSubresource(import_type)
281+
).render()
279282
queue = django_rq.get_queue(settings.CVAT_QUEUES.IMPORT_DATA.value)
280283
finished_job_ids = queue.finished_job_registry.get_job_ids()
281284
failed_job_ids = queue.failed_job_registry.get_job_ids()
@@ -473,7 +476,7 @@ def export_dataset_v2(self, request: HttpRequest, pk: int):
473476
return dataset_export_manager.export()
474477

475478
# FUTURE-TODO: migrate to new API
476-
def import_annotations(self, request, db_obj, import_func, rq_func, rq_id_template):
479+
def import_annotations(self, request, db_obj, import_func, rq_func, rq_id_factory):
477480
is_tus_request = request.headers.get('Upload-Length', None) is not None or \
478481
request.method == 'OPTIONS'
479482
if is_tus_request:
@@ -492,7 +495,7 @@ def import_annotations(self, request, db_obj, import_func, rq_func, rq_id_templa
492495

493496
return import_func(
494497
request=request,
495-
rq_id_template=rq_id_template,
498+
rq_id_factory=rq_id_factory,
496499
rq_func=rq_func,
497500
db_obj=self._object,
498501
format_name=format_name,

cvat/apps/engine/models.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from django.core.exceptions import ValidationError
2020
from django.db import IntegrityError, models, transaction
2121
from django.db.models.fields import FloatField
22-
from django.db.models import Q
22+
from django.db.models import Q, TextChoices
2323
from drf_spectacular.types import OpenApiTypes
2424
from drf_spectacular.utils import extend_schema_field
2525
from cvat.apps.engine.lazy_list import LazyList
@@ -1196,3 +1196,24 @@ def organization_id(self):
11961196

11971197
def get_asset_dir(self):
11981198
return os.path.join(settings.ASSETS_ROOT, str(self.uuid))
1199+
1200+
class RequestStatus(TextChoices):
1201+
QUEUED = "queued"
1202+
STARTED = "started"
1203+
FAILED = "failed"
1204+
FINISHED = "finished"
1205+
1206+
class RequestAction(TextChoices):
1207+
CREATE = "create"
1208+
IMPORT = "import"
1209+
EXPORT = "export"
1210+
1211+
class RequestTarget(TextChoices):
1212+
PROJECT = "project"
1213+
TASK = "task"
1214+
JOB = "job"
1215+
1216+
class RequestSubresource(TextChoices):
1217+
ANNOTATIONS = "annotations"
1218+
DATASET = "dataset"
1219+
BACKUP = "backup"

cvat/apps/engine/permissions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,7 +1227,7 @@ def create(cls, request, view, obj: Optional[RQJob], iam_context: Dict):
12271227
('export', 'task', 'backup'): (TaskPermission, TaskPermission.Scopes.EXPORT_BACKUP),
12281228
('export', 'job', 'annotations'): (JobPermission, JobPermission.Scopes.EXPORT_ANNOTATIONS),
12291229
('export', 'job', 'dataset'): (JobPermission, JobPermission.Scopes.EXPORT_DATASET),
1230-
}[(parsed_rq_id.action, parsed_rq_id.resource, parsed_rq_id.subresource)]
1230+
}[(parsed_rq_id.action, parsed_rq_id.target, parsed_rq_id.subresource)]
12311231

12321232

12331233
resource = None
@@ -1236,12 +1236,12 @@ def create(cls, request, view, obj: Optional[RQJob], iam_context: Dict):
12361236
'project': Project,
12371237
'task': Task,
12381238
'job': Job,
1239-
}[parsed_rq_id.resource]
1239+
}[parsed_rq_id.target]
12401240

12411241
try:
12421242
resource = resource_model.objects.get(id=resource_id)
12431243
except resource_model.DoesNotExist as ex:
1244-
raise NotFound(f'The {parsed_rq_id.resource!r} with specified id#{resource_id} does not exist') from ex
1244+
raise NotFound(f'The {parsed_rq_id.target!r} with specified id#{resource_id} does not exist') from ex
12451245

12461246
permissions.append(permission_class.create_base_perm(request, view, scope=resource_scope, iam_context=iam_context, obj=resource))
12471247

cvat/apps/engine/rq_job_handler.py

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5+
from __future__ import annotations
6+
57
import attrs
68

79
from typing import Optional, Union
810
from uuid import UUID
911
from rq.job import Job as RQJob
1012

13+
from .models import RequestAction, RequestTarget, RequestSubresource
14+
1115
class RQJobMetaField:
1216
# common fields
1317
FORMATTED_EXCEPTION = "formatted_exception"
@@ -28,82 +32,85 @@ class RQJobMetaField:
2832
def is_rq_job_owner(rq_job: RQJob, user_id: int) -> bool:
2933
return rq_job.meta.get(RQJobMetaField.USER, {}).get('id') == user_id
3034

31-
@attrs.define(kw_only=True)
35+
@attrs.frozen()
3236
class RQId:
33-
action: str = attrs.field(
34-
validator=attrs.validators.instance_of(str)
37+
action: RequestAction = attrs.field(
38+
validator=attrs.validators.instance_of(RequestAction)
3539
)
36-
resource: str = attrs.field(
37-
validator=attrs.validators.instance_of(str)
40+
target: RequestTarget = attrs.field(
41+
validator=attrs.validators.instance_of(RequestTarget)
3842
)
3943
identifier: Union[int, UUID] = attrs.field(
4044
validator=attrs.validators.instance_of((int, UUID))
4145
)
42-
subresource: Optional[str] = attrs.field(
46+
subresource: Optional[RequestSubresource] = attrs.field(
4347
validator=attrs.validators.optional(
44-
attrs.validators.instance_of(str)
45-
)
48+
attrs.validators.instance_of(RequestSubresource)
49+
),
50+
kw_only=True, default=None,
4651
)
4752
user_id: Optional[int] = attrs.field(
48-
validator=attrs.validators.optional(attrs.validators.instance_of(int))
53+
validator=attrs.validators.optional(attrs.validators.instance_of(int)),
54+
kw_only=True, default=None,
4955
)
5056
format: Optional[str] = attrs.field(
51-
validator=attrs.validators.optional(attrs.validators.instance_of(str))
57+
validator=attrs.validators.optional(attrs.validators.instance_of(str)),
58+
kw_only=True, default=None,
5259
)
5360

61+
def __attrs_post_init__(self) -> None:
62+
if self.action == RequestAction.CREATE and self.target != RequestTarget.TASK:
63+
raise ValueError(f"Unsupported target {self.target!r} for the {self.action!r} action")
5464

55-
class RQIdManager:
5665
# RQ ID templates:
5766
# import:<task|project|job>-<id|uuid>-<annotations|dataset|backup>
5867
# create:task-<tid>
5968
# export:<project|task|job>-<id>-<annotations|dataset>-in-<format>-format-by-<user_id>
6069
# export:<project|task>-<id>-backup-by-<user_id>
6170

62-
@staticmethod
63-
def build(
64-
action: str,
65-
resource: str,
66-
identifier: Union[int, UUID],
67-
*,
68-
subresource: Optional[str] = None,
69-
user_id: Optional[int] = None,
70-
anno_format: Optional[str] = None,
71+
def render(
72+
self,
7173
) -> str:
72-
if "import" == action:
73-
return f"{action}:{resource}-{identifier}-{subresource}"
74-
elif "export" == action:
75-
if anno_format is None:
74+
common_prefix = f"{self.action}:{self.target}-{self.identifier}"
75+
76+
if RequestAction.IMPORT == self.action:
77+
return f"{common_prefix}-{self.subresource}"
78+
elif RequestAction.EXPORT == self.action:
79+
if self.format is None:
7680
return (
77-
f"{action}:{resource}-{identifier}-{subresource}-by-{user_id}"
81+
f"{common_prefix}-{self.subresource}-by-{self.user_id}"
7882
)
79-
format_to_be_used_in_urls = anno_format.replace(" ", "_").replace(".", "@")
80-
return f"{action}:{resource}-{identifier}-{subresource}-in-{format_to_be_used_in_urls}-format-by-{user_id}"
81-
elif "create" == action:
82-
assert "task" == resource
83-
return f"{action}:{resource}-{identifier}"
83+
84+
format_to_be_used_in_urls = self.format.replace(" ", "_").replace(".", "@")
85+
return f"{common_prefix}-{self.subresource}-in-{format_to_be_used_in_urls}-format-by-{self.user_id}"
86+
elif RequestAction.CREATE == self.action:
87+
return common_prefix
8488
else:
85-
raise ValueError(f"Unsupported action {action!r} was found")
89+
assert False, f"Unsupported action {self.action!r} was found"
8690

8791
@staticmethod
8892
def parse(rq_id: str) -> RQId:
89-
action: Optional[str] = None
90-
resource: Optional[str] = None
9193
identifier: Optional[Union[UUID, int]] = None
92-
subresource: Optional[str] = None
94+
subresource: Optional[RequestSubresource] = None
9395
user_id: Optional[int] = None
9496
anno_format: Optional[str] = None
9597

9698
try:
9799
action_and_resource, unparsed = rq_id.split("-", maxsplit=1)
98-
action, resource = action_and_resource.split(":")
100+
action_str, target_str = action_and_resource.split(":")
101+
action = RequestAction(action_str)
102+
target = RequestTarget(target_str)
99103

100-
if "create" == action:
104+
if RequestAction.CREATE == action:
101105
identifier = unparsed
102-
elif "import" == action:
103-
identifier, subresource = unparsed.rsplit("-", maxsplit=1)
106+
elif RequestAction.IMPORT == action:
107+
identifier, subresource_str = unparsed.rsplit("-", maxsplit=1)
108+
subresource = RequestSubresource(subresource_str)
104109
else: # action == export
105-
identifier, subresource, unparsed = unparsed.split("-", maxsplit=2)
106-
if "backup" == subresource:
110+
identifier, subresource_str, unparsed = unparsed.split("-", maxsplit=2)
111+
subresource = RequestSubresource(subresource_str)
112+
113+
if RequestSubresource.BACKUP == subresource:
107114
_, user_id = unparsed.split("-")
108115
else:
109116
unparsed, _, user_id = unparsed.rsplit("-", maxsplit=2)
@@ -122,7 +129,7 @@ def parse(rq_id: str) -> RQId:
122129

123130
return RQId(
124131
action=action,
125-
resource=resource,
132+
target=target,
126133
identifier=identifier,
127134
subresource=subresource,
128135
user_id=user_id,

0 commit comments

Comments
 (0)