diff --git a/README.md b/README.md index 864db3b..a3e0a83 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,12 @@ Run continuously and push new annotations to Readwise every 60 minutes. wallabag2readwise daemon --wait-time 60 ``` +#### Import all Wallabag entries to Readwise reader + +```bash +wallabag2readwise reader +``` + ### Configuration Get a new Readwise API Token from . diff --git a/wallabag2readwise/cli.py b/wallabag2readwise/cli.py index b3888ee..3e31192 100644 --- a/wallabag2readwise/cli.py +++ b/wallabag2readwise/cli.py @@ -1,6 +1,6 @@ import typer from wallabag2readwise.misc import push_annotations -from wallabag2readwise.readwise import ReadwiseConnector +from wallabag2readwise.readwise import ReadwiseConnector, ReadwiseReaderConnector from wallabag2readwise.wallabag import WallabagConnector from time import sleep @@ -46,7 +46,7 @@ def daemon( wait_time: int = typer.Option( 60, help='time to wait between runs in minutes', envvar='WAIT_TIME' ), -): +) -> None: console.print(f'> Starting daemon with {wait_time} minutes wait time') while True: @@ -64,6 +64,32 @@ def daemon( sleep(wait_time * 60) +@app.command() +def reader( + wallabag_url: str = typer.Option( + ..., envvar='WALLABAG_URL', help='url to your wallabag instance' + ), + wallabag_user: str = typer.Option(..., envvar='WALLABAG_USER'), + wallabag_password: str = typer.Option(..., envvar='WALLABAG_PASSWORD', prompt=True), + wallabag_client_id: str = typer.Option(..., envvar='WALLABAG_CLIENT_ID'), + wallabag_client_secret: str = typer.Option(..., envvar='WALLABAG_CLIENT_SECRET'), + readwise_token: str = typer.Option(..., envvar='READWISE_TOKEN', prompt=True), +): + console.print(f'> Starting readwise reader import') + wallabag = WallabagConnector( + wallabag_url, + wallabag_user, + wallabag_password, + wallabag_client_id, + wallabag_client_secret, + ) + readwise = ReadwiseReaderConnector(readwise_token) + wallabag_entries = wallabag.get_entries() + for entry in wallabag_entries: + console.print(f'=> Importing {entry.title}') + readwise.create(entry.url, tags=[t.label for t in entry.tags]) + + @app.command() def version(): console.print(importlib.metadata.version('wallabag2readwise')) diff --git a/wallabag2readwise/readwise.py b/wallabag2readwise/readwise.py index 42660a2..6076584 100644 --- a/wallabag2readwise/readwise.py +++ b/wallabag2readwise/readwise.py @@ -6,6 +6,7 @@ from ratelimit import limits, RateLimitException, sleep_and_retry from backoff import on_exception, expo from time import sleep +from dataclasses import dataclass from wallabag2readwise.models import ( WallabagAnnotation, @@ -173,3 +174,61 @@ def new_highlights( note=item.text, category='articles', ) + + +class ReadwiseReaderConnector: + def __init__( + self, + token: str, + ): + self.token = token + self.url = 'https://readwise.io/api/v3' + + @property + def _session(self) -> requests.Session: + session = requests.Session() + session.headers.update( + { + 'Accept': 'application/json', + 'Authorization': f'Token {self.token}', + } + ) + return session + + @on_exception(expo, RateLimitException, max_tries=8) + @sleep_and_retry + @limits(calls=20, period=60) + def _request( + self, method: str, endpoint: str, params: dict = {}, data: dict = {} + ) -> requests.Response: + url = self.url + endpoint + logger.debug(f'Calling "{method}" on "{url}" with params: {params}') + response = self._session.request(method, url, params=params, json=data) + while response.status_code == 429: + seconds = int(response.headers['Retry-After']) + logger.warning(f'Rate limited by Readwise, retrying in {seconds} seconds') + sleep(seconds) + response = self._session.request(method, url, params=params, data=data) + response.raise_for_status() + return response + + def get(self, endpoint: str, params: dict = {}) -> requests.Response: + logger.debug(f'Getting "{endpoint}" with params: {params}') + return self._request('GET', endpoint, params=params) + + def post(self, endpoint: str, data: dict = {}) -> requests.Response: + url = self.url + endpoint + logger.debug(f'Posting "{url}" with data: {data}') + response = self._request('POST', endpoint, data=data) + response.raise_for_status() + return response + + def create(self, url: str, saved_using: str = 'wallabag', tags: list[str] = []): + _ = self.post( + '/save/', + { + 'url': url, + 'saved_using': saved_using, + 'tags': tags, + }, + )