Skip to content

Commit

Permalink
API function wrappers return the parsed failure structure rather than…
Browse files Browse the repository at this point in the history
… throw an error
  • Loading branch information
rkhwaja committed May 23, 2024
1 parent a985a4d commit 81ebddd
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 45 deletions.
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,26 @@ except RTMError as e:
# Usage of API functions directly
```python
from rtmilk.api_sync import API
from rtmmilk.models import RTMError
from rtmmilk.models import FailStat

api = API(API_KEY, SHARED_SECRET, TOKEN)

timeline = api.TimelinesCreate().timeline
try:
api.TasksAdd(timeline, 'task name')
except RTMError as e:
print(e)
result = api.TasksAdd(timeline, 'task name')
if isinstance(result, FailStat):
print(f'Error: {result}')
```

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

apiAsync = APIAsync(API_KEY, SHARED_SECRET, TOKEN)

timeline = await apiAsync.TimelinesCreate().timeline
try:
await apiAsync.TasksAdd(timeline, 'task name')
except RTMError as e:
print(e)
result = await apiAsync.TasksAdd(timeline, 'task name')
if isinstance(result, FailStat):
print(f'Error: {result}')
```

# Authorization
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "rtmilk"
version = "1.0.4"
version = "2.0.0"
description = "RTM API wrapper"
authors = ["Rehan Khwaja <[email protected]>"]
license = "MIT"
Expand Down
8 changes: 5 additions & 3 deletions src/rtmilk/_sansio.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ def _RebuildArgs(**kwargs):
return result

def _ValidateReturn(type_, rsp):
_log.debug(f'Parsing {type_}:\n{pformat(rsp)}')
try:
_log.debug(f'Parsing {type_}:\n{pformat(rsp)}')
return type_(**rsp)
except ValidationError as e:
_log.error(f'Failed to validate against {type_}:\n{pformat(rsp)}\n{e}')
failStat = FailStat(**rsp)
raise RTMError(failStat.err.code, failStat.err.msg) from e
try:
return FailStat(**rsp)
except ValidationError as e:
raise RTMError from e

def ApiSig(sharedSecret, params):
sortedItems = sorted(params.items(), key=lambda x: x[0])
Expand Down
5 changes: 3 additions & 2 deletions src/rtmilk/authorization.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .api_sync import UnauthorizedAPI
from .models import _RaiseIfError
from ._sansio import ApiSig

_AUTHORIZATION_URL = 'https://www.rememberthemilk.com/services/auth/'
Expand All @@ -8,10 +9,10 @@ class AuthorizationSession:

def __init__(self, apiKey, sharedSecret, perms):
self._api = UnauthorizedAPI(apiKey, sharedSecret)
self._frob = self._api.AuthGetFrob()
self._frob = _RaiseIfError(self._api.AuthGetFrob())
params = {'api_key': apiKey, 'perms': perms, 'frob': self._frob}
params['api_sig'] = ApiSig(sharedSecret, params)
self.url = _AUTHORIZATION_URL + '?' + '&'.join([f'{k}={v}' for k, v in params.items()])

def Done(self):
return self._api.AuthGetToken(self._frob)
return _RaiseIfError(self._api.AuthGetToken(self._frob))
21 changes: 11 additions & 10 deletions src/rtmilk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .api_async import APIAsync
from .api_sync import API
from .models import _RaiseIfError
from ._properties import CompleteProperty, DueDateProperty, NameProperty, NotesProperty, StartDateProperty, TagsProperty

_log = getLogger(__name__)
Expand Down Expand Up @@ -35,18 +36,18 @@ def __repr__(self):
@validate_call
def Delete(self):
_log.info(f'{self}.Delete')
self._client.api.TasksDelete(timeline=self._client.timeline,
_RaiseIfError(self._client.api.TasksDelete(timeline=self._client.timeline,
list_id=self._listId,
taskseries_id=self._taskSeriesId,
task_id=self._taskId)
task_id=self._taskId))

@validate_call
async def DeleteAsync(self):
_log.info(f'{self}.DeleteAsync')
await self._client.apiAsync.TasksDelete(timeline=self._client.timeline,
_RaiseIfError(await self._client.apiAsync.TasksDelete(timeline=self._client.timeline,
list_id=self._listId,
taskseries_id=self._taskSeriesId,
task_id=self._taskId)
task_id=self._taskId))

# Serialize python datetime object to string for use by filters
def FilterDate(date_):
Expand Down Expand Up @@ -104,31 +105,31 @@ def __repr__(self):
return 'Client()'

def _CreateTimeline(self):
self.timeline = self.api.TimelinesCreate().timeline
self.timeline = _RaiseIfError(self.api.TimelinesCreate().timeline)

async def _CreateTimelineAsync(self):
self.timeline = await self.apiAsync.TimelinesCreate().timeline
self.timeline = _RaiseIfError(await self.apiAsync.TimelinesCreate().timeline)

@validate_call
def Get(self, filter_: str, lastSync: datetime | None = None) -> list[Task]:
_log.info(f'Get: {filter_}, {lastSync}')
listResponse = self.api.TasksGetList(filter=filter_, last_sync=lastSync)
listResponse = _RaiseIfError(self.api.TasksGetList(filter=filter_, last_sync=lastSync))
return _CreateListOfTasks(self, listResponse)

@validate_call
def Add(self, name: str) -> Task:
_log.info(f'Add: {name}')
taskResponse = self.api.TasksAdd(self.timeline, name)
taskResponse = _RaiseIfError(self.api.TasksAdd(self.timeline, name))
return _CreateFromTaskSeries(self, listId=taskResponse.list.id, taskSeries=taskResponse.list.taskseries[0])

@validate_call
async def GetAsync(self, filter_: str, lastSync: datetime | None = None) -> list[Task]:
_log.info(f'GetAsync: {filter_}, {lastSync}')
listResponse = await self.apiAsync.TasksGetList(filter=filter_, last_sync=lastSync)
listResponse = _RaiseIfError(await self.apiAsync.TasksGetList(filter=filter_, last_sync=lastSync))
return _CreateListOfTasks(self, listResponse)

@validate_call
async def AddAsync(self, name: str) -> Task:
_log.info(f'AddAsync: {name}')
taskResponse = await self.apiAsync.TasksAdd(self.timeline, name)
taskResponse = _RaiseIfError(await self.apiAsync.TasksAdd(self.timeline, name))
return _CreateFromTaskSeries(self, listId=taskResponse.list.id, taskSeries=taskResponse.list.taskseries[0])
5 changes: 5 additions & 0 deletions src/rtmilk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class FailStat(BaseModel):
stat: Annotated[str, StringConstraints(pattern='fail')]
err: ErrorData

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

class EchoResponse(OkStat):
__test__ = False # avoid pytest warning
method: Annotated[str, StringConstraints(pattern='rtm.test.echo')]
Expand Down
45 changes: 26 additions & 19 deletions tests/test_api_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from contextlib import suppress
from datetime import datetime, timedelta
from logging import info
from random import randint
from uuid import uuid4

from dateutil.tz import gettz
from pydantic import ValidationError
from pytest import fail, mark, raises
from pytest import mark, raises

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

def test_validation(api, timeline):
Expand Down Expand Up @@ -159,10 +158,8 @@ def test_add_and_delete_complex_task(api, timeline):
api.TasksDelete(timeline, task.list.id, task.list.taskseries[0].id, task.list.taskseries[0].task[0].id)

def test_delete_non_existing_task(api, timeline):
with raises(RTMError):
_ = api.TasksDelete(
timeline, '42',
'43', '')
result = api.TasksDelete(timeline, '42', '43', '')
assert isinstance(result, FailStat)

def test_tags(api, timeline, task):
listId = task.list.id
Expand Down Expand Up @@ -199,6 +196,23 @@ def test_dates(api, timeline, task):
updatedTask = api.TasksSetStartDate(timeline, listId, taskSeriesId, taskId, start=startDate2)
assert updatedTask.list.taskseries[0].task[0].start == startDate2

def test_set_wrong_start_due_dates(api, timeline, task):
listId = task.list.id
taskSeriesId = task.list.taskseries[0].id
taskId = task.list.taskseries[0].task[0].id

settings = api.SettingsGetList()
userTimezone = gettz(settings.settings.timezone)

dueDate = datetime(2021, 6, 1, 0, 0, 0, tzinfo=userTimezone)
startDate = datetime(2021, 7, 1, 0, 0, 0, tzinfo=userTimezone)

updatedTask = api.TasksSetDueDate(timeline, listId, taskSeriesId, taskId, due=dueDate)
assert updatedTask.list.taskseries[0].task[0].due == dueDate

updatedTask = api.TasksSetStartDate(timeline, listId, taskSeriesId, taskId, start=startDate)
assert isinstance(updatedTask, FailStat)

def test_get_list(api, task): # noqa: ARG001
allTasks = api.TasksGetList()
if allTasks.tasks.list is None:
Expand Down Expand Up @@ -252,25 +266,18 @@ def test_add_smart_list(api, timeline, newSmartList):
assert newSmartList.list.archived is False, newSmartList

# Can do this on the website but API *does* fail
try:
_ = api.ListsSetName(timeline, newSmartList.list.id, newSmartList.list.name + ' renamed')
fail('Should have failed to change the name on a smart list')
except RTMError:
pass
result = api.ListsSetName(timeline, newSmartList.list.id, newSmartList.list.name + ' renamed')
assert isinstance(result, FailStat), 'Should have failed to change the name on a smart list'

# Looks like you can't archive a smart list
try:
_ = api.ListsArchive(timeline, newSmartList.list.id)
fail('Should have failed to archive a smart list')
except RTMError:
pass
result = api.ListsArchive(timeline, newSmartList.list.id)
assert isinstance(result, FailStat), 'Should have failed to archive a smart list'

def test_subscriptions(api, timeline):
api.PushGetSubscriptions()
topics = api.PushGetTopics()
info(topics)
with suppress(RTMError):
api.PushSubscribe(timeline=timeline, url='https://hook.example', topics='task_created', filter='', push_format='json', lease_seconds='60')
api.PushSubscribe(timeline=timeline, url='https://hook.example', topics='task_created', filter='', push_format='json', lease_seconds='60')

def test_url():
fake = {'stat': 'ok',
Expand Down

0 comments on commit 81ebddd

Please sign in to comment.