Skip to content

Commit 07658ef

Browse files
Merge pull request #90 from aravindhbalaji04/feature/set-retry-status
⚙️ FEATURE: Set task status to RETRY during retry attempts
2 parents 4583043 + a484199 commit 07658ef

10 files changed

Lines changed: 76 additions & 30 deletions

File tree

.code_quality/ruff.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ ignore = [
4343
"B027", # Empty method in ABC without abstractmethod (intentional in base providers)
4444
"ARG001", # Unused function argument (intentional in interface implementations)
4545
"ARG002", # Unused method argument (intentional in *args/**kwargs constructors and providers)
46+
"UP007", # Use X | Y for unions — disabled due to Python 3.9 lacking support for this syntax
4647
]
4748

4849
# Allowed configurations for specific rules

LAST_VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.13.2.dev2
1+
0.13.2

dotflow/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Dotflow __init__ module."""
22

3-
__version__ = "0.13.2.dev2"
3+
__version__ = "0.13.2"
44
__description__ = "🎲 Dotflow turns an idea into flow!"
55

66
from .core.action import Action as action

dotflow/cli/validators/start.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
from __future__ import annotations
44

5+
from typing import Optional
6+
57
from pydantic import BaseModel, Field # type: ignore
68

79
from dotflow.settings import Settings as settings
810

911

1012
class StartValidator(BaseModel):
1113
step: str
12-
callable: str | None = Field(default=None)
13-
initial_context: str | None = Field(default=None)
14-
output: bool | None = Field(default=True)
15-
path: str | None = Field(default=settings.START_PATH)
14+
callable: Optional[str] = Field(default=None)
15+
initial_context: Optional[str] = Field(default=None)
16+
output: Optional[bool] = Field(default=True)
17+
path: Optional[str] = Field(default=settings.START_PATH)

dotflow/core/action.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from dotflow.core.context import Context
99
from dotflow.core.exception import ExecutionWithClassError
10+
from dotflow.core.types.status import TypeStatus
1011

1112

1213
def is_execution_with_class_internal_error(error: Exception) -> bool:
@@ -97,54 +98,62 @@ def __call__(self, *args, **kwargs):
9798
if self.func:
9899
self._set_params()
99100

100-
task = self._get_task(kwargs=kwargs)
101+
self._current_task = self._get_task(kwargs=kwargs)
101102
contexts = self._get_context(kwargs=kwargs)
102103

103104
if contexts:
104105
return Context(
105106
storage=self._run_action(*args, **contexts),
106-
task_id=task.task_id,
107-
workflow_id=task.workflow_id,
107+
task_id=self._current_task.task_id,
108+
workflow_id=self._current_task.workflow_id,
108109
)
109110

110111
return Context(
111112
storage=self._run_action(*args),
112-
task_id=task.task_id,
113-
workflow_id=task.workflow_id,
113+
task_id=self._current_task.task_id,
114+
workflow_id=self._current_task.workflow_id,
114115
)
115116

116117
# No parameters
117118
def action(*_args, **_kwargs):
118119
self.func = args[0]
119120
self._set_params()
120121

121-
task = self._get_task(kwargs=_kwargs)
122+
self._current_task = self._get_task(kwargs=_kwargs)
122123
contexts = self._get_context(kwargs=_kwargs)
123124

124125
if contexts:
125126
return Context(
126127
storage=self._run_action(*_args, **contexts),
127-
task_id=task.task_id,
128-
workflow_id=task.workflow_id,
128+
task_id=self._current_task.task_id,
129+
workflow_id=self._current_task.workflow_id,
129130
)
130131

131132
return Context(
132133
storage=self._run_action(*_args),
133-
task_id=task.task_id,
134-
workflow_id=task.workflow_id,
134+
task_id=self._current_task.task_id,
135+
workflow_id=self._current_task.workflow_id,
135136
)
136137

137138
return action
138139

139140
def _run_action(self, *args, **kwargs):
141+
task = getattr(self, '_current_task', None)
142+
140143
for attempt in range(1, self.retry + 1):
141144
try:
142145
if self.timeout:
143146
with ThreadPoolExecutor(max_workers=1) as executor:
144147
future = executor.submit(self.func, *args, **kwargs)
145-
return future.result(timeout=self.timeout)
148+
result = future.result(timeout=self.timeout)
149+
else:
150+
result = self.func(*args, **kwargs)
151+
152+
# Reset status to IN_PROGRESS after successful retry
153+
if task and attempt > 1:
154+
task.status = TypeStatus.IN_PROGRESS
146155

147-
return self.func(*args, **kwargs)
156+
return result
148157

149158
except Exception as error:
150159
last_exception = error
@@ -157,6 +166,9 @@ def _run_action(self, *args, **kwargs):
157166
if attempt == self.retry:
158167
raise last_exception from last_exception
159168

169+
if task:
170+
task.status = TypeStatus.RETRY
171+
160172
sleep(self.retry_delay)
161173
if self.backoff:
162174
self.retry_delay *= 2

dotflow/core/serializers/task.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import json
6-
from typing import Any
6+
from typing import Any, Optional
77
from uuid import UUID
88

99
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -20,16 +20,16 @@ class SerializerTask(BaseModel):
2020
model_config = ConfigDict(title="task")
2121

2222
task_id: int = Field(default=None)
23-
workflow_id: UUID | None = Field(default=None)
23+
workflow_id: Optional[UUID] = Field(default=None)
2424
status: str = Field(default=None, alias="_status")
25-
error: SerializerTaskError | None = Field(default=None, alias="_error")
26-
duration: float | None = Field(default=None, alias="_duration")
25+
error: Optional[SerializerTaskError] = Field(default=None, alias="_error")
26+
duration: Optional[float] = Field(default=None, alias="_duration")
2727
initial_context: Any = Field(default=None, alias="_initial_context")
2828
current_context: Any = Field(default=None, alias="_current_context")
2929
previous_context: Any = Field(default=None, alias="_previous_context")
3030
group_name: str = Field(default=None)
31-
max: int | None = Field(default=None, exclude=True)
32-
size_message: str | None = Field(
31+
max: Optional[int] = Field(default=None, exclude=True)
32+
size_message: Optional[str] = Field(
3333
default="Context size exceeded", exclude=True
3434
)
3535

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "dotflow"
3-
version = "0.13.2.dev2"
3+
version = "0.13.2"
44
authors = [
55
{ name="Fernando Celmer", email="email@fernandocelmer.com" },
66
]
@@ -37,7 +37,7 @@ mongodb = ["dotflow-mongodb"]
3737

3838
[tool.poetry]
3939
name = "dotflow"
40-
version = "0.13.2.dev2"
40+
version = "0.13.2"
4141
description = "🎲 Dotflow turns an idea into flow!"
4242
authors = ["Fernando Celmer <email@fernandocelmer.com>"]
4343
readme = "README.md"

tests/core/decorators/test_time.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ def run():
2727
self.assertGreaterEqual(task.duration, 0)
2828

2929
def test_return_value_is_preserved(self):
30-
expected_task = Task(task_id=0, step=action_step, callback=simple_callback)
30+
expected_task = Task(
31+
task_id=0, step=action_step, callback=simple_callback
32+
)
3133

3234
@time
3335
def run():

tests/core/test_action.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from dotflow.core.action import Action
99
from dotflow.core.context import Context
1010
from dotflow.core.task import Task
11+
from dotflow.core.types.status import TypeStatus
12+
1113
from tests.mocks import (
1214
action_step,
1315
simple_step,
@@ -61,6 +63,27 @@ def test_instantiating_action_class_with_fail_retry(self):
6163
for record in self._caplog.records:
6264
self.assertEqual(record.message, error_message)
6365

66+
def test_sets_retry_status_before_retrying(self):
67+
calls = {"count": 0}
68+
statuses = []
69+
70+
def flaky_step():
71+
calls["count"] += 1
72+
if calls["count"] == 1:
73+
raise Exception("Fail once")
74+
# On second attempt, capture status (should be RETRY)
75+
statuses.append(self.task.status)
76+
return "ok"
77+
78+
inside = Action(flaky_step, retry=2, retry_delay=0)
79+
inside(task=self.task)
80+
81+
# Verify status was RETRY during the retry attempt
82+
self.assertEqual(len(statuses), 1)
83+
self.assertEqual(statuses[0], TypeStatus.RETRY)
84+
# Verify status was reset to IN_PROGRESS after successful retry
85+
self.assertEqual(self.task.status, TypeStatus.IN_PROGRESS)
86+
6487
def test_action_class_with_previous_context(self):
6588
inside = Action(simple_step_with_previous_context, task=self.task)
6689

tests/providers/test_api_default.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ def test_none_returns_none(self):
7474
self.assertIsNone(ApiDefault._callable_path(None))
7575

7676
def test_string_returns_string(self):
77-
self.assertEqual(ApiDefault._callable_path("my.module.func"), "my.module.func")
77+
self.assertEqual(
78+
ApiDefault._callable_path("my.module.func"), "my.module.func"
79+
)
7880

7981
def test_callable_returns_module_name(self):
8082
def my_func():
@@ -146,7 +148,9 @@ def test_calls_post_when_ready(self):
146148
mock_response = MagicMock()
147149
mock_response.raise_for_status = MagicMock()
148150

149-
with patch("dotflow.providers.api_default.post", return_value=mock_response):
151+
with patch(
152+
"dotflow.providers.api_default.post", return_value=mock_response
153+
):
150154
api.create_workflow("workflow-id")
151155

152156
mock_response.raise_for_status.assert_called_once()
@@ -158,7 +162,9 @@ def test_logs_error_on_exception(self):
158162
user_token="token",
159163
)
160164

161-
with patch("dotflow.providers.api_default.post", side_effect=Exception("fail")):
165+
with patch(
166+
"dotflow.providers.api_default.post", side_effect=Exception("fail")
167+
):
162168
result = api.create_workflow("workflow-id")
163169

164170
self.assertIsNone(result)

0 commit comments

Comments
 (0)