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.
- {rtmilk-3.0.2 → rtmilk-3.0.4}/.github/workflows/ci.yml +8 -6
- {rtmilk-3.0.2 → rtmilk-3.0.4}/PKG-INFO +5 -5
- rtmilk-3.0.4/pyproject.toml +109 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_sansio.py +22 -14
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/filter.py +56 -54
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/mirror.py +2 -2
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/models.py +1 -1
- {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/test_api_wrapper.py +11 -2
- rtmilk-3.0.2/pyproject.toml +0 -77
- {rtmilk-3.0.2 → rtmilk-3.0.4}/.gitignore +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/LICENSE +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/README.md +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/__init__.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_properties.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_secrets.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/_utils.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/api_async.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/api_base.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/api_sync.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/authorization.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/client.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/src/rtmilk/py.typed +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/conftest.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/generate_credentials.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/test_client.py +0 -0
- {rtmilk-3.0.2 → rtmilk-3.0.4}/tests/test_filter.py +0 -0
- {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: [
|
|
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@
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: rtmilk
|
|
3
|
-
Version: 3.0.
|
|
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.
|
|
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,
|
|
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(
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import datetime, timedelta,
|
|
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(
|
|
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()}')
|
rtmilk-3.0.2/pyproject.toml
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|