Skip to content

Commit b64939f

Browse files
authored
Merge pull request #63 from danfimov/feat-schedule-edit
2 parents 8b01700 + 0295552 commit b64939f

File tree

8 files changed

+469
-147
lines changed

8 files changed

+469
-147
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ authors = [
3434
requires-python = ">=3.10"
3535
dependencies = [
3636
# api
37-
"fastapi>=0.128.0",
37+
"fastapi>=0.134.0",
38+
"python-multipart>=0.0.22",
3839
# html templates
3940
"jinja2>=3.1.6",
4041
# db

taskiq_dashboard/api/routers/schedule.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import json
12
import typing as tp
3+
from datetime import datetime
24
from logging import getLogger
35
from urllib.parse import urlencode
46

57
import fastapi
68
import pydantic
79
from dishka.integrations import fastapi as dishka_fastapi
8-
from fastapi.responses import HTMLResponse
10+
from fastapi.responses import HTMLResponse, Response
911
from starlette import status
12+
from taskiq import ScheduledTask
1013

1114
from taskiq_dashboard.api.templates import jinja_templates
1215

@@ -15,6 +18,9 @@
1518
from taskiq import TaskiqScheduler
1619

1720

21+
logger = getLogger(__name__)
22+
23+
1824
router = fastapi.APIRouter(
1925
prefix='/schedules',
2026
tags=['Schedule'],
@@ -121,3 +127,122 @@ async def handle_schedule_details(
121127
},
122128
status_code=status.HTTP_404_NOT_FOUND,
123129
)
130+
131+
132+
@router.delete(
133+
'/{schedule_id}',
134+
name='Delete schedule',
135+
)
136+
async def handle_schedule_delete(
137+
request: fastapi.Request,
138+
schedule_id: str,
139+
) -> Response:
140+
scheduler: TaskiqScheduler | None = request.app.state.scheduler
141+
if not scheduler:
142+
return Response(status_code=status.HTTP_400_BAD_REQUEST, content=b'Scheduler not configured.')
143+
144+
for schedule_source in scheduler.sources:
145+
for schedule in await schedule_source.get_schedules():
146+
if schedule.schedule_id != str(schedule_id):
147+
continue
148+
try:
149+
await schedule_source.delete_schedule(schedule_id)
150+
except NotImplementedError:
151+
return Response(
152+
status_code=status.HTTP_400_BAD_REQUEST,
153+
content=b'This schedule source does not support deleting schedules.',
154+
)
155+
return Response(
156+
status_code=status.HTTP_200_OK,
157+
headers={'HX-Redirect': str(request.url_for('Schedule list view'))},
158+
)
159+
160+
logger.warning('Schedule with id %s not found for deletion.', schedule_id)
161+
return Response(
162+
status_code=status.HTTP_200_OK,
163+
headers={'HX-Redirect': str(request.url_for('Schedule list view'))},
164+
)
165+
166+
167+
def create_error_notification(request: fastapi.Request, message: str) -> Response:
168+
return jinja_templates.TemplateResponse(
169+
'partial/notification.html',
170+
{'request': request, 'message': message, 'level': 'error'},
171+
status_code=status.HTTP_200_OK,
172+
)
173+
174+
175+
@router.post(
176+
'/{schedule_id}',
177+
name='Edit schedule',
178+
)
179+
async def handle_schedule_edit( # noqa: PLR0911, PLR0913, C901, PLR0912 Too
180+
request: fastapi.Request,
181+
schedule_id: str,
182+
cron: tp.Annotated[str | None, fastapi.Form()] = None,
183+
time: tp.Annotated[str | None, fastapi.Form()] = None,
184+
cron_offset: tp.Annotated[str | None, fastapi.Form()] = None,
185+
args: tp.Annotated[str, fastapi.Form()] = '[]',
186+
kwargs: tp.Annotated[str, fastapi.Form()] = '{}',
187+
labels: tp.Annotated[str, fastapi.Form()] = '{}',
188+
) -> Response:
189+
scheduler: TaskiqScheduler | None = request.app.state.scheduler
190+
if not scheduler:
191+
return create_error_notification(request, 'Scheduler not configured.')
192+
193+
# Normalize empty strings to None for optional fields
194+
cron = cron or None
195+
cron_offset = cron_offset or None
196+
parsed_time: datetime | None = None
197+
if time:
198+
try:
199+
parsed_time = datetime.fromisoformat(time)
200+
except ValueError:
201+
return create_error_notification(request, 'Invalid time format. Expected YYYY-MM-DDTHH:MM.')
202+
203+
try:
204+
parsed_args = json.loads(args)
205+
if not isinstance(parsed_args, list):
206+
return create_error_notification(request, 'Positional arguments must be a JSON array, e.g. [1, "two"].')
207+
except json.JSONDecodeError:
208+
return create_error_notification(request, 'Invalid JSON in "Positional arguments".')
209+
try:
210+
parsed_kwargs = json.loads(kwargs)
211+
if not isinstance(parsed_kwargs, dict):
212+
return create_error_notification(request, 'Keyword arguments must be a JSON object, e.g. {"key": "value"}.')
213+
except json.JSONDecodeError:
214+
return create_error_notification(request, 'Invalid JSON in "Keyword arguments".')
215+
try:
216+
parsed_labels = json.loads(labels)
217+
if not isinstance(parsed_labels, dict):
218+
return create_error_notification(request, 'Labels must be a JSON object, e.g. {"key": "value"}.')
219+
except json.JSONDecodeError:
220+
return create_error_notification(request, 'Invalid JSON in "Labels".')
221+
222+
for schedule_source in scheduler.sources:
223+
for schedule in await schedule_source.get_schedules():
224+
if schedule.schedule_id != str(schedule_id):
225+
continue
226+
227+
updated = ScheduledTask(
228+
task_name=schedule.task_name,
229+
schedule_id=schedule.schedule_id,
230+
cron=cron,
231+
cron_offset=cron_offset,
232+
time=parsed_time,
233+
interval=schedule.interval,
234+
args=parsed_args,
235+
kwargs=parsed_kwargs,
236+
labels=parsed_labels,
237+
)
238+
try:
239+
await schedule_source.delete_schedule(schedule_id)
240+
await schedule_source.add_schedule(updated)
241+
except NotImplementedError:
242+
return create_error_notification(request, 'This schedule source does not support editing schedules.')
243+
return Response(
244+
status_code=status.HTTP_200_OK,
245+
headers={'HX-Redirect': str(request.url_for('Schedule details view', schedule_id=schedule_id))},
246+
)
247+
248+
return create_error_notification(request, 'Schedule not found.')

0 commit comments

Comments
 (0)