Skip to content

Commit 27b5123

Browse files
feat: Date range preset based filtering for execution / file_execution (#1173)
* refactor: Moved file centric view and serializer to file execution app * feat: Date range preset based filtering and other fields support for exec and file_exec Refactored date time processing functions to utils/ * feat: Filtering support by file name for file execution, order by execution time * feat: Filter changes for file execution status to support multi choice Execution log filter support added for log level, file execution ID * fix: Addressed review comments, fixed minor issue with date range preset for today, yesterday. Added preset for last_2_days * feat: Data migration to update total_files column of WorkflowExecution (#1181) * feat: Data migration to update total_files column of WorkflowExecution * feat: Removed redundant execution statuses (#1185) * feat: Modified execution log to use foreign key for execution (#1187) * feat: Data migration to update total_files column of WorkflowExecution * feat: Removed redundant execution statuses * feat: Modified execution_id to be a foreign key in ExecutionLog table * minor: Refactored schema migrations under 1 file Handled data migration of execution_log for last 30 days Added schema migration to remove the unused field * feat: Avoided data migration on execution logs, backward compatible changes for viewing logs * minor: Removed minor change on an old migration * minor: Log message updated --------- Signed-off-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> Co-authored-by: Rahul Johny <116638720+johnyrahul@users.noreply.github.com>
1 parent d8810cf commit 27b5123

26 files changed

Lines changed: 451 additions & 182 deletions

backend/usage_v2/dto.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

backend/usage_v2/enums.py

Lines changed: 0 additions & 35 deletions
This file was deleted.

backend/usage_v2/filter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.db.models import Q, QuerySet
44
from django_filters import rest_framework as filters
55
from usage_v2.models import Usage, UsageType
6-
from usage_v2.utils import DateTimeProcessor
6+
from utils.date import DateTimeProcessor
77
from workflow_manager.file_execution.models import WorkflowFileExecution
88
from workflow_manager.workflow_v2.models.execution import WorkflowExecution
99

backend/usage_v2/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from rest_framework.permissions import IsAuthenticated
1010
from rest_framework.response import Response
1111
from usage_v2.filter import UsageFilter
12-
from usage_v2.utils import DateTimeProcessor
12+
from utils.date import DateTimeProcessor
1313
from utils.pagination import CustomPagination
1414
from utils.user_context import UserContext
1515

backend/utils/date/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
from .constants import DateRangeKeys # noqa: F401
2+
from .enums import DateRangePresets # noqa: F401
3+
from .exceptions import InvalidDateRange, InvalidDatetime # noqa: F401
4+
from .processor import DateRange, DateTimeProcessor # noqa: F401
25
from .serializer import DateRangeSerializer # noqa: F401

backend/utils/date/enums.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from datetime import datetime, timedelta
2+
from enum import Enum
3+
from typing import Optional
4+
5+
from django.utils import timezone
6+
from utils.date.exceptions import InvalidDateRange
7+
8+
9+
class DateRangePresets(Enum):
10+
"""Represents relative time presets.
11+
12+
Can be used for filtering entities
13+
"""
14+
15+
TODAY = ("today", 0, "Today")
16+
YESTERDAY = ("yesterday", 1, "Yesterday")
17+
LAST_2_DAYS = ("last_2_days", 2, "Last 2 Days")
18+
LAST_7_DAYS = ("last_7_days", 7, "Last 7 Days")
19+
LAST_30_DAYS = ("last_30_days", 30, "Last 30 Days")
20+
21+
def __init__(self, key: str, days: int, display_name: str):
22+
self.key = key
23+
self.days = days
24+
self.display_name = display_name
25+
26+
def get_start_date(self) -> datetime:
27+
now = timezone.now()
28+
if self == DateRangePresets.TODAY:
29+
return now.replace(hour=0, minute=0, second=0, microsecond=0)
30+
elif self == DateRangePresets.YESTERDAY:
31+
return (now - timedelta(days=1)).replace(
32+
hour=0, minute=0, second=0, microsecond=0
33+
)
34+
return now - timedelta(days=self.days)
35+
36+
def get_end_date(self) -> datetime:
37+
now = timezone.now()
38+
if self == DateRangePresets.YESTERDAY:
39+
return now.replace(hour=0, minute=0, second=0, microsecond=0)
40+
return now
41+
42+
def get_date_range(self) -> tuple[datetime, datetime]:
43+
return self.get_start_date(), self.get_end_date()
44+
45+
@classmethod
46+
def from_value(cls, value: str) -> Optional["DateRangePresets"]:
47+
try:
48+
return next(preset for preset in cls if preset.key == value)
49+
except StopIteration as e:
50+
valid_values = [preset.key for preset in cls]
51+
raise InvalidDateRange(
52+
f"Invalid date range value: '{value}'. "
53+
f"Valid values are: {', '.join(valid_values)}"
54+
) from e
55+
56+
@classmethod
57+
def choices(cls) -> list[tuple[str, str]]:
58+
return [(preset.key, preset.display_name) for preset in cls]
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from rest_framework.exceptions import APIException
22

33

4-
class InvalidDatetime(APIException):
4+
class InvalidDateRange(APIException):
55
status_code = 400
6-
default_detail = "Invalid datetime format"
6+
default_detail = "Invalid date range"
77

88

9-
class InvalidDateRange(APIException):
9+
class InvalidDatetime(APIException):
1010
status_code = 400
11-
default_detail = "Invalid date range"
11+
default_detail = "Invalid datetime format"
Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
import logging
2+
from dataclasses import dataclass
23
from datetime import datetime, timedelta
34
from typing import Optional, Union
45

56
from dateutil.parser import parse
67
from django.utils import timezone
78
from isodate import parse_datetime
8-
from usage_v2.dto import DateRange
9-
from usage_v2.enums import DateRangePresets
10-
from usage_v2.exceptions import InvalidDatetime
9+
from utils.date.enums import DateRangePresets
10+
from utils.date.exceptions import InvalidDatetime
1111

1212
logger = logging.getLogger(__name__)
1313

1414

15+
@dataclass
16+
class DateRange:
17+
"""
18+
Represents a validated date range with start and end dates.
19+
20+
Attributes:
21+
start_date: Beginning of the date range
22+
end_date: End of the date range
23+
"""
24+
25+
start_date: datetime
26+
end_date: datetime
27+
28+
1529
class DateTimeProcessor:
1630
DEFAULT_DAYS_RANGE = 1
1731
MAX_DAYS_RANGE = 60
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from api_v2.models import APIDeployment
2+
from django.db.models import QuerySet
3+
from django_filters import rest_framework as filters
4+
from pipeline_v2.models import Pipeline
5+
from utils.date import DateRangePresets, DateTimeProcessor
6+
from workflow_manager.execution.enum import ExecutionEntity
7+
from workflow_manager.workflow_v2.enums import ExecutionStatus
8+
from workflow_manager.workflow_v2.models import Workflow, WorkflowExecution
9+
10+
11+
class ExecutionFilter(filters.FilterSet):
12+
execution_entity = filters.ChoiceFilter(
13+
choices=[
14+
(ExecutionEntity.API.value, "API"),
15+
(ExecutionEntity.ETL.value, "ETL"),
16+
(ExecutionEntity.TASK.value, "TASK"),
17+
(ExecutionEntity.WORKFLOW.value, "WF"),
18+
],
19+
method="filter_execution_entity",
20+
)
21+
# Lookup with query params: created_at_after, created_at_before
22+
created_at = filters.DateTimeFromToRangeFilter()
23+
status = filters.MultipleChoiceFilter(
24+
field_name="status", choices=ExecutionStatus.choices
25+
)
26+
date_range = filters.ChoiceFilter(
27+
choices=DateRangePresets.choices(),
28+
method="filter_by_date_range",
29+
)
30+
31+
class Meta:
32+
model = WorkflowExecution
33+
fields = []
34+
35+
def filter_execution_entity(
36+
self, queryset: QuerySet, name: str, value: str
37+
) -> QuerySet:
38+
if value == ExecutionEntity.API.value:
39+
return queryset.filter(
40+
pipeline_id__in=APIDeployment.objects.values_list("id", flat=True)
41+
)
42+
elif value == ExecutionEntity.ETL.value:
43+
return queryset.filter(
44+
pipeline_id__in=Pipeline.objects.filter(
45+
pipeline_type=Pipeline.PipelineType.ETL
46+
).values_list("id", flat=True)
47+
)
48+
elif value == ExecutionEntity.TASK.value:
49+
return queryset.filter(
50+
pipeline_id__in=Pipeline.objects.filter(
51+
pipeline_type=Pipeline.PipelineType.TASK
52+
).values_list("id", flat=True)
53+
)
54+
elif value == ExecutionEntity.WORKFLOW.value:
55+
return queryset.filter(
56+
pipeline_id=None,
57+
workflow_id__in=Workflow.objects.values_list("id", flat=True),
58+
)
59+
return queryset
60+
61+
def filter_by_date_range(
62+
self, queryset: QuerySet, name: str, value: str
63+
) -> QuerySet:
64+
"""
65+
Filters Usages based on the provided date range.
66+
"""
67+
date_span = DateTimeProcessor.filter_date_range(value)
68+
if date_span:
69+
queryset = queryset.filter(
70+
created_at__gte=date_span.start_date,
71+
created_at__lte=date_span.end_date,
72+
)
73+
return queryset
Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import logging
2-
from typing import Optional
32

4-
from api_v2.models import APIDeployment
5-
from django.db.models import Q, QuerySet
6-
from pipeline_v2.models import Pipeline
3+
from django_filters.rest_framework import DjangoFilterBackend
74
from rest_framework import viewsets
85
from rest_framework.filters import OrderingFilter
96
from rest_framework.permissions import IsAuthenticated
10-
from utils.date import DateRangeKeys, DateRangeSerializer
117
from utils.pagination import CustomPagination
12-
from workflow_manager.execution.enum import ExecutionEntity
8+
from workflow_manager.execution.filter import ExecutionFilter
139
from workflow_manager.execution.serializer import ExecutionSerializer
14-
from workflow_manager.workflow_v2.models import Workflow, WorkflowExecution
10+
from workflow_manager.workflow_v2.models import WorkflowExecution
1511

1612
logger = logging.getLogger(__name__)
1713

@@ -20,50 +16,8 @@ class ExecutionViewSet(viewsets.ReadOnlyModelViewSet):
2016
permission_classes = [IsAuthenticated]
2117
serializer_class = ExecutionSerializer
2218
pagination_class = CustomPagination
23-
filter_backends = [OrderingFilter]
24-
ordering_fields = ["created_at"]
19+
filter_backends = [DjangoFilterBackend, OrderingFilter]
20+
ordering_fields = ["created_at", "execution_time"]
2521
ordering = ["-created_at"]
26-
27-
def get_queryset(self) -> Optional[QuerySet]:
28-
execution_entity = self.request.query_params.get("execution_entity")
29-
30-
queryset = WorkflowExecution.objects.all()
31-
32-
# Filter based on execution entity
33-
if execution_entity == ExecutionEntity.API.value:
34-
queryset = queryset.filter(
35-
pipeline_id__in=APIDeployment.objects.values_list("id", flat=True)
36-
)
37-
elif execution_entity == ExecutionEntity.ETL.value:
38-
queryset = queryset.filter(
39-
pipeline_id__in=Pipeline.objects.filter(
40-
pipeline_type=Pipeline.PipelineType.ETL
41-
).values_list("id", flat=True)
42-
)
43-
elif execution_entity == ExecutionEntity.TASK.value:
44-
queryset = queryset.filter(
45-
pipeline_id__in=Pipeline.objects.filter(
46-
pipeline_type=Pipeline.PipelineType.TASK
47-
).values_list("id", flat=True)
48-
)
49-
elif execution_entity == ExecutionEntity.WORKFLOW.value:
50-
queryset = queryset.filter(
51-
pipeline_id=None,
52-
workflow_id__in=Workflow.objects.values_list("id", flat=True),
53-
)
54-
55-
# Parse and apply date filters
56-
date_range_serializer = DateRangeSerializer(data=self.request.query_params)
57-
date_range_serializer.is_valid(raise_exception=True)
58-
59-
filters = Q()
60-
if start_date := date_range_serializer.validated_data.get(
61-
DateRangeKeys.START_DATE
62-
):
63-
filters &= Q(created_at__gte=start_date)
64-
if end_date := date_range_serializer.validated_data.get(DateRangeKeys.END_DATE):
65-
filters &= Q(created_at__lte=end_date)
66-
67-
queryset = queryset.filter(filters)
68-
69-
return queryset
22+
filterset_class = ExecutionFilter
23+
queryset = WorkflowExecution.objects.all()

0 commit comments

Comments
 (0)