Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V3 #98

Merged
merged 5 commits into from
Oct 10, 2024
Merged

V3 #98

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ Python wrapper for "Remember the Milk" [API](https://www.rememberthemilk.com/ser

# Usage of client
```python
from rtmilk.client import Client
from rtmmilk.models import RTMError
from rtmilk import APIError, CreateClient, CreateClientAsync

# These are the equivalent objects, created differently
client = Client.Create(API_KEY, SHARED_SECRET, TOKEN)
client2 = await Client.CreateAsync(API_KEY, SHARED_SECRET, TOKEN)
client = CreateClient(API_KEY, SHARED_SECRET, TOKEN)
client2 = await CreateClientAsync(API_KEY, SHARED_SECRET, TOKEN)

try:
task = client.Add(name='name 1')
Expand All @@ -22,14 +21,13 @@ try:
await task.tags.SetAsync({'tag1', 'tag2'})
tasks = client2.Get('name:"name 1"')
assert tasks[0].tags.value == {'tag1', 'tag2'}
except RTMError as e:
except APIError as e:
print(e)
```

# Usage of API functions directly
```python
from rtmilk.api_sync import API
from rtmmilk.models import FailStat
from rtmilk import API, FailStat

api = API(API_KEY, SHARED_SECRET, TOKEN)

Expand All @@ -40,8 +38,7 @@ if isinstance(result, FailStat):
```

```python
from rtmilk.api_async import APIAsync
from rtmmilk.models import FailStat
from rtmilk import APIAsync, FailStat

apiAsync = APIAsync(API_KEY, SHARED_SECRET, TOKEN)

Expand All @@ -53,7 +50,7 @@ if isinstance(result, FailStat):

# Authorization
```python
from rtmilk.authorization import AuthorizationSession
from rtmilk import AuthorizationSession

authenticationSession = AuthorizationSession(API_KEY, SHARED_SECRET, 'delete')
input(f'Go to {authenticationSession.url} and authorize. Then Press ENTER')
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rtmilk"
version = "2.1.1"
version = "3.0.0"
description = "Remember The Milk API wrapper"
maintainers = [
{ name = "Rehan Khwaja", email = "[email protected]" }
Expand Down Expand Up @@ -51,6 +51,7 @@ lint.select = ["ALL"]
target-version = "py39"

[tool.ruff.lint.extend-per-file-ignores]
"__init__.py" = ["F403"]
"tests/*" = ["ANN201", "D103", "INP001", "PT006"]
"_properties.py" = ["SLF001"]
"client.py" = ["SLF001"]
Expand All @@ -61,6 +62,7 @@ multiline-quotes = "single"

[tool.pytest.ini_options]
pythonpath = ["src"]
asyncio_default_fixture_loop_scope = "function"

[tool.uv]
dev-dependencies = [
Expand All @@ -69,7 +71,7 @@ dev-dependencies = [
"python-dateutil>=2.8.1",
"python-dotenv>=0.17.1",
"pytest-cov>=3",
"pytest-asyncio>=0.16.0",
"pytest-asyncio>=0.24.0",
"poethepoet>=0.16.4",
"ruff>=0.6.3",
]
8 changes: 8 additions & 0 deletions src/rtmilk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
from logging import getLogger, NullHandler

from .api_async import *
from .api_sync import *
from .authorization import *
from .client import *
from .filter import *
from .mirror import *
from .models import *

getLogger(__name__).addHandler(NullHandler())
4 changes: 2 additions & 2 deletions src/rtmilk/_sansio.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from pydantic import validate_call, ValidationError

from .models import AuthResponse, EchoResponse, FailStat, ListsResponse, NotesResponse, PriorityDirectionEnum, PriorityEnum, RTMError, SettingsResponse, SingleListResponse, SubscriptionListResponse, SubscriptionResponse, TagListResponse, TaskListResponse, TaskPayload, TaskResponse, TimelineResponse, TopicListResponse
from .models import APIError, AuthResponse, EchoResponse, FailStat, ListsResponse, NotesResponse, PriorityDirectionEnum, PriorityEnum, SettingsResponse, SingleListResponse, SubscriptionListResponse, SubscriptionResponse, TagListResponse, TaskListResponse, TaskPayload, TaskResponse, TimelineResponse, TopicListResponse
from ._utils import HttpsUrl

REST_URL = 'https://api.rememberthemilk.com/services/rest/'
Expand Down Expand Up @@ -35,7 +35,7 @@
try:
return FailStat(**rsp)
except ValidationError as e:
raise RTMError from e
raise APIError from e

Check warning on line 38 in src/rtmilk/_sansio.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/_sansio.py#L38

Added line #L38 was not covered by tests

def ApiSig(sharedSecret, params):
sortedItems = sorted(params.items(), key=lambda x: x[0])
Expand Down
13 changes: 9 additions & 4 deletions src/rtmilk/api_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from aiohttp import ClientResponseError, ClientSession
from pydantic import validate_call

from .api_base import RTMError, UnauthorizedAPIBase
from .models import AuthResponse, EchoResponse, ListsResponse, NotesResponse, PriorityDirectionEnum, PriorityEnum, SettingsResponse, SingleListResponse, SubscriptionListResponse, SubscriptionResponse, TagListResponse, TaskListResponse, TaskPayload, TaskResponse, TimelineResponse, TopicListResponse
from .api_base import UnauthorizedAPIBase
from .models import AuthResponse, BaseError, EchoResponse, ListsResponse, NotesResponse, PriorityDirectionEnum, PriorityEnum, SettingsResponse, SingleListResponse, SubscriptionListResponse, SubscriptionResponse, TagListResponse, TaskListResponse, TaskPayload, TaskResponse, TimelineResponse, TopicListResponse
from ._sansio import AuthCheckToken, AuthGetFrob, AuthGetToken, ListsAdd, ListsArchive, ListsDelete, ListsGetList, ListsSetDefaultList, ListsSetName, ListsUnarchive, PushGetSubscriptions, PushGetTopics, PushSubscribe, PushUnsubscribe, TagsGetList, TasksAdd, TasksAddTags, TasksComplete, TasksDelete, TasksGetList, TasksMovePriority, TasksNotesAdd, TasksRemoveTags, TasksSetDueDate, TasksSetName, TasksSetPriority, TasksSetStartDate, TasksSetTags, TasksUncomplete, TestEcho, TimelinesCreate, SettingsGetList, REST_URL
from ._secrets import SecretsWithAuthorization
from ._utils import HttpsUrl
Expand All @@ -21,7 +21,7 @@
text = await resp.text()
return loads(text)['rsp']
except (ClientResponseError, ValueError) as e:
raise RTMError from e
raise BaseError from e

Check warning on line 24 in src/rtmilk/api_async.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/api_async.py#L24

Added line #L24 was not covered by tests

class UnauthorizedAPIAsync(UnauthorizedAPIBase):
"""Async wrappers for API calls that don't need authorization"""
Expand All @@ -42,7 +42,12 @@
return AuthCheckToken.Out(** await _CallAsync(AuthCheckToken(self._secrets).In(auth_token)))

class APIAsync(UnauthorizedAPIAsync):
"""Async wrappers for all API calls"""
"""Low-level asynchronous API wrapper
Handles the authorization/authentication token and API signature
There is (almost) a 1-1 relationship between API calls and public member functions
Parameter names are the same as the API
The inputs are python types
The outputs are parsed into pydantic types, including errors"""

def __init__(self, apiKey: str, sharedSecret: str, token: str):
super().__init__(apiKey, sharedSecret)
Expand Down
3 changes: 0 additions & 3 deletions src/rtmilk/api_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@ class AuthorizedAPIBase:
"""Holds secrets for authorized calls"""
def __init__(self, apiKey, sharedSecret, token):
self._authSecrets = SecretsWithAuthorization(apiKey, sharedSecret, token)

class RTMError(Exception):
"""Base class for all errors"""
13 changes: 5 additions & 8 deletions src/rtmilk/api_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from requests import get
from requests.exceptions import RequestException

from .api_base import RTMError, UnauthorizedAPIBase
from .models import AuthResponse, EchoResponse, ListsResponse, NotesResponse, PriorityDirectionEnum, PriorityEnum, SettingsResponse, SingleListResponse, SubscriptionListResponse, SubscriptionResponse, TagListResponse, TaskListResponse, TaskPayload, TaskResponse, TimelineResponse, TopicListResponse
from .api_base import UnauthorizedAPIBase
from .models import AuthResponse, BaseError, EchoResponse, ListsResponse, NotesResponse, PriorityDirectionEnum, PriorityEnum, SettingsResponse, SingleListResponse, SubscriptionListResponse, SubscriptionResponse, TagListResponse, TaskListResponse, TaskPayload, TaskResponse, TimelineResponse, TopicListResponse
from ._sansio import AuthCheckToken, AuthGetFrob, AuthGetToken, ListsAdd, ListsArchive, ListsDelete, ListsGetList, ListsSetDefaultList, ListsSetName, ListsUnarchive, PushGetSubscriptions, PushGetTopics, PushSubscribe, PushUnsubscribe, TagsGetList, TasksAdd, TasksAddTags, TasksComplete, TasksDelete, TasksGetList, TasksMovePriority, TasksNotesAdd, TasksRemoveTags, TasksSetDueDate, TasksSetName, TasksSetPriority, TasksSetStartDate, TasksSetTags, TasksUncomplete, TestEcho, TimelinesCreate, SettingsGetList, REST_URL
from ._secrets import SecretsWithAuthorization
from ._utils import HttpsUrl
Expand All @@ -23,7 +23,7 @@
_log.debug(f'JSON response:\n{pformat(json)}')
return json['rsp']
except (RequestException, ValueError) as e:
raise RTMError from e
raise BaseError from e

Check warning on line 26 in src/rtmilk/api_sync.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/api_sync.py#L26

Added line #L26 was not covered by tests

class UnauthorizedAPI(UnauthorizedAPIBase):
"""Synchronous wrappers for API calls that don't need authorization"""
Expand All @@ -45,15 +45,12 @@
# replace self._secrets with the authorized version
# allow to call unauthorized secrets with the same object
class API(UnauthorizedAPI):
"""
Low-level API wrapper
"""Low-level synchronous API wrapper
Handles the authorization/authentication token and API signature
There is (almost) a 1-1 relationship between API calls and public member functions
Parameter names are the same as the API
The inputs are python types
The outputs are parsed into pydantic types, including errors
Translate API errors into exceptions
"""
The outputs are parsed into pydantic types, including errors"""

def __init__(self, apiKey: str, sharedSecret: str, token: str):
super().__init__(apiKey, sharedSecret)
Expand Down
30 changes: 14 additions & 16 deletions src/rtmilk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,30 +79,28 @@
tasks.extend([_CreateFromTaskSeries(client, listId=list_.id, taskSeries=ts) for ts in list_.taskseries])
return tasks

class Client:
def CreateClient(clientId: str, clientSecret: str, token: str) -> _Client:
"""Create RTM client object synchronously"""
client = _Client(clientId, clientSecret, token)
client._CreateTimeline()
return client

async def CreateClientAsync(clientId: str, clientSecret: str, token: str) -> _Client:
"""Create RTM client object asynchronously"""
client = _Client(clientId, clientSecret, token)
await client._CreateTimelineAsync()
return client

Check warning on line 92 in src/rtmilk/client.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/client.py#L90-L92

Added lines #L90 - L92 were not covered by tests

class _Client:
"""Wraps the timeline and adds convenience functions to add and query tasks"""

@classmethod
def Create(cls, clientId: str, clientSecret: str, token: str) -> Client:
client = Client(clientId, clientSecret, token)
client._CreateTimeline()
return client

@classmethod
async def CreateAsync(cls, clientId: str, clientSecret: str, token: str) -> Client:
client = Client(clientId, clientSecret, token)
await client._CreateTimelineAsync()
return client

# TODO - pass timeline in constructor to at least prevent people who accidentally call this from making an invalid object?
# else change Client to _Client and make the factory functions free
def __init__(self, clientId: str, clientSecret: str, token: str):
self.api = API(clientId, clientSecret, token)
self.apiAsync = APIAsync(clientId, clientSecret, token)
self.timeline = None

def __repr__(self):
return 'Client()'
return '_Client()'

Check warning on line 103 in src/rtmilk/client.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/client.py#L103

Added line #L103 was not covered by tests

def _CreateTimeline(self):
self.timeline = _RaiseIfError(self.api.TimelinesCreate().timeline)
Expand Down
6 changes: 3 additions & 3 deletions src/rtmilk/mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from logging import getLogger

from listdiff import DiffUnsortedLists
from rtmilk.models import RTMError
from rtmilk.models import APIError

_log = getLogger(__name__)

Expand Down Expand Up @@ -43,7 +43,7 @@
# Move the due date first because you're more likely to be moving both dates to be later
_MirrorProperty(task.dueDate, taskData.dueDate)
_MirrorProperty(task.startDate, taskData.startDate)
except RTMError:
except APIError:

Check warning on line 46 in src/rtmilk/mirror.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/mirror.py#L46

Added line #L46 was not covered by tests
_MirrorProperty(task.startDate, taskData.startDate)
_MirrorProperty(task.dueDate, taskData.dueDate)
if taskData.complete is not None:
Expand All @@ -55,7 +55,7 @@
# Move the due date first because you're more likely to be moving both dates to be later
await _MirrorPropertyAsync(task.dueDate, taskData.dueDate)
await _MirrorPropertyAsync(task.startDate, taskData.startDate)
except RTMError:
except APIError:

Check warning on line 58 in src/rtmilk/mirror.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/mirror.py#L58

Added line #L58 was not covered by tests
await _MirrorPropertyAsync(task.startDate, taskData.startDate)
await _MirrorPropertyAsync(task.dueDate, taskData.dueDate)
if taskData.complete is not None:
Expand Down
12 changes: 7 additions & 5 deletions src/rtmilk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@

from ._utils import EmptyStrToNone

class RTMError(Exception):
class BaseError(Exception):
"""Base class for all errors"""

class APIError(BaseError):
"""Error documented in RTM API"""
def __init__(self, code, message):
super().__init__(self)
self.code = code
self.message = message

def __repr__(self):
return f'RTMError({self.code=}, {self.message=})'
return f'APIError({self.code=}, {self.message=})'

Check warning on line 23 in src/rtmilk/models.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/models.py#L23

Added line #L23 was not covered by tests

class ErrorData(BaseModel):
code: int
Expand All @@ -31,7 +35,7 @@

def _RaiseIfError(result):
if isinstance(result, FailStat):
raise RTMError(result.err.code, result.err.msg)
raise APIError(result.err.code, result.err.msg)

Check warning on line 38 in src/rtmilk/models.py

View check run for this annotation

Codecov / codecov/patch

src/rtmilk/models.py#L38

Added line #L38 was not covered by tests
return result

class EchoResponse(OkStat):
Expand Down Expand Up @@ -226,8 +230,6 @@
pending: bool
topics: Topic | list[str]

# SubscriptionPayload.model_rebuild()

class SubscriptionResponse(OkStat):
transaction: Transaction
subscription: SubscriptionPayload
Expand Down
6 changes: 2 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

from pytest import fixture

from rtmilk.api_sync import API
from rtmilk.api_async import APIAsync
from rtmilk.client import Client
from rtmilk import API, APIAsync, CreateClient

try:
from dotenv import load_dotenv
Expand Down Expand Up @@ -108,7 +106,7 @@ def taskCreator(client):
@fixture
def client():
apiKey, sharedSecret, token = _GetConfig()
return Client.Create(apiKey, sharedSecret, token)
return CreateClient(apiKey, sharedSecret, token)

@fixture
def mockClient():
Expand Down
2 changes: 1 addition & 1 deletion tests/generate_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from contextlib import suppress
from os import environ

from rtmilk.authorization import AuthorizationSession
from rtmilk import AuthorizationSession

try:
from dotenv import load_dotenv
Expand Down
2 changes: 1 addition & 1 deletion tests/test_api_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pydantic import ValidationError
from pytest import mark, raises

from rtmilk.models import AuthResponse, EchoResponse, FailStat, NotePayload, PriorityDirectionEnum, PriorityEnum, RTMList, RTMSmartList, Tags, TaskResponse, TaskSeries
from rtmilk import AuthResponse, EchoResponse, FailStat, NotePayload, PriorityDirectionEnum, PriorityEnum, RTMList, RTMSmartList, Tags, TaskResponse, TaskSeries
from rtmilk._sansio import TasksGetList

def test_validation(api, timeline):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_filter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import date
from uuid import uuid4

from rtmilk.filter import And, Due, NameIs, Or, Priority, PriorityEnum, Status
from rtmilk import And, Due, NameIs, Or, Priority, PriorityEnum, Status

def testFilterString():
assert And(NameIs('the-name'), Status(True)).Text() == '(name:"the-name") AND (status:completed)'
Expand Down
2 changes: 1 addition & 1 deletion tests/test_mirror.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime, timedelta

from rtmilk.mirror import Mirror, TaskData
from rtmilk import Mirror, TaskData

def testMirror(mockClient):
Mirror(mockClient, [], [TaskData('name')])
Expand Down
Loading