Skip to content

Commit 564f309

Browse files
committed
refactor: add reschedule params fixture
1 parent 38bb0eb commit 564f309

File tree

3 files changed

+75
-125
lines changed

3 files changed

+75
-125
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,21 @@ capacity:
122122
- [x] Update deployment to use new scheme
123123
- [x] Add proper logging
124124
- [x] Add proper README
125-
- [ ] Use Session from requests to implement retry logic?
126125
- [x] Update to latest Todoist API
127126
- [ ] Add tests
128-
- [ ] Create pytest fixture for the reschedule params kwargs
127+
- [x] Create pytest fixture for the reschedule params kwargs
128+
- [ ] Parametrize the reschedule params fixture?
129129
- [ ] Create pytest fixture for the Fake TodoistAPI
130130
- [ ] Create pytest fixture for the current datetime
131131
- [ ] Refactor FakeTodoistAPI to create "distribution" of final task dates
132132
for easier comparison, refactor test cases to use this
133133
- [ ] Make logs not look like ass lol
134+
- [ ] Switch to toml config???
135+
- [ ] Merge CLI args and rules config into a unified configuration
134136
- [ ] Allow disabling "smart" rescheduling
135137
- [x] Consider retrying individual Todoist API calls instead of the entire
138+
- [ ] Use Session from requests to implement retry logic?
136139
rescheduling function
137-
- [ ] Switch to toml config???
138-
- [ ] Merge CLI args and rules config into a unified configuration
139140
- [ ] Catch improper cron string
140141
- [ ] Try using V4 API of scheduler for typing
141142
- [ ] Add limits as alternative to weights

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,23 @@
44

55
import pytest
66

7+
from postpwn.cli import RescheduleParams
8+
79

810
@pytest.fixture
911
def event_loop() -> Generator[AbstractEventLoop, None, None]:
1012
loop = asyncio.new_event_loop()
1113
yield loop
1214
loop.close()
15+
16+
17+
@pytest.fixture
18+
def reschedule_params() -> RescheduleParams:
19+
return {
20+
"token": "VALID_TOKEN",
21+
"filter": "label:test",
22+
"rules": None,
23+
"dry_run": False,
24+
"time_zone": "UTC",
25+
"schedule": None,
26+
}

tests/rescheduler_test.py

Lines changed: 56 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -28,84 +28,61 @@
2828
logger.setLevel(logging.INFO)
2929

3030

31-
def test_no_token_provided(event_loop: AbstractEventLoop) -> None:
31+
def test_no_token_provided(
32+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
33+
) -> None:
3234
"""when no token is provided, it raises an error"""
3335

34-
kwargs: RescheduleParams = {
35-
"token": None,
36-
"filter": "label:test",
37-
"rules": None,
38-
"dry_run": True,
39-
"time_zone": "UTC",
40-
"schedule": None,
41-
}
36+
reschedule_params["token"] = None
4237

4338
fake_api = FakeTodoistAPI("")
4439

4540
curr_datetime = datetime(2022, 1, 5, 12, 0, 0)
4641

4742
with set_env({"RETRY_ATTEMPTS": "1"}):
4843
with pytest.raises(HTTPError, match="401 Client Error: Unauthorized for url"):
49-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
44+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
5045

5146

5247
def test_passing_invalid_cron_string_raises_error(
5348
event_loop: AbstractEventLoop,
49+
reschedule_params: RescheduleParams,
5450
) -> None:
5551
"""when an invalid cron string is provided, it raises an error"""
5652

57-
kwargs: RescheduleParams = {
58-
"token": "VALID_TOKEN",
59-
"filter": "",
60-
"rules": None,
61-
"dry_run": False,
62-
"time_zone": "UTC",
63-
"schedule": "invalid_cron_string",
64-
}
53+
reschedule_params["schedule"] = "invalid_cron_string"
6554

6655
fake_api = FakeTodoistAPI("VALID_TOKEN")
6756

6857
curr_datetime = datetime(2025, 1, 5, 12, 0, 0)
6958

7059
with set_env({"RETRY_ATTEMPTS": "1"}):
7160
with pytest.raises(ValueError, match="Invalid cron schedule."):
72-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
61+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
7362

7463

75-
def test_no_filter_provided(event_loop: AbstractEventLoop) -> None:
64+
def test_no_filter_provided(
65+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
66+
) -> None:
7667
"""when no filter is provided, it does nothing"""
7768

78-
kwargs: RescheduleParams = {
79-
"token": "VALID_TOKEN",
80-
"filter": "",
81-
"rules": None,
82-
"dry_run": False,
83-
"time_zone": "UTC",
84-
"schedule": None,
85-
}
69+
reschedule_params["filter"] = ""
8670

8771
fake_api = FakeTodoistAPI("VALID_TOKEN")
8872

8973
curr_datetime = datetime(2025, 1, 5, 12, 0, 0)
9074

9175
with set_env({"RETRY_ATTEMPTS": "1"}):
92-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
76+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
9377

9478
assert fake_api.update_task.call_count == 0
9579

9680

97-
def test_no_rules_provided(event_loop: asyncio.AbstractEventLoop) -> None:
81+
def test_no_rules_provided(
82+
event_loop: asyncio.AbstractEventLoop, reschedule_params: RescheduleParams
83+
) -> None:
9884
"""when no rules are provided, it reschedules all tasks to the current day"""
9985

100-
kwargs: RescheduleParams = {
101-
"token": "VALID_TOKEN",
102-
"filter": "label:test",
103-
"rules": None,
104-
"dry_run": False,
105-
"time_zone": "UTC",
106-
"schedule": None,
107-
}
108-
10986
fake_api = FakeTodoistAPI("VALID_TOKEN")
11087

11188
task = build_task()
@@ -114,24 +91,17 @@ def test_no_rules_provided(event_loop: asyncio.AbstractEventLoop) -> None:
11491
curr_date = datetime(2025, 1, 5, 0, 0, 0).date()
11592

11693
with set_env({"RETRY_ATTEMPTS": "1"}):
117-
postpwn(fake_api, event_loop, curr_date, **kwargs)
94+
postpwn(fake_api, event_loop, curr_date, **reschedule_params)
11895

11996
assert fake_api.update_task.call_count == 1
12097
assert fake_api.update_task.call_args.kwargs["due_date"] == curr_date
12198

12299

123-
def test_datetime_is_preserved(event_loop: AbstractEventLoop) -> None:
100+
def test_datetime_is_preserved(
101+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
102+
) -> None:
124103
"""when a task has a datetime due date, the specific time is preserved"""
125104

126-
kwargs: RescheduleParams = {
127-
"token": "VALID_TOKEN",
128-
"filter": "label:test",
129-
"rules": None,
130-
"dry_run": False,
131-
"time_zone": "UTC",
132-
"schedule": None,
133-
}
134-
135105
fake_api = FakeTodoistAPI("VALID_TOKEN")
136106

137107
task = build_task(is_datetime=True)
@@ -140,23 +110,18 @@ def test_datetime_is_preserved(event_loop: AbstractEventLoop) -> None:
140110
curr_datetime = datetime(2025, 1, 5, 0, 0, 0)
141111

142112
with set_env({"RETRY_ATTEMPTS": "1"}):
143-
postpwn(fake_api, event_loop, curr_date=curr_datetime, **kwargs)
113+
postpwn(fake_api, event_loop, curr_date=curr_datetime, **reschedule_params)
144114

145115
assert fake_api.update_task.call_count == 1
146116
assert fake_api.update_task.call_args.kwargs["due_datetime"] == curr_datetime
147117

148118

149-
def test_weight_exceeds_single_max_weight(event_loop: AbstractEventLoop) -> None:
119+
def test_weight_exceeds_single_max_weight(
120+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
121+
) -> None:
150122
"""when a rule weight exceeds the singular max weight, it raises an error"""
151123

152-
kwargs: RescheduleParams = {
153-
"token": "VALID_TOKEN",
154-
"filter": "label:test",
155-
"rules": "tests/fixtures/excessive_single_max_weight_rules.json",
156-
"dry_run": False,
157-
"time_zone": "UTC",
158-
"schedule": None,
159-
}
124+
reschedule_params["rules"] = "tests/fixtures/excessive_single_max_weight_rules.json"
160125

161126
fake_api = FakeTodoistAPI("VALID_TOKEN")
162127

@@ -172,20 +137,15 @@ def test_weight_exceeds_single_max_weight(event_loop: AbstractEventLoop) -> None
172137
ValueError,
173138
match="Invalid rule config: @weight_two exceeds max weight 2",
174139
):
175-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
140+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
176141

177142

178-
def test_weight_exceeds_daily_max_weight(event_loop: AbstractEventLoop) -> None:
143+
def test_weight_exceeds_daily_max_weight(
144+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
145+
) -> None:
179146
"""when a rule exceeds one of the daily max weights, it raises an error"""
180147

181-
kwargs: RescheduleParams = {
182-
"token": "VALID_TOKEN",
183-
"filter": "label:test",
184-
"rules": "tests/fixtures/excessive_daily_max_weight_rules.json",
185-
"dry_run": False,
186-
"time_zone": "UTC",
187-
"schedule": None,
188-
}
148+
reschedule_params["rules"] = "tests/fixtures/excessive_daily_max_weight_rules.json"
189149

190150
fake_api = FakeTodoistAPI("VALID_TOKEN")
191151

@@ -201,20 +161,15 @@ def test_weight_exceeds_daily_max_weight(event_loop: AbstractEventLoop) -> None:
201161
ValueError,
202162
match="Invalid rule config: @weight_two exceeds max weight 6",
203163
):
204-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
164+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
205165

206166

207-
def test_no_matching_label(event_loop: asyncio.AbstractEventLoop) -> None:
167+
def test_no_matching_label(
168+
event_loop: asyncio.AbstractEventLoop, reschedule_params: RescheduleParams
169+
) -> None:
208170
"""when tasks have no matching labels, they are not rescheduled"""
209171

210-
kwargs: RescheduleParams = {
211-
"token": "VALID_TOKEN",
212-
"filter": "label:test",
213-
"rules": "tests/fixtures/single_max_weight_rules.json",
214-
"dry_run": False,
215-
"time_zone": "UTC",
216-
"schedule": None,
217-
}
172+
reschedule_params["rules"] = "tests/fixtures/single_max_weight_rules.json"
218173

219174
fake_api = FakeTodoistAPI("VALID_TOKEN")
220175

@@ -226,23 +181,18 @@ def test_no_matching_label(event_loop: asyncio.AbstractEventLoop) -> None:
226181
curr_datetime = datetime(2025, 1, 5, 12, 0, 0)
227182

228183
with set_env({"RETRY_ATTEMPTS": "1"}):
229-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
184+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
230185

231186
assert fake_api.update_task.call_count == 1
232187
assert fake_api.update_task.call_args.args[0] == labeled_task.id
233188

234189

235-
def test_reschedule_with_rules(event_loop: AbstractEventLoop) -> None:
190+
def test_reschedule_with_rules(
191+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
192+
) -> None:
236193
"""when rules are provided, it reschedules tasks using smart rescheduling, respecting max weight"""
237194

238-
kwargs: RescheduleParams = {
239-
"token": "VALID_TOKEN",
240-
"filter": "label:test",
241-
"rules": "tests/fixtures/single_max_weight_rules.json",
242-
"dry_run": False,
243-
"time_zone": "UTC",
244-
"schedule": None,
245-
}
195+
reschedule_params["rules"] = "tests/fixtures/single_max_weight_rules.json"
246196

247197
fake_api = FakeTodoistAPI("VALID_TOKEN")
248198

@@ -256,7 +206,7 @@ def test_reschedule_with_rules(event_loop: AbstractEventLoop) -> None:
256206
curr_datetime = datetime(2025, 1, 5, 0, 0, 0)
257207

258208
with set_env({"RETRY_ATTEMPTS": "1"}):
259-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
209+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
260210

261211
assert fake_api.update_task.call_count == 4
262212

@@ -304,17 +254,12 @@ def test_reschedule_with_rules(event_loop: AbstractEventLoop) -> None:
304254
)
305255

306256

307-
def test_reschedule_with_priority(event_loop: AbstractEventLoop) -> None:
257+
def test_reschedule_with_priority(
258+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
259+
) -> None:
308260
"""when tasks have different priorities, it prioritizes the higher priority tasks first for rescheduling"""
309261

310-
kwargs: RescheduleParams = {
311-
"token": "VALID_TOKEN",
312-
"filter": "label:test",
313-
"rules": "tests/fixtures/single_max_weight_rules.json",
314-
"dry_run": False,
315-
"time_zone": "UTC",
316-
"schedule": None,
317-
}
262+
reschedule_params["rules"] = "tests/fixtures/single_max_weight_rules.json"
318263

319264
fake_api = FakeTodoistAPI("VALID_TOKEN")
320265

@@ -330,7 +275,7 @@ def test_reschedule_with_priority(event_loop: AbstractEventLoop) -> None:
330275
curr_datetime = datetime(2025, 1, 5, 0, 0, 0)
331276

332277
with set_env({"RETRY_ATTEMPTS": "1"}):
333-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
278+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
334279

335280
assert fake_api.update_task.call_count == 4
336281

@@ -380,17 +325,12 @@ def test_reschedule_with_priority(event_loop: AbstractEventLoop) -> None:
380325
)
381326

382327

383-
def test_reschedule_with_rules_and_daily_weight(event_loop: AbstractEventLoop):
328+
def test_reschedule_with_rules_and_daily_weight(
329+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
330+
):
384331
"""when rules with a daily max weight are provided, it reschedules tasks using smart rescheduling, respecting the daily max weight"""
385332

386-
kwargs: RescheduleParams = {
387-
"token": "VALID_TOKEN",
388-
"filter": "label:test",
389-
"rules": "tests/fixtures/daily_max_weight_rules.json",
390-
"dry_run": False,
391-
"time_zone": "UTC",
392-
"schedule": None,
393-
}
333+
reschedule_params["rules"] = "tests/fixtures/daily_max_weight_rules.json"
394334

395335
fake_api = FakeTodoistAPI("VALID_TOKEN")
396336

@@ -404,7 +344,7 @@ def test_reschedule_with_rules_and_daily_weight(event_loop: AbstractEventLoop):
404344
curr_datetime = datetime(2025, 1, 5, 0, 0, 0)
405345

406346
with set_env({"RETRY_ATTEMPTS": "1"}):
407-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
347+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
408348

409349
assert fake_api.update_task.call_count == 5
410350

@@ -468,17 +408,12 @@ def test_reschedule_with_rules_and_daily_weight(event_loop: AbstractEventLoop):
468408
)
469409

470410

471-
def test_dry_run_doesn_not_update_tasks(event_loop: AbstractEventLoop) -> None:
411+
def test_dry_run_doesn_not_update_tasks(
412+
event_loop: AbstractEventLoop, reschedule_params: RescheduleParams
413+
) -> None:
472414
"""when dry run is enabled, it does not reschedule tasks"""
473415

474-
kwargs: RescheduleParams = {
475-
"token": "VALID_TOKEN",
476-
"filter": "label:test",
477-
"rules": None,
478-
"dry_run": True,
479-
"time_zone": "UTC",
480-
"schedule": None,
481-
}
416+
reschedule_params["dry_run"] = True
482417

483418
fake_api = FakeTodoistAPI("VALID_TOKEN")
484419

@@ -488,6 +423,6 @@ def test_dry_run_doesn_not_update_tasks(event_loop: AbstractEventLoop) -> None:
488423
curr_datetime = datetime(2025, 1, 5, 12, 0, 0)
489424

490425
with set_env({"RETRY_ATTEMPTS": "1"}):
491-
postpwn(fake_api, event_loop, curr_datetime, **kwargs)
426+
postpwn(fake_api, event_loop, curr_datetime, **reschedule_params)
492427

493428
assert fake_api.update_task.call_count == 0

0 commit comments

Comments
 (0)