From b9551ff65d9a61450c7ad3dbea6d20acd5bb113a Mon Sep 17 00:00:00 2001 From: connorlee487 Date: Sat, 1 Nov 2025 17:45:38 -0400 Subject: [PATCH 01/37] added to README --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 6022e0e..ef43105 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,28 @@ # Python Package Exercise An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. + +Group Members +- [Connor Lee](https://github.com/Connorlee487) +- [Lanxi](https://github.com/player1notfound) +- [Alex](https://github.com/axie22) +- 4th ? + + +## 1: `pyPet` + +**Theme:** A tiny virtual pet in your terminal. + +**Concept:** +`pyPet` creates a small, stateful pet that lives in your terminal. You can feed it, play with it, and check on its mood. + +**Core Functions** +- `create_pet(name: str, species: str, mood: str, hunger: int)` + - Initializes a new pet with starting attributes. +- `feed(pet: dict, food: str, portion: int, treat: bool)` + - Reduces hunger, may improve mood based on treat type. +- `play(pet: dict, game: str, energy: int, reward: bool)` + - Boosts happiness and decreases energy over time. +- `status(pet: dict, color: bool, verbose: bool, ascii_art: bool)` + - Prints the current pet’s stats and ASCII representation. + From b5ab24108b5167a938aa9a163f42bd0b9bcc46a4 Mon Sep 17 00:00:00 2001 From: connorlee487 Date: Sat, 1 Nov 2025 17:55:08 -0400 Subject: [PATCH 02/37] setup --- pyproject.toml | 0 requirements.txt | 3 +++ src/pypet/init__.py | 0 src/pypet/main__.py | 11 +++++++++++ src/pypet/pet.py | 15 +++++++++++++++ 5 files changed, 29 insertions(+) create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/pypet/init__.py create mode 100644 src/pypet/main__.py create mode 100644 src/pypet/pet.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b02bf0c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pipenv +twine +build \ No newline at end of file diff --git a/src/pypet/init__.py b/src/pypet/init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pypet/main__.py b/src/pypet/main__.py new file mode 100644 index 0000000..9fda63f --- /dev/null +++ b/src/pypet/main__.py @@ -0,0 +1,11 @@ +import pypet.pet as pet + + +def main(): + """ + For pypet + """ + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/pypet/pet.py b/src/pypet/pet.py new file mode 100644 index 0000000..e77a664 --- /dev/null +++ b/src/pypet/pet.py @@ -0,0 +1,15 @@ +def create_pet(name: str, species: str, mood: str, hunger: int): + # code + return {} + +def feed(pet: dict, food: str, portion: int, treat: bool): + # code + return {} + +def play(pet: dict, game: str, energy: int, reward: bool): + # code + return {} + +def status(pet: dict, color: bool, verbose: bool, ascii_art: bool): + # code + return {} \ No newline at end of file From 7a99197f00cbceeda8b035e5f14d1db8c8a17d17 Mon Sep 17 00:00:00 2001 From: axie22 Date: Fri, 31 Oct 2025 16:14:18 -0400 Subject: [PATCH 03/37] Added basic toml file --- pyproject.toml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e69de29..4e138d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "examplepackagefb1258" +description = "An example of a package developed with pipenv, built with build using setuptools, uploaded to PyPI using twine, and distributed via pip." +version = "0.1.2" +authors = [ + { name="Foo Barstein", email="foo.barstein@onepotcooking.com" }, +] +license = { file = "LICENSE" } +readme = "README.md" +keywords = ["python", "package", "build", "tutorial"] +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "Intended Audience :: Education", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", +] + +[project.optional-dependencies] +dev = ["pytest"] + +[project.urls] +"Homepage" = "https://github.com/nyu-software-engineering/python-package-example" +"Repository" = "https://github.com/nyu-software-engineering/python-package-example.git" +"Bug Tracker" = "https://github.com/nyu-software-engineering/python-package-example/issues" + +[project.scripts] +examplepackagefb1258 = "examplepackagefb1258.__main__:main" \ No newline at end of file From a1369ec6a21dce2aebe91b85215697bb4fe4db5b Mon Sep 17 00:00:00 2001 From: axie22 Date: Sat, 1 Nov 2025 16:28:09 -0400 Subject: [PATCH 04/37] project toml file setup --- pyproject.toml | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4e138d5..8719c2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,30 +3,33 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "examplepackagefb1258" -description = "An example of a package developed with pipenv, built with build using setuptools, uploaded to PyPI using twine, and distributed via pip." -version = "0.1.2" +name = "pypet" +description = "A tiny virtual pet that lives in your terminal." +version = "0.1.0" authors = [ - { name="Foo Barstein", email="foo.barstein@onepotcooking.com" }, + { name = "Alexander Xie", email = "alexxie9667@gmail.com" }, ] license = { file = "LICENSE" } readme = "README.md" -keywords = ["python", "package", "build", "tutorial"] -requires-python = ">=3.7" +keywords = ["virtual pet", "terminal", "game", "python"] +requires-python = ">=3.8" classifiers = [ "Programming Language :: Python :: 3", - "Intended Audience :: Education", + "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ] +dependencies = [] + [project.optional-dependencies] dev = ["pytest"] [project.urls] -"Homepage" = "https://github.com/nyu-software-engineering/python-package-example" -"Repository" = "https://github.com/nyu-software-engineering/python-package-example.git" -"Bug Tracker" = "https://github.com/nyu-software-engineering/python-package-example/issues" +"Homepage" = "https://github.com/swe-students-fall2025/3-python-package-team_pioneer" +"Repository" = "https://github.com/swe-students-fall2025/3-python-package-team_pioneer.git" +"Bug Tracker" = "https://github.com/swe-students-fall2025/3-python-package-team_pioneer/issues" -[project.scripts] -examplepackagefb1258 = "examplepackagefb1258.__main__:main" \ No newline at end of file +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] From 4cecd7f67d77ffe34030f68c4dc5a4e9e68ba5aa Mon Sep 17 00:00:00 2001 From: axie22 Date: Sat, 1 Nov 2025 21:53:56 -0400 Subject: [PATCH 05/37] Added create pet function --- src/pypet/main.py | 11 ++++ src/pypet/pet.py | 127 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/pypet/main.py diff --git a/src/pypet/main.py b/src/pypet/main.py new file mode 100644 index 0000000..9fda63f --- /dev/null +++ b/src/pypet/main.py @@ -0,0 +1,11 @@ +import pypet.pet as pet + + +def main(): + """ + For pypet + """ + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/pypet/pet.py b/src/pypet/pet.py index e77a664..192e6e7 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -1,6 +1,127 @@ -def create_pet(name: str, species: str, mood: str, hunger: int): - # code - return {} +from __future__ import annotations + +from dataclasses import dataclass +from time import time +from typing import TypedDict, Dict +from uuid import uuid4 + +ALLOWED_SPECIES = {"cat", "dog", "otter", "capybara", "duck"} +ALLOWED_MOODS = {"happy", "neutral", "grumpy", "sleepy", "hungry", "sad"} +BOUNDS: Dict[str, tuple[int, int]] = { + "hunger": (0, 100), # 0 = not hungry, 100 = hungry + "energy": (0, 100), # 0 = empty, 100 = full + "happiness": (0, 100), # 0 = sad, 100 = happy +} + + +class Pet(TypedDict, total=False): + """ + Schema for a pet instance. + + Keys: + id: unique identifier + version: schema version + name: non-empty name + species: lowercase species key in ALLOWED_SPECIES + mood: lowercase mood key in ALLOWED_MOODS + hunger: 0-100 (int) + energy: 0-100 (int) + happiness: 0-100 (int) + created_at: timestamp + last_interaction_at: timestamp + ascii_key: f"{species}:{mood}" + """ + id: str + version: int + name: str + species: str + mood: str + hunger: int + energy: int + happiness: int + created_at: float + last_interaction_at: float + ascii_key: str + +def clamp(x: int, lo: int, hi: int) -> int: + """Clamp integer x to [lo, hi]""" + try: + xi = int(x) + except (TypeError, ValueError): + raise ValueError(f"value {x!r} must be an integer") + return max(lo, min(hi, xi)) + + +def baseline_happiness(hunger: int) -> int: + """ + Compute a baseline happiness from hunger + happiness = clamp(80 - hunger // 2, 0, 100) + """ + lo, hi = BOUNDS["happiness"] + return clamp(80 - clamp(hunger, *BOUNDS["hunger"]) // 2, lo, hi) + +def create_pet( + name: str, + species: str, + mood: str = "neutral", + hunger: int = 50, + energy: int | None = None, +) -> Pet: + """ + Create and return a new pet dict + + Args: + name: Pet's display name. Must be a non-empty string after trimming. + species: One of ALLOWED_SPECIES (normalized to lowercase). + mood: One of ALLOWED_MOODS (normalized to lowercase). Default "neutral". + hunger: Integer 0-100 (0=not hungry, 100=hungry) + energy: Optional integer 0-100 (default 70 if omitted) + + Returns: + Pet: a new pet object adhering to the schema defined by `Pet`. + + Raises: + ValueError: on invalid name/species/mood types or unsupported values. + """ + + if not isinstance(name, str): + raise ValueError("name must be a string") + name = name.strip() + if not name: + raise ValueError("name must be non-empty") + + if not isinstance(species, str): + raise ValueError("species must be a string") + species = species.strip().lower() + if species not in ALLOWED_SPECIES: + raise ValueError(f"unsupported species: {species!r}. Allowed: {sorted(ALLOWED_SPECIES)}") + + if not isinstance(mood, str): + raise ValueError("mood must be a string") + mood = mood.strip().lower() + if mood not in ALLOWED_MOODS: + raise ValueError(f"unsupported mood: {mood!r}. Allowed: {sorted(ALLOWED_MOODS)}") + + + hunger = clamp(hunger, *BOUNDS["hunger"]) + energy = clamp(energy if energy is not None else 70, *BOUNDS["energy"]) + happiness = baseline_happiness(hunger) + + pet: Pet = { + "id": str(uuid4()), + "version": 1, + "name": name, + "species": species, + "mood": mood, + "hunger": hunger, + "energy": energy, + "happiness": happiness, + "created_at": time(), + "last_interaction_at": time(), + "ascii_key": f"{species}:{mood}" + } + return pet + def feed(pet: dict, food: str, portion: int, treat: bool): # code From 1cdfdd5c7e8776fd2e85f47c5074a0e1f784e768 Mon Sep 17 00:00:00 2001 From: axie22 Date: Sat, 1 Nov 2025 21:57:04 -0400 Subject: [PATCH 06/37] Added instructions on how to run & test --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ef43105..aa390b0 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. Group Members -- [Connor Lee](https://github.com/Connorlee487) -- [Lanxi](https://github.com/player1notfound) -- [Alex](https://github.com/axie22) -- 4th ? +- [Connor Lee](https://github.com/Connorlee487) +- [Lanxi](https://github.com/player1notfound) +- [Alex](https://github.com/axie22) +- 4th ? -## 1: `pyPet` +## `pyPet` **Theme:** A tiny virtual pet in your terminal. @@ -17,12 +17,13 @@ Group Members `pyPet` creates a small, stateful pet that lives in your terminal. You can feed it, play with it, and check on its mood. **Core Functions** + - `create_pet(name: str, species: str, mood: str, hunger: int)` - - Initializes a new pet with starting attributes. + - Initializes a new pet with starting attributes. - `feed(pet: dict, food: str, portion: int, treat: bool)` - - Reduces hunger, may improve mood based on treat type. + - Reduces hunger, may improve mood based on treat type. - `play(pet: dict, game: str, energy: int, reward: bool)` - - Boosts happiness and decreases energy over time. + - Boosts happiness and decreases energy over time. - `status(pet: dict, color: bool, verbose: bool, ascii_art: bool)` - - Prints the current pet’s stats and ASCII representation. + - Prints the current pet’s stats and ASCII representation. From 607bf42befcaf528dc082c3bcec33528add34985 Mon Sep 17 00:00:00 2001 From: axie22 Date: Sat, 1 Nov 2025 21:58:09 -0400 Subject: [PATCH 07/37] Pipfile created --- Pipfile | 11 +++++++++++ Pipfile.lock | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..d61ea53 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..bc0ddb5 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,20 @@ +{ + "_meta": { + "hash": { + "sha256": "494d5b4f482f0ef471f49afe28f00ec1a2ff75da2ce65060d8cabaeb3da2f100" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": {} +} From b46ee946feb0430b9d5cd8cf57f8a320e0f2f680 Mon Sep 17 00:00:00 2001 From: axie22 Date: Sat, 1 Nov 2025 21:58:31 -0400 Subject: [PATCH 08/37] created demo file --- src/demo.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/demo.py diff --git a/src/demo.py b/src/demo.py new file mode 100644 index 0000000..674ea70 --- /dev/null +++ b/src/demo.py @@ -0,0 +1,3 @@ +from pypet import create_pet + +pet = create_pet("Mochi", "otter") From 39da5f6b0da5c12e91ab8192f7ab425eb10656d0 Mon Sep 17 00:00:00 2001 From: axie22 Date: Sat, 1 Nov 2025 21:58:45 -0400 Subject: [PATCH 09/37] Added unit tests for create pet --- tests/test_create_pet.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/test_create_pet.py diff --git a/tests/test_create_pet.py b/tests/test_create_pet.py new file mode 100644 index 0000000..a1b9378 --- /dev/null +++ b/tests/test_create_pet.py @@ -0,0 +1,36 @@ +from pypet import create_pet, ALLOWED_SPECIES, ALLOWED_MOODS + +def test_create_pet_happy_path(): + pet = create_pet("Mochi", "Otter", mood="Happy", hunger=10, energy=90) + assert pet["name"] == "Mochi" + assert pet["species"] == "otter" + assert pet["mood"] == "happy" + assert 0 <= pet["hunger"] <= 100 + assert 0 <= pet["energy"] <= 100 + assert 0 <= pet["happiness"] <= 100 + assert pet["ascii_key"] == "otter:happy" + +def test_create_pet_validation_errors(): + for bad in ("", " "): + try: + create_pet(bad, "cat") + assert False, "expected ValueError for empty name" + except ValueError: + pass + try: + create_pet("Ok", "dragon") + assert False, "expected ValueError for species" + except ValueError: + pass + try: + create_pet("Ok", "cat", mood="ecstatic") + assert False, "expected ValueError for mood" + except ValueError: + pass + +def test_defaults_and_normalization(): + pet = create_pet(" PIP ", "DoG") + assert pet["name"] == "PIP" + assert pet["species"] == "dog" + assert pet["mood"] == "neutral" + assert pet["energy"] == 70 # default From 7073a9c1dcff18f54dfbb537b232f035d1c56324 Mon Sep 17 00:00:00 2001 From: axie22 Date: Sat, 1 Nov 2025 21:59:09 -0400 Subject: [PATCH 10/37] Created init file with create pet --- src/pypet/__init__.py | 27 +++++++++++++++++++++++++++ src/pypet/init__.py | 0 src/pypet/main__.py | 11 ----------- 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 src/pypet/__init__.py delete mode 100644 src/pypet/init__.py delete mode 100644 src/pypet/main__.py diff --git a/src/pypet/__init__.py b/src/pypet/__init__.py new file mode 100644 index 0000000..ee85dba --- /dev/null +++ b/src/pypet/__init__.py @@ -0,0 +1,27 @@ +""" +pyPet + +APIs: +- create_pet() +- feed(fill in args when done) +- play(fill in args when done) +- status(fill in args when done) +""" + +from .pet import ( + create_pet, + ALLOWED_SPECIES, + ALLOWED_MOODS, + BOUNDS, + Pet, +) + +__all__ = [ + "create_pet", + "ALLOWED_SPECIES", + "ALLOWED_MOODS", + "BOUNDS", + "Pet", +] + +__version__ = "0.1.0" diff --git a/src/pypet/init__.py b/src/pypet/init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/pypet/main__.py b/src/pypet/main__.py deleted file mode 100644 index 9fda63f..0000000 --- a/src/pypet/main__.py +++ /dev/null @@ -1,11 +0,0 @@ -import pypet.pet as pet - - -def main(): - """ - For pypet - """ - - -if __name__ == "__main__": - main() \ No newline at end of file From 5a2288a2e8c5fc06db029d8e87d676134a268b9f Mon Sep 17 00:00:00 2001 From: player1notfound Date: Sun, 2 Nov 2025 16:39:18 -0500 Subject: [PATCH 11/37] add --- Pipfile | 1 + Pipfile.lock | 46 ++++++++++++++++++++++++-- src/demo.py | 18 ++++++++-- src/pypet/__init__.py | 2 ++ src/pypet/pet.py | 76 ++++++++++++++++++++++++++++++++++++++++--- tests/test_status.py | 30 +++++++++++++++++ 6 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 tests/test_status.py diff --git a/Pipfile b/Pipfile index d61ea53..e915499 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] [dev-packages] +pytest = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index bc0ddb5..c6d048c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "494d5b4f482f0ef471f49afe28f00ec1a2ff75da2ce65060d8cabaeb3da2f100" + "sha256": "5b4a9aad2361219c16cb44268f75fed3122edbaff027f7316f5d694354668276" }, "pipfile-spec": 6, "requires": { @@ -16,5 +16,47 @@ ] }, "default": {}, - "develop": {} + "develop": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + } + } } diff --git a/src/demo.py b/src/demo.py index 674ea70..c462380 100644 --- a/src/demo.py +++ b/src/demo.py @@ -1,3 +1,17 @@ -from pypet import create_pet +from pypet import create_pet, status +import time -pet = create_pet("Mochi", "otter") +def main(): + pet = create_pet("Mochi", "capybara", mood="happy", hunger=20, energy=80) + print(status(pet, color=True, verbose=True, ascii_art=True)) + time.sleep(1) + + pet["hunger"]+= 15 + pet["energy"]-= 10 + pet["happiness"]-= 5 + pet["last_interaction_at"]=time.time() + + print(status(pet, color=True, verbose=True, ascii_art=False)) + +if __name__=="__main__": + main() diff --git a/src/pypet/__init__.py b/src/pypet/__init__.py index ee85dba..0218005 100644 --- a/src/pypet/__init__.py +++ b/src/pypet/__init__.py @@ -10,6 +10,7 @@ from .pet import ( create_pet, + status, ALLOWED_SPECIES, ALLOWED_MOODS, BOUNDS, @@ -18,6 +19,7 @@ __all__ = [ "create_pet", + "status", "ALLOWED_SPECIES", "ALLOWED_MOODS", "BOUNDS", diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 192e6e7..3b5ecdd 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -1,10 +1,43 @@ from __future__ import annotations - +from datetime import datetime from dataclasses import dataclass from time import time from typing import TypedDict, Dict from uuid import uuid4 +ANIMAL_ART = { + "cat": r""" + /\_/\ +( o.o ) + > ^ < +""", + "dog": r""" + / \__ + ( @\___ + / O +/ (_____/ +/_____/ U +""", + "otter": r""" + (\_._/) + ( o o ) + > ^ < +""", + "capybara": r""" + ( \_______/ ) + ( o o ) + ( - ) + """, + "duck": r""" +<(o )___ + ( ._> / + `---' +""" +} + + + + ALLOWED_SPECIES = {"cat", "dog", "otter", "capybara", "duck"} ALLOWED_MOODS = {"happy", "neutral", "grumpy", "sleepy", "hungry", "sad"} BOUNDS: Dict[str, tuple[int, int]] = { @@ -131,6 +164,41 @@ def play(pet: dict, game: str, energy: int, reward: bool): # code return {} -def status(pet: dict, color: bool, verbose: bool, ascii_art: bool): - # code - return {} \ No newline at end of file +def status(pet: dict, color: bool = False, verbose: bool = False, ascii_art: bool = False): + if not isinstance(pet, dict): + raise ValueError("pet must be a dictionary") + + name = pet.get("name", "Unknown") + species = pet.get("species", "unknown") + mood = pet.get("mood", "neutral") + hunger = pet.get("hunger", 0) + energy = pet.get("energy", 0) + happiness = pet.get("happiness", 0) + + + if color: + colors = { + "happy": "\033[92m", #green + "neutral": "\033[93m", #yellow + "grumpy": "\033[91m", #red + "sleepy": "\033[94m", #blue + "hungry": "\033[95m", #magenta + "sad": "\033[90m", #gray + } + end = "\033[0m" + mood_text = f"{colors.get(mood, '')}{mood}{end}" + else: + mood_text = mood + + summary = f"{name} the {species} looks {mood_text}." + + if verbose: + summary += f"\nHunger: {hunger}/100 | Energy: {energy}/100 | Happiness: {happiness}/100" + if "last_interaction_at" in pet: + summary += f"\nLast interaction: {datetime.fromtimestamp(pet['last_interaction_at']).strftime('%Y-%m-%d %H:%M:%S')}" + + if ascii_art: + art = ANIMAL_ART.get(species, "(•ᴗ•)") + summary += "\n" + art.strip() + + return summary \ No newline at end of file diff --git a/tests/test_status.py b/tests/test_status.py new file mode 100644 index 0000000..bef6982 --- /dev/null +++ b/tests/test_status.py @@ -0,0 +1,30 @@ +from pypet import create_pet, status + +def test_status_basic_output(): + pet = create_pet("Mochi", "otter", mood="happy") + s = status(pet) + assert "Mochi" in s + assert "otter" in s + assert "happy" in s + +def test_status_verbose(): + pet = create_pet("Pip", "cat") + s = status(pet, verbose=True) + assert "Hunger" in s + assert "Energy" in s + assert "Happiness" in s + +def test_status_ascii_art(): + # Check that ASCII art for each species appears + animals = ["cat", "dog", "otter", "capybara", "duck"] + for species in animals: + pet = create_pet("Test", species) + s = status(pet, ascii_art=True) + # Just check for one non-empty line of ASCII art + assert "(" in s or "\\" in s or "_" in s + +def test_status_color(): + pet = create_pet("Mochi", "otter", mood="happy") + s = status(pet, color=True) + # ANSI escape codes always start with \033[ + assert "\033[" in s From 500831915da133b915542130111d63aa6e42d858 Mon Sep 17 00:00:00 2001 From: player1notfound Date: Sun, 2 Nov 2025 16:43:03 -0500 Subject: [PATCH 12/37] add --- tests/test_status.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_status.py b/tests/test_status.py index bef6982..a45723a 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -15,16 +15,14 @@ def test_status_verbose(): assert "Happiness" in s def test_status_ascii_art(): - # Check that ASCII art for each species appears animals = ["cat", "dog", "otter", "capybara", "duck"] for species in animals: pet = create_pet("Test", species) s = status(pet, ascii_art=True) - # Just check for one non-empty line of ASCII art + assert "(" in s or "\\" in s or "_" in s def test_status_color(): pet = create_pet("Mochi", "otter", mood="happy") s = status(pet, color=True) - # ANSI escape codes always start with \033[ assert "\033[" in s From 46df437e6fb43a9d0a0774403965b0bfed2e3a95 Mon Sep 17 00:00:00 2001 From: player1notfound <123220161+player1notfound@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:01:45 -0500 Subject: [PATCH 13/37] Update demo.py --- src/demo.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/demo.py b/src/demo.py index c462380..35d58c1 100644 --- a/src/demo.py +++ b/src/demo.py @@ -4,14 +4,7 @@ def main(): pet = create_pet("Mochi", "capybara", mood="happy", hunger=20, energy=80) print(status(pet, color=True, verbose=True, ascii_art=True)) - time.sleep(1) - - pet["hunger"]+= 15 - pet["energy"]-= 10 - pet["happiness"]-= 5 - pet["last_interaction_at"]=time.time() - - print(status(pet, color=True, verbose=True, ascii_art=False)) + if __name__=="__main__": main() From 762b544be3689c00e9e6a6453576a43e7d96d5ca Mon Sep 17 00:00:00 2001 From: player1notfound <123220161+player1notfound@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:02:02 -0500 Subject: [PATCH 14/37] add --- src/demo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/demo.py b/src/demo.py index 35d58c1..69e0d83 100644 --- a/src/demo.py +++ b/src/demo.py @@ -5,6 +5,5 @@ def main(): pet = create_pet("Mochi", "capybara", mood="happy", hunger=20, energy=80) print(status(pet, color=True, verbose=True, ascii_art=True)) - if __name__=="__main__": main() From 7b04184a3ede4d62272acf12b49444ba0830fb57 Mon Sep 17 00:00:00 2001 From: player1notfound Date: Sun, 2 Nov 2025 18:10:37 -0500 Subject: [PATCH 15/37] Update demo.py --- src/demo.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/demo.py b/src/demo.py index c462380..35d58c1 100644 --- a/src/demo.py +++ b/src/demo.py @@ -4,14 +4,7 @@ def main(): pet = create_pet("Mochi", "capybara", mood="happy", hunger=20, energy=80) print(status(pet, color=True, verbose=True, ascii_art=True)) - time.sleep(1) - - pet["hunger"]+= 15 - pet["energy"]-= 10 - pet["happiness"]-= 5 - pet["last_interaction_at"]=time.time() - - print(status(pet, color=True, verbose=True, ascii_art=False)) + if __name__=="__main__": main() From 475764f17e71112efb060110a7ba4752e74d8f1b Mon Sep 17 00:00:00 2001 From: player1notfound Date: Sun, 2 Nov 2025 18:33:50 -0500 Subject: [PATCH 16/37] Revert --- src/demo.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/demo.py b/src/demo.py index 69e0d83..c462380 100644 --- a/src/demo.py +++ b/src/demo.py @@ -4,6 +4,14 @@ def main(): pet = create_pet("Mochi", "capybara", mood="happy", hunger=20, energy=80) print(status(pet, color=True, verbose=True, ascii_art=True)) - + time.sleep(1) + + pet["hunger"]+= 15 + pet["energy"]-= 10 + pet["happiness"]-= 5 + pet["last_interaction_at"]=time.time() + + print(status(pet, color=True, verbose=True, ascii_art=False)) + if __name__=="__main__": main() From d9891109e336cbb504372b24496820feaa7ad190 Mon Sep 17 00:00:00 2001 From: player1notfound <123220161+player1notfound@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:35:30 -0500 Subject: [PATCH 17/37] Revert "Code" --- Pipfile | 1 - Pipfile.lock | 46 ++------------------------ src/demo.py | 18 ++-------- src/pypet/__init__.py | 2 -- src/pypet/pet.py | 76 +++---------------------------------------- tests/test_status.py | 28 ---------------- 6 files changed, 8 insertions(+), 163 deletions(-) delete mode 100644 tests/test_status.py diff --git a/Pipfile b/Pipfile index e915499..d61ea53 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,6 @@ name = "pypi" [packages] [dev-packages] -pytest = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index c6d048c..bc0ddb5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5b4a9aad2361219c16cb44268f75fed3122edbaff027f7316f5d694354668276" + "sha256": "494d5b4f482f0ef471f49afe28f00ec1a2ff75da2ce65060d8cabaeb3da2f100" }, "pipfile-spec": 6, "requires": { @@ -16,47 +16,5 @@ ] }, "default": {}, - "develop": { - "iniconfig": { - "hashes": [ - "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", - "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" - ], - "markers": "python_version >= '3.10'", - "version": "==2.3.0" - }, - "packaging": { - "hashes": [ - "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", - "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" - ], - "markers": "python_version >= '3.8'", - "version": "==25.0" - }, - "pluggy": { - "hashes": [ - "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", - "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" - ], - "markers": "python_version >= '3.9'", - "version": "==1.6.0" - }, - "pygments": { - "hashes": [ - "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", - "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" - ], - "markers": "python_version >= '3.8'", - "version": "==2.19.2" - }, - "pytest": { - "hashes": [ - "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", - "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==8.4.2" - } - } + "develop": {} } diff --git a/src/demo.py b/src/demo.py index c462380..674ea70 100644 --- a/src/demo.py +++ b/src/demo.py @@ -1,17 +1,3 @@ -from pypet import create_pet, status -import time +from pypet import create_pet -def main(): - pet = create_pet("Mochi", "capybara", mood="happy", hunger=20, energy=80) - print(status(pet, color=True, verbose=True, ascii_art=True)) - time.sleep(1) - - pet["hunger"]+= 15 - pet["energy"]-= 10 - pet["happiness"]-= 5 - pet["last_interaction_at"]=time.time() - - print(status(pet, color=True, verbose=True, ascii_art=False)) - -if __name__=="__main__": - main() +pet = create_pet("Mochi", "otter") diff --git a/src/pypet/__init__.py b/src/pypet/__init__.py index 0218005..ee85dba 100644 --- a/src/pypet/__init__.py +++ b/src/pypet/__init__.py @@ -10,7 +10,6 @@ from .pet import ( create_pet, - status, ALLOWED_SPECIES, ALLOWED_MOODS, BOUNDS, @@ -19,7 +18,6 @@ __all__ = [ "create_pet", - "status", "ALLOWED_SPECIES", "ALLOWED_MOODS", "BOUNDS", diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 3b5ecdd..192e6e7 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -1,43 +1,10 @@ from __future__ import annotations -from datetime import datetime + from dataclasses import dataclass from time import time from typing import TypedDict, Dict from uuid import uuid4 -ANIMAL_ART = { - "cat": r""" - /\_/\ -( o.o ) - > ^ < -""", - "dog": r""" - / \__ - ( @\___ - / O -/ (_____/ -/_____/ U -""", - "otter": r""" - (\_._/) - ( o o ) - > ^ < -""", - "capybara": r""" - ( \_______/ ) - ( o o ) - ( - ) - """, - "duck": r""" -<(o )___ - ( ._> / - `---' -""" -} - - - - ALLOWED_SPECIES = {"cat", "dog", "otter", "capybara", "duck"} ALLOWED_MOODS = {"happy", "neutral", "grumpy", "sleepy", "hungry", "sad"} BOUNDS: Dict[str, tuple[int, int]] = { @@ -164,41 +131,6 @@ def play(pet: dict, game: str, energy: int, reward: bool): # code return {} -def status(pet: dict, color: bool = False, verbose: bool = False, ascii_art: bool = False): - if not isinstance(pet, dict): - raise ValueError("pet must be a dictionary") - - name = pet.get("name", "Unknown") - species = pet.get("species", "unknown") - mood = pet.get("mood", "neutral") - hunger = pet.get("hunger", 0) - energy = pet.get("energy", 0) - happiness = pet.get("happiness", 0) - - - if color: - colors = { - "happy": "\033[92m", #green - "neutral": "\033[93m", #yellow - "grumpy": "\033[91m", #red - "sleepy": "\033[94m", #blue - "hungry": "\033[95m", #magenta - "sad": "\033[90m", #gray - } - end = "\033[0m" - mood_text = f"{colors.get(mood, '')}{mood}{end}" - else: - mood_text = mood - - summary = f"{name} the {species} looks {mood_text}." - - if verbose: - summary += f"\nHunger: {hunger}/100 | Energy: {energy}/100 | Happiness: {happiness}/100" - if "last_interaction_at" in pet: - summary += f"\nLast interaction: {datetime.fromtimestamp(pet['last_interaction_at']).strftime('%Y-%m-%d %H:%M:%S')}" - - if ascii_art: - art = ANIMAL_ART.get(species, "(•ᴗ•)") - summary += "\n" + art.strip() - - return summary \ No newline at end of file +def status(pet: dict, color: bool, verbose: bool, ascii_art: bool): + # code + return {} \ No newline at end of file diff --git a/tests/test_status.py b/tests/test_status.py deleted file mode 100644 index a45723a..0000000 --- a/tests/test_status.py +++ /dev/null @@ -1,28 +0,0 @@ -from pypet import create_pet, status - -def test_status_basic_output(): - pet = create_pet("Mochi", "otter", mood="happy") - s = status(pet) - assert "Mochi" in s - assert "otter" in s - assert "happy" in s - -def test_status_verbose(): - pet = create_pet("Pip", "cat") - s = status(pet, verbose=True) - assert "Hunger" in s - assert "Energy" in s - assert "Happiness" in s - -def test_status_ascii_art(): - animals = ["cat", "dog", "otter", "capybara", "duck"] - for species in animals: - pet = create_pet("Test", species) - s = status(pet, ascii_art=True) - - assert "(" in s or "\\" in s or "_" in s - -def test_status_color(): - pet = create_pet("Mochi", "otter", mood="happy") - s = status(pet, color=True) - assert "\033[" in s From 003703641b421b31d92c8111c8beb54caad40356 Mon Sep 17 00:00:00 2001 From: axie22 Date: Sun, 2 Nov 2025 23:05:45 -0500 Subject: [PATCH 18/37] Add CI workflow --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..969ab8e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + pull_request: + branches: [ pipfile-experiment ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-and-build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install build + test tooling + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[dev]" + # build tools + python -m pip install build + + - name: Run tests + run: | + pytest -q + + - name: Build package (sdist & wheel) + run: | + python -m build + + - name: Upload dist artifacts + uses: actions/upload-artifact@v4 + with: + name: pypet-dist-${{ matrix.python-version }} + path: dist/* From ceb286d5311ddd0a577c4c355d1ce63218e32b9d Mon Sep 17 00:00:00 2001 From: mzhou3299 Date: Mon, 3 Nov 2025 13:53:53 -0500 Subject: [PATCH 19/37] Implemented feed() and helper functions for pyPet --- src/pypet/pet.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 192e6e7..6ee31fb 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -124,8 +124,53 @@ def create_pet( def feed(pet: dict, food: str, portion: int, treat: bool): - # code - return {} + """ + Feed the pet to reduce hunger and improve happiness slightly. + Also updates mood and timestamps. + """ + if not isinstance(pet, dict): + raise ValueError("pet must be a dict created by create_pet()") + if "hunger" not in pet or "happiness" not in pet: + raise ValueError("invalid pet schema") + + if not isinstance(food, str): + raise ValueError("food must be a string") + if not isinstance(portion, int) or not (0 <= portion <= 100): + raise ValueError("portion must be between 0 and 100") + + # Time since last interaction + pet = time_passes(pet, seconds=1800) + + FOOD_EFFECTS = { + "kibble": 15, + "fish": 25, + "carrot": 10, + "steak": 30, + "cookie": 20, + } + + base_effect = FOOD_EFFECTS.get(food.lower(), 10) + hunger_delta = base_effect * (portion / 100) + if treat: + hunger_delta += 5 + + lo, hi = BOUNDS["hunger"] + pet["hunger"] = clamp(pet["hunger"] - hunger_delta, lo, hi) + + # Increase happiness based on fullness + fullness_bonus = (100 - pet["hunger"]) / 15 + pet["happiness"] = clamp( + pet["happiness"] + fullness_bonus + (5 if treat else 0), + *BOUNDS["happiness"], + ) + + # Update last interaction + pet["last_interaction_at"] = time() + + # Recalculate mood + pet = update_mood(pet) + + return pet def play(pet: dict, game: str, energy: int, reward: bool): # code @@ -133,4 +178,38 @@ def play(pet: dict, game: str, energy: int, reward: bool): def status(pet: dict, color: bool, verbose: bool, ascii_art: bool): # code - return {} \ No newline at end of file + return {} + +def update_mood(pet: Pet) -> Pet: + """Recalculate mood based on hunger, energy, and happiness.""" + hunger, energy, happiness = pet["hunger"], pet["energy"], pet["happiness"] + + if hunger > 80: + pet["mood"] = "hungry" + elif energy < 20: + pet["mood"] = "sleepy" + elif happiness > 70: + pet["mood"] = "happy" + elif happiness < 30: + pet["mood"] = "sad" + else: + pet["mood"] = "neutral" + + pet["ascii_key"] = f"{pet['species']}:{pet['mood']}" + return pet + +def time_passes(pet: Pet, seconds: int = 3600) -> Pet: + """Simulate time passing: hunger increases and happiness decreases.""" + decay_factor = seconds / 3600 + pet["hunger"] = clamp(pet["hunger"] + 5 * decay_factor, *BOUNDS["hunger"]) + pet["happiness"] = clamp(pet["happiness"] - 3 * decay_factor, *BOUNDS["happiness"]) + pet["last_interaction_at"] = time() + return update_mood(pet) + +def describe_pet(pet: Pet) -> str: + """Return a readable summary of the pet's state.""" + return ( + f"{pet['name']} the {pet['species']} looks {pet['mood']}! " + f"Hunger: {pet['hunger']:.1f}/100, Energy: {pet['energy']}/100, " + f"Happiness: {pet['happiness']:.1f}/100." + ) \ No newline at end of file From 02b7e23a6e38fc0ccff30a1fba2808ff4912a2f8 Mon Sep 17 00:00:00 2001 From: connorlee487 Date: Mon, 3 Nov 2025 14:36:14 -0500 Subject: [PATCH 20/37] finished func and added tests --- requirements.txt | 3 +- src/demo.py | 2 + src/pypet/__init__.py | 2 + src/pypet/pet.py | 74 +++++++++++++- tests/test_play.py | 223 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 tests/test_play.py diff --git a/requirements.txt b/requirements.txt index b02bf0c..bcaa5c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pipenv twine -build \ No newline at end of file +build +pytest diff --git a/src/demo.py b/src/demo.py index 674ea70..acdaf5e 100644 --- a/src/demo.py +++ b/src/demo.py @@ -1,3 +1,5 @@ from pypet import create_pet +from pypet.pet import play pet = create_pet("Mochi", "otter") +play(pet, "fetch", 10, True) \ No newline at end of file diff --git a/src/pypet/__init__.py b/src/pypet/__init__.py index ee85dba..198634e 100644 --- a/src/pypet/__init__.py +++ b/src/pypet/__init__.py @@ -10,6 +10,7 @@ from .pet import ( create_pet, + play, ALLOWED_SPECIES, ALLOWED_MOODS, BOUNDS, @@ -18,6 +19,7 @@ __all__ = [ "create_pet", + "play", "ALLOWED_SPECIES", "ALLOWED_MOODS", "BOUNDS", diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 192e6e7..73f703d 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -128,8 +128,78 @@ def feed(pet: dict, food: str, portion: int, treat: bool): return {} def play(pet: dict, game: str, energy: int, reward: bool): - # code - return {} + """ + Play a game with the pet, updating its energy and happiness. + + Args: + pet: Pet dict to play with + game: Name of the game being played + energy: Amount of energy to deduct (must be positive) + reward: Whether to give a treat/happy boost + + Returns: + dict: Updated pet dict with modified energy, happiness, and last_interaction_at + + Raises: + ValueError: if energy is not a positive integer + """ + if not isinstance(energy, int) or energy <= 0: + raise ValueError("energy must be a positive integer") + + # Get current stats + current_energy = pet.get("energy", 0) + current_happiness = pet.get("happiness", 0) + species = pet.get("species", "").lower() + + # Check if pet has enough energy to play + if current_energy < energy: + # Not enough energy - small happiness penalty + new_energy = 0 + new_happiness = clamp(current_happiness - 2, *BOUNDS["happiness"]) + else: + # Deduct energy + new_energy = clamp(current_energy - energy, *BOUNDS["energy"]) + + # Base happiness boost + happiness_boost = 10 if reward else 5 + + # Species-specific multipliers for certain games + species_boost = 0 + if species == "dog" and "fetch" in game.lower(): + species_boost = 3 + elif species == "cat" and ("laser" in game.lower() or "string" in game.lower()): + species_boost = 3 + elif species == "duck" and ("water" in game.lower() or "swim" in game.lower()): + species_boost = 3 + elif species == "otter" and ("water" in game.lower() or "swim" in game.lower()): + species_boost = 4 + elif species == "capybara" and "relax" in game.lower(): + species_boost = 2 + + new_happiness = clamp(current_happiness + happiness_boost + species_boost, *BOUNDS["happiness"]) + + # Update mood based on new stats + new_mood = pet.get("mood", "neutral") + if new_energy < 20: + new_mood = "sleepy" + elif new_happiness >= 80: + new_mood = "happy" + elif new_happiness < 30: + new_mood = "sad" + elif new_happiness < 50: + new_mood = "grumpy" + else: + new_mood = "neutral" + + # Create updated pet + updated_pet = pet.copy() + updated_pet["energy"] = new_energy + updated_pet["happiness"] = new_happiness + updated_pet["mood"] = new_mood + updated_pet["last_interaction_at"] = time() + updated_pet["ascii_key"] = f"{species}:{new_mood}" + + return updated_pet def status(pet: dict, color: bool, verbose: bool, ascii_art: bool): # code diff --git a/tests/test_play.py b/tests/test_play.py new file mode 100644 index 0000000..4d579ef --- /dev/null +++ b/tests/test_play.py @@ -0,0 +1,223 @@ +from pypet import create_pet +from pypet import play + +def test_play_basic_happy_path(): + """Test basic play functionality with reward""" + pet = create_pet("Buddy", "dog", energy=80, hunger=20) + initial_energy = pet["energy"] + initial_happiness = pet["happiness"] + + updated_pet = play(pet, "fetch", energy=30, reward=True) + + # Check energy decreased by expected amount + assert updated_pet["energy"] == initial_energy - 30 + assert updated_pet["energy"] == 50 + + # Check happiness increased (base + reward + species bonus) + # reward=True gives 10 base + 3 species bonus for dog+fetch = 13 total + assert updated_pet["happiness"] > initial_happiness + assert updated_pet["happiness"] == initial_happiness + 13 + + # Check last_interaction_at was updated + assert updated_pet["last_interaction_at"] > pet["last_interaction_at"] + + +def test_play_without_reward(): + """Test play without giving reward""" + pet = create_pet("Whiskers", "cat", energy=60, hunger=30) + initial_energy = pet["energy"] + initial_happiness = pet["happiness"] + + updated_pet = play(pet, "laser pointer", energy=20, reward=False) + + # Check energy decreased + assert updated_pet["energy"] == initial_energy - 20 + assert updated_pet["energy"] == 40 + + # Check happiness increased (base + species bonus, no reward) + assert updated_pet["happiness"] > initial_happiness + assert updated_pet["happiness"] >= initial_happiness + 8 # 5 base + 3 species + + +def test_play_species_bonus_dog_fetch(): + """Test dog gets bonus for fetch game""" + pet = create_pet("Max", "dog", energy=100, hunger=10) + initial_happiness = pet["happiness"] + + updated_pet = play(pet, "Fetch the Ball", energy=10, reward=False) + + # Dog + fetch should give extra happiness + assert updated_pet["happiness"] >= initial_happiness + 8 # 5 base + 3 species + + +def test_play_species_bonus_cat_laser(): + """Test cat gets bonus for laser/string games""" + pet = create_pet("Shadow", "cat", energy=90, hunger=15) + initial_happiness = pet["happiness"] + + updated_pet = play(pet, "laser tag", energy=15, reward=False) + + # Cat + laser should give extra happiness + assert updated_pet["happiness"] >= initial_happiness + 8 # 5 base + 3 species + + +def test_play_species_bonus_otter_water(): + """Test otter gets bigger bonus for water games""" + pet = create_pet("Splash", "otter", energy=85, hunger=20) + initial_happiness = pet["happiness"] + + updated_pet = play(pet, "swim in pool", energy=25, reward=False) + + # Otter + water should give biggest bonus + assert updated_pet["happiness"] >= initial_happiness + 9 # 5 base + 4 species + + +def test_play_insufficient_energy(): + """Test play when pet doesn't have enough energy""" + pet = create_pet("Sleepy", "capybara", energy=10, hunger=40) + initial_happiness = pet["happiness"] + + # Try to use more energy than available + updated_pet = play(pet, "relax", energy=50, reward=True) + + # Energy should go to 0 + assert updated_pet["energy"] == 0 + + # Happiness should decrease slightly + assert updated_pet["happiness"] < initial_happiness + assert updated_pet["happiness"] == initial_happiness - 2 + + +def test_play_energy_clamping(): + """Test energy is clamped to 0-100""" + pet = create_pet("Active", "duck", energy=5, hunger=25) + + updated_pet = play(pet, "water play", energy=10, reward=False) + + # Should not go below 0 + assert updated_pet["energy"] >= 0 + + +def test_play_happiness_clamping(): + """Test happiness is clamped to 0-100""" + pet = create_pet("Joy", "dog", energy=80, hunger=5) + # Set very high happiness + pet["happiness"] = 95 + + updated_pet = play(pet, "fetch", energy=20, reward=True) + + # Should not exceed 100 + assert updated_pet["happiness"] <= 100 + + +def test_play_mood_changes_to_happy(): + """Test mood changes to happy when happiness is high""" + pet = create_pet("Cheerful", "cat", energy=90, hunger=10) + pet["happiness"] = 75 # Start close to happy + + updated_pet = play(pet, "laser", energy=10, reward=True) + + # Should become happy + assert updated_pet["mood"] == "happy" + assert updated_pet["ascii_key"] == "cat:happy" + + +def test_play_mood_changes_to_sleepy(): + """Test mood changes to sleepy when energy is low""" + pet = create_pet("Tired", "otter", energy=50, hunger=30) + + updated_pet = play(pet, "water games", energy=40, reward=False) + + # Should become sleepy + assert updated_pet["mood"] == "sleepy" + assert updated_pet["ascii_key"] == "otter:sleepy" + + +def test_play_mood_changes_to_sad(): + """Test mood changes to sad when happiness is low""" + pet = create_pet("Down", "duck", energy=60, hunger=50) + pet["happiness"] = 20 # Low happiness + + updated_pet = play(pet, "generic game", energy=10, reward=False) + + # Should become sad + assert updated_pet["mood"] == "sad" + assert updated_pet["ascii_key"] == "duck:sad" + + +def test_play_mood_changes_to_grumpy(): + """Test mood changes to grumpy when happiness is medium-low""" + pet = create_pet("Grouchy", "capybara", energy=70, hunger=45) + pet["happiness"] = 40 # Medium-low happiness + + updated_pet = play(pet, "relax", energy=30, reward=False) + + # Should become grumpy + assert updated_pet["mood"] == "grumpy" + assert updated_pet["ascii_key"] == "capybara:grumpy" + + +def test_play_invalid_energy(): + """Test play raises error for invalid energy values""" + pet = create_pet("Test", "dog", energy=50, hunger=30) + + # Test negative energy + try: + play(pet, "fetch", energy=-10, reward=False) + assert False, "expected ValueError for negative energy" + except ValueError: + pass + + # Test zero energy + try: + play(pet, "fetch", energy=0, reward=False) + assert False, "expected ValueError for zero energy" + except ValueError: + pass + + # Test non-integer energy + try: + play(pet, "fetch", energy="ten", reward=False) + assert False, "expected ValueError for non-integer energy" + except ValueError: + pass + + +def test_play_original_pet_not_modified(): + """Test that playing doesn't modify the original pet dict""" + pet = create_pet("Original", "dog", energy=80, hunger=25) + original_energy = pet["energy"] + original_happiness = pet["happiness"] + original_mood = pet["mood"] + + updated_pet = play(pet, "fetch", energy=30, reward=True) + + # Original pet should be unchanged + assert pet["energy"] == original_energy + assert pet["happiness"] == original_happiness + assert pet["mood"] == original_mood + + # Updated pet should be different + assert updated_pet["energy"] != pet["energy"] + + +def test_play_multiple_species_games(): + """Test all species with their preferred games""" + species_games = [ + ("dog", "fetch", 10), + ("cat", "string toy", 10), + ("otter", "swimming", 10), + ("duck", "water play", 10), + ("capybara", "relaxing", 10), + ] + + for species, game, expected_bonus in species_games: + pet = create_pet(f"{species.capitalize()}Test", species, energy=70, hunger=30) + initial_happiness = pet["happiness"] + + updated_pet = play(pet, game, energy=20, reward=False) + + # Each should get some bonus + assert updated_pet["happiness"] > initial_happiness + assert updated_pet["happiness"] >= initial_happiness + 5 # At least base boost + From 53a7f6459f89ed944af92055e13f3b8d2ff5efc9 Mon Sep 17 00:00:00 2001 From: player1notfound Date: Mon, 3 Nov 2025 14:48:02 -0500 Subject: [PATCH 21/37] Save --- src/demo.py | 4 +++- src/pypet/pet.py | 40 ++++++++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/demo.py b/src/demo.py index 35d58c1..983f948 100644 --- a/src/demo.py +++ b/src/demo.py @@ -2,9 +2,11 @@ import time def main(): + pet = create_pet("Mochi", "capybar", mood="happy", hunger=20, energy=80) + print(status(pet, color=True, verbose=True, ascii_art=True)) + time.sleep(3) pet = create_pet("Mochi", "capybara", mood="happy", hunger=20, energy=80) print(status(pet, color=True, verbose=True, ascii_art=True)) - if __name__=="__main__": main() diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 3b5ecdd..f65a683 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -168,37 +168,45 @@ def status(pet: dict, color: bool = False, verbose: bool = False, ascii_art: boo if not isinstance(pet, dict): raise ValueError("pet must be a dictionary") + pet["last_interaction_at"] = time() + name = pet.get("name", "Unknown") species = pet.get("species", "unknown") mood = pet.get("mood", "neutral") - hunger = pet.get("hunger", 0) - energy = pet.get("energy", 0) - happiness = pet.get("happiness", 0) - if color: colors = { - "happy": "\033[92m", #green - "neutral": "\033[93m", #yellow - "grumpy": "\033[91m", #red - "sleepy": "\033[94m", #blue - "hungry": "\033[95m", #magenta - "sad": "\033[90m", #gray + "happy": "\033[92m", # green + "neutral": "\033[93m", # yellow + "grumpy": "\033[91m", # red + "sleepy": "\033[94m", # blue + "hungry": "\033[95m", # magenta + "sad": "\033[90m", # gray } end = "\033[0m" mood_text = f"{colors.get(mood, '')}{mood}{end}" else: mood_text = mood - - summary = f"{name} the {species} looks {mood_text}." + + summary = "\n" + "-" * 40 + "\n" + summary += f"{name} the {species} looks {mood_text}." if verbose: - summary += f"\nHunger: {hunger}/100 | Energy: {energy}/100 | Happiness: {happiness}/100" - if "last_interaction_at" in pet: - summary += f"\nLast interaction: {datetime.fromtimestamp(pet['last_interaction_at']).strftime('%Y-%m-%d %H:%M:%S')}" + summary += "\n" + "-" * 40 + "\n" + for key, value in pet.items(): + if key in {"name", "species", "mood"}: + continue + if key in {"created_at", "last_interaction_at"}: + value = datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M:%S") + label = "Created at:" if key == "created_at" else "Last interaction:" + summary += f"{label} {value}\n" + else: + summary += f"{key}: {value}\n" + summary = summary.strip() + summary += "\n" + "-" * 40 if ascii_art: art = ANIMAL_ART.get(species, "(•ᴗ•)") - summary += "\n" + art.strip() + summary += "\n" + art.strip()+"\n" return summary \ No newline at end of file From 4b5d59a72932e5e50d45f4168a4cb1e73d8073ca Mon Sep 17 00:00:00 2001 From: mzhou3299 Date: Mon, 3 Nov 2025 15:01:39 -0500 Subject: [PATCH 22/37] Refined feed tests for realistic behavior and mood logic --- tests/test_feed_helper.py | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/test_feed_helper.py diff --git a/tests/test_feed_helper.py b/tests/test_feed_helper.py new file mode 100644 index 0000000..be9f1ea --- /dev/null +++ b/tests/test_feed_helper.py @@ -0,0 +1,93 @@ +from pypet.pet import create_pet, feed, update_mood, time_passes, describe_pet + + +def test_feed_reduces_hunger(): + + pet = create_pet("Mochi", "otter", "neutral", hunger=80) + updated_pet = feed(pet, "fish", 30, treat=False) + + assert 0 <= updated_pet["hunger"] < 80, "Hunger did not decrease as expected" + assert isinstance(updated_pet["hunger"], (int, float)) + + +def test_feed_increases_happiness(): + + pet = create_pet("Mochi", "otter", "neutral", hunger=70) + before = pet["happiness"] + updated_pet = feed(pet, "fish", 40, treat=False) + + assert updated_pet["happiness"] >= before, "Happiness should not decrease after feeding" + assert updated_pet["happiness"] <= 100, "Happiness should not exceed 100" + + +def test_feed_treat_makes_pet_happy(): + + pet = create_pet("Mochi", "otter", "neutral", hunger=60) + before_happiness = pet["happiness"] + + updated_pet = feed(pet, "fish", 20, treat=True) + + assert updated_pet["happiness"] > before_happiness, "Treat should boost happiness" + + assert updated_pet["mood"] in {"happy", "content", "neutral"}, ( + f"Mood after treat should be positive, got {updated_pet['mood']}" + ) + + + +def test_update_mood_adjusts_based_on_hunger_energy_happiness(): + + pet = { + "name": "Mochi", + "species": "otter", + "mood": "neutral", + "hunger": 90, + "energy": 50, + "happiness": 50, + } + pet = update_mood(pet) + + assert pet["mood"] in {"hungry", "sleepy", "happy", "sad", "neutral"}, "Invalid mood after update" + assert "ascii_key" in pet, "ascii_key not updated" + + +def test_time_passes_increases_hunger_and_updates_mood(): + + pet = create_pet("Mochi", "otter", "neutral", hunger=30) + before_hunger = pet["hunger"] + updated_pet = time_passes(pet, seconds=7200) # 2 hours later + + assert updated_pet["hunger"] > before_hunger, "Hunger should increase over time" + assert updated_pet["mood"] in {"hungry", "neutral", "sleepy", "sad", "happy"} + + +def test_describe_pet_returns_readable_summary(): + + pet = create_pet("Mochi", "otter", "neutral", hunger=40) + desc = describe_pet(pet) + + assert isinstance(desc, str), "Description should be a string" + assert "Mochi" in desc and "otter" in desc, "Missing pet info in description" + assert any(word in desc for word in ["happy", "neutral", "hungry", "sleepy", "sad"]), "Mood not in description" + + +def test_feed_does_not_exceed_bounds(): + + pet = create_pet("Mochi", "otter", "neutral", hunger=5) + updated_pet = feed(pet, "steak", 80, treat=True) + + assert 0 <= updated_pet["hunger"] <= 100, "Hunger out of bounds" + assert 0 <= updated_pet["happiness"] <= 100, "Happiness out of bounds" + + +def test_multiple_interactions_keep_state_consistent(): + + pet = create_pet("Mochi", "otter", "neutral", hunger=70) + pet = feed(pet, "fish", 30, treat=False) + pet = time_passes(pet, seconds=3600) + pet = feed(pet, "cookie", 50, treat=True) + + assert 0 <= pet["hunger"] <= 100 + assert 0 <= pet["energy"] <= 100 + assert 0 <= pet["happiness"] <= 100 + assert isinstance(pet["mood"], str) From f6a180f2bc5a0d4305b914f198aa3af4b4588aa2 Mon Sep 17 00:00:00 2001 From: player1notfound Date: Mon, 3 Nov 2025 15:53:00 -0500 Subject: [PATCH 23/37] Save --- src/demo.py | 6 +++--- src/pypet/pet.py | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/demo.py b/src/demo.py index 1cf55ee..19ae5cb 100644 --- a/src/demo.py +++ b/src/demo.py @@ -4,11 +4,11 @@ pet = create_pet("Mochi", "otter") play(pet, "fetch", 10, True) -print(status(pet, color=True, verbose=False, ascii_art=True)) +print(status(pet, color=True, verbose=True, ascii_art=True)) -pet = create_pet("Mochi", "otter") +pet = create_pet("Mochi", "cat") play(pet, "fetch", 10, True) -print(status(pet, color=True, verbose=False, ascii_art=True)) +print(status(pet, color=True, verbose=True, ascii_art=True)) diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 2cbd779..34767a3 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -305,19 +305,21 @@ def status(pet: dict, color: bool = False, verbose: bool = False, ascii_art: boo summary += "-" * 40 if verbose: - summary += "\n" + summary += "\n" for key, value in pet.items(): if key in {"name", "species", "mood"}: continue + # Format timestamps if key in {"created_at", "last_interaction_at"}: value = datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M:%S") label = "Created at:" if key == "created_at" else "Last interaction:" - summary += f"{label} {value}\n" else: - summary += f"{key}: {value}\n" - + # Capitalize first letter for keys + label = key.capitalize() + ":" + summary += f"{label} {value}\n" summary = summary.strip() - summary+= "\n"+"-" * 40 + summary += "\n" + "-" * 40 + if ascii_art: art=ANIMAL_ART.get(species, "(•ᴗ•)") From 3c12d7129def42cf095c55f6ccd612b4fbe8e29c Mon Sep 17 00:00:00 2001 From: axie22 Date: Mon, 3 Nov 2025 15:05:52 -0500 Subject: [PATCH 24/37] Added info for uploading to testpypi --- .github/workflows/Pipfile | 3 +++ README.md | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/workflows/Pipfile b/.github/workflows/Pipfile index f5d05e5..9b6bc31 100644 --- a/.github/workflows/Pipfile +++ b/.github/workflows/Pipfile @@ -10,6 +10,9 @@ pytz = "*" python-dateutil = "*" [dev-packages] +pytest = "*" +build = "*" +twine = "*" [requires] python_version = "3" diff --git a/README.md b/README.md index aa390b0..3a1234a 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,17 @@ Group Members - `status(pet: dict, color: bool, verbose: bool, ascii_art: bool)` - Prints the current pet’s stats and ASCII representation. +### Instructions for Running & Testing + +``` bash +pipenv install --dev +pipenv run pip install -e . +pipenv run pytest -q +``` + +Build + +``` bash +pipenv run python -m build +pipenv run twine upload --repository testpypi dist/* +``` From 0fb115bdaef133a54d25980a6c6521a77c3020dc Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 12:13:28 -0500 Subject: [PATCH 25/37] Setup status badge in event logger --- .github/workflows/ci.yml | 2 +- .github/workflows/event-logger.yml | 123 +++++++++++++++++++++++------ 2 files changed, 102 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 969ab8e..6244933 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,4 +49,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: pypet-dist-${{ matrix.python-version }} - path: dist/* + path: dist/* \ No newline at end of file diff --git a/.github/workflows/event-logger.yml b/.github/workflows/event-logger.yml index 31f231e..b3c8673 100644 --- a/.github/workflows/event-logger.yml +++ b/.github/workflows/event-logger.yml @@ -1,54 +1,133 @@ name: log github events + on: push: - branches: [main, master, pipfile-experiment] + branches: [ main, master, pipfile-experiment ] pull_request: - types: [opened, closed] - branches: [main, master, pipfile-experiment] + types: [ opened, closed ] + branches: [ main, master, pipfile-experiment ] + workflow_dispatch: {} # allow manual runs so the badge can show status any time + +permissions: + contents: read + +concurrency: + group: event-logger-${{ github.ref }} + cancel-in-progress: true + jobs: log: runs-on: ubuntu-latest env: - PIPENV_PIPFILE: .github/workflows/Pipfile # so this script doesn't use the Pipfile in the root directory + # keep using the workflow-local Pipfile so we don't pollute the project env + PIPENV_PIPFILE: .github/workflows/Pipfile COMMIT_LOG_API: ${{ secrets.COMMIT_LOG_API }} - GITHUB_LOGIN: ${{ github.actor }} # github login also available in github.triggering_actor, github.event.sender.login + GITHUB_LOGIN: ${{ github.actor }} COMMITS: ${{ toJSON(github.event.commits) }} REPOSITORY_URL: ${{ github.repositoryUrl }} EVENT_TYPE: ${{ github.event_name }} EVENT_ACTION: ${{ github.event.action }} PR_MERGED: ${{ github.event.pull_request.merged }} - PR_CREATED_AT: ${{ github.event.pull_request.created_at}} - PR_CLOSED_AT: ${{ github.event.pull_request.closed_at}} + PR_CREATED_AT: ${{ github.event.pull_request.created_at }} + PR_CLOSED_AT: ${{ github.event.pull_request.closed_at }} + steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: - fetch-depth: 0 # this is important so git fetches all history.. the actions/checkout by default fetches all history as one commit which throws off stats - - uses: actions/setup-python@v3 + fetch-depth: 0 # needed for accurate stats + + - name: Setup Python + uses: actions/setup-python@v5 with: - python-version: "^3.9" - - name: Install dependencies + python-version: "3.10" + cache: pip + + - name: Install dependencies (pipenv in workflow scope) run: | + set -euo pipefail python -m pip install --upgrade pip - pip install --user pipenv - pipenv --python $(which python) + python -m pip install pipenv + pipenv --python "$(which python)" pipenv install + + # --- helpful diagnostics --- + - name: Dump GitHub context + run: | + set -euo pipefail + echo "Event: ${{ github.event_name }}" + echo "Action: ${{ github.event.action }}" + echo "Ref: ${{ github.ref }}" + echo "SHA: ${{ github.sha }}" + echo "Actor: ${{ github.actor }}" + echo "Repo: ${{ github.repository }}" + echo '${{ toJson(github.event) }}' > event.json + cat event.json | head -c 2000 || true + + # --- event-specific logging --- - name: Log pull request opened if: github.event_name == 'pull_request' && github.event.action == 'opened' run: | - pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t pull_request_opened -d $(echo $PR_CREATED_AT) -un $(echo $GITHUB_LOGIN) -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v + set -euo pipefail + pipenv run gitcommitlogger \ + -r "${REPOSITORY_URL}" \ + -t pull_request_opened \ + -d "${PR_CREATED_AT}" \ + -un "${GITHUB_LOGIN}" \ + -o commit_stats.csv \ + -u "${COMMIT_LOG_API}" \ + -v + - name: Log pull request closed and merged if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true run: | - echo $COMMITS > commits.json - cat commits.json # debugging - pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t pull_request_merged -d $(echo $PR_CLOSED_AT) -un $(echo $GITHUB_LOGIN) -i commits.json -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v + set -euo pipefail + echo '${{ toJson(github.event.pull_request) }}' > pr.json + echo "${COMMITS}" > commits.json + pipenv run gitcommitlogger \ + -r "${REPOSITORY_URL}" \ + -t pull_request_merged \ + -d "${PR_CLOSED_AT}" \ + -un "${GITHUB_LOGIN}" \ + -i commits.json \ + -o commit_stats.csv \ + -u "${COMMIT_LOG_API}" \ + -v + - name: Log pull request closed without merge if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == false run: | - pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t pull_request_closed -d $(echo $PR_CLOSED_AT) -un $(echo $GITHUB_LOGIN) -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v + set -euo pipefail + pipenv run gitcommitlogger \ + -r "${REPOSITORY_URL}" \ + -t pull_request_closed \ + -d "${PR_CLOSED_AT}" \ + -un "${GITHUB_LOGIN}" \ + -o commit_stats.csv \ + -u "${COMMIT_LOG_API}" \ + -v + - name: Log push if: github.event_name == 'push' run: | - echo $COMMITS > commits.json - cat commits.json # debugging - pipenv run gitcommitlogger -r $(echo $REPOSITORY_URL) -t $(echo $EVENT_TYPE) -i commits.json -o commit_stats.csv -u $(echo $COMMIT_LOG_API) -v + set -euo pipefail + echo "${COMMITS}" > commits.json + pipenv run gitcommitlogger \ + -r "${REPOSITORY_URL}" \ + -t "${EVENT_TYPE}" \ + -i commits.json \ + -o commit_stats.csv \ + -u "${COMMIT_LOG_API}" \ + -v + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: event-logger-${{ github.run_number }} + path: | + event.json + pr.json + commits.json + commit_stats.csv + if-no-files-found: ignore From e120a74839252eb2117742f067898bd683925550 Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 12:14:13 -0500 Subject: [PATCH 26/37] Added coverage badge --- Pipfile | 3 + Pipfile.lock | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 7 ++ 3 files changed, 297 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index d61ea53..b9f8677 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,9 @@ name = "pypi" [packages] [dev-packages] +pytest-cov = "*" +codecov = "*" +coveralls = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index bc0ddb5..e0c1a5b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "494d5b4f482f0ef471f49afe28f00ec1a2ff75da2ce65060d8cabaeb3da2f100" + "sha256": "ee01805fb13888671ff48eaeb0cae8c61bfc13466557c470afb2774abb142a7e" }, "pipfile-spec": 6, "requires": { @@ -16,5 +16,290 @@ ] }, "default": {}, - "develop": {} + "develop": { + "certifi": { + "hashes": [ + "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", + "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" + ], + "markers": "python_version >= '3.7'", + "version": "==2025.10.5" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "codecov": { + "hashes": [ + "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c", + "sha256:7d2b16c1153d01579a89a94ff14f9dbeb63634ee79e18c11036f34e7de66cbc9", + "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.1.13" + }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", + "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", + "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", + "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", + "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", + "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", + "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", + "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", + "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", + "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", + "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", + "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", + "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", + "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", + "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", + "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", + "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", + "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", + "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", + "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", + "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", + "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", + "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", + "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", + "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", + "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", + "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", + "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", + "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", + "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", + "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", + "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", + "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", + "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", + "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", + "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", + "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", + "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", + "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", + "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", + "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", + "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", + "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", + "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", + "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", + "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", + "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", + "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", + "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", + "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + ], + "markers": "python_version >= '3.7'", + "version": "==6.5.0" + }, + "coveralls": { + "hashes": [ + "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea", + "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026" + ], + "index": "pypi", + "markers": "python_version >= '3.5'", + "version": "==3.3.1" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + }, + "pytest-cov": { + "hashes": [ + "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", + "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==7.0.0" + }, + "requests": { + "hashes": [ + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" + ], + "markers": "python_version >= '3.9'", + "version": "==2.32.5" + }, + "urllib3": { + "hashes": [ + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + ], + "markers": "python_version >= '3.9'", + "version": "==2.5.0" + } + } } diff --git a/README.md b/README.md index 3a1234a..4845372 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Python Package Exercise +[![log github events](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml/badge.svg?branch=main)](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml) + An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. Group Members @@ -39,5 +41,10 @@ Build ``` bash pipenv run python -m build +``` + +Upload to TestPyPi + +``` bash pipenv run twine upload --repository testpypi dist/* ``` From d229892e18e57dc0fbcdaabb44e52bf341774576 Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 15:16:56 -0500 Subject: [PATCH 27/37] Badge fix --- .github/workflows/event-logger.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/event-logger.yml b/.github/workflows/event-logger.yml index b3c8673..2e26b07 100644 --- a/.github/workflows/event-logger.yml +++ b/.github/workflows/event-logger.yml @@ -24,7 +24,7 @@ jobs: COMMIT_LOG_API: ${{ secrets.COMMIT_LOG_API }} GITHUB_LOGIN: ${{ github.actor }} COMMITS: ${{ toJSON(github.event.commits) }} - REPOSITORY_URL: ${{ github.repositoryUrl }} + REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository }} EVENT_TYPE: ${{ github.event_name }} EVENT_ACTION: ${{ github.event.action }} PR_MERGED: ${{ github.event.pull_request.merged }} diff --git a/README.md b/README.md index 4845372..a3f4115 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python Package Exercise -[![log github events](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml/badge.svg?branch=main)](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml) +[![log github events](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml/badge.svg?branch=pipfile-experiment)](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml) An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. From a766badafb3a1ba94d69d4ad99896ed44db4d1fb Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 15:32:49 -0500 Subject: [PATCH 28/37] Updated badge to point to CI file --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3f4115..c8edd70 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Python Package Exercise -[![log github events](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml/badge.svg?branch=pipfile-experiment)](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/event-logger.yml) +[![CI / CD](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/ci.yml/badge.svg)](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/ci.yml) + An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. From 4d9f8c009aaa6e3cb087ffcd98bbc30165683e12 Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 16:17:32 -0500 Subject: [PATCH 29/37] created more comprehensive demo --- src/demo.py | 146 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 7 deletions(-) diff --git a/src/demo.py b/src/demo.py index 19ae5cb..b3cc5cc 100644 --- a/src/demo.py +++ b/src/demo.py @@ -1,16 +1,148 @@ -from pypet import create_pet,status,play +#!/usr/bin/env python3 +""" +Interactive pyPet demo (terminal) + +Run: + pipenv run python examples/demo.py +""" + +from __future__ import annotations + +import sys import time +from typing import Optional + +# Public API from your package +from pypet import create_pet, play, status, feed, ALLOWED_SPECIES, ALLOWED_MOODS + +# ---------- UI helpers ---------- + +def clear() -> None: + print("\033[2J\033[H", end="") + +def pause(msg: str = "Press Enter to continue...") -> None: + try: + input(msg) + except EOFError: + pass + +def choose(prompt: str, choices: list[str], default: Optional[str] = None) -> str: + """Simple numbered picker.""" + while True: + print(prompt) + for i, c in enumerate(choices, 1): + print(f" {i}. {c}") + if default: + print(f"[Enter for default: {default}]") + sel = input("> ").strip() + if not sel and default: + return default + if sel.isdigit() and 1 <= int(sel) <= len(choices): + return choices[int(sel) - 1] + print("Invalid choice. Try again.\n") + +def spinner(text: str, seconds: float = 1.2) -> None: + glyphs = "|/-\\" + end = time.time() + seconds + i = 0 + print(text, end=" ", flush=True) + while time.time() < end: + print(glyphs[i % len(glyphs)], end="\r", flush=True) + time.sleep(0.08) + i += 1 + print(" " * 20, end="\r") + +# ---------- Game loop ---------- + +def main() -> int: + clear() + print("🐾 Welcome to pyPet (interactive demo)\n") + + name = input("Name your pet: ").strip() or "Mochi" + species = choose("Pick a species:", sorted(ALLOWED_SPECIES), default="otter") + mood = choose("Pick a starting mood:", sorted(ALLOWED_MOODS), default="neutral") + + + # Start a medium-hungry, energetic pet + pet = create_pet(name=name, species=species, mood=mood, hunger=40, energy=80) + + while True: + clear() + print("=== pyPet ===\n") + print(status(pet, color=True, verbose=True, ascii_art=True)) # uses your package’s status() + print("\nActions:") + + menu = [ + ("p", "Play"), + ("f", "Feed"), + ("r", "Rename pet"), + ("m", "Change mood"), + ("s", "Show status"), + ("q", "Quit") + ] + + print(" " + " ".join(f"[{k}] {label}" for k, label in menu)) + choice = input("\n> ").strip().lower() + + if choice == "q": + print("\nBye!") + return 0 + + elif choice == "s": + print() + print(status(pet, color=True, verbose=True, ascii_art=True)) + pause() + continue + elif choice == "r": + new_name = input("New name: ").strip() + if new_name: + pet["name"] = new_name + continue -pet = create_pet("Mochi", "otter") -play(pet, "fetch", 10, True) -print(status(pet, color=True, verbose=True, ascii_art=True)) + elif choice == "m": + new_mood = choose("New mood:", sorted(ALLOWED_MOODS), default=pet.get("mood", "neutral")) + pet["mood"] = new_mood -pet = create_pet("Mochi", "cat") -play(pet, "fetch", 10, True) -print(status(pet, color=True, verbose=True, ascii_art=True)) + pet["ascii_key"] = f"{pet['species']}:{pet['mood']}" + continue + elif choice == "p": + game = choose("Game:", ["fetch", "tug", "chase", "hide-n-seek"], default="fetch") + try: + energy = int(input("How energetic is the play? (1–30, default 10): ").strip() or "10") + except ValueError: + energy = 10 + reward = input("Give a reward after? [y/N]: ").strip().lower().startswith("y") + spinner("Playing...") + # Uses your package function + play(pet, game, energy, reward) + print("Done!") + pause() + continue + elif choice == "f": + food = choose("Food:", ["kibble", "fish", "berries", "treat"], default="kibble") + try: + portion = int(input("Portion size (1–30, default 8): ").strip() or "8") + except ValueError: + portion = 8 + treat = input("Is it a special treat? [y/N]: ").strip().lower().startswith("y") + spinner("Feeding...") + # Uses your package function, if available + feed(pet, food, portion, treat) # type: ignore + print("Yum!") + pause() + continue + else: + print("Unknown option.") + time.sleep(0.6) +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + print("\nInterrupted. See you next time! 🐾") + raise SystemExit(130) From e6cd608b18a0d2d0ef405ae1a37ceda02f5444a6 Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 16:17:49 -0500 Subject: [PATCH 30/37] Added all new funcs to init file: --- src/pypet/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pypet/__init__.py b/src/pypet/__init__.py index 5e67a1d..c131653 100644 --- a/src/pypet/__init__.py +++ b/src/pypet/__init__.py @@ -10,22 +10,30 @@ from .pet import ( create_pet, + feed, play, status, ALLOWED_SPECIES, ALLOWED_MOODS, BOUNDS, Pet, + update_mood, + time_passes, + describe_pet, ) __all__ = [ "create_pet", - "status", + "feed", "play", + "status", "ALLOWED_SPECIES", "ALLOWED_MOODS", "BOUNDS", "Pet", + "update_mood", + "time_passes", + "describe_pet", ] __version__ = "0.1.0" From f81cdaf07578e214260eb17175cbed280215c895 Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 16:18:10 -0500 Subject: [PATCH 31/37] Fixed play func to update pet inplace --- src/pypet/pet.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 34767a3..33d9946 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -1,6 +1,5 @@ from __future__ import annotations from datetime import datetime -from dataclasses import dataclass from time import time from typing import TypedDict, Dict from uuid import uuid4 @@ -265,16 +264,13 @@ def play(pet: dict, game: str, energy: int, reward: bool): new_mood = "grumpy" else: new_mood = "neutral" + + pet["energy"] = new_energy + pet["happiness"] = new_happiness + pet["mood"] = new_mood + pet['ascii_key'] = f"{species}:{new_mood}" - # Create updated pet - updated_pet = pet.copy() - updated_pet["energy"] = new_energy - updated_pet["happiness"] = new_happiness - updated_pet["mood"] = new_mood - updated_pet["last_interaction_at"] = time() - updated_pet["ascii_key"] = f"{species}:{new_mood}" - - return updated_pet + return pet def status(pet: dict, color: bool = False, verbose: bool = False, ascii_art: bool = False): if not isinstance(pet, dict): @@ -359,4 +355,4 @@ def describe_pet(pet: Pet) -> str: f"{pet['name']} the {pet['species']} looks {pet['mood']}! " f"Hunger: {pet['hunger']:.1f}/100, Energy: {pet['energy']}/100, " f"Happiness: {pet['happiness']:.1f}/100." - ) \ No newline at end of file + ) From f823d5ee169b9e496f6f061a132d22f27fc3324e Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 16:20:18 -0500 Subject: [PATCH 32/37] clean up file --- src/demo.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/demo.py b/src/demo.py index b3cc5cc..c304119 100644 --- a/src/demo.py +++ b/src/demo.py @@ -1,21 +1,18 @@ #!/usr/bin/env python3 """ -Interactive pyPet demo (terminal) +Interactive pyPet demo Run: - pipenv run python examples/demo.py + pipenv run python src/demo.py """ from __future__ import annotations -import sys import time from typing import Optional -# Public API from your package from pypet import create_pet, play, status, feed, ALLOWED_SPECIES, ALLOWED_MOODS -# ---------- UI helpers ---------- def clear() -> None: print("\033[2J\033[H", end="") @@ -52,18 +49,14 @@ def spinner(text: str, seconds: float = 1.2) -> None: i += 1 print(" " * 20, end="\r") -# ---------- Game loop ---------- - def main() -> int: clear() - print("🐾 Welcome to pyPet (interactive demo)\n") + print("🐾 Welcome to pyPet\n") name = input("Name your pet: ").strip() or "Mochi" species = choose("Pick a species:", sorted(ALLOWED_SPECIES), default="otter") mood = choose("Pick a starting mood:", sorted(ALLOWED_MOODS), default="neutral") - - # Start a medium-hungry, energetic pet pet = create_pet(name=name, species=species, mood=mood, hunger=40, energy=80) while True: @@ -115,7 +108,6 @@ def main() -> int: energy = 10 reward = input("Give a reward after? [y/N]: ").strip().lower().startswith("y") spinner("Playing...") - # Uses your package function play(pet, game, energy, reward) print("Done!") pause() @@ -129,8 +121,7 @@ def main() -> int: portion = 8 treat = input("Is it a special treat? [y/N]: ").strip().lower().startswith("y") spinner("Feeding...") - # Uses your package function, if available - feed(pet, food, portion, treat) # type: ignore + feed(pet, food, portion, treat) print("Yum!") pause() continue From 244c79408ff72c4d76b82747b76031d917ecd6f9 Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 16:29:03 -0500 Subject: [PATCH 33/37] updated tests to match func changes in play --- src/pypet/pet.py | 1 + tests/test_play.py | 20 -------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/pypet/pet.py b/src/pypet/pet.py index 33d9946..20394df 100644 --- a/src/pypet/pet.py +++ b/src/pypet/pet.py @@ -269,6 +269,7 @@ def play(pet: dict, game: str, energy: int, reward: bool): pet["happiness"] = new_happiness pet["mood"] = new_mood pet['ascii_key'] = f"{species}:{new_mood}" + pet["last_interaction_at"] = time() return pet diff --git a/tests/test_play.py b/tests/test_play.py index 4d579ef..b1e4984 100644 --- a/tests/test_play.py +++ b/tests/test_play.py @@ -17,9 +17,6 @@ def test_play_basic_happy_path(): # reward=True gives 10 base + 3 species bonus for dog+fetch = 13 total assert updated_pet["happiness"] > initial_happiness assert updated_pet["happiness"] == initial_happiness + 13 - - # Check last_interaction_at was updated - assert updated_pet["last_interaction_at"] > pet["last_interaction_at"] def test_play_without_reward(): @@ -183,23 +180,6 @@ def test_play_invalid_energy(): pass -def test_play_original_pet_not_modified(): - """Test that playing doesn't modify the original pet dict""" - pet = create_pet("Original", "dog", energy=80, hunger=25) - original_energy = pet["energy"] - original_happiness = pet["happiness"] - original_mood = pet["mood"] - - updated_pet = play(pet, "fetch", energy=30, reward=True) - - # Original pet should be unchanged - assert pet["energy"] == original_energy - assert pet["happiness"] == original_happiness - assert pet["mood"] == original_mood - - # Updated pet should be different - assert updated_pet["energy"] != pet["energy"] - def test_play_multiple_species_games(): """Test all species with their preferred games""" From 0ea6d1712ece25119b06d8d282ae294c2d88d0c3 Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 20:10:11 -0500 Subject: [PATCH 34/37] Bumped version and added demo to testpypi --- pyproject.toml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8719c2b..65fba28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "pypet" description = "A tiny virtual pet that lives in your terminal." -version = "0.1.0" +version = "0.1.2" authors = [ { name = "Alexander Xie", email = "alexxie9667@gmail.com" }, ] @@ -33,3 +33,14 @@ dev = ["pytest"] [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["src"] + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] +exclude = ["tests*", "examples*"] + +[project.scripts] +pypet-demo = "pypet.examples.demo:main" + From 35401a7d6ffc7628dc46e73eac81fb64dadeb17d Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 20:11:29 -0500 Subject: [PATCH 35/37] Made demo accessible to pypi --- src/pypet/__init__.py | 2 +- src/pypet/examples/__init__.py | 0 src/{ => pypet/examples}/demo.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/pypet/examples/__init__.py rename src/{ => pypet/examples}/demo.py (100%) diff --git a/src/pypet/__init__.py b/src/pypet/__init__.py index c131653..9534684 100644 --- a/src/pypet/__init__.py +++ b/src/pypet/__init__.py @@ -36,4 +36,4 @@ "describe_pet", ] -__version__ = "0.1.0" +__version__ = "0.1.2" diff --git a/src/pypet/examples/__init__.py b/src/pypet/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/demo.py b/src/pypet/examples/demo.py similarity index 100% rename from src/demo.py rename to src/pypet/examples/demo.py From 33199f75b5e60351c8aa6790a2945d803167170c Mon Sep 17 00:00:00 2001 From: axie22 Date: Tue, 4 Nov 2025 20:15:25 -0500 Subject: [PATCH 36/37] Added instructions on usage in README --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index c8edd70..74d80ae 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,31 @@ Upload to TestPyPi ``` bash pipenv run twine upload --repository testpypi dist/* ``` + +### How to install and run from TestPyPi + +Create virtual env. and install + +``` bash +python3 -m venv .venv +source .venv/bin/activate + +pip install -i https://test.pypi.org/simple/ pypet==0.1.2 +``` + +Interact with our demo script + +``` bash +pypet-demo +``` + +Or build your own pet + +``` python +from pypet import create_pet, play, feed, status + +pet = create_pet("Mochi", "otter", mood="happy", hunger=25, energy=85) +play(pet, "fetch", 10, True) +feed(pet, "kibble", 20, False) +print(status(pet, color=True, verbose=True, ascii_art=True)) +``` From 63303e52ac752817dabdc6a6c745e6898c1d7bd0 Mon Sep 17 00:00:00 2001 From: connorlee487 Date: Tue, 4 Nov 2025 23:34:55 -0500 Subject: [PATCH 37/37] change readme --- README.md | 330 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 286 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 74d80ae..842a64d 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,321 @@ -# Python Package Exercise +[![CI / CD](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/ci.yml/badge.svg)](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/ci.yml) -[![CI / CD](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/ci.yml/badge.svg)](https://github.com/swe-students-fall2025/3-python-package-team_pioneer/actions/workflows/ci.yml) +# pyPet 🐾 +**A tiny virtual pet that lives in your terminal** -An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. +pyPet is a delightful Python package that lets you create and care for a virtual pet right in your terminal. Feed your pet, play games with them, and watch their mood change based on how well you care for them. Perfect for developers who want a little companionship while coding! -Group Members +📦 **[View on PyPI](https://pypi.org/project/pypet/)** + +--- + +## 👥 Team Members - [Connor Lee](https://github.com/Connorlee487) - [Lanxi](https://github.com/player1notfound) - [Alex](https://github.com/axie22) -- 4th ? +- 4th...? -## `pyPet` +--- -**Theme:** A tiny virtual pet in your terminal. +## 🚀 Quick Start -**Concept:** -`pyPet` creates a small, stateful pet that lives in your terminal. You can feed it, play with it, and check on its mood. +### Installation -**Core Functions** +Install pyPet from PyPI: -- `create_pet(name: str, species: str, mood: str, hunger: int)` - - Initializes a new pet with starting attributes. -- `feed(pet: dict, food: str, portion: int, treat: bool)` - - Reduces hunger, may improve mood based on treat type. -- `play(pet: dict, game: str, energy: int, reward: bool)` - - Boosts happiness and decreases energy over time. -- `status(pet: dict, color: bool, verbose: bool, ascii_art: bool)` - - Prints the current pet’s stats and ASCII representation. +```bash +pip install pypet +``` -### Instructions for Running & Testing +Or install from TestPyPI: -``` bash -pipenv install --dev -pipenv run pip install -e . -pipenv run pytest -q +```bash +pip install -i https://test.pypi.org/simple/ pypet==0.1.2 ``` -Build +### Basic Usage + +```python +from pypet import create_pet, feed, play, status -``` bash -pipenv run python -m build +# Create a new pet +pet = create_pet("Mochi", "otter", mood="happy", hunger=25, energy=85) + +# Feed your pet +feed(pet, "fish", portion=20, treat=False) + +# Play with your pet +play(pet, "fetch", energy=10, reward=True) + +# Check on your pet +print(status(pet, color=True, verbose=True, ascii_art=True)) ``` -Upload to TestPyPi +--- -``` bash -pipenv run twine upload --repository testpypi dist/* +## 📚 Function Documentation + +### `create_pet(name: str, species: str, mood: str = "neutral", hunger: int = 50, energy: int | None = None)` + +Creates and returns a new virtual pet with the specified attributes. + +**Parameters:** +- `name` (str): The pet's display name. Must be a non-empty string after trimming. +- `species` (str): The pet's species. Must be one of: `cat`, `dog`, `otter`, `capybara`, or `duck` (case-insensitive). +- `mood` (str, optional): The pet's initial mood. Must be one of: `happy`, `neutral`, `grumpy`, `sleepy`, `hungry`, or `sad`. Defaults to `"neutral"`. +- `hunger` (int, optional): Initial hunger level (0-100, where 0 = not hungry, 100 = very hungry). Defaults to `50`. +- `energy` (int, optional): Initial energy level (0-100, where 0 = exhausted, 100 = full energy). If not provided, defaults to `70`. + +**Returns:** +- `Pet`: A dictionary representing the pet with the following keys: + - `id`: Unique identifier (UUID) + - `name`: Pet's name + - `species`: Pet's species + - `mood`: Current mood + - `hunger`: Current hunger level (0-100) + - `energy`: Current energy level (0-100) + - `happiness`: Current happiness level (0-100, automatically calculated) + - `created_at`: Timestamp of creation + - `last_interaction_at`: Timestamp of last interaction + - `ascii_key`: Key for ASCII art representation + +**Raises:** +- `ValueError`: If name, species, or mood are invalid types or contain unsupported values. + +**Example:** +```python +from pypet import create_pet + +pet = create_pet("Fluffy", "cat", mood="happy", hunger=30, energy=90) +print(f"Created {pet['name']} the {pet['species']}") ``` -### How to install and run from TestPyPi +--- -Create virtual env. and install +### `feed(pet: dict, food: str, portion: int, treat: bool)` -``` bash -python3 -m venv .venv -source .venv/bin/activate +Feeds your pet, reducing their hunger and potentially improving their mood. The function also simulates time passing (30 minutes) and updates the pet's mood based on their new hunger and happiness levels. -pip install -i https://test.pypi.org/simple/ pypet==0.1.2 +**Parameters:** +- `pet` (dict): A pet dictionary created by `create_pet()`. +- `food` (str): Type of food to give. Supported foods include: `kibble`, `fish`, `carrot`, `steak`, `cookie`. Each food has different hunger-reduction effects. +- `portion` (int): Portion size (0-100). Larger portions reduce more hunger. +- `treat` (bool): Whether the food is a special treat. If `True`, provides an additional hunger reduction and happiness boost. + +**Returns:** +- `dict`: The updated pet dictionary with modified hunger, happiness, mood, and `last_interaction_at` timestamp. + +**Raises:** +- `ValueError`: If pet is not a valid dictionary, food is not a string, or portion is not an integer between 0 and 100. + +**Example:** +```python +from pypet import create_pet, feed + +pet = create_pet("Buddy", "dog", hunger=80) +pet = feed(pet, "steak", portion=25, treat=True) +print(f"Hunger after feeding: {pet['hunger']}") ``` -Interact with our demo script +--- -``` bash -pypet-demo +### `play(pet: dict, game: str, energy: int, reward: bool)` + +Plays a game with your pet, which increases happiness but consumes energy. Different games may provide bonus happiness for certain species (e.g., dogs love "fetch", cats enjoy "laser" or "string" games). + +**Parameters:** +- `pet` (dict): A pet dictionary created by `create_pet()`. +- `game` (str): Name of the game to play (e.g., `"fetch"`, `"tug"`, `"chase"`, `"hide-n-seek"`, `"laser"`, `"string"`, `"swim"`, `"relax"`). +- `energy` (int): Amount of energy to consume (must be a positive integer). If the pet doesn't have enough energy, they won't be able to play fully and may receive a small happiness penalty. +- `reward` (bool): Whether to give a reward after playing. If `True`, provides an additional happiness boost. + +**Returns:** +- `dict`: The updated pet dictionary with modified energy, happiness, mood, and `last_interaction_at` timestamp. + +**Raises:** +- `ValueError`: If energy is not a positive integer. + +**Example:** +```python +from pypet import create_pet, play + +pet = create_pet("Max", "dog", energy=50) +pet = play(pet, "fetch", energy=15, reward=True) +print(f"Energy after playing: {pet['energy']}, Happiness: {pet['happiness']}") ``` -Or build your own pet +--- -``` python -from pypet import create_pet, play, feed, status +### `status(pet: dict, color: bool = False, verbose: bool = False, ascii_art: bool = False)` -pet = create_pet("Mochi", "otter", mood="happy", hunger=25, energy=85) -play(pet, "fetch", 10, True) -feed(pet, "kibble", 20, False) +Returns a formatted string displaying your pet's current status and mood. + +**Parameters:** +- `pet` (dict): A pet dictionary created by `create_pet()`. +- `color` (bool, optional): If `True`, the mood will be displayed in color (green for happy, yellow for neutral, red for grumpy, blue for sleepy, magenta for hungry, gray for sad). Defaults to `False`. +- `verbose` (bool, optional): If `True`, displays detailed information including all pet attributes (id, version, hunger, energy, happiness, timestamps, etc.). Defaults to `False`. +- `ascii_art` (bool, optional): If `True`, displays ASCII art representing the pet's species. Defaults to `False`. + +**Returns:** +- `str`: A formatted string showing the pet's status. + +**Raises:** +- `ValueError`: If pet is not a dictionary. + +**Example:** +```python +from pypet import create_pet, status + +pet = create_pet("Mochi", "otter", mood="happy") print(status(pet, color=True, verbose=True, ascii_art=True)) ``` + +--- + +## 💡 Example Program + +We've included an interactive demo program that showcases all the functions. After installing pyPet, you can run it with: + +```bash +pypet-demo +``` + +Or run it directly: + +```bash +python -m pypet.examples.demo +``` + +The demo program allows you to: +- Create a custom pet with your choice of name, species, and starting mood +- Feed your pet with different foods +- Play various games with your pet +- Check your pet's status with colorful, detailed output +- Rename your pet or change their mood + +**View the full example code:** [`src/pypet/examples/demo.py`](src/pypet/examples/demo.py) + +--- + +## 🛠️ For Contributors + +Interested in contributing to pyPet? Here's how to set up your development environment: + +### Prerequisites + +- Python 3.8 or higher +- pipenv (for dependency management) + +### Setup Steps + +1. **Clone the repository:** + ```bash + git clone https://github.com/swe-students-fall2025/3-python-package-team_pioneer.git + cd 3-python-package-team_pioneer + ``` + +2. **Set up a virtual environment and install dependencies:** + ```bash + pipenv install --dev + ``` + +3. **Install the package in editable mode:** + ```bash + pipenv run pip install -e . + ``` + +4. **Run the tests:** + ```bash + pipenv run pytest -q + ``` + + To run tests with more verbose output: + ```bash + pipenv run pytest -v + ``` + +5. **Build the package:** + ```bash + pipenv run python -m build + ``` + + This creates distribution files in the `dist/` directory. + +6. **Test the build locally:** + ```bash + pipenv run pip install dist/pypet-*.whl + ``` + +### Running Tests + +The test suite uses pytest and includes tests for all core functions. Run all tests with: + +```bash +pipenv run pytest -q +``` + +Or run specific test files: + +```bash +pipenv run pytest tests/test_create_pet.py +pipenv run pytest tests/test_feed_helper.py +pipenv run pytest tests/test_play.py +pipenv run pytest tests/test_status.py +``` + +### Project Structure + +``` +3-python-package-team_pioneer/ +├── src/ +│ └── pypet/ +│ ├── __init__.py # Package exports +│ ├── pet.py # Core pet functions +│ ├── main.py # Entry point +│ └── examples/ +│ └── demo.py # Interactive demo +├── tests/ # Test suite +│ ├── test_create_pet.py +│ ├── test_feed_helper.py +│ ├── test_play.py +│ └── test_status.py +├── pyproject.toml # Package configuration +├── Pipfile # Dependency management +└── README.md # This file +``` + +### Uploading to PyPI (for maintainers) + +To upload a new version to TestPyPI: + +```bash +pipenv run twine upload --repository testpypi dist/* +``` + +To upload to production PyPI: + +```bash +pipenv run twine upload dist/* +``` + +--- + +## 📄 License + +This project is licensed under the GNU General Public License v3 (GPLv3). See the [LICENSE](LICENSE) file for details. + +--- + +## 🔗 Links + +- **PyPI Package:** https://pypi.org/project/pypet/ +- **GitHub Repository:** https://github.com/swe-students-fall2025/3-python-package-team_pioneer +- **Issue Tracker:** https://github.com/swe-students-fall2025/3-python-package-team_pioneer/issues + +--- + +## 🙏 Acknowledgments + +Thanks for using pyPet! We hope your virtual pet brings a smile to your terminal. 🐾