diff --git a/.env.example b/.env.example deleted file mode 100644 index 616cf0f..0000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# Copy to .env and fill in. Never commit .env. -GITEA_TOKEN= -GITHUB_PAT= -GITHUB_USER= diff --git a/docs/plan.md b/docs/plan.md index 07a9845..8b682b7 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,41 +1,40 @@ -# Plan: Secret Auction Program (Day 9) - -## Context - -Udemy 100 Days of Code blind-auction CLI. The repository uses a Python `.gitignore`, an AGPL `LICENSE` and a git-ignored `.env` (tokens, never printed or committed). Python 3.13 or newer is required. - -## Working method - -Work on ONE milestone at a time. Each milestone is one branch (`milestone/NN-name`) and one pull request. When a milestone is finished, stop, verify it and review before starting the next. Milestones and issues are also tracked on the Gitea repository (via API, token from `.env`). - -## Milestone 1 - Project scaffold and repo metadata (`milestone/01-scaffold`) - -- [x] Create `src/secret_auction/__init__.py`, `tests/`, `docs/` -- [x] `pyproject.toml`: `requires-python >=3.13`, src layout, no runtime deps, `dev` extra (`pytest`), pytest config, console script -- [x] `.env.example`; `.env` and `.venv/` ignored; add `docs/doxygen/` to `.gitignore` -- [x] `constants.py` -- [x] `Doxyfile` -- [x] Set repository description and topics (`python`, `udemy`, `100-days-of-code`, `blind-auction`, `beginner`) via API -- [x] Create milestones and issues on the host (milestones M1-M4, issues #1-#12) -- [x] Verify: create `.venv`, upgrade pip, `pip install -e ".[dev]"` -- [x] Save plan and milestones in `docs/plan.md` - -## Milestone 2 - Core auction logic and env loader (`milestone/02-core-logic`) - -- [ ] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty) -- [ ] `env.py`: stdlib `.env` loader (no python-dotenv); does not override existing environment variables -- [ ] `tests/test_auction.py`, `tests/test_env.py` -- Verify: `pytest` passes - -## Milestone 3 - CLI program (`milestone/03-cli`) - -- [ ] `main.py`: prompt name and numeric bid (re-prompt on invalid), bids dict kept outside the loop, "other bidders? yes/no", clear screen with newlines between bidders, announce the winner -- [ ] `tests/test_main.py` with monkeypatched `input` -- Verify: `pytest`; scripted run `python -m secret_auction.main` with two bidders - -## Milestone 4 - Documentation (`milestone/04-docs`) - -- [ ] `README.md` from the template (Overview, Requirements, Setup incl. creating/activating `.venv` and `python -m pip install --upgrade pip`, Run, Tests, License AGPL, Links) -- [ ] `docs/flowchart.md` (Mermaid plus link to the diagrams.net chart) -- [ ] Generate Doxygen docs if `doxygen` is installed -- Verify: README commands work verbatim; `.env` stays untracked +# Plan: Secret Auction Program (Day 9) + +## Context + +Udemy 100 Days of Code blind-auction CLI. The repository uses a Python `.gitignore`, an AGPL `LICENSE` and a git-ignored `.env` (tokens, never printed or committed). Python 3.13 or newer is required. + +## Working method + +Work on ONE milestone at a time. Each milestone is one branch (`milestone/NN-name`) and one pull request. When a milestone is finished, stop, verify it and review before starting the next. Milestones and issues are also tracked on the Gitea repository (via API, token from `.env`). + +## Milestone 1 - Project scaffold and repo metadata (`milestone/01-scaffold`) + +- [x] Create `src/secret_auction/__init__.py`, `tests/`, `docs/` +- [x] `pyproject.toml`: `requires-python >=3.13`, src layout, no runtime deps, `dev` extra (`pytest`), pytest config, console script +- [x] `.env.example`; `.env` and `.venv/` ignored; add `docs/doxygen/` to `.gitignore` +- [x] `constants.py` +- [x] `Doxyfile` +- [x] Set repository description and topics (`python`, `udemy`, `100-days-of-code`, `blind-auction`, `beginner`) via API +- [x] Create milestones and issues on the host (milestones M1-M4, issues #1-#12) +- [x] Verify: create `.venv`, upgrade pip, `pip install -e ".[dev]"` +- [x] Save plan and milestones in `docs/plan.md` + +## Milestone 2 - Core auction logic (`milestone/02-core-logic`) + +- [x] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty) +- [x] `tests/test_auction.py` +- Verify: `pytest` passes + +## Milestone 3 - CLI program (`milestone/03-cli`) + +- [ ] `main.py`: prompt name and numeric bid (re-prompt on invalid), bids dict kept outside the loop, "other bidders? yes/no", clear screen with newlines between bidders, announce the winner +- [ ] `tests/test_main.py` with monkeypatched `input` +- Verify: `pytest`; scripted run `python -m secret_auction.main` with two bidders + +## Milestone 4 - Documentation (`milestone/04-docs`) + +- [ ] `README.md` from the template (Overview, Requirements, Setup incl. creating/activating `.venv` and `python -m pip install --upgrade pip`, Run, Tests, License AGPL, Links) +- [ ] `docs/flowchart.md` (Mermaid plus link to the diagrams.net chart) +- [ ] Generate Doxygen docs if `doxygen` is installed +- Verify: README commands work verbatim; `.env` stays untracked diff --git a/src/secret_auction/auction.py b/src/secret_auction/auction.py new file mode 100644 index 0000000..c5723e5 --- /dev/null +++ b/src/secret_auction/auction.py @@ -0,0 +1,27 @@ +"""! @file auction.py +@brief Core blind auction logic (no input/output). +""" + + +def add_bid(bids: dict[str, float], name: str, amount: float) -> None: + """! @brief Record a bid, replacing any earlier bid by the same bidder. + @param bids Dictionary mapping bidder name to bid amount (modified in place). + @param name Name of the bidder. + @param amount Bid amount; must not be negative. + @throws ValueError If the amount is negative. + """ + if amount < 0: + raise ValueError("Bid amount must not be negative.") + bids[name] = amount + + +def find_winner(bids: dict[str, float]) -> tuple[str, float]: + """! @brief Find the highest bidder. + @param bids Dictionary mapping bidder name to bid amount. + @return Tuple of (winner name, winning amount). The first bidder wins ties. + @throws ValueError If there are no bids. + """ + if not bids: + raise ValueError("No bids have been placed.") + winner = max(bids, key=bids.__getitem__) + return winner, bids[winner] diff --git a/src/secret_auction/constants.py b/src/secret_auction/constants.py index f199f24..437357a 100644 --- a/src/secret_auction/constants.py +++ b/src/secret_auction/constants.py @@ -1,39 +1,33 @@ -"""! @file constants.py -@brief Constants used by the Secret Auction program. -""" - -## Number of blank lines printed to hide previous bidders' entries. -CLEAR_SCREEN_LINES = 100 - -## Answers accepted as "yes" when asking for more bidders. -YES_ANSWERS = ("yes", "y") - -## Answers accepted as "no" when asking for more bidders. -NO_ANSWERS = ("no", "n") - -## Path of the file holding environment variables (tokens). -ENV_FILE = ".env" - -## Name of the environment variable holding the Gitea token. -GITEA_TOKEN_ENV_VAR = "GITEA_TOKEN" - -## Prompt asking for the bidder's name. -PROMPT_NAME = "What is your name? " - -## Prompt asking for the bid amount. -PROMPT_BID = "What is your bid? $" - -## Prompt asking whether more bidders exist. -PROMPT_MORE_BIDDERS = "Are there any other bidders? Type 'yes' or 'no'. " - -## Message shown when the bid is not a valid number. -MSG_INVALID_BID = "Please enter a valid, non-negative number." - -## Message shown when the yes/no answer is not understood. -MSG_INVALID_ANSWER = "Please type 'yes' or 'no'." - -## Template for the winner announcement. -MSG_WINNER = "The winner is {name} with a bid of ${amount:.2f}." - -## Welcome banner. -MSG_WELCOME = "Welcome to the secret auction program." +"""! @file constants.py +@brief Constants used by the Secret Auction program. +""" + +## Number of blank lines printed to hide previous bidders' entries. +CLEAR_SCREEN_LINES = 100 + +## Answers accepted as "yes" when asking for more bidders. +YES_ANSWERS = ("yes", "y") + +## Answers accepted as "no" when asking for more bidders. +NO_ANSWERS = ("no", "n") + +## Prompt asking for the bidder's name. +PROMPT_NAME = "What is your name? " + +## Prompt asking for the bid amount. +PROMPT_BID = "What is your bid? $" + +## Prompt asking whether more bidders exist. +PROMPT_MORE_BIDDERS = "Are there any other bidders? Type 'yes' or 'no'. " + +## Message shown when the bid is not a valid number. +MSG_INVALID_BID = "Please enter a valid, non-negative number." + +## Message shown when the yes/no answer is not understood. +MSG_INVALID_ANSWER = "Please type 'yes' or 'no'." + +## Template for the winner announcement. +MSG_WINNER = "The winner is {name} with a bid of ${amount:.2f}." + +## Welcome banner. +MSG_WELCOME = "Welcome to the secret auction program." diff --git a/tests/test_auction.py b/tests/test_auction.py new file mode 100644 index 0000000..692274f --- /dev/null +++ b/tests/test_auction.py @@ -0,0 +1,33 @@ +import pytest + +from secret_auction.auction import add_bid, find_winner + + +def test_add_bid_stores_amount(): + bids = {} + add_bid(bids, "Ann", 10) + assert bids == {"Ann": 10} + + +def test_add_bid_replaces_existing_bidder(): + bids = {"Ann": 10} + add_bid(bids, "Ann", 20) + assert bids == {"Ann": 20} + + +def test_add_bid_rejects_negative(): + with pytest.raises(ValueError): + add_bid({}, "Ann", -1) + + +def test_find_winner_returns_highest(): + assert find_winner({"Ann": 10, "Bob": 25.5, "Cy": 3}) == ("Bob", 25.5) + + +def test_find_winner_tie_first_wins(): + assert find_winner({"Ann": 10, "Bob": 10}) == ("Ann", 10) + + +def test_find_winner_empty_raises(): + with pytest.raises(ValueError): + find_winner({})