ããã®ä»äºã3æ¥åŸã§ãè¯ããããšæã£ãŠå å»¶ã°ãããããããå¿ããŠããŸã£ããšããçµéšã¯ãªãã ãããããããªãšãã«ãã¿ãŒããã«ãã䜿ããç°¡åãªãªãã€ã³ããŒCLIãäœã£ãŠã¿ãããPythonã®ã³ãã³ãã©ã€ã³ããŒã«ã®äœãæ¹ãããã¡ã€ã«æäœã®æ¹æ³ãåŠã¶è¯ãæ©äŒã«ããªãã ããã
Pythonã©ã€ãã©ãªãTyperãã§æè»œã«CLIããŒã«ãäœãã
ä»åã¯ãæè»œãªã³ãã³ãã©ã€ã³ããŒã«(CLI)ãäœãããã«ãPythonã©ã€ãã©ãªã®ãTyperãã䜿ã£ãŠã¿ãããTyperã¯Pythonã§ã³ãã³ãã©ã€ã³ããŒã«ãç°¡åã«äœãããã®å°çšã©ã€ãã©ãªã ãç¹åŸŽã¯ãPythonã®åãã³ãã䜿ã£ãŠãã³ãã³ãã®åŒæ°ããªãã·ã§ã³ãèªç¶ã«å®çŸ©ã§ããç¹ã«ããã
Typerãã€ã³ã¹ããŒã«ããã«ã¯ãã¿ãŒããã«(WindowsãªãPowerShellãmacOSãªãã¿ãŒããã«.app)ã§äžèšã®ãããªã³ãã³ããå®è¡ãããããŸããCLIã§ã®åºåãèŠãããããããã®ã©ã€ãã©ãªãRichããäžç·ã«ã€ã³ã¹ããŒã«ãããã
# Pythonããã±ãŒãžãã€ã³ã¹ããŒã«ãã
pip install typer==0.25.1
pip install rich==15.0.0
Næ¥åŸãèšç®ããã«ã¯ïŒ
Næ¥åŸã®æ¥æãèšç®ããã«ã¯ãPythonã®æšæºã©ã€ãã©ãªã§ãããdatetimeãã䜿ããšäŸ¿å©ã ãdatetimeã䜿ããšãçŸåšã®æ¥ä»ãæéãååŸããããæ¥ä»ãå ç®ãããããããšãç°¡åã«ã§ãããäŸãã°ãçŸåšã®æ¥ä»ããNæ¥åŸã®æ¥ä»ãèšç®ããã«ã¯ã以äžã®ãããªã³ãŒãã䜿ããã
from datetime import datetime, timedelta
def calc_n_days(days: int) -> datetime:
"""Næ¥åŸã®æ¥ä»ãèšç®ãã颿°"""
current_date = datetime.now()
future_date = current_date + timedelta(days=days)
return future_date
d = calc_n_days(3)
print(f"çŸåšã®æ¥ä»: {datetime.now()}")
print(f"3æ¥åŸã®æ¥ä»: {d}")
ããã§å®çŸ©ãã颿°calcndays(days:int)ã¯ãåŒæ°ãšããŠNæ¥ãåãåããçŸåšã®æ¥ä»ã«ãã®æ¥æ°ãå ç®ããŠæªæ¥ã®æ¥ä»ãè¿ããããããªãã€ã³ããŒCLIã®äžã§äœ¿ãããšã§ããŠãŒã¶ãŒãæå®ããNæ¥åŸã®æ¥æãç°¡åã«èšç®ã§ããããã«ãªãã
Pythonã®REPL(峿å®è¡ã¢ãŒã)ã§ããã®ã³ãŒããå®è¡ããŠã¿ããããããšãçŸåšã®æ¥ä»ãš3æ¥åŸã®æ¥ä»ã衚瀺ãããã¯ãã ãããããªãã€ã³ããŒCLIã®äžã§æŽ»çšããŠããŠãŒã¶ãŒãæå®ããNæ¥åŸã«éç¥ããæ©èœãå®è£ ããŠã¿ããã
ãšããã§ã3æ¥åŸã«ã¿ã¹ã¯ãå®è¡ããããšããå Žåãå€ãã¯16æã«ç»é²ãããšããŠãããã®æ¥ã®æã«ã¿ã¹ã¯ãéç¥ããŠæ¬²ããã¯ãã ãããã§ãNæ¥åŸã®æ¥æãèšç®ããéã«ã¯ãæéãèæ ®ããŠãäŸãã°16æã«ç»é²ããå Žåã¯ã3æ¥åŸã®16æã§ã¯ãªãããã®æ¥ã®æ8æã«éç¥ãããããªããžãã¯ãçµã¿èŸŒãããã«ãããããã®ãããäžèšã®é¢æ°ããæ¬¡ã®ããã«ä¿®æ£ããã
def calc_n_days(days: int) -> datetime:
"""Næ¥åŸã®æ8æã®æ¥ä»æå»ãèšç®ãã颿°"""
current_date = datetime.now()
future_date = current_date + timedelta(days=days)
future_date_8am = future_date.replace(
hour=8,
minute=0,
second=0,
microsecond=0
)
return future_date_8am
ããã°ã©ã ã宿ãããã
ããã§ã¯ãããã°ã©ã ã宿ããããã以äžã®ããã°ã©ã ã¯ãã¿ã¹ã¯ã远å ããããæéãæ¥ãã¿ã¹ã¯ã衚瀺ããããæéãéããã¿ã¹ã¯ãåé€ãããããæ©èœãäœã£ãŠã¿ãããå°ãããã°ã©ã ãé·ãã®ã§ãçšéããšã«ãã¡ã€ã«ãåããŠã¿ããã
- later.py: ã¡ã€ã³ããã°ã©ã - ã¿ã¹ã¯ã®è¿œå ã衚瀺ãªã©ãè¡ã
- storage.py: ã¿ã¹ã¯ã®æ å ±ãä¿åããããèªã¿èŸŒãã ããã颿°ãå®çŸ©ãããã®
ãã¡ãã«å®æããããã°ã©ã ãã¢ããããŒãããã
ããŒã¿ãã¡ã€ã«ã«ä¿åããã
ãã ããå®éã«ãªãã€ã³ããŒãäœãå Žåãåã«Næ¥åŸã®æ¥æãèšç®ããã ãã§ã¯äžååã ããŠãŒã¶ãŒãç»é²ãããªãã€ã³ããŒã®å å®¹ãæ¥æãã©ããã«ä¿åããŠããå¿ èŠããããããã§ãä»åã¯JSON圢åŒã§ãªãã€ã³ããŒã®æ å ±ããã¡ã€ã«ã«ä¿åããæ¹æ³ã玹ä»ãããã
Pythonã®æšæºã©ã€ãã©ãªã§ãããjsonãã䜿ããšãPythonã®ããŒã¿æ§é ãç°¡åã«JSON圢åŒã«å€æããŠãã¡ã€ã«ã«ä¿åããããéã«JSONãã¡ã€ã«ããããŒã¿ãèªã¿èŸŒãã ãããããšãã§ãããããã§ã¯ãäžèšã®ããã«ãJSONãèªã¿æžããã颿°ãå®çŸ©ããã以äžã®ããã°ã©ã ããstorage.pyããšããååã§ä¿åããŠå©çšãããã
import json
import os
# ã¿ã¹ã¯ãä¿åãã JSON ãã¡ã€ã«ã®ãã¹ --- (*1)
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_FILE = os.path.join(ROOT_DIR, "tasks.json")
def save_tasks(tasks):
"""ã¿ã¹ã¯ã JSON ãã¡ã€ã«ã«ä¿åãã""" # --- (*2)
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(tasks, f, indent=4, ensure_ascii=False)
def load_tasks():
"""ã¿ã¹ã¯ã JSON ãã¡ã€ã«ããèªã¿èŸŒã""" # --- (*3)
if not os.path.exists(DATA_FILE):
return []
with open(DATA_FILE, "r", encoding="utf-8") as f:
tasks = json.load(f)
tasks.sort(key=lambda x: x["date"])
return tasks
ããã°ã©ã ã®(ïŒ1)ã§ã¯ãã¿ã¹ã¯ãä¿åããJSONãã¡ã€ã«ã®ãã¹ãå®çŸ©ããŠããã(ïŒ2)ã§ã¯ãã¿ã¹ã¯ã®ãªã¹ããJSONãã¡ã€ã«ã«ä¿åãã颿°ãå®çŸ©ããŠããã(ïŒ3)ã§ã¯ãJSONãã¡ã€ã«ããã¿ã¹ã¯ã®ãªã¹ããèªã¿èŸŒã颿°ãå®çŸ©ããŠããã
ãã ãããã®åŠçã¯JSONãã¡ã€ã«ã®åçŽãªèªã¿æžããšãªã£ãŠãããåæã«ã¿ã¹ã¯ãèªã¿æžããããšããã¡ã€ã«ãå£ããå¯èœæ§ããããè€æ°äººã«ããèªã¿æžããåæã®å Žåã¯ãæä»åŠçããã£ããããŠããSQLiteããã®ä»ã®ããŒã¿ããŒã¹ãå©çšãããšè¯ãã ãããä»åã¯ãããã°ã©ã ã®åããããããåªå ããããã«ãç°¡åãªãã¡ã€ã«åŠçã«ããã
ã¡ã€ã³ããã°ã©ã ã確èªããã
ç¶ããŠãã¡ã€ã³ããã°ã©ã ãlater.pyãã確èªãããã以äžã®ã³ãŒãã¯ãã¿ã¹ã¯ã®è¿œå ã衚瀺ãæéåãã®åé€ãæéå°æ¥ã®ç¢ºèªãªã©ã®æ©èœãå®è£ ããŠãããTyperã䜿ã£ãŠã³ãã³ãã©ã€ã³åŒæ°ãåŠçããRichã䜿ã£ãŠã¿ã¹ã¯ã®ãªã¹ãã衚圢åŒã§è¡šç€ºããŠããã
#!/usr/bin/env python
""" CLIã§ã¿ã¹ã¯ã管çããããã°ã©ã """
from datetime import datetime, timedelta
import re
import typer
from rich.console import Console
from rich.table import Table
from storage import load_tasks, save_tasks
# TyperãConsoleã®ã€ã³ã¹ã¿ã³ã¹ãäœæ --- (*1)
app = typer.Typer()
console = Console()
def calc_due_date(due: str) -> datetime:
"""æéã®è¡šçŸãè§£æããŠãéç¥æ¥æãèšç®ãã""" # --- (*2)
now = datetime.now()
normalized = due.strip().lower()
match = re.fullmatch(r"(\d+)([dh])", normalized)
if not match:
raise typer.BadParameter("æé㯠'3d' / '2h' ã®åœ¢åŒã§æå®ããŠãã ããã")
amount = int(match.group(1))
unit = match.group(2)
if unit == "d":
return (now + timedelta(days=amount)).replace(hour=8, minute=0, second=0, microsecond=0)
if unit == "h":
return now + timedelta(hours=amount)
return now
@app.command()
def add(due: str, task: str):
"""ã¿ã¹ã¯ã远å ãã (äŸ: later.py add "3d" "ã¬ããŒãæåº")""" # --- (*3)
tasks = load_tasks()
notify_at = calc_due_date(due)
notify_at_s = notify_at.strftime("%Y-%m-%d %H:%M:%S")
# æ¢åã® date ããŒã¯ç¶æãã€ã€ãæå»ä»ãæ
å ±ã notify_at ã«ä¿å
tasks.append({"date": notify_at_s, "task": task})
save_tasks(tasks)
print(f"ã¿ã¹ã¯ã远å ããŸãã: {task} (éç¥æ¥æ: {notify_at_s})")
def show_tasks(tasks: list[dict], title: str):
"""ã¿ã¹ã¯ã®ãªã¹ãã衚圢åŒã§è¡šç€ºãã""" # -- (*4)
if len(tasks) == 0:
return print("ã¿ã¹ã¯ã¯ãããŸããã")
table = Table(title=title, show_lines=True)
table.add_column("çªå·", justify="right")
table.add_column("ã¿ã¹ã¯", style="red")
table.add_column("æé", style="green")
for idx, task in enumerate(tasks, start=1):
table.add_row(f"{idx}", task["task"], task["date"])
console.print(table)
@app.command()
def show():
"""ä¿åãããã¿ã¹ã¯ã衚瀺ãã""" # --- (*5)
tasks = load_tasks()
show_tasks(tasks, "â ä¿åããã¿ã¹ã¯äžèЧ")
@app.command()
def clear():
"""æéãéããã¿ã¹ã¯ãåé€ãã""" # --- (*6)
tasks = load_tasks()
now = datetime.now()
tasks_due = []
for task in tasks:
notify_at = datetime.strptime(task["date"], "%Y-%m-%d %H:%M:%S")
remove_date = notify_at - timedelta(days=1) # æéã1æ¥éãããã®
if remove_date > now:
tasks_due.append(task)
save_tasks(tasks_due)
print(f"æéãéããã¿ã¹ã¯ãåé€ããŸãããæ®ãã®ã¿ã¹ã¯æ°: {len(tasks_due)}")
show() # æŽæ°åŸã®ã¿ã¹ã¯ã衚瀺
@app.command()
def check():
"""æéãæ¥ãã¿ã¹ã¯ã衚瀺ãã""" # --- (*7)
tasks = load_tasks()
now = datetime.now()
tasks_due = []
for task in tasks:
notify_at = datetime.strptime(task["date"], "%Y-%m-%d %H:%M:%S")
if notify_at <= now:
tasks_due.append(task)
show_tasks(tasks_due, "â æéãæ¥ãã¿ã¹ã¯")
if __name__ == "__main__":
app()
ç°¡åã«ããã°ã©ã ã確èªãããã
(ïŒ1)ã§ã¯ãTyperã®ã¢ããªã±ãŒã·ã§ã³ãšRichã®ã³ã³ãœãŒã«ã®ã€ã³ã¹ã¿ã³ã¹ãäœæããŠããã(ïŒ2)ã§ã¯ããŠãŒã¶ãŒãæå®ããæéã®è¡šçŸãè§£æããŠãéç¥æ¥æãèšç®ãã颿°ãå®çŸ©ããŠããã(ïŒ3)ã§ã¯ãã¿ã¹ã¯ã远å ããã³ãã³ããå®çŸ©ããŠããããã®ä»ã®é¢æ°ã§ã¯ãã¿ã¹ã¯ã®è¡šç€ºãæéåãã®åé€ãæéå°æ¥ã®ç¢ºèªãªã©ã®æ©èœãå®è£ ããŠããã(ïŒ4)ã§ã¯ãã¿ã¹ã¯ã®ãªã¹ãã衚圢åŒã§è¡šç€ºãã颿°ãå®çŸ©ããŠããã(ïŒ5)ã§ã¯ãä¿åãããã¿ã¹ã¯ã衚瀺ããã³ãã³ããå®çŸ©ããŠããã(ïŒ6)ã§ã¯ãæéãéããã¿ã¹ã¯ãåé€ããã³ãã³ããå®çŸ©ããŠããã(ïŒ7)ã§ã¯ãæéãæ¥ãã¿ã¹ã¯ã衚瀺ããã³ãã³ããå®çŸ©ããŠããããªããã¿ã¹ã¯ã®ãdateãããŒã«æéãæååã§èšé²ããŠãããããã§ãdatetime.strptimeã䜿ã£ãŠãæéã®æ¥ä»ãdatetimeãªããžã§ã¯ãã«å€æããŠããçŸåšã®æ¥æãšæ¯èŒããŠãããããã«ãã£ãŠãæéãæ¥ãã¿ã¹ã¯ãæéãéããã¿ã¹ã¯ãæ£ããå€å®ã§ããããã«ãªã£ãŠããã
ãªãã颿°ã®å®çŸ©ã®äžã§ã@app.command()ããšãããã³ã¬ãŒã¿ãŒãèšè¿°ããããšã§ããã®é¢æ°ãã³ãã³ããšããŠèªèãããããã«ãªããäŸãã°ããaddã颿°ã¯ãlater.py addããšããã³ãã³ãã§åŒã³åºããããã«ãªããTyperã¯ã颿°ã®åŒæ°ãåãã³ããããšã«ãã³ãã³ãã®åŒæ°ããªãã·ã§ã³ãèªåçã«åŠçããŠããããããã³ãã³ãã©ã€ã³ããŒã«ã®äœæãéåžžã«ç°¡åã«ãªãã
ã¿ã¹ã¯ããŒã«ã®äœ¿ãæ¹ã確èªããã
Typerã䜿ã£ãŠäœã£ãããã°ã©ã ã§ã¯ãèªåçã«ããã°ã©ã ã®äœ¿ãæ¹ã衚瀺ã§ããããã«ãªã£ãŠãããTyperã颿°ã®å®çŸ©ããäœ¿ãæ¹ãèªã¿åã£ãŠãããã®ã ãã¿ãŒããã«ã§äžèšã®ããã«ã--helpããªãã·ã§ã³ãä»ããŠã³ãã³ããå®è¡ããŠã¿ããã
# ããã°ã©ã ã®äœ¿ãæ¹ã衚瀺ãã
python later.py --help
ãããšãäžèšã®ããã«ãã³ãã³ãã®äœ¿ãæ¹ã衚瀺ããããã³ãã³ãã®åŒæ°ããªãã·ã§ã³ã®èª¬æã衚瀺ãããŠããããšã確èªã§ããã ããã
2æ¥åŸã«è¡šç€ºããã¿ã¹ã¯ã远å ããã«ã¯ãäžèšã®ããã«ãlater.py add 2d "ã¿ã¹ã¯å 容"ãã®æžåŒã§ã³ãã³ããå®è¡ããã3æ¥åŸã§ããã°ãlater.py add 3d "ã¿ã¹ã¯å 容"ãã®ããã«å®è¡ããã以äžã¯ã2æ¥åŸã®æéãæã€ããšã¢ã³ã³ã®æé€ããšããã¿ã¹ã¯ã远å ããäŸã ã
# ã¿ã¹ã¯ã远å ãã
python later.py add 2d ãšã¢ã³ã³ã®æé€
# ã¿ã¹ã¯ã®äžèЧã衚瀺ãã
python later.py show
å®è¡ãããšã次ã®ããã«è¡šç€ºãããããªãããpython later.py showããå®è¡ãããšã¿ã¹ã¯ã®äžèЧã衚瀺ã§ããã
æéãè¿ããã¿ã¹ã¯ã衚瀺ããã«ã¯ãäžèšã®ããã«ãlater.py checkãã®ã³ãã³ããå®è¡ãããæéãæ¥ãã¿ã¹ã¯ã衚圢åŒã§è¡šç€ºãããã
# æéãæ¥ãã¿ã¹ã¯ã衚瀺ãã
python later.py check
ãããŠãã¿ã¹ã¯ã確èªããåŸãæéãéããã¿ã¹ã¯ãåé€ããã«ã¯ãäžèšã®ããã«ãlater.py clearãã®ã³ãã³ããå®è¡ãããæéãéããã¿ã¹ã¯ãåé€ãããæ®ãã®ã¿ã¹ã¯æ°ã衚瀺ãããã
# æéãéããã¿ã¹ã¯ãåé€ãã
python later.py clear
ã¿ãŒããã«ãéããæã«ã¿ã¹ã¯æéã確èªããããã«èšå®ããã
ãªããæ°èŠã§ã¿ãŒããã«ãéãããšãã«ãèªåçã«æéãæ¥ãã¿ã¹ã¯äžèЧã衚瀺ããããã«èšå®ããŠã¿ãããããããã°ãæéã®ç¢ºèªå¿ãã鲿¢ã§ããã ãããåºæ¬çã«ã¯ãã¿ãŒããã«èµ·åæã«èªåå®è¡ãããã¹ã¯ãªããã«ãlater.py checkããèšè¿°ããã ãã ãOSããšã«èšå®æ¹æ³ã確èªãããã
ãWindowsã®å Žåã
Windowsã®PowerShellã§ããã°ããŠãŒã¶ãŒãã©ã«ãã«ããã~\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1ããšãããã¡ã€ã«($PROFILEã®å€)ãããã¹ããšãã£ã¿ã§éããŠãäžèšã®ãããªå 容ã远å ãããããªãããã¡ã€ã«ããã©ã«ãããªãå Žåã¯äœæããŠè¿œå ããå¿ èŠãããã
$script = Join-Path (Split-Path $PROFILE) "later.py"
python $script check
ãããŠããã®ãã¡ã€ã«ãšåããã©ã«ãã«ãå ã»ã©äœæãããlater.pyããšãstorage.pyããä¿åãããããããããšãæ°èŠPowerShellãŠã£ã³ããŠãéãããšãã«ããã®ã¹ã¯ãªãããèªåçã«å®è¡ãããæéãæ¥ãã¿ã¹ã¯ã衚瀺ãããããã«ãªãã
ãmacOSã®å Žåã
macOSã§ããã©ã«ãã®ãZ shell(zsh)ãã䜿ã£ãŠããå Žåã«ã¯ããŠãŒã¶ãŒãã©ã«ãã«ããã~/.zshrcããšãããã¡ã€ã«ãããã¹ããšãã£ã¿ã§éããŠãäžèšã®ãããªå 容ã远èšãããã
LATER_SCRIPT="$HOME/later.py"
alias later='python $LATER_SCRIPT'
later check
ãŠãŒã¶ãŒãã©ã«ãã«ãlater.pyããšãstorage.pyããä¿åãããããããããšãæ°èŠã¿ãŒããã«ãéãããšãã«ããã®ã¹ã¯ãªãããèªåçã«å®è¡ãããæéãæ¥ãã¿ã¹ã¯ã衚瀺ãããããã«ãªãããªããaliasã§laterã³ãã³ããå®çŸ©ããŠããã®ã§ãã¿ãŒããã«ãéããåŸã«ãlater checkããšå ¥åããŠããæéãæ¥ãã¿ã¹ã¯ã衚瀺ã§ããããã«ãªãã
ãŸãšã
ä»åã¯ãPythonã®Typerã䜿ã£ãŠãNæ¥åŸ(ãããã¯ãNæéåŸ)ã«éç¥ãããªãã€ã³ããŒCLIãäœã£ãŠã¿ãããŠãŒã¶ãŒãæå®ããæéã®è¡šçŸãè§£æããŠãéç¥æ¥æãèšç®ããã¿ã¹ã¯ã®æ å ±ãJSONãã¡ã€ã«ã«ä¿åããããšã§ãç°¡åãªãªãã€ã³ããŒæ©èœãå®è£ ãããããã«ãã¿ãŒããã«ãéãããšãã«æéãæ¥ãã¿ã¹ã¯ãèªåçã«è¡šç€ºããããã«èšå®ããæ¹æ³ã玹ä»ããããã®ãããªCLIããŒã«ã¯ãæ¥åžžã®ã¿ã¹ã¯ç®¡çã«äŸ¿å©ã«äœ¿ããã ãã§ãªããPythonã®ã³ãã³ãã©ã€ã³ããŒã«ã®äœãæ¹ãããã¡ã€ã«æäœã®æ¹æ³ãåŠã¶è¯ãæ©äŒã«ããªãã ããããã²ãèªåã®ç°å¢ã«åãããŠã«ã¹ã¿ãã€ãºããŠã¿ããã
èªç±åããã°ã©ããŒããããã¯ãã©ã«ãŠãããã°ã©ãã³ã°ã®æ¥œãããäŒããæŽ»åãããŠããã代衚äœã«ãæ¥æ¬èªããã°ã©ãã³ã°èšèªããªã§ããã ãããã¹ã鳿¥œããµã¯ã©ããªã©ã2001幎ãªã³ã©ã€ã³ãœãã倧è³å ¥è³ã2004幎床æªèžãŠãŒã¹ ã¹ãŒããŒã¯ãªãšãŒã¿èªå®ã2010幎 OSSè²¢ç®è ç« åè³ããããŸã§50å以äžã®æè¡æžãå·çãããçŽè¿ã§ã¯ããå€§èŠæš¡èšèªã¢ãã«ã䜿ãããªãããã®ããã³ãããšã³ãžãã¢ãªã³ã°ã®æç§æž(ãã€ããåºç)ããPythonã§ã€ãããã¹ã¯ãããã¢ããª(ãœã·ã )ããå®è·µåã身ã«ã€ãã Pythonã®æç§æž 第2çããã·ãŽããã¯ãã©ã PythonèªååŠçã®æç§æž(ãã€ããåºç)ããªã©ã






