Add python cli

This commit is contained in:
Hubert Van De Walle 2020-09-02 21:45:17 +02:00
parent c7cf71441f
commit 31f538c7f5
8 changed files with 279 additions and 0 deletions

3
.gitignore vendored
View File

@ -133,3 +133,6 @@ app/src/main/resources/static/styles*
# lucene index
.lucene/
# python
__pycache__

38
cli/Config.py Normal file
View File

@ -0,0 +1,38 @@
import configparser
import os
from pathlib import Path
import appdirs
import click
class Config:
def __init__(self):
config_dir: str = appdirs.user_config_dir("SimpleNotesCli")
Path(config_dir).mkdir(exist_ok=True)
self.file = os.path.join(config_dir, "config.ini")
self.configparser = configparser.ConfigParser()
self.token = None
self.base_url = None
def load(self):
self.configparser.read(self.file)
self.token = self.configparser.get("DEFAULT", "token", fallback=None)
self.base_url = self.configparser.get(
"DEFAULT", "base_url", fallback="https://simplenotes.be"
)
def save(self):
if self.token:
self.configparser.set("DEFAULT", "token", self.token)
if self.base_url:
self.configparser.set("DEFAULT", "base_url", self.base_url)
try:
with open(self.file, "w") as f:
self.configparser.write(f)
except IOError:
click.secho("An error occurred while saving config", fg="red", err=True)
exit(1)

66
cli/SimpleNotesApi.py Normal file
View File

@ -0,0 +1,66 @@
from typing import List, Optional
import click
import requests
from requests.models import Response
from domain import NoteMetadata
class SimplenotesApi:
def __init__(self, base_url: str):
self.base_url = base_url
self.s = requests.Session()
self.s.hooks["response"] = [self.__exit_unauthorized]
def __url(self, path: str) -> str:
return f"{self.base_url}{path}"
def __exit_unauthorized(self, response: Response, *args, **kwargs):
if response.status_code == 401:
click.secho("Unauthorized, please login again", fg="red", err=True)
exit(1)
def login(self, username: str, password: str) -> Optional[str]:
url = self.__url("/api/login")
r = self.s.post(
url,
json={"username": username, "password": password},
)
if r.status_code == 200:
return r.json()["token"]
def set_token(self, token: str):
self.s.headers.update({"Authorization": f"Bearer {token}"})
def find_note(self, uuid: str) -> Optional[str]:
url = self.__url(f"/api/notes/{uuid}")
r = self.s.get(url)
if r.status_code == 200:
return r.json()["markdown"]
def list_notes(self) -> List[NoteMetadata]:
url = self.__url("/api/notes")
r = self.s.get(url)
return list(map(lambda x: NoteMetadata(**x), r.json()))
def search_notes(self, query: str) -> List[NoteMetadata]:
url = self.__url("/api/notes/search")
r = self.s.post(url, json={"query": query})
if r.status_code == 200:
j = r.json()
return list(map(lambda x: NoteMetadata(**x), j))
else:
return []
def create_note(self, content: str) -> Optional[str]:
url = self.__url("/api/notes/")
r = self.s.post(url, json={"content": content})
if r.status_code == 200:
return r.json()["uuid"]
def update_note(self, uuid: str, content: str) -> bool:
url = self.__url(f"/api/notes/{uuid}")
r = self.s.put(url, json={"content": content})
return r.status_code == 200

112
cli/SimpleNotesCli.py Executable file
View File

@ -0,0 +1,112 @@
import click
from click import Context
import SimpleNotesApi
import utils
from Config import Config
from jwtutils import is_expired
from utils import edit_md
api: SimpleNotesApi
base_url: str
conf = Config()
@click.group()
@click.pass_context
def cli(ctx: Context):
global base_url
global api
conf.load()
base_url = conf.base_url
api = SimpleNotesApi.SimplenotesApi(base_url)
if ctx.invoked_subcommand == "login" or ctx.invoked_subcommand == "config":
return
token = conf.token
if token is None:
click.secho("Please login", err=True, fg="red")
exit(1)
elif is_expired(token):
click.secho("Login expired, please login again", err=True, fg="red")
exit(1)
else:
api.set_token(token)
@cli.command()
@click.option("--username", prompt=True)
@click.option("--password", prompt=True, hide_input=True)
def login(username: str, password: str):
token = api.login(username, password)
if token:
conf.token = token
conf.save()
click.secho(f"Welcome {username}", fg="green")
else:
click.echo("Invalid credentials")
exit(1)
@cli.command()
@click.option("--url", prompt=True)
def config(url: str):
conf.base_url = url
conf.save()
@cli.command(name="list")
def list_notes():
utils.print_notes(api.list_notes())
@cli.command()
@click.argument("uuid")
def edit(uuid: str):
note = api.find_note(uuid)
if not note:
click.secho("Note not found", err=True, fg="red")
exit(1)
edited = edit_md(note)
if edited == note:
exit(1)
if not api.update_note(uuid, edited):
click.secho("An error occurred", err=True, fg="red")
exit(1)
else:
utils.print_note_url(uuid, "updated", conf.base_url)
@cli.command()
def new():
placeholder = "---\ntitle: ''\ntags: []\n---\n"
md = edit_md(placeholder)
if md == placeholder:
exit(1)
uuid = api.create_note(md)
if uuid:
utils.print_note_url(uuid, "created", conf.base_url)
else:
click.secho("An error occurred", err=True, fg="red")
exit(1)
@cli.command(name="search")
@click.argument("search", nargs=-1, required=True)
def search_notes(search: str):
query = " ".join(search)
notes = api.search_notes(query)
if not notes:
print("No match")
else:
utils.print_notes(notes)
if __name__ == "__main__":
cli()

12
cli/domain.py Normal file
View File

@ -0,0 +1,12 @@
from typing import List
class NoteMetadata:
def __init__(self, uuid: str, title: str, tags: List[str], updatedAt: str):
self.uuid = uuid
self.title = title
self.tags = tags
self.updated_at = updatedAt
def __str__(self) -> str:
return f"NoteMetadata(uuid={self.uuid}, title={self.title}, tags={self.tags})"

10
cli/jwtutils.py Normal file
View File

@ -0,0 +1,10 @@
from datetime import datetime
import jwt
def is_expired(token: str) -> bool:
exp = jwt.decode(token, verify=False)["exp"]
dt = datetime.utcfromtimestamp(exp)
now = datetime.utcnow()
return dt < now

6
cli/requirements.txt Normal file
View File

@ -0,0 +1,6 @@
requests==2.24.0
python-editor==1.0.4
click==7.1.2
PyJWT==1.7.1
appdirs==1.4.4
tabulate==0.8.7

32
cli/utils.py Normal file
View File

@ -0,0 +1,32 @@
from typing import List
import editor
import click
from tabulate import tabulate
from domain import NoteMetadata
def edit_md(content: str = "") -> str:
b = bytes(content, "utf-8")
result = editor.edit(contents=b, suffix=".md")
return result.decode("utf-8")
def print_notes(notes: List[NoteMetadata]):
data = []
for n in notes:
uuid = click.style(n.uuid, fg="blue")
title = click.style(n.title, fg="green", bold=True)
tags = ["#" + e for e in n.tags]
tags = " ".join(tags)
data.append([uuid, title, tags])
headers = ["UUID", "title", "tags"]
click.echo(tabulate(data, headers=headers))
def print_note_url(uuid: str, action: str, base_url: str):
url = f"{base_url}/notes/{uuid}"
s = click.style(url, fg="green", underline=True)
click.echo(f"Note {action}: {s}")