Compare commits
5
Commits
f6397185c4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af14c80803 | ||
|
|
44549aaff3 | ||
|
|
e9cc2cc3d0 | ||
|
|
d745444609 | ||
|
|
50e9323b4b |
@@ -0,0 +1,9 @@
|
|||||||
|
# PyN
|
||||||
|
|
||||||
|
CLI tool for manipulating text notes
|
||||||
|
|
||||||
|
### Git support
|
||||||
|
|
||||||
|
If the notes directory is under git control then modifications will be auto staged.
|
||||||
|
|
||||||
|
You can then execute the `pyn sync` command to pull and push from the upstream server.
|
||||||
|
|||||||
+4
-1
@@ -3,6 +3,8 @@ from pyn.config import load_config
|
|||||||
|
|
||||||
from pyn import commands, todo # type: ignore
|
from pyn import commands, todo # type: ignore
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
options = vars(parser.parse_args())
|
options = vars(parser.parse_args())
|
||||||
@@ -22,7 +24,8 @@ def main():
|
|||||||
try:
|
try:
|
||||||
f(**options)
|
f(**options)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
parser.exit(e)
|
sys.stderr.write(f"{e}\n")
|
||||||
|
parser.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+64
-7
@@ -1,11 +1,19 @@
|
|||||||
from pyn.common import parser, logger
|
from pyn.common import parser, logger
|
||||||
from pyn.config import Config
|
from pyn.config import Config
|
||||||
from pyn import notes
|
from pyn import notes, templating
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
group = parser.add_subparsers(help="available commands")
|
group = parser.add_subparsers(help="available note commands")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config(config):
|
||||||
|
print(config)
|
||||||
|
|
||||||
|
|
||||||
|
show_config = group.add_parser("config", help="show config")
|
||||||
|
show_config.set_defaults(func=cmd_config)
|
||||||
|
|
||||||
### Add a note
|
### Add a note
|
||||||
|
|
||||||
@@ -16,11 +24,15 @@ def cmd_add_note(config, title, notebook):
|
|||||||
|
|
||||||
body = "" if sys.stdin.isatty() else sys.stdin.read()
|
body = "" if sys.stdin.isatty() else sys.stdin.read()
|
||||||
|
|
||||||
notes.append_template(config, filename, "note", title=title, body=body)
|
notes.append_template(filename, "note", title=title, body=body)
|
||||||
|
|
||||||
if not body:
|
if not body:
|
||||||
notes.edit_file(config, filename)
|
notes.edit_file(config, filename)
|
||||||
|
|
||||||
|
notes.run_git(
|
||||||
|
config, ["add", str(filename.relative_to(config.directory))], quiet=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
add_note = group.add_parser("add", aliases=["a"])
|
add_note = group.add_parser("add", aliases=["a"])
|
||||||
add_note.add_argument("--notebook", "-n", help="add to this notebook [general]")
|
add_note.add_argument("--notebook", "-n", help="add to this notebook [general]")
|
||||||
@@ -45,7 +57,7 @@ def cmd_del_note(config: Config, title: str, notebook: str, yes: bool) -> None:
|
|||||||
del_note = group.add_parser("delete", aliases=["del"])
|
del_note = group.add_parser("delete", aliases=["del"])
|
||||||
del_note.add_argument("--notebook", "-n", help="add to this notebook [general]")
|
del_note.add_argument("--notebook", "-n", help="add to this notebook [general]")
|
||||||
del_note.add_argument("--yes", action="store_true", help="dont prompt for confirmation")
|
del_note.add_argument("--yes", action="store_true", help="dont prompt for confirmation")
|
||||||
del_note.add_argument("title", help="title for note", nargs="?")
|
del_note.add_argument("title", help="title for note", nargs="*")
|
||||||
del_note.set_defaults(func=cmd_del_note)
|
del_note.set_defaults(func=cmd_del_note)
|
||||||
|
|
||||||
### List notes
|
### List notes
|
||||||
@@ -55,7 +67,7 @@ def cmd_list_notes(config: Config, notebook: str, pretty: bool) -> None:
|
|||||||
for filename in notes.get_file_list(config, notebook):
|
for filename in notes.get_file_list(config, notebook):
|
||||||
if pretty:
|
if pretty:
|
||||||
e = notes.file_entry(config, filename)
|
e = notes.file_entry(config, filename)
|
||||||
print(f"{e.notebook:>20s}: {e.filename}")
|
print(f"\033[32m{e.notebook:>20s}\033[0m: {e.filename}")
|
||||||
else:
|
else:
|
||||||
print(filename)
|
print(filename)
|
||||||
|
|
||||||
@@ -71,10 +83,13 @@ list_notes.set_defaults(func=cmd_list_notes)
|
|||||||
def cmd_edit_note(config, title, notebook: str) -> None:
|
def cmd_edit_note(config, title, notebook: str) -> None:
|
||||||
filename = notes.search_filenames(config, title, notebook)
|
filename = notes.search_filenames(config, title, notebook)
|
||||||
notes.edit_file(config, filename)
|
notes.edit_file(config, filename)
|
||||||
|
notes.run_git(
|
||||||
|
config, ["add", str(filename.relative_to(config.directory))], quiet=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
edit_note = group.add_parser("edit", aliases=["e"])
|
edit_note = group.add_parser("edit", aliases=["e"])
|
||||||
edit_note.add_argument("title", help="note title", nargs="?")
|
edit_note.add_argument("title", help="note title", nargs="*")
|
||||||
edit_note.add_argument("--notebook", "-n", help="limit to this notebook")
|
edit_note.add_argument("--notebook", "-n", help="limit to this notebook")
|
||||||
edit_note.set_defaults(func=cmd_edit_note)
|
edit_note.set_defaults(func=cmd_edit_note)
|
||||||
|
|
||||||
@@ -87,6 +102,48 @@ def cmd_show_note(config, title, notebook: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
show_note = group.add_parser("show", aliases=["s", "cat"])
|
show_note = group.add_parser("show", aliases=["s", "cat"])
|
||||||
show_note.add_argument("title", help="note title", nargs="?")
|
show_note.add_argument("title", help="note title", nargs="*")
|
||||||
show_note.add_argument("--notebook", "-n", help="limit to this notebook")
|
show_note.add_argument("--notebook", "-n", help="limit to this notebook")
|
||||||
show_note.set_defaults(func=cmd_show_note)
|
show_note.set_defaults(func=cmd_show_note)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_render_note(config, title, notebook: str) -> None:
|
||||||
|
filename = notes.search_filenames(config, title, notebook)
|
||||||
|
|
||||||
|
with open(filename, "r", encoding="utf-8") as input_file:
|
||||||
|
text = input_file.read()
|
||||||
|
|
||||||
|
html = templating.render_note(text, title=filename.stem)
|
||||||
|
|
||||||
|
print(html)
|
||||||
|
|
||||||
|
|
||||||
|
render_note = group.add_parser("render")
|
||||||
|
render_note.add_argument("title", help="note title", nargs="*")
|
||||||
|
render_note.add_argument("--notebook", "-n", help="limit to this notebook")
|
||||||
|
render_note.set_defaults(func=cmd_render_note)
|
||||||
|
|
||||||
|
### Git
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_git(config: Config, cmd: list[str]) -> None:
|
||||||
|
output = notes.run_git(config, cmd)
|
||||||
|
sys.stdout.write(output)
|
||||||
|
|
||||||
|
|
||||||
|
git = group.add_parser("git", help="run a git command in the note directory")
|
||||||
|
git.add_argument("cmd", help="git command", nargs="*")
|
||||||
|
git.set_defaults(func=cmd_git)
|
||||||
|
|
||||||
|
### Sync
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_sync(config: Config) -> None:
|
||||||
|
output = notes.run_git(config, ["pull"])
|
||||||
|
sys.stdout.write(f"Pull: {output}\n")
|
||||||
|
output = notes.run_git(config, ["push"])
|
||||||
|
sys.stdout.write(f"Push: {output}\n")
|
||||||
|
|
||||||
|
|
||||||
|
git = group.add_parser("sync", help="synchronise the notes")
|
||||||
|
git.set_defaults(func=cmd_sync)
|
||||||
|
|||||||
+45
-18
@@ -3,11 +3,12 @@ import pathlib
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pyn.config import Config
|
from pyn.config import Config
|
||||||
|
from pyn.common import logger
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import Any
|
from typing import Any, Generator
|
||||||
|
|
||||||
Entry = namedtuple("Entry", ("notebook", "filename"))
|
Entry = namedtuple("Entry", ("notebook", "filename"))
|
||||||
SearchResult = namedtuple("SearchResult", ("filename", "line_number", "snippet"))
|
SearchResult = namedtuple("SearchResult", ("filename", "line_number", "snippet"))
|
||||||
@@ -15,12 +16,13 @@ SearchResult = namedtuple("SearchResult", ("filename", "line_number", "snippet")
|
|||||||
NOTE_HEADING = """---
|
NOTE_HEADING = """---
|
||||||
date: {date}
|
date: {date}
|
||||||
notebook: {notebook}
|
notebook: {notebook}
|
||||||
---"""
|
---
|
||||||
|
"""
|
||||||
|
|
||||||
TEMPLATES = {
|
TEMPLATES = {
|
||||||
"heading": NOTE_HEADING,
|
"heading": NOTE_HEADING,
|
||||||
"note": "\n\n# {title}\n\n{body}\n---",
|
"note": "\n# {title}\n\n{body}\n---\n",
|
||||||
"todo": "\n* [ ] {item}",
|
"todo": "* [ ] {item}\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +46,6 @@ def get_note_filename(
|
|||||||
notebook_path.mkdir(parents=True, exist_ok=True)
|
notebook_path.mkdir(parents=True, exist_ok=True)
|
||||||
note_path.touch()
|
note_path.touch()
|
||||||
append_template(
|
append_template(
|
||||||
config,
|
|
||||||
note_path,
|
note_path,
|
||||||
"heading",
|
"heading",
|
||||||
date=now.isoformat(timespec="seconds"),
|
date=now.isoformat(timespec="seconds"),
|
||||||
@@ -54,9 +55,7 @@ def get_note_filename(
|
|||||||
return note_path
|
return note_path
|
||||||
|
|
||||||
|
|
||||||
def append_template(
|
def append_template(filename: pathlib.Path, template: str, **context: Any) -> None:
|
||||||
config: Config, filename: pathlib.Path, template: str, **context: dict[str, Any]
|
|
||||||
) -> None:
|
|
||||||
rendered = TEMPLATES[template].format(**context)
|
rendered = TEMPLATES[template].format(**context)
|
||||||
with open(filename, "a") as f:
|
with open(filename, "a") as f:
|
||||||
f.write(rendered)
|
f.write(rendered)
|
||||||
@@ -64,26 +63,39 @@ def append_template(
|
|||||||
|
|
||||||
def edit_file(config: Config, filename: pathlib.Path) -> None:
|
def edit_file(config: Config, filename: pathlib.Path) -> None:
|
||||||
cmd = config.editor.copy()
|
cmd = config.editor.copy()
|
||||||
cmd.append(filename)
|
cmd.append(str(filename))
|
||||||
subprocess.check_call(cmd)
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
|
||||||
def get_file_list(config: Config, notebook: str = None) -> list[pathlib.Path]:
|
def get_file_list(
|
||||||
|
config: Config, notebook: str | None = None, show_hidden: bool = False
|
||||||
|
) -> Generator[pathlib.Path]:
|
||||||
root = config.directory / notebook if notebook else config.directory
|
root = config.directory / notebook if notebook else config.directory
|
||||||
|
|
||||||
for d, _, files in root.walk():
|
for d, dirs, files in root.walk():
|
||||||
|
if not show_hidden:
|
||||||
|
dirs[:] = [d for d in dirs if d[0] != "."]
|
||||||
|
|
||||||
for filename in files:
|
for filename in files:
|
||||||
yield d / filename
|
if filename.endswith(".md"):
|
||||||
|
yield d.relative_to(root) / filename
|
||||||
|
|
||||||
|
|
||||||
def search_filenames(config: Config, title: str, notebook: str = None) -> pathlib.Path:
|
def search_filenames(
|
||||||
|
config: Config, title: str | list[str], notebook: str | None = None
|
||||||
|
) -> pathlib.Path:
|
||||||
root = config.directory / notebook if notebook else config.directory
|
root = config.directory / notebook if notebook else config.directory
|
||||||
|
|
||||||
|
files = "\n".join((str(x) for x in get_file_list(config, notebook)))
|
||||||
|
|
||||||
|
if isinstance(title, list):
|
||||||
|
title = " ".join(title)
|
||||||
|
|
||||||
cmd = ["fzf", "-1", "-i"]
|
cmd = ["fzf", "-1", "-i"]
|
||||||
if title:
|
if title:
|
||||||
cmd.extend(["-q", title])
|
cmd.extend(["-q", title])
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(cmd, cwd=root)
|
output = subprocess.check_output(cmd, cwd=root, input=files.encode())
|
||||||
file_path = root / output.decode("utf-8").strip()
|
file_path = root / output.decode("utf-8").strip()
|
||||||
return file_path
|
return file_path
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
@@ -97,7 +109,7 @@ def file_entry(config: Config, filepath: pathlib.Path) -> Entry:
|
|||||||
return Entry(str(relative.parent), relative.name)
|
return Entry(str(relative.parent), relative.name)
|
||||||
|
|
||||||
|
|
||||||
def cat_files(*filenames: list[pathlib.Path]) -> None:
|
def cat_files(*filenames: pathlib.Path) -> None:
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
with open(filename, "r") as f:
|
with open(filename, "r") as f:
|
||||||
copyfileobj(f, sys.stdout)
|
copyfileobj(f, sys.stdout)
|
||||||
@@ -106,10 +118,10 @@ def cat_files(*filenames: list[pathlib.Path]) -> None:
|
|||||||
def search_contents(
|
def search_contents(
|
||||||
config: Config,
|
config: Config,
|
||||||
query: str,
|
query: str,
|
||||||
notebook: str = None,
|
notebook: str | None = None,
|
||||||
file_matcher: str = None,
|
file_matcher: str = "*.md",
|
||||||
full_line: bool = False,
|
full_line: bool = False,
|
||||||
) -> list[SearchResult]:
|
) -> Generator[SearchResult]:
|
||||||
root = config.directory / notebook if notebook else config.directory
|
root = config.directory / notebook if notebook else config.directory
|
||||||
|
|
||||||
cmd = ["grep", "-rFn", query, str(root)]
|
cmd = ["grep", "-rFn", query, str(root)]
|
||||||
@@ -118,6 +130,7 @@ def search_contents(
|
|||||||
if full_line:
|
if full_line:
|
||||||
cmd.append("-x")
|
cmd.append("-x")
|
||||||
|
|
||||||
|
logger.debug("Executing: %r", cmd)
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(cmd)
|
output = subprocess.check_output(cmd)
|
||||||
for line in output.decode("utf-8").strip().split("\n"):
|
for line in output.decode("utf-8").strip().split("\n"):
|
||||||
@@ -127,3 +140,17 @@ def search_contents(
|
|||||||
if e.returncode == 1:
|
if e.returncode == 1:
|
||||||
return
|
return
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def run_git(config: Config, cmd: list[str], quiet: bool = False) -> str:
|
||||||
|
if not (config.directory / ".git").is_dir():
|
||||||
|
if quiet:
|
||||||
|
return ""
|
||||||
|
raise RuntimeError("Notes not currently tracked")
|
||||||
|
|
||||||
|
cmd = ["git", "-C", str(config.directory)] + cmd
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||||
|
return output.decode("utf-8").strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise RuntimeError(str(e))
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<!doctype html5>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<style>
|
||||||
|
BODY {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
DIV.page {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
HR {
|
||||||
|
color: #DDD;
|
||||||
|
}
|
||||||
|
H1, H2, H3 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
P {
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
H1 {
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
||||||
|
H2 {
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
H3 {
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
DIV.page P:first-of-type {
|
||||||
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
{{ content|safe }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
env = Environment(loader=PackageLoader("pyn"), autoescape=select_autoescape())
|
||||||
|
|
||||||
|
|
||||||
|
def render_note(note: str, title: str = "") -> str:
|
||||||
|
content = markdown.markdown(note)
|
||||||
|
|
||||||
|
template = env.get_template("render_note.html")
|
||||||
|
return template.render(content=content, title=title)
|
||||||
+16
-9
@@ -1,9 +1,13 @@
|
|||||||
from pyn.common import parser, logger
|
|
||||||
from pyn.config import Config
|
from pyn.config import Config
|
||||||
from pyn import notes
|
from pyn import notes
|
||||||
from pyn.commands import group as parent_group
|
from pyn.commands import group as parent_group
|
||||||
from shutil import get_terminal_size
|
from shutil import get_terminal_size
|
||||||
|
import sys
|
||||||
|
|
||||||
|
LIST_CHAR = "-"
|
||||||
|
COMPLETE_CHAR = "x"
|
||||||
|
PENDING_PATTERN = r"^[+\-\*]\s+\[ \]"
|
||||||
|
COMPLETED_PATTERN = r"^[+\-\*]\s+\[" + COMPLETE_CHAR + r"\]"
|
||||||
|
|
||||||
p = parent_group.add_parser("todo", help="manipulate todo items")
|
p = parent_group.add_parser("todo", help="manipulate todo items")
|
||||||
group = p.add_subparsers()
|
group = p.add_subparsers()
|
||||||
@@ -12,9 +16,9 @@ group = p.add_subparsers()
|
|||||||
|
|
||||||
|
|
||||||
def cmd_add_todo(config: Config, item: list[str], notebook: str, create: bool) -> None:
|
def cmd_add_todo(config: Config, item: list[str], notebook: str, create: bool) -> None:
|
||||||
item = " ".join(item)
|
item_str = " ".join(item)
|
||||||
filename = notes.get_note_filename(config, "todo", notebook)
|
filename = notes.get_note_filename(config, "todo", notebook)
|
||||||
notes.append_template(config, filename, "todo", item=item)
|
notes.append_template(filename, "todo", item=item_str)
|
||||||
|
|
||||||
|
|
||||||
add_todo = group.add_parser("add", aliases=["a"])
|
add_todo = group.add_parser("add", aliases=["a"])
|
||||||
@@ -29,15 +33,18 @@ add_todo.set_defaults(func=cmd_add_todo)
|
|||||||
def cmd_list_pending(config: Config, notebook: str):
|
def cmd_list_pending(config: Config, notebook: str):
|
||||||
nb = None
|
nb = None
|
||||||
cols, _ = get_terminal_size()
|
cols, _ = get_terminal_size()
|
||||||
fmt = "{snippet:" + str(cols - 20) + "s} {name}"
|
|
||||||
for result in notes.search_contents(
|
for result in notes.search_contents(
|
||||||
config, "* [ ]", notebook, file_matcher="*-todo.md"
|
config, "* [ ]", notebook, file_matcher="*-todo.md"
|
||||||
):
|
):
|
||||||
entry = notes.file_entry(config, result.filename)
|
entry = notes.file_entry(config, result.filename)
|
||||||
if entry.notebook != nb:
|
if entry.notebook != nb:
|
||||||
nb = entry.notebook
|
nb = entry.notebook
|
||||||
print(f"\nNotebook: {entry.notebook}")
|
print(f"\nNotebook: \033[32m{entry.notebook}\033[0m")
|
||||||
print(fmt.format(snippet=result.snippet, name=result.filename.name))
|
name = result.filename.name
|
||||||
|
sys.stdout.write(result.snippet)
|
||||||
|
sys.stdout.write(" " * (cols - len(result.snippet) - len(name) - 1))
|
||||||
|
sys.stdout.write(name)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
|
||||||
|
|
||||||
pending_todo = group.add_parser("pending", aliases="p", help="list pending tasks")
|
pending_todo = group.add_parser("pending", aliases="p", help="list pending tasks")
|
||||||
@@ -56,10 +63,10 @@ def cmd_mark_complete(
|
|||||||
if reverse:
|
if reverse:
|
||||||
checked, unchecked = unchecked, checked
|
checked, unchecked = unchecked, checked
|
||||||
|
|
||||||
item = " ".join(item)
|
item_str = " ".join(item)
|
||||||
results = list(
|
results = list(
|
||||||
notes.search_contents(
|
notes.search_contents(
|
||||||
config, f"* [{unchecked}] {item}", notebook, full_line=True
|
config, f"* [{unchecked}] {item_str}", notebook, full_line=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +76,7 @@ def cmd_mark_complete(
|
|||||||
if not multiple and len(results) > 1:
|
if not multiple and len(results) > 1:
|
||||||
raise RuntimeError("Multiple items matched - use --multiple to allow update")
|
raise RuntimeError("Multiple items matched - use --multiple to allow update")
|
||||||
|
|
||||||
update = f"* [{checked}] {item}"
|
update = f"* [{checked}] {item_str}"
|
||||||
for result in results:
|
for result in results:
|
||||||
entry = notes.file_entry(config, result.filename)
|
entry = notes.file_entry(config, result.filename)
|
||||||
with open(result.filename, "r") as f:
|
with open(result.filename, "r") as f:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ authors = [
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"markdown (>=3.10.1,<4.0.0)",
|
||||||
|
"jinja2 (>=3.1.6,<4.0.0)"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
Reference in New Issue
Block a user