aio Todoist

貓橘毛 aka Lanfon
6 min readMar 2, 2021

--

前陣子打算拿 Todoist 來當 backend (?!) 用,發些通知之類的,總之大概記錄一下有趣的東西 & Link 之類的雜七雜八。

Repo 在這,原本是打算直接發 PR 進官方版的API ,後來稍微改了 api.py 試了一下才發現事情要是拿摸簡單,還需要發廢文嗎QQ

詳細改到的 methods 都列在 README 裡面了,還是來說說實作的部份8

Coroutine

一般的情況就是直接轉成 async def ... 的 async function ,這樣在呼叫的時候就會直接回傳 coroutine ,但以最小改動 & 向前相容為原則來說,咱們還是能用的就加減用…

官方原本的 API 是直接 rely on requests.session ,也就是說實際上 user 本來就可以直接傳 aiohttp.ClientSession 用來轉成 async 的方式操作,但可惜就是(官方)沒有實作而已,所以我們來弄一個像這樣:

def _get(self, call, url=None, **kwargs):
url = url or self.get_api_url()
resp = self.session.get(url + call, **kwargs)
if iscoroutine(resp):
resp.close()
return self._get_async(call, url, **kwargs)
# ...後略,詳細見 AsyncTodoistAPI._get

簡單解,原本的 sync function 在拿到 response 之後檢查是不是摸到 coroutine ,如果是 coroutine 的話改由 async function 處理,這樣原本的 function 就可以直接升格(?)成支援 async-call (depends on your session)。

** resp.close() 是為了避免 coroutine never awaited.

Future

另一種作法是回傳 Future type (also awaitable),以下用 api.commit 來當例子:

def commit(self, raise_on_error=True):
def _callback(fut=None, ret=None):
try:
ret = ret or fut.result()
except Exception as e:
self.queue[:] = queue + self.queue[:]
raise e
# ...(後略)

if self.queue:
queue = self.queue[:]
ret = self.sync(commands=queue)
self.queue[:] = []
if iscoroutine(ret):
ret = ensure_future(ret)
ret.add_done_callback(_callback)
else:
_callback(ret=ret)
return ret

回傳的 asyncio.Future 在被 await 之後會直接拿到 fut.result() ,使用上和 coroutine (from async-function) 沒啥太大的差別。(雖然和 concurrent.futures 同名為 Future ,但使用的場合不太一樣)

api.commit 比較有趣的地方在於 self.queue 的刪除操作,在 async 的情況下得考量到拿到 exception 的時間比其他操作來得晚的問題。(that’s why _callback 裡面會把 queue 接回去)

實作考量

簡單原則(?),如果原本的設計就綁在同步(or maybe multi-threaded?)操作的話就不用這麼麻煩,寫新的 async-function (but maybe with similar signatures?) 就好,以官方的 API 來說,原本就沒有限定 session 的傳遞,所以才考慮直接堆在原本的 methods 上面。

第二個部份是 return Types,雖然 Python 是 duck typing ,但對使用者來說,新增後出現「新」的型別就會是一種阻礙(但把 async 堆上去,會多 awaitable 算是無法避免的)。

CoroutineFuture 的選擇倒是蠻隨意的(?),我自己的考量主要是以 function 最後回傳的東西為主,如果是來自 async-calls (例如 api.commit)的話,直接 wrap Future 會是比較簡單的作法(return 前的操作可以用 callback 解決);以 api._get 的例子來說,最後回傳的東西是 resp.json() or resp.text 兩種,直接 wrap Future 是不行的。(aiohttp 的這兩個都是 async methods)

但就算不是 async methods ,我自己應該還是會 prefer 另外實作 async-function ,add_done_callback 早期在 Future 還是以 Python 實作的時候是可以直接把 fut._result 替換成別的值,但 3.7 之後原則上都是 C 實作的版本了,還是別考慮從拿到的 future 來下手ㄅ…

雜七雜八

用 Mac commit 的時候發現 GPG sign 不過(Windows 就沒這問題哭哭),稍微研究了一下…把找到的東西就堆在這惹。

Create a GPG key on keybase.io
>
感覺很潮但完全沒用到的 Tutorial… maybe 改天會心血來潮註冊一下 keybase.io (?)

git — gpg onto mac osx: error: gpg failed to sign the data
> 最後沒有裝 gpg2 (我就已經有 gpg 了為什麼還要我移掉再裝 gpg2 !!!)
> 但裝了 pinentry-mac (雖然覺得好像沒用到)
> 最後的能 sign 的作法大概是:

$ echo "GPG_TTY=$(tty)"
# 原本沒這個第二步不會動 QQ, 後來 echo 進 bash_profile 惹
$ echo "test" | gpg --clearsign
# 確定 gpg 正常能動,這步會要輸入密碼(terminal prompt, but pinentry-mac didn't installed)
$ gpg --list-secret-keys --keyid-format LONG
# 這步會拿到一大串像是 https://stackoverflow.com/a/65232996 的圖
$ git config --global user.signing.key <上面看到的完整的 hash code>

** P.S. mac 裡面已經有能用的 key 了

其他什麼雜七雜八的像是寫進 gpg.conf 之類的根本不影響R…,pinentry-mac 裝起來可能有點影響吧,畢竟 commit 的時候跳的是 GUI prompt (輸入密碼),但感覺就算沒裝也可以跳 terminal prompt …

btw, git 可以用 git config --global commit.gpgsign true 來設定每個 commits 都要 gpgsign ,這樣就不用每次 commit 的時候在那邊多打一個 -S 惹,讚啦。

aio-todoist 可能這幾天稍微測一下之後補個 setup.py 就傳到 pypi 上面去惹,應該可能也許會再寫點 test cases 吧大概(?)

--

--

貓橘毛 aka Lanfon
貓橘毛 aka Lanfon

Written by 貓橘毛 aka Lanfon

知,不知,上;不知,知,病。

No responses yet