move from click cli -> pydantic-settings cli

This commit is contained in:
GitMatt 2025-10-02 08:20:12 -07:00
parent 5411e55c17
commit c0c53dc5fc
12 changed files with 177 additions and 261 deletions

View File

@ -6,8 +6,6 @@ authors = [{ name = "gitmatt", email = "git@publicmatt.com" }]
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"click>=8.1.8",
"click-default-group>=1.2.4",
"jinja2>=3.1.5", "jinja2>=3.1.5",
"lxml>=5.3.1", "lxml>=5.3.1",
"markdown>=3.7", "markdown>=3.7",
@ -18,7 +16,7 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
plan = "plan:cli.cli" plan = "plan:__main__.cli"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]

View File

@ -1,80 +0,0 @@
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from jinja2 import Environment, PackageLoader, select_autoescape
from .config import Config
config = Config()
def get_template(template_name: str = "plan.html", plan_path: Path | None = None):
""" """
templates = Environment(
loader=PackageLoader("plan"), autoescape=select_autoescape()
)
template = templates.get_template(template_name)
today = datetime.today().strftime("%Y-%m-%d")
return template.render(today=today, plan_path=plan_path)
def plan_path() -> Path:
"""
returns: Path to current date's .plan.md
"""
today = datetime.today().strftime("%Y-%m-%d")
path = config.app_path / f"{today}.md"
return path
def print_plan(plan_path: Path):
"""
send .plan.md to printer.
uses pandoc and lp shell tools.
"""
# preamble ="""
# ---
# documentclass: extarticle
# fontsize: 20pt
# ---
# """
cat_process = subprocess.Popen(["cat", str(plan_path)], stdout=subprocess.PIPE)
pandoc_process = subprocess.Popen(
["pandoc", "-t", "pdf", "-V", "geometry:margin=.5in", "-"],
stdin=cat_process.stdout,
stdout=subprocess.PIPE,
)
subprocess.run(
["lp", "-o", "sides=two-sided-long-edge", "-o", "number-up=2"],
stdin=pandoc_process.stdout,
)
def create_and_open_template(plan: Path, name: str = "plan.html") -> int:
plan.parent.mkdir(parents=True, exist_ok=True)
plan.touch()
template = get_template(name, plan_path=plan)
with plan.open("w") as f:
f.write(template)
editor = os.getenv("EDITOR", "nvim")
os.chdir(plan.parent)
p = subprocess.Popen([editor, str(plan)])
sys.exit(p.wait())
def open_in_editor(plan: Path):
plan.parent.mkdir(parents=True, exist_ok=True)
if not plan.exists():
plan.touch()
with plan.open("w") as f:
f.write(default_template(plan_path=plan))
editor = os.getenv("EDITOR", "nvim")
os.chdir(Path().home() / ".plan")
subprocess.run([editor, str(plan)])

10
src/plan/__main__.py Normal file
View File

@ -0,0 +1,10 @@
from pydantic_settings import (
CliApp,
)
from .cli import PlanCli
def cli():
app = CliApp()
app.run(PlanCli)

View File

@ -1,54 +1,20 @@
import sys from pydantic import Field
from pathlib import Path from pydantic_settings import (
BaseSettings,
import click CliApp,
from click_default_group import DefaultGroup CliSubCommand,
get_subcommand,
from .main import (
create_plan,
edit_plan,
plan_path,
print_plan,
) )
from .commands import main
@click.group(cls=DefaultGroup, default="main", default_if_no_args=True)
def cli():
pass
@cli.command("print") class PlanCli(BaseSettings):
@click.option( main_: CliSubCommand[main.Main] = Field(alias="main")
"--plan_path", print_: CliSubCommand[main.Print] = Field(alias="print")
"-p",
"plan_path",
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
default=plan_path(),
)
def send_to_printer(plan_path: Path):
print_plan(plan_path)
def cli_cmd(self) -> None:
@cli.command("main") if get_subcommand(self, is_required=False) is None:
@click.argument("template_name", required=True, default="plan.html") CliApp.run(main.Main)
@click.option( else:
"--print/--no-print", CliApp.run_subcommand(self)
"is_print",
is_flag=True,
default=False,
)
@click.option("--quiet", "quiet", type=bool, is_flag=True, default=True)
def open_today(template_name, is_print: bool = False, quiet: bool = True):
"""
open today
plan {:template|plan.html} {--quiet} {--print}
:template can be one of: {plan|work}.html
"""
path = plan_path()
content = create_plan(plan_path=path, name=template_name)
if is_print:
print(content)
else:
sys.exit(edit_plan(path))

View File

@ -0,0 +1,13 @@
from pathlib import Path
from pydantic import Field
from pydantic_settings import (
BaseSettings,
)
class Base(BaseSettings):
model_config = {"env_file": ".env", "env_prefix": "PLAN_", "extra": "ignore"}
base_path: Path = Field(
default=Path().home() / ".plan", description="base path for .plan files"
)

111
src/plan/commands/main.py Normal file
View File

@ -0,0 +1,111 @@
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import ClassVar
from pydantic import Field, computed_field
from pydantic_settings import (
CliImplicitFlag,
CliPositionalArg,
)
from plan.commands import Base
from plan.repository import PlanRepository
class Main(Base):
"""
open today's plan
:template can be one of: {plan|work}
"""
template_name: CliPositionalArg[str] = Field(default="plan")
@computed_field()
@property
def template(self) -> str:
return f"{self.template_name}.md"
print: CliImplicitFlag[bool] = Field(default=False)
def create(self, force=False) -> str:
plan_path = PlanRepository.path_for_date(base_path=self.base_path)
plan_path.parent.mkdir(parents=True, exist_ok=True)
if not plan_path.exists() or force:
content = PlanRepository.get_template(self.template)
with plan_path.open("w") as f:
f.write(content)
else:
content = self.get(plan_path)
return content
def get(self, plan_path: Path) -> str:
with plan_path.open("r") as f:
content = f.read()
return content
def edit(self) -> int:
plan_path = PlanRepository.path_for_date(base_path=self.base_path)
editor = os.getenv("EDITOR", "nvim")
os.chdir(plan_path.parent)
p = subprocess.Popen([editor, str(plan_path)])
return p.wait()
def cli_cmd(self) -> None:
content = self.create()
if self.print:
print(content)
else:
sys.exit(self.edit())
class Print(Base):
PREAMBLE: ClassVar[str] = """
---
documentclass: extarticle
fontsize: 20pt
---
"""
def print_plan(self, plan_path: Path):
"""
send .plan.md to printer.
uses pandoc and lp shell tools.
"""
preamble = subprocess.Popen(
["echo", "-n", self.PREAMBLE], stdout=subprocess.PIPE
)
cat = subprocess.Popen(
["cat", "-", str(plan_path)],
stdin=preamble.stdout,
stdout=subprocess.PIPE,
)
pandoc = subprocess.Popen(
["pandoc", "-t", "pdf", "-V", "geometry:margin=.5in", "-"],
stdin=cat.stdout,
stdout=subprocess.PIPE,
)
send = subprocess.Popen(
["lp", "-o", "sides=two-sided-long-edge", "-o", "number-up=2"],
stdin=pandoc.stdout,
)
sys.exit(send.wait())
def plan_path(self, date: datetime | None = None) -> Path:
"""
returns: Path to current date's .plan.md
"""
if date is None:
date = datetime.today()
today = date.strftime("%Y-%m-%d")
path = self.base_path / f"{today}.md"
return path
def cli_cmd(self):
path = self.plan_path()
self.print_plan(path)

View File

@ -1,14 +0,0 @@
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings
def default_app_path():
path = Path().home() / ".plan"
return path
class Config(BaseSettings):
model_config = {"env_file": ".env", "env_prefix": "PLAN_", "extra": "ignore"}
app_path: Path = Field(default_factory=default_app_path)

View File

@ -1,87 +0,0 @@
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from .config import Config
from jinja2 import Environment, PackageLoader, select_autoescape
config = Config()
def get_template(template_name: str = "plan.html", plan_path: Path | None = None):
""" """
templates = Environment(
loader=PackageLoader("plan"), autoescape=select_autoescape()
)
template = templates.get_template(template_name)
today = datetime.today().strftime("%Y-%m-%d")
return template.render(today=today, plan_path=plan_path)
def plan_path(date: datetime | None = None) -> Path:
"""
returns: Path to current date's .plan.md
"""
if date is None:
date = datetime.today()
today = date.strftime("%Y-%m-%d")
path = config.app_path / f"{today}.md"
return path
def print_plan(plan_path: Path):
"""
send .plan.md to printer.
uses pandoc and lp shell tools.
"""
preamble = """
---
documentclass: extarticle
fontsize: 20pt
---
"""
preamble = subprocess.Popen(["echo", "-n", preamble], stdout=subprocess.PIPE)
cat = subprocess.Popen(
["cat", "-", str(plan_path)],
stdin=preamble.stdout,
stdout=subprocess.PIPE,
)
pandoc = subprocess.Popen(
["pandoc", "-t", "pdf", "-V", "geometry:margin=.5in", "-"],
stdin=cat.stdout,
stdout=subprocess.PIPE,
)
send = subprocess.Popen(
["lp", "-o", "sides=two-sided-long-edge", "-o", "number-up=2"],
stdin=pandoc.stdout,
)
sys.exit(send.wait())
def create_plan(plan_path: Path, name: str = "plan.html", force=False) -> str:
plan_path.parent.mkdir(parents=True, exist_ok=True)
if not plan_path.exists() or force:
content = get_template(name, plan_path=plan_path)
with plan_path.open("w") as f:
f.write(content)
else:
content = get_plan(plan_path)
return content
def get_plan(plan_path: Path) -> str:
with plan_path.open("r") as f:
content = f.read()
return content
def edit_plan(plan_path: Path) -> int:
editor = os.getenv("EDITOR", "nvim")
os.chdir(plan_path.parent)
p = subprocess.Popen([editor, str(plan_path)])
return p.wait()

27
src/plan/repository.py Normal file
View File

@ -0,0 +1,27 @@
from datetime import datetime
from pathlib import Path
from jinja2 import Environment, PackageLoader, select_autoescape
class PlanRepository:
@classmethod
def path_for_date(cls, base_path: Path, date: datetime | None = None) -> Path:
"""
returns: Path to current date's .plan.md
"""
if date is None:
date = datetime.today()
today = date.strftime("%Y-%m-%d")
path = base_path / f"{today}.md"
return path
@classmethod
def get_template(cls, template_name: str = "plan.html"):
""" """
templates = Environment(
loader=PackageLoader("plan"), autoescape=select_autoescape()
)
template = templates.get_template(template_name)
today = datetime.today().strftime("%Y-%m-%d")
return template.render(today=today)

28
uv.lock
View File

@ -24,30 +24,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 },
] ]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "click-default-group"
version = "1.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123 },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@ -215,8 +191,6 @@ name = "plan"
version = "0.1.1" version = "0.1.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" },
{ name = "click-default-group" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "lxml" }, { name = "lxml" },
{ name = "markdown" }, { name = "markdown" },
@ -234,8 +208,6 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "click", specifier = ">=8.1.8" },
{ name = "click-default-group", specifier = ">=1.2.4" },
{ name = "jinja2", specifier = ">=3.1.5" }, { name = "jinja2", specifier = ">=3.1.5" },
{ name = "lxml", specifier = ">=5.3.1" }, { name = "lxml", specifier = ">=5.3.1" },
{ name = "markdown", specifier = ">=3.7" }, { name = "markdown", specifier = ">=3.7" },