Skip to content

Commit

Permalink
feat: add CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinHjelmare committed May 20, 2022
1 parent 3bb9347 commit 8955f32
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 6 deletions.
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ readme = "README.md"
repository = "https://github.com/MartinHjelmare/aiortm"
version = "0.1.0"

[tool.poetry.scripts]
aiortm = 'aiortm.cli:cli'

[tool.poetry.urls]
"Bug Tracker" = "https://github.com/MartinHjelmare/aiortm/issues"
"Changelog" = "https://github.com/MartinHjelmare/aiortm/blob/main/CHANGELOG.md"

[tool.poetry.dependencies]
aiohttp = "^3.8"
click = "^8.1"
python = "^3.9"
yarl = "^1.7"

Expand Down
26 changes: 26 additions & 0 deletions src/aiortm/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Provide a CLI for aiortm."""
import logging

import click

from .. import __version__
from .app import authorize, check_token

SETTINGS = {"help_option_names": ["-h", "--help"]}


@click.group(
options_metavar="", subcommand_metavar="<command>", context_settings=SETTINGS
)
@click.option("--debug", is_flag=True, help="Start aiortm in debug mode.")
@click.version_option(__version__)
def cli(debug: bool) -> None:
"""Run aiortm as an app for testing purposes."""
if debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)


cli.add_command(authorize)
cli.add_command(check_token)
85 changes: 85 additions & 0 deletions src/aiortm/cli/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Provide an application to run the client."""
from __future__ import annotations

import asyncio
import logging
import webbrowser
from collections.abc import Awaitable, Callable
from typing import Any

import aiohttp
import click

from ..client import Auth

LOGGER = logging.getLogger("aiortm")


@click.command(options_metavar="<options>")
@click.option("-k", "--api_key", required=True, help="API key.")
@click.option("-s", "--secret", required=True, help="Shared secret.")
def authorize(api_key: str, secret: str) -> None:
"""Authorize the app."""
run_app(authorize_app, api_key, secret)


@click.command(options_metavar="<options>")
@click.option("-k", "--api_key", required=True, help="API key.")
@click.option("-s", "--secret", required=True, help="Shared secret.")
@click.option("-t", "--token", required=True, help="Authentication token.")
def check_token(api_key: str, secret: str, token: str) -> None:
"""Check if the authentication token is valid."""
run_app(check_auth_token, api_key, secret, token=token)


def run_app(
command: Callable[..., Awaitable[None]],
api_key: str,
secret: str,
**kwargs: Any,
) -> None:
"""Run the app."""
LOGGER.debug("Starting app")
try:
asyncio.run(command(api_key, secret, **kwargs))
except KeyboardInterrupt:
pass
finally:
LOGGER.debug("Exiting app")


async def authorize_app(api_key: str, secret: str, **kwargs: Any) -> None:
"""Authorize the application."""
async with aiohttp.ClientSession() as session:
auth = Auth(client_session=session, api_key=api_key, shared_secret=secret)

url, frob = await auth.authenticate_desktop()

loop = asyncio.get_running_loop()
await loop.run_in_executor(None, webbrowser.open, url)

if not click.confirm("Have you authorized this app at RTM?"):
click.echo("Exiting")
return

result = await auth.get_token(frob)
token = result["token"]

click.echo(f"token: {token}")


async def check_auth_token(
api_key: str, secret: str, token: str, **kwargs: Any
) -> None:
"""Check the authentication token."""
async with aiohttp.ClientSession() as session:
auth = Auth(
client_session=session,
api_key=api_key,
shared_secret=secret,
auth_token=token,
)

result = await auth.check_token()

click.echo(f"token is valid: {result}")
16 changes: 13 additions & 3 deletions src/aiortm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from __future__ import annotations

import hashlib
import json
import logging
from http import HTTPStatus
from typing import Any, cast

Expand All @@ -17,6 +19,7 @@

AUTH_URL = "https://www.rememberthemilk.com/services/auth/"
REST_URL = "https://api.rememberthemilk.com/services/rest/"
_LOGGER = logging.getLogger(__package__)


class Auth:
Expand Down Expand Up @@ -95,9 +98,9 @@ async def call_api_auth(self, api_method: str, **params: Any) -> dict[str, Any]:

async def call_api(self, api_method: str, **params: Any) -> dict[str, Any]:
"""Call an api method."""
all_params = params | {"format": "json"}
all_params = {"method": api_method} | params | {"format": "json"}
all_params |= {"api_sig": self._sign_request(all_params)}
response = await self.request(REST_URL, method=api_method, **all_params)
response = await self.request(REST_URL, params=all_params)

try:
response.raise_for_status()
Expand All @@ -106,7 +109,14 @@ async def call_api(self, api_method: str, **params: Any) -> dict[str, Any]:
raise TransportAuthError(err) from err
raise TransportResponseError(err) from err

data: dict[str, Any] = (await response.json())["rsp"]
response_text = await response.text()

if "rtm.auth" not in api_method:
_LOGGER.debug("Response text: %s", response_text)

# API doesn't return a JSON encoded response.
# It's text/javascript mimetype but with a JSON string in the text.
data: dict[str, Any] = json.loads(response_text)["rsp"]

if data["stat"] == "fail":
code = data["err"]["code"]
Expand Down

0 comments on commit 8955f32

Please sign in to comment.