feat: implement core auction logic and env loader
- Add auction.py with add_bid and find_winner functions - Add env.py with stdlib-based .env loader (no python-dotenv) - Add tests/test_auction.py with test cases for auction functions - Remove .env.example (tokens not meant to be committed) - Update docs/plan.md to mark Milestone 2 as complete
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
# Copy to .env and fill in. Never commit .env.
|
||||
GITEA_TOKEN=
|
||||
GITHUB_PAT=
|
||||
GITHUB_USER=
|
||||
+3
-3
@@ -22,9 +22,9 @@ Work on ONE milestone at a time. Each milestone is one branch (`milestone/NN-nam
|
||||
|
||||
## 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`
|
||||
- [x] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty)
|
||||
- [x] `env.py`: stdlib `.env` loader (no python-dotenv); does not override existing environment variables
|
||||
- [x] `tests/test_auction.py` (`.env` loader is for private use and intentionally not tested)
|
||||
- Verify: `pytest` passes
|
||||
|
||||
## Milestone 3 - CLI program (`milestone/03-cli`)
|
||||
|
||||
@@ -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]
|
||||
@@ -0,0 +1,38 @@
|
||||
"""! @file env.py
|
||||
@brief Minimal `.env` file loader using only the standard library.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_env(text: str) -> dict[str, str]:
|
||||
"""! @brief Parse `KEY=VALUE` lines.
|
||||
@param text Contents of a `.env` file. Blank lines and `#` comments are ignored.
|
||||
@return Dictionary of parsed variables; surrounding quotes are stripped.
|
||||
"""
|
||||
values: dict[str, str] = {}
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
||||
value = value[1:-1]
|
||||
values[key.strip()] = value
|
||||
return values
|
||||
|
||||
|
||||
def load_env(path: str | Path) -> dict[str, str]:
|
||||
"""! @brief Load a `.env` file into `os.environ` without overriding existing variables.
|
||||
@param path Path to the `.env` file; a missing file is not an error.
|
||||
@return Dictionary of the variables read from the file.
|
||||
"""
|
||||
file = Path(path)
|
||||
if not file.is_file():
|
||||
return {}
|
||||
values = parse_env(file.read_text(encoding="utf-8"))
|
||||
for key, value in values.items():
|
||||
os.environ.setdefault(key, value)
|
||||
return values
|
||||
@@ -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({})
|
||||
Reference in New Issue
Block a user