Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
![quality](https://github.com/matt-dray/pet/actions/workflows/code-quality.yaml/badge.svg)
![tests](https://github.com/matt-dray/pet/actions/workflows/tests.yaml/badge.svg)

A virtual pet that lives on the command line and is remembered between terminal sessions.
A persistent cyberpet that lives on the command line.

A proof-of-concept to learn more about packaging-up command-line interfaces (CLIs) made with Python and tools like [InquirerPy](https://inquirerpy.readthedocs.io/en/latest/) and [platformdirs](https://platformdirs.readthedocs.io/en/latest/index.html).

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "pet"
version = "0.0.0.9000"
description = "A virtual pet that lives on the command line and is remembered between terminal sessions."
version = "0.0.0.9001"
description = "A persistent cyberpet that lives on the command line."
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
Expand Down
28 changes: 17 additions & 11 deletions src/pet/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
"""
A virtual pet that lives on the command line and is remembered between terminal sessions.
A persistent cyberpet that lives on the command line.
"""

from .utils import (
write_pet_data,
read_pet_data,
delete_pet_data,
extract_timestamp,
calculate_time_delta,
init_stats,
read_stats,
delete_stats,
get_datetime,
update_time_stats,
update_health_stats,
feed_pet,
print_pet,
)

__all__ = [
"write_pet_data",
"read_pet_data",
"delete_pet_data",
"extract_timestamp",
"calculate_time_delta",
"init_stats",
"read_stats",
"delete_stats",
"get_datetime",
"update_time_stats",
"update_health_stats",
"feed_pet",
"print_pet",
]
2 changes: 1 addition & 1 deletion src/pet/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
A virtual pet that lives on the command line and is remembered between terminal sessions.
A persistent cyberpet that lives on the command line.
"""

from .cli import main
Expand Down
80 changes: 55 additions & 25 deletions src/pet/cli.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,82 @@
"""
CLI entry with user input.
Command-line interface (CLI) for interacting with pet statistics.
Accepts user input to read, write or delete pet data.
"""

import datetime as dt
from InquirerPy import inquirer
from pathlib import Path
from platformdirs import user_data_dir
from .utils import (
write_pet_data,
read_pet_data,
delete_pet_data,
extract_timestamp,
calculate_time_delta,
init_stats,
read_stats,
delete_stats,
get_datetime,
update_time_stats,
update_health_stats,
feed_pet,
print_pet,
)


def main():
pet_data_path = Path(user_data_dir("pet")) / "pet.json"
stats_path = Path(user_data_dir("pet")) / "pet.json"

while True:
if not pet_data_path.exists():
timestamp = dt.datetime.now()
if not stats_path.exists():
name = inquirer.text(message="Your pet's name:").execute()
write_pet_data(pet_data_path, name, timestamp)
init_stats(stats_path, name)

stats = read_pet_data(pet_data_path)
stats = read_stats(stats_path)
update_time_stats(stats, stats_path)
stats = read_stats(stats_path)
update_health_stats(stats, stats_path)
stats = read_stats(stats_path)

if stats["HEALTH"] <= 0:
print("🪫 Uh-oh, your pet's health is low!")

action = inquirer.select(
message="What would you like to do?",
choices=["Check", "Delete", "Quit"],
choices=["📊 Stats", "👀 See", "🍣 Feed", "❌ Quit", "👋 Release"],
).execute()

if action == "Check":
timestamp = extract_timestamp(stats["TIMESTAMP"])
delta = calculate_time_delta(stats["TIMESTAMP"])
print(f"Name: {stats['NAME']}")
print(f"Birth: {timestamp}")
print(f"Age: {delta} seconds")
if "Stats" in action:
birth = get_datetime(stats["BORN"])
print(
f"📛 Name: {stats['NAME']}",
f"🐣 Birth: {birth['DATE']} at {birth['TIME']}",
f"📅 Age: {stats['AGE']} days",
f"🔋 Health: {stats['HEALTH']}/10",
sep="\n",
)

if action == "Delete":
delete_pet_data(pet_data_path)
print("Pet data deleted. Goodbye!")
break
if "See" in action:
print_pet()

if action == "Quit":
print("Goodbye!")
if "Feed" in action:
if stats["HEALTH"] == 10:
print(f"🤢 {stats['NAME']} is full (health = 10).")
else:
feed_pet(stats, stats_path)
print(
f"😋 {stats['NAME']} ate the food (health = stats['HEALTH'] + 1)."
)

if "Quit" in action:
print(f"👋 Goodbye {stats['NAME']}!")
break

if "Release" in action:
confirm = inquirer.confirm(
message=f"🤔 Are you sure? {stats['NAME']} will be gone forever..."
).execute()
if confirm:
delete_stats(stats_path)
print(
f"🥲 {stats['NAME']} was released and their data deleted. Farewell!"
)
break


if __name__ == "__main__":
main()
154 changes: 117 additions & 37 deletions src/pet/utils.py
Original file line number Diff line number Diff line change
@@ -1,83 +1,163 @@
"""
Handle pet data.
Functions to manage pet stats stored in a JSON file on disk.
"""

import datetime as dt
import datetime
import json
from pathlib import Path


def write_pet_data(pet_data_path: Path, name: str, timestamp: dt.datetime) -> None:
def init_stats(stats_path: Path, name: str) -> None:
"""
Write pet data.
Write initial pet stats to a JSON file on disk.

Args:
pet_data_path (Path): The path to the pet data file.
name (str): The pet name.
timestamp (datettime.datetime) The date-time of pet data creation.
stats_path (Path): Path to where the pet's stats json file will be written.
name (str): The pet's name provided by the user.

Returns:
None: Writes to disk.
None: File is written to disk.
"""
json_dict = {"NAME": name, "TIMESTAMP": timestamp.isoformat()}
pet_data_path.parent.mkdir(parents=True, exist_ok=True)
with pet_data_path.open("w", encoding="utf-8") as f:
timestamp_now = datetime.datetime.now().isoformat()
json_dict = {
"NAME": name,
"BORN": timestamp_now,
"LAST": timestamp_now,
"DELTA": 0, # mins
"AGE": 0, # days
"HEALTH": 10,
}
stats_path.parent.mkdir(parents=True, exist_ok=True)
with stats_path.open("w", encoding="utf-8") as f:
json.dump(json_dict, f)


def read_pet_data(pet_data_path: Path) -> dict:
def read_stats(stats_path: Path) -> dict:
"""
Read pet data.
Read pet statistics from a JSON file on disk.

Args:
pet_data_path (Path): The path to the pet data file.
stats_path (Path): Path to the pet's stats file.

Returns:
dict: Pet data.
dict: A dictionary containing the pet's statistics with keys:
- 'NAME' (str): The pet's name.
- 'BORN' (str): The datetime of the pet's birth.
- 'LAST' (str): The datetime of the last interaction with the pet.
- 'DELTA' (str): Difference in minutes from last interaction to now.
- 'AGE' (int): The pet's age in days.
- 'HEALTH' (int): The pet's health value (out of 10).
"""
pet_data_text = pet_data_path.read_text(encoding="utf-8")
pet_data_json = json.loads(pet_data_text)
return pet_data_json
stats_text = stats_path.read_text(encoding="utf-8")
stats_json = json.loads(stats_text)
return stats_json


def delete_pet_data(pet_data_path: Path) -> None:
def delete_stats(stats_path: Path) -> None:
"""
Delete pet data.
Delete the stats file on disk.

Args:
pet_data_path (Path): The path to the pet data file.
stats_path (Path): Path to the pet's stats file.

Returns:
None: Pet data on disk deleted.
None: File is deleted from disk.
"""
if pet_data_path.exists():
pet_data_path.unlink()
if stats_path.exists():
stats_path.unlink()


def extract_timestamp(timestamp: str) -> str:
def get_datetime(timestamp: str) -> dict:
"""
Extract timestamp from pet data.
Extract a date and time from an ISO timestamp string.

Args:
timestamp (str): The path to the pet data file.
timestamp (str): The ISO string timestamp for conversion.

Returns:
str: Formatted date-time.
dict: A dictionary with keys:
- 'DATE' (str): The date portion of the ISO timestamp.
- 'TIME' (str): The time portion of the ISO timestamp.
"""
timestamp_iso = dt.datetime.fromisoformat(timestamp)
return timestamp_iso.strftime("%d %B %Y at %H:%M:%S")
timestamp_iso = datetime.datetime.fromisoformat(timestamp)
date = timestamp_iso.strftime("%d %b %Y")
time = timestamp_iso.strftime("%H:%M")
return {"DATE": date, "TIME": time}


def calculate_time_delta(timestamp: str) -> int:
def update_time_stats(stats: dict, stats_path: Path) -> None:
"""
Calculate difference between now and timestamp from pet data.
Overwrite time-related keys in the stats json file on disk.

Args:
timestamp (str): The path to the pet data file.
stats (dict): Pet stats read from the stats json file on disk.
stats_path (Path): Path to where the pet's stats json file will be written.

Returns:
int: Elapsed time in seconds.
None: File is written to disk.
"""
timestamp_iso = dt.datetime.fromisoformat(timestamp)
delta = dt.datetime.now() - timestamp_iso
return int(delta.total_seconds())
now = datetime.datetime.now()
last = stats["LAST"]
last_dt = datetime.datetime.fromisoformat(last)
delta = now - last_dt
delta_mins = delta.total_seconds() // 60

stats["LAST"] = now.isoformat()
stats["DELTA"] = int(delta_mins)
stats["AGE"] = delta.days

with stats_path.open("w", encoding="utf-8") as f:
json.dump(stats, f)


def update_health_stats(stats: dict, stats_path: Path) -> None:
"""
Overwrite health-related keys in the stats json file on disk.

Args:
stats (dict): Pet stats read from the stats json file on disk.
stats_path (Path): Path to where the pet's stats json file will be written.

Returns:
None: File is written to disk.
"""
delta = stats["DELTA"]
health = stats["HEALTH"]

health_loss = delta // 60 # lose one health per hour
new_health = health - health_loss
if new_health < 0:
new_health = 0

stats["HEALTH"] = new_health

with stats_path.open("w", encoding="utf-8") as f:
json.dump(stats, f)


def feed_pet(stats: dict, stats_path: Path) -> None:
health = stats["HEALTH"]
health += 1
new_health = min(health, 10)

stats["HEALTH"] = new_health

with stats_path.open("w", encoding="utf-8") as f:
json.dump(stats, f)


def print_pet() -> None:
"""
Print an image of your pet.

Returns:
None: Text is printed to the screen.
"""
print(
r" ",
r" /\__/\ ",
r" ={ o x o}= < meow ",
r" L( u u ) ",
r" ",
sep="\n",
)
Loading