Add python cli
This commit is contained in:
parent
c7cf71441f
commit
31f538c7f5
3
.gitignore
vendored
3
.gitignore
vendored
@ -133,3 +133,6 @@ app/src/main/resources/static/styles*
|
||||
|
||||
# lucene index
|
||||
.lucene/
|
||||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
38
cli/Config.py
Normal file
38
cli/Config.py
Normal 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
66
cli/SimpleNotesApi.py
Normal 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
112
cli/SimpleNotesCli.py
Executable 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
12
cli/domain.py
Normal 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
10
cli/jwtutils.py
Normal 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
6
cli/requirements.txt
Normal 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
32
cli/utils.py
Normal 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}")
|
||||
Loading…
x
Reference in New Issue
Block a user