Python Showcase Project: CLI Journal
This project demonstrates how to combine multiple Python modules and concepts into a functional Command Line Interface (CLI) tool.
Goal: Create a tool named pyjournal that allows users to add and list journal entries from their terminal.
The Code
Save the following code as pyjournal.py.
import argparse
import json
import logging
import sys
from datetime import datetime
from pathlib import Path
from typing import List, Dict
# 1. Logging Setup
logging.basicConfig(level=logging.INFO, format='%(message)s')
# 2. Class Definition (OOP)
class JournalEntry:
def __init__(self, title: str, content: str, timestamp: str = None):
self.title = title
self.content = content
self.timestamp = timestamp or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def to_dict(self) -> Dict[str, str]:
return {
"title": self.title,
"content": self.content,
"timestamp": self.timestamp
}
@classmethod
def from_dict(cls, data: Dict[str, str]):
return cls(data["title"], data["content"], data["timestamp"])
# 3. File Management Class
class JournalManager:
def __init__(self, filepath: Path):
self.filepath = filepath
self.entries: List[JournalEntry] = []
self.load()
def load(self):
if not self.filepath.exists():
return
try:
# 4. JSON Handling
content = self.filepath.read_text()
data = json.loads(content)
self.entries = [JournalEntry.from_dict(e) for e in data]
except (json.JSONDecodeError, OSError) as e:
logging.error(f"Error loading journal: {e}")
def save(self):
try:
data = [e.to_dict() for e in self.entries]
# 4. JSON Handling
self.filepath.write_text(json.dumps(data, indent=4))
except OSError as e:
logging.error(f"Error saving journal: {e}")
def add_entry(self, title: str, content: str):
entry = JournalEntry(title, content)
self.entries.append(entry)
self.save()
logging.info(f"Entry '{title}' added.")
def list_entries(self):
if not self.entries:
print("No entries found.")
return
print(f"{'TIMESTAMP':<20} | {'TITLE':<20} | {'CONTENT'}")
print("-" * 60)
for entry in self.entries:
print(f"{entry.timestamp:<20} | {entry.title:<20} | {entry.content}")
def main():
# 5. Argument Parsing
parser = argparse.ArgumentParser(description="Simple CLI Journal")
subparsers = parser.add_subparsers(dest="command", required=True)
# 'add' command
add_parser = subparsers.add_parser("add", help="Add a new entry")
add_parser.add_argument("title", help="Title of the entry")
add_parser.add_argument("content", help="Content of the entry")
# 'list' command
subparsers.add_parser("list", help="List all entries")
args = parser.parse_args()
# 6. Pathlib for cross-platform paths
journal_file = Path.home() / "my_journal.json"
manager = JournalManager(journal_file)
if args.command == "add":
manager.add_entry(args.title, args.content)
elif args.command == "list":
manager.list_entries()
if __name__ == "__main__":
main()
Concepts Used
- Logging: Used instead of
printfor status messages, allowing for different severity levels. - Classes & OOP: The
JournalEntryclass encapsulates the data, whileJournalManagerencapsulates the logic for handling that data. - Pathlib: Used
Path.home()to find the user's home directory reliably on Windows, Mac, or Linux. - JSON: Used
json.loadsandjson.dumpsto serialize the Python objects into a text format for storage. - Argparse: Used
subparsersto create distinct commands (addvslist) with their own specific arguments. - Type Hinting: Used
ListandDictto make the code self-documenting and IDE-friendly.
How to Run
python pyjournal.py add "First Day" "Started learning Python today."
python pyjournal.py add "Progress" "Wrote a CLI tool."
python pyjournal.py list