rtmilk 3.0.2__tar.gz → 3.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {rtmilk-3.0.2 → rtmilk-3.0.4}/.github/workflows/ci.yml +8 -6
  2. {rtmilk-3.0.2 → rtmilk-3.0.4}/PKG-INFO +5 -5
  3. rtmilk-3.0.4/pyproject.toml +109 -0
  4. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_sansio.py +22 -14
  5. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/filter.py +56 -54
  6. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/mirror.py +2 -2
  7. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/models.py +1 -1
  8. {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/test_api_wrapper.py +11 -2
  9. rtmilk-3.0.2/pyproject.toml +0 -77
  10. {rtmilk-3.0.2 → rtmilk-3.0.4}/.gitignore +0 -0
  11. {rtmilk-3.0.2 → rtmilk-3.0.4}/LICENSE +0 -0
  12. {rtmilk-3.0.2 → rtmilk-3.0.4}/README.md +0 -0
  13. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/__init__.py +0 -0
  14. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_properties.py +0 -0
  15. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_secrets.py +0 -0
  16. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_utils.py +0 -0
  17. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/api_async.py +0 -0
  18. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/api_base.py +0 -0
  19. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/api_sync.py +0 -0
  20. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/authorization.py +0 -0
  21. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/client.py +0 -0
  22. {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/py.typed +0 -0
  23. {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/conftest.py +0 -0
  24. {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/generate_credentials.py +0 -0
  25. {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/test_client.py +0 -0
  26. {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/test_filter.py +0 -0
  27. {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/test_mirror.py +0 -0
@@ -15,20 +15,21 @@ jobs:
15
15
  strategy:
16
16
  fail-fast: false
17
17
  matrix:
18
- python-version: [3.9, "3.10", "3.11", "3.12", "3.13"]
18
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
19
19
 
20
20
  steps:
21
21
  - uses: actions/checkout@v4
22
22
  - name: Install uv
23
- uses: astral-sh/setup-uv@v3
24
- - name: Set up Python ${{ matrix.python-version }}
25
- run: uv python install ${{ matrix.python-version }}
23
+ uses: astral-sh/setup-uv@v6
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+ activate-environment: true
26
27
  - name: Dependencies
27
28
  run: |
28
29
  uv sync --no-cache
29
30
  - name: Lint
30
31
  run: |
31
- uv run poe lint
32
+ poe lint
32
33
  - name: Tests
33
34
  env:
34
35
  RTM_API_KEY: ${{ secrets.RTM_API_KEY }}
@@ -36,8 +37,9 @@ jobs:
36
37
  RTM_TOKEN: ${{ secrets.RTM_TOKEN }}
37
38
  if: ${{needs.secrets.outputs.valid}} == 'true'
38
39
  run: |
40
+ python -c "import rtmilk"
39
41
  if python --version | grep -q 'Python 3.13' ; then
40
- uv run poe test-with-coverage
42
+ poe test-with-coverage
41
43
  fi
42
44
  - name: Codecov
43
45
  uses: codecov/codecov-action@v4
@@ -1,19 +1,19 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: rtmilk
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: Remember The Milk API wrapper
5
5
  Project-URL: Homepage, https://github.com/rkhwaja/rtmilk
6
6
  Maintainer-email: Rehan Khwaja <rehan@khwaja.name>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
7
9
  Keywords: Milk,Remember,RememberTheMilk
8
10
  Classifier: Development Status :: 5 - Production/Stable
9
11
  Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3.9
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
- Requires-Python: >=3.9
16
+ Requires-Python: >=3.10
17
17
  Requires-Dist: aiohttp>=3.9
18
18
  Requires-Dist: listdiff>=1.0.2
19
19
  Requires-Dist: pydantic>=2.8
@@ -0,0 +1,109 @@
1
+ [project]
2
+ name = "rtmilk"
3
+ version = "3.0.4"
4
+ description = "Remember The Milk API wrapper"
5
+ maintainers = [
6
+ { name = "Rehan Khwaja", email = "rehan@khwaja.name" }
7
+ ]
8
+ keywords = ["RememberTheMilk", "Remember", "Milk"]
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ classifiers = [
13
+ "Development Status :: 5 - Production/Stable",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ ]
20
+ requires-python = ">=3.10"
21
+ dependencies = [
22
+ "requests>=2.23.0",
23
+ "urllib3>=1.26; python_version>='3.10'",
24
+ "pydantic>=2.8",
25
+ "aiohttp>=3.9",
26
+ "listdiff>=1.0.2",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/rkhwaja/rtmilk"
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.poe.tasks]
37
+ build = "uv build"
38
+ test = "pytest"
39
+ lint = "ruff check src tests"
40
+
41
+ [tool.poe.tasks.test-with-coverage]
42
+ shell = """
43
+ pytest --cov=src || exit 1
44
+ coverage xml
45
+ """
46
+
47
+ [tool.ruff]
48
+ line-length = 1000
49
+ lint.ignore = [
50
+ "A002", # Disallows argument names which shadow builtins
51
+ "ANN", # Missing annotations
52
+ "COM812", # Missing trailing comma
53
+ "D", # Missing docstrings
54
+ "DTZ", # datetime timezone rules
55
+ "EM", # Rules about strings in exception constructors
56
+ "ERA001", # Commented out code
57
+ "FBT", # Disallows bool positional arguments
58
+ "FIX", # Disallows TODO-type comments
59
+ "G004", # Disallows f-strings in logging
60
+ "N802", # Function naming rules
61
+ "I", # Import formatting
62
+ "N803", # Argument naming rule
63
+ "N806", # Variable naming rule
64
+ "N815", # Variable naming rule
65
+ "PLR0913", # Too many arguments
66
+ "PT013", # Only allows one way of importing pytest
67
+ "PTH", # Forces use of pathlib
68
+ "TCH", # Requires type-checking blocks
69
+ "TD", # Requires special formatting of TODO-type comments
70
+ "TRY003", # Disallows long messages in exceptions
71
+ "W191", # Disallows tab spacing
72
+ ]
73
+ lint.select = ["ALL"]
74
+
75
+ [tool.ruff.lint.extend-per-file-ignores]
76
+ "__init__.py" = ["F403"]
77
+ "tests/*" = [
78
+ "INP001", # Disallows implicit namespace
79
+ "LOG015", # Disallows use of root logger
80
+ "S101", # Disallows asserts
81
+ "S311", # Disallows use of randint
82
+ "T20", # Disallows print
83
+ ]
84
+ "_properties.py" = [
85
+ "SLF001", # Disallows private member access
86
+ ]
87
+ "client.py" = [
88
+ "SLF001", # Disallows private member access
89
+ ]
90
+
91
+ [tool.ruff.lint.flake8-quotes]
92
+ inline-quotes = "single"
93
+ multiline-quotes = "single"
94
+
95
+ [tool.pytest.ini_options]
96
+ pythonpath = ["src"]
97
+ asyncio_default_fixture_loop_scope = "function"
98
+
99
+ [dependency-groups]
100
+ dev = [
101
+ "pytest>=7.1",
102
+ "pyperclip>=1.8.0",
103
+ "python-dateutil>=2.8.1",
104
+ "python-dotenv>=0.17.1",
105
+ "pytest-cov>=3",
106
+ "pytest-asyncio>=0.24.0",
107
+ "poethepoet>=0.16.4",
108
+ "ruff>=0.11.10",
109
+ ]
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import date, datetime, UTC
3
+ from datetime import date, datetime, timezone
4
4
  from hashlib import md5
5
5
  from logging import getLogger
6
6
  from pprint import pformat
@@ -223,6 +223,8 @@ class TagsGetList(AuthorizedCall):
223
223
  class TasksAdd(AuthorizedCall):
224
224
  def In(self, timeline: str, name: str, list_id: str | None = None, parse: bool | None = None, parent_task_id: str | None = None, external_id: str | None = None):
225
225
  kwargs = _RebuildArgs(list_id=list_id, parse=parse, parent_task_id=parent_task_id, external_id=external_id)
226
+ if 'parse' in kwargs:
227
+ kwargs['parse'] = '1' if kwargs['parse'] else '0'
226
228
  return self.CommonParams('rtm.tasks.add', timeline=timeline, name=name, **kwargs)
227
229
 
228
230
  @classmethod
@@ -263,7 +265,7 @@ class TasksGetList(AuthorizedCall):
263
265
  lastSyncDt = kwargs['last_sync']
264
266
  if lastSyncDt.tzinfo is not None:
265
267
  # switch to naive datetime in UTC
266
- lastSyncDt = lastSyncDt.astimezone(UTC).replace(tzinfo=None)
268
+ lastSyncDt = lastSyncDt.astimezone(timezone.utc).replace(tzinfo=None)
267
269
  kwargs['last_sync'] = _RtmDatetime(lastSyncDt)
268
270
  return self.CommonParams('rtm.tasks.getList', **kwargs)
269
271
 
@@ -303,12 +305,15 @@ class TasksSetDueDate(AuthorizedCall):
303
305
  if 'has_due_time' in kwargs:
304
306
  kwargs['has_due_time'] = '1' if kwargs['has_due_time'] else '0'
305
307
  if 'due' in kwargs:
306
- if isinstance(kwargs['due'], datetime):
307
- kwargs['due'] = _RtmDatetime(kwargs['due'])
308
- elif isinstance(kwargs['due'], date):
309
- kwargs['due'] = _RtmDate(kwargs['due'])
310
- else:
311
- assert isinstance(kwargs['due'], str)
308
+ match kwargs['due']:
309
+ case datetime():
310
+ kwargs['due'] = _RtmDatetime(kwargs['due'])
311
+ case date():
312
+ kwargs['due'] = _RtmDate(kwargs['due'])
313
+ case str():
314
+ pass
315
+ case _:
316
+ raise RuntimeError('due should be a date, datetime or a str')
312
317
  if 'parse' in kwargs:
313
318
  kwargs['parse'] = '1' if kwargs['parse'] else '0'
314
319
  return self.CommonParams('rtm.tasks.setDueDate', timeline=timeline, list_id=list_id, taskseries_id=taskseries_id, task_id=task_id, **kwargs)
@@ -342,12 +347,15 @@ class TasksSetStartDate(AuthorizedCall):
342
347
  if 'has_start_time' in kwargs:
343
348
  kwargs['has_start_time'] = '1' if kwargs['has_start_time'] else '0'
344
349
  if 'start' in kwargs:
345
- if isinstance(kwargs['start'], datetime):
346
- kwargs['start'] = _RtmDatetime(kwargs['start'])
347
- elif isinstance(kwargs['start'], date):
348
- kwargs['start'] = _RtmDate(kwargs['start'])
349
- else:
350
- assert isinstance(kwargs['start'], str)
350
+ match kwargs['start']:
351
+ case datetime():
352
+ kwargs['start'] = _RtmDatetime(kwargs['start'])
353
+ case date():
354
+ kwargs['start'] = _RtmDate(kwargs['start'])
355
+ case str():
356
+ pass
357
+ case _:
358
+ raise RuntimeError('start should be a date, datetime or a str')
351
359
  if 'parse' in kwargs:
352
360
  kwargs['parse'] = '1' if kwargs['parse'] is True else '0'
353
361
  return self.CommonParams('rtm.tasks.setStartDate', timeline=timeline, list_id=list_id, taskseries_id=taskseries_id, task_id=task_id, **kwargs)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import ABC, abstractmethod
3
4
  from dataclasses import dataclass
4
5
  from datetime import date, datetime
5
6
 
@@ -17,26 +18,27 @@ def _DateText(value):
17
18
  return datetime.strftime(value, '%m/%d/%Y')
18
19
  return f'"{value}"'
19
20
 
20
- class ConditionABC:
21
+ class ConditionABC(ABC):
22
+ @abstractmethod
21
23
  def Text(self) -> str:
22
24
  pass
23
25
 
24
26
  @dataclass
25
- class ListIs:
27
+ class ListIs(ConditionABC):
26
28
  name: str
27
29
 
28
30
  def Text(self):
29
31
  return f'list:"{self.name}"'
30
32
 
31
33
  @dataclass
32
- class ListContains:
34
+ class ListContains(ConditionABC):
33
35
  substr: str
34
36
 
35
37
  def Text(self):
36
38
  return f'listContains:"{self.substr}"'
37
39
 
38
40
  @dataclass
39
- class Priority:
41
+ class Priority(ConditionABC):
40
42
  value: PriorityEnum
41
43
 
42
44
  def Text(self):
@@ -49,336 +51,336 @@ class Priority:
49
51
  return f'priority:{text}'
50
52
 
51
53
  @dataclass
52
- class Status:
54
+ class Status(ConditionABC):
53
55
  complete: bool
54
56
 
55
57
  def Text(self):
56
58
  return 'status:' + ('completed' if self.complete else 'incomplete')
57
59
 
58
60
  @dataclass
59
- class TagIs:
61
+ class TagIs(ConditionABC):
60
62
  name: str
61
63
 
62
64
  def Text(self):
63
65
  return f'tag:{self.name}'
64
66
 
65
67
  @dataclass
66
- class TagContains:
68
+ class TagContains(ConditionABC):
67
69
  substr: str
68
70
 
69
71
  def Text(self):
70
72
  return f'tagContains:{self.substr}'
71
73
 
72
74
  @dataclass
73
- class IsTagged:
75
+ class IsTagged(ConditionABC):
74
76
  value: bool
75
77
 
76
78
  def Text(self):
77
79
  return f'isTagged:{_BoolText(self.value)}'
78
80
 
79
81
  @dataclass
80
- class LocationIs:
82
+ class LocationIs(ConditionABC):
81
83
  name: str
82
84
 
83
85
  def Text(self):
84
86
  return f'location:{self.name}'
85
87
 
86
88
  @dataclass
87
- class LocationContains:
89
+ class LocationContains(ConditionABC):
88
90
  location: str
89
91
 
90
92
  def Text(self):
91
93
  return f'locationContains:"{self.location}"'
92
94
 
93
95
  @dataclass
94
- class LocatedWithin:
96
+ class LocatedWithin(ConditionABC):
95
97
  location: str
96
98
 
97
99
  def Text(self):
98
100
  return f'locatedWithin:"{self.location}"'
99
101
 
100
102
  @dataclass
101
- class IsLocated:
103
+ class IsLocated(ConditionABC):
102
104
  value: bool
103
105
 
104
106
  def Text(self):
105
107
  return f'isLocated:{_BoolText(self.value)}'
106
108
 
107
109
  @dataclass
108
- class IsRepeating:
110
+ class IsRepeating(ConditionABC):
109
111
  value: bool
110
112
 
111
113
  def Text(self):
112
114
  return f'isRepeating:{_BoolText(self.value)}'
113
115
 
114
116
  @dataclass
115
- class NameIs:
117
+ class NameIs(ConditionABC):
116
118
  name: str
117
119
 
118
120
  def Text(self):
119
121
  return f'name:"{self.name}"'
120
122
 
121
123
  @dataclass
122
- class NoteContains:
124
+ class NoteContains(ConditionABC):
123
125
  substr: str
124
126
 
125
127
  def Text(self):
126
128
  return f'noteContains:"{self.substr}"'
127
129
 
128
130
  @dataclass
129
- class HasNotes:
131
+ class HasNotes(ConditionABC):
130
132
  value: bool
131
133
 
132
134
  def Text(self):
133
135
  return f'hasNotes:"{_BoolText(self.value)}"'
134
136
 
135
137
  @dataclass
136
- class FilenameContains:
138
+ class FilenameContains(ConditionABC):
137
139
  substr: str
138
140
 
139
141
  def Text(self):
140
142
  return f'filename:"{self.substr}"'
141
143
 
142
144
  @dataclass
143
- class HasAttachments:
145
+ class HasAttachments(ConditionABC):
144
146
  value: bool
145
147
 
146
148
  def Text(self):
147
149
  return f'hasAttachments:{_BoolText(self.value)}'
148
150
 
149
151
  @dataclass
150
- class Due:
152
+ class Due(ConditionABC):
151
153
  value: str | date
152
154
 
153
155
  def Text(self):
154
156
  return f'due:{_DateText(self.value)}'
155
157
 
156
158
  @dataclass
157
- class DueBefore:
159
+ class DueBefore(ConditionABC):
158
160
  value: str | date
159
161
 
160
162
  def Text(self):
161
163
  return f'dueBefore:{_DateText(self.value)}'
162
164
 
163
165
  @dataclass
164
- class DueAfter:
166
+ class DueAfter(ConditionABC):
165
167
  value: str | date
166
168
 
167
169
  def Text(self):
168
170
  return f'dueAfter:{_DateText(self.value)}'
169
171
 
170
172
  @dataclass
171
- class DueWithin:
173
+ class DueWithin(ConditionABC):
172
174
  value: str
173
175
 
174
176
  def Text(self):
175
177
  return f'dueWithin:{self.value}'
176
178
 
177
179
  @dataclass
178
- class Start:
180
+ class Start(ConditionABC):
179
181
  value: str | date
180
182
 
181
183
  def Text(self):
182
184
  return f'start:{_DateText(self.value)}'
183
185
 
184
186
  @dataclass
185
- class StartBefore:
187
+ class StartBefore(ConditionABC):
186
188
  value: str | date
187
189
 
188
190
  def Text(self):
189
191
  return f'startBefore:{_DateText(self.value)}'
190
192
 
191
193
  @dataclass
192
- class StartAfter:
194
+ class StartAfter(ConditionABC):
193
195
  value: str | date
194
196
 
195
197
  def Text(self):
196
198
  return f'startAfter:{_DateText(self.value)}'
197
199
 
198
200
  @dataclass
199
- class StartWithin:
201
+ class StartWithin(ConditionABC):
200
202
  value: str
201
203
 
202
204
  def Text(self):
203
205
  return f'startWithin:{self.value}'
204
206
 
205
207
  @dataclass
206
- class TimeEstimate:
208
+ class TimeEstimate(ConditionABC):
207
209
  value: str
208
210
 
209
211
  def Text(self):
210
212
  return f'timeEstimate: "{self.value}"'
211
213
 
212
214
  @dataclass
213
- class HasTimeEstimate:
215
+ class HasTimeEstimate(ConditionABC):
214
216
  value: bool
215
217
 
216
218
  def Text(self):
217
219
  return f'hasTimeEstimate:{_BoolText(self.value)}'
218
220
 
219
221
  @dataclass
220
- class HasURL:
222
+ class HasURL(ConditionABC):
221
223
  value: bool
222
224
 
223
225
  def Text(self):
224
226
  return f'hasURL:{_BoolText(self.value)}'
225
227
 
226
228
  @dataclass
227
- class HasSubtasks:
229
+ class HasSubtasks(ConditionABC):
228
230
  value: bool
229
231
 
230
232
  def Text(self):
231
233
  return f'hasSubtasks:{_BoolText(self.value)}'
232
234
 
233
235
  @dataclass
234
- class IsSubtask:
236
+ class IsSubtask(ConditionABC):
235
237
  value: bool
236
238
 
237
239
  def Text(self):
238
240
  return f'isSubtask:{_BoolText(self.value)}'
239
241
 
240
242
  @dataclass
241
- class Completed:
243
+ class Completed(ConditionABC):
242
244
  value: str | date
243
245
 
244
246
  def Text(self):
245
247
  return f'completed:{_DateText(self.value)}'
246
248
 
247
249
  @dataclass
248
- class CompletedBefore:
250
+ class CompletedBefore(ConditionABC):
249
251
  value: str | date
250
252
 
251
253
  def Text(self):
252
254
  return f'completedBefore:{_DateText(self.value)}'
253
255
 
254
256
  @dataclass
255
- class CompletedAfter:
257
+ class CompletedAfter(ConditionABC):
256
258
  value: str | date
257
259
 
258
260
  def Text(self):
259
261
  return f'completedAfter:{_DateText(self.value)}'
260
262
 
261
263
  @dataclass
262
- class CompletedWithin:
264
+ class CompletedWithin(ConditionABC):
263
265
  value: str
264
266
 
265
267
  def Text(self):
266
268
  return f'completedWithin:{self.value}'
267
269
 
268
270
  @dataclass
269
- class Added:
271
+ class Added(ConditionABC):
270
272
  value: str | date
271
273
 
272
274
  def Text(self):
273
275
  return f'added:{_DateText(self.value)}'
274
276
 
275
277
  @dataclass
276
- class AddedBefore:
278
+ class AddedBefore(ConditionABC):
277
279
  value: str | date
278
280
 
279
281
  def Text(self):
280
282
  return f'addedBefore:{_DateText(self.value)}'
281
283
 
282
284
  @dataclass
283
- class AddedAfter:
285
+ class AddedAfter(ConditionABC):
284
286
  value: str | date
285
287
 
286
288
  def Text(self):
287
289
  return f'addedAfter:{_DateText(self.value)}'
288
290
 
289
291
  @dataclass
290
- class AddedWithin:
292
+ class AddedWithin(ConditionABC):
291
293
  value: str
292
294
 
293
295
  def Text(self):
294
296
  return f'addedWithin:{self.value}'
295
297
 
296
298
  @dataclass
297
- class Updated:
299
+ class Updated(ConditionABC):
298
300
  value: str | date
299
301
 
300
302
  def Text(self):
301
303
  return f'updated:{_DateText(self.value)}'
302
304
 
303
305
  @dataclass
304
- class UpdatedBefore:
306
+ class UpdatedBefore(ConditionABC):
305
307
  value: str | date
306
308
 
307
309
  def Text(self):
308
310
  return f'updatedBefore:{_DateText(self.value)}'
309
311
 
310
312
  @dataclass
311
- class UpdatedAfter:
313
+ class UpdatedAfter(ConditionABC):
312
314
  value: str | date
313
315
 
314
316
  def Text(self):
315
317
  return f'updatedAfter:{_DateText(self.value)}'
316
318
 
317
319
  @dataclass
318
- class UpdatedWithin:
320
+ class UpdatedWithin(ConditionABC):
319
321
  value: str
320
322
 
321
323
  def Text(self):
322
324
  return f'updatedWithin:{self.value}'
323
325
 
324
326
  @dataclass
325
- class Postponed:
327
+ class Postponed(ConditionABC):
326
328
  value: str
327
329
 
328
330
  def Text(self):
329
331
  return f'postponed:"{self.value}"'
330
332
 
331
333
  @dataclass
332
- class IsShared:
334
+ class IsShared(ConditionABC):
333
335
  value: bool
334
336
 
335
337
  def Text(self):
336
338
  return f'isShared:{_BoolText(self.value)}'
337
339
 
338
340
  @dataclass
339
- class SharedWith:
341
+ class SharedWith(ConditionABC):
340
342
  value: str
341
343
 
342
344
  def Text(self):
343
345
  return f'sharedWith:{self.value}'
344
346
 
345
347
  @dataclass
346
- class GivenTo:
348
+ class GivenTo(ConditionABC):
347
349
  value: str
348
350
 
349
351
  def Text(self):
350
352
  return f'givenTo:{self.value}'
351
353
 
352
354
  @dataclass
353
- class GivenBy:
355
+ class GivenBy(ConditionABC):
354
356
  value: str
355
357
 
356
358
  def Text(self):
357
359
  return f'givenBy:{self.value}'
358
360
 
359
361
  @dataclass
360
- class IsGiven:
362
+ class IsGiven(ConditionABC):
361
363
  value: bool
362
364
 
363
365
  def Text(self):
364
366
  return f'isGiven:{_BoolText(self.value)}'
365
367
 
366
368
  @dataclass
367
- class Source:
369
+ class Source(ConditionABC):
368
370
  value: str
369
371
 
370
372
  def Text(self):
371
373
  return f'source:{self.value}'
372
374
 
373
375
  @dataclass
374
- class IncludeArchived:
376
+ class IncludeArchived(ConditionABC):
375
377
  value: bool
376
378
 
377
379
  def Text(self):
378
380
  return f'includeArchived:{_BoolText(self.value)}'
379
381
 
380
382
  @dataclass
381
- class And:
383
+ class And(ConditionABC):
382
384
  lhs: ConditionABC
383
385
  rhs: ConditionABC
384
386
 
@@ -386,7 +388,7 @@ class And:
386
388
  return f'({self.lhs.Text()}) AND ({self.rhs.Text()})'
387
389
 
388
390
  @dataclass
389
- class Or:
391
+ class Or(ConditionABC):
390
392
  lhs: ConditionABC
391
393
  rhs: ConditionABC
392
394
 
@@ -394,7 +396,7 @@ class Or:
394
396
  return f'({self.lhs.Text()}) OR ({self.rhs.Text()})'
395
397
 
396
398
  @dataclass
397
- class Not:
399
+ class Not(ConditionABC):
398
400
  condition: ConditionABC
399
401
 
400
402
  def Text(self):
@@ -14,8 +14,8 @@ _log = getLogger(__name__)
14
14
  class TaskData:
15
15
  name: str
16
16
  tags: set[str] = field(default_factory=dict)
17
- startDate: date | datetime = None
18
- dueDate: date | datetime = None
17
+ startDate: date | datetime | None = None
18
+ dueDate: date | datetime | None = None
19
19
  notes: str = ''
20
20
  complete: bool | None = False # optional here means that either is acceptable i.e. don't change an existing task's complete value
21
21
 
@@ -128,7 +128,7 @@ class Note(BaseModel):
128
128
  created: datetime
129
129
  modified: datetime
130
130
  title: str
131
- body: str = Field(None, alias='$t')
131
+ body: str | None = Field(None, alias='$t')
132
132
 
133
133
  class Transaction(BaseModel):
134
134
  id: str
@@ -1,4 +1,4 @@
1
- from datetime import datetime, timedelta, UTC
1
+ from datetime import datetime, timedelta, timezone
2
2
  from logging import info
3
3
  from random import randint
4
4
  from uuid import uuid4
@@ -97,6 +97,15 @@ def test_add_and_delete_basic_task(api, timeline):
97
97
  task.list.taskseries[0].id,
98
98
  task.list.taskseries[0].task[0].id)
99
99
 
100
+ def test_smart_add_task(api, timeline):
101
+ task = api.TasksAdd(timeline, 'Pick up the milk tomorrow', parse=1)
102
+ assert task.list.taskseries[0].name == 'Pick up the milk'
103
+ assert task.list.taskseries[0].task[0].due is not None
104
+ api.TasksDelete(
105
+ timeline, task.list.id,
106
+ task.list.taskseries[0].id,
107
+ task.list.taskseries[0].task[0].id)
108
+
100
109
  @mark.asyncio
101
110
  async def test_async_add_and_delete_basic_task(apiAsync, timeline):
102
111
  task = await apiAsync.TasksAdd(timeline, f'test_async_add_and_delete_basic_task {uuid4()}')
@@ -222,7 +231,7 @@ def test_get_list(api, task): # noqa: ARG001
222
231
  info(f'List ID: {list_.id}')
223
232
 
224
233
  def test_last_sync(api, taskCreatorAPI):
225
- start = datetime.now(UTC)
234
+ start = datetime.now(timezone.utc)
226
235
  noChange = api.TasksGetList(last_sync=start)
227
236
  assert noChange.tasks.list is None, 'No tasks added at the start'
228
237
  task1 = taskCreatorAPI.Add(f'task 1 {uuid4()}')
@@ -1,77 +0,0 @@
1
- [project]
2
- name = "rtmilk"
3
- version = "3.0.2"
4
- description = "Remember The Milk API wrapper"
5
- maintainers = [
6
- { name = "Rehan Khwaja", email = "rehan@khwaja.name" }
7
- ]
8
- keywords = ["RememberTheMilk", "Remember", "Milk"]
9
- readme = "README.md"
10
- classifiers = [
11
- "License :: OSI Approved :: MIT License",
12
- "Development Status :: 5 - Production/Stable",
13
- "Intended Audience :: Developers",
14
- "Programming Language :: Python :: 3.9",
15
- "Programming Language :: Python :: 3.10",
16
- "Programming Language :: Python :: 3.11",
17
- "Programming Language :: Python :: 3.12",
18
- "Programming Language :: Python :: 3.13",
19
- ]
20
- requires-python = ">=3.9"
21
- dependencies = [
22
- "requests>=2.23.0",
23
- "urllib3>=1.26; python_version>='3.10'",
24
- "pydantic>=2.8",
25
- "aiohttp>=3.9",
26
- "listdiff>=1.0.2",
27
- ]
28
-
29
- [project.urls]
30
- Homepage = "https://github.com/rkhwaja/rtmilk"
31
-
32
- [build-system]
33
- requires = ["hatchling"]
34
- build-backend = "hatchling.build"
35
-
36
- [tool.poe.tasks]
37
- build = "uv build"
38
- test = "pytest"
39
- lint = "ruff check src tests"
40
-
41
- [tool.poe.tasks.test-with-coverage]
42
- shell = """
43
- pytest --cov=src || exit 1
44
- coverage xml
45
- """
46
-
47
- [tool.ruff]
48
- line-length = 1000
49
- lint.ignore = ["A002", "A003", "ANN", "COM812", "D", "DTZ", "EM", "ERA001", "FBT001", "FBT003", "FIX", "G004", "N802", "I", "N803", "N806", "N815", "PLR0913", "PT013", "PTH", "S101", "S311", "T20", "TCH", "TD", "TRY003", "W191"]
50
- lint.select = ["ALL"]
51
- target-version = "py39"
52
-
53
- [tool.ruff.lint.extend-per-file-ignores]
54
- "__init__.py" = ["F403"]
55
- "tests/*" = ["ANN201", "D103", "INP001", "PT006"]
56
- "_properties.py" = ["SLF001"]
57
- "client.py" = ["SLF001"]
58
-
59
- [tool.ruff.lint.flake8-quotes]
60
- inline-quotes = "single"
61
- multiline-quotes = "single"
62
-
63
- [tool.pytest.ini_options]
64
- pythonpath = ["src"]
65
- asyncio_default_fixture_loop_scope = "function"
66
-
67
- [tool.uv]
68
- dev-dependencies = [
69
- "pytest>=7.1",
70
- "pyperclip>=1.8.0",
71
- "python-dateutil>=2.8.1",
72
- "python-dotenv>=0.17.1",
73
- "pytest-cov>=3",
74
- "pytest-asyncio>=0.24.0",
75
- "poethepoet>=0.16.4",
76
- "ruff>=0.6.3",
77
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes