diff --git a/.github/workflows/create-puzzle-issue.yml b/.github/workflows/create-puzzle-issue.yml new file mode 100644 index 0000000..cc2ed7e --- /dev/null +++ b/.github/workflows/create-puzzle-issue.yml @@ -0,0 +1,50 @@ +name: Create Puzzle Issue + +on: + workflow_call: + inputs: + year: + required: true + type: number + description: "The year of the puzzle" + day: + required: true + type: number + description: "The day of the puzzle" + part: + required: true + type: number + description: "The part of the puzzle (1 or 2)" + +permissions: + issues: write + +jobs: + create-issue: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Create puzzle issue + id: create-issue + run: | + ISSUE_URL=$(gh issue create \ + --title "Advent of code - ${{ inputs.year }} day ${{ inputs.day }} part ${{ inputs.part }}" \ + --body "Write solution in \`puzzles/year_${{ inputs.year }}/day_${{ inputs.day }}/solution.py\`, and write tests in \`puzzles/year_${{ inputs.year }}/day_${{ inputs.day }}/test_solution.py\` using relative imports") + echo "issue_url=${ISSUE_URL}" >> $GITHUB_OUTPUT + + - name: Add sourcery comment + run: | + gh issue comment "${ISSUE_URL}" --body "@sourcery-ai develop - you can read the puzzle statement with \`uv run advent_of_code.py read ${{ inputs.year }} ${{ inputs.day }} ${{ inputs.part }}\`. Run the solution on \`puzzles/year_${{ inputs.year }}/day_${{ inputs.day }}/input.txt\` and write answer to \`puzzles/year_${{ inputs.year }}/day_${{ inputs.day }}/answer_part${{ inputs.part }}.txt\`" + env: + ISSUE_URL: ${{ steps.create-issue.outputs.issue_url }} + + - name: Merge develop into main + run: | + git fetch origin + git checkout main + git merge develop + gh pr merge --squash --auto --delete-branch diff --git a/.github/workflows/merge-repos.yml b/.github/workflows/merge-repos.yml new file mode 100644 index 0000000..fe59443 --- /dev/null +++ b/.github/workflows/merge-repos.yml @@ -0,0 +1,28 @@ +name: Merge Repos + +on: + repository_dispatch: + types: [merge-repos] + +permissions: + contents: write + +jobs: + merge-repos: + runs-on: ubuntu-latest + steps: + - name: Check out target repository + uses: actions/checkout@v4 + with: + repository: ${{ github.event.client_payload.target_repo }} + token: ${{ secrets.GH_TOKEN }} + + - name: Merge source repository + run: | + git remote add source https://github.com/${{ github.event.client_payload.source_repo }}.git + git fetch source + git merge source/main --allow-unrelated-histories + + - name: Push changes to target repository + run: | + git push origin main diff --git a/.github/workflows/merge-sourcery-ai-bot-pr.yml b/.github/workflows/merge-sourcery-ai-bot-pr.yml new file mode 100644 index 0000000..664ca4e --- /dev/null +++ b/.github/workflows/merge-sourcery-ai-bot-pr.yml @@ -0,0 +1,34 @@ +name: Auto-merge Sourcery AI Bot PRs + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + +env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + +jobs: + merge-sourcery-pr: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'sourcery-ai[bot]' + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Merge Pull Request + run: | + gh pr merge "${{ github.event.pull_request.number }}" \ + --squash \ + --auto \ + --delete-branch \ + --repo "${{ github.repository }}" + + - name: Merge develop into main + run: | + git fetch origin + git checkout main + git merge develop + gh pr merge --squash --auto --delete-branch diff --git a/.github/workflows/submit-advent-of-code.yml b/.github/workflows/submit-advent-of-code.yml new file mode 100644 index 0000000..6195193 --- /dev/null +++ b/.github/workflows/submit-advent-of-code.yml @@ -0,0 +1,98 @@ +name: Submit Advent of Code Solution + +on: + push: + branches: + - main + paths: + - 'puzzles/year_*/day_*/answer_part*.txt' + workflow_dispatch: + +permissions: + contents: write + issues: write + repository-dispatch: write + +env: + AOC_SESSION: ${{ secrets.AOC_SESSION }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + +jobs: + submit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v4 + + - name: Install dependencies + run: uv sync + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v42 + with: + files: puzzles/year_*/day_*/answer_part*.txt + + - name: Extract year, day and part from filename + id: extract-info + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "No file changes to process" + exit 0 + fi + + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + if [[ $file =~ puzzles/year_([0-9]+)/day_([0-9]+)/answer_part([12]).txt ]]; then + echo "year=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT + echo "day=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT + echo "part=${BASH_REMATCH[3]}" >> $GITHUB_OUTPUT + fi + done + + - name: Submit solution + id: submit + if: steps.extract-info.outputs.year != '' + continue-on-error: true + run: | + output=$(uv run advent_of_code.py submit ${{ steps.extract-info.outputs.year }} ${{ steps.extract-info.outputs.day }} ${{ steps.extract-info.outputs.part }}) + echo "submission_result=$output" >> $GITHUB_OUTPUT + + - name: Update results in README + if: steps.extract-info.outputs.year != '' + run: uv run advent_of_code.py update-results ${{ steps.extract-info.outputs.year }} + + - name: Download part 2 if part 1 was correct + id: download-part2 + if: steps.extract-info.outputs.part == '1' && steps.submit.outcome == 'success' + run: | + file_path=$(uv run advent_of_code.py download ${{ steps.extract-info.outputs.year }} ${{ steps.extract-info.outputs.day }}) + echo "file_path=$file_path" >> $GITHUB_OUTPUT + + - name: Configure Git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Commit and push changes + run: | + git add puzzles/year_${{ steps.extract-info.outputs.year }}/day_${{ steps.extract-info.outputs.day }} + git add README.md + git commit -m "Update puzzle files for year ${{ steps.extract-info.outputs.year }} day ${{ steps.extract-info.outputs.day }}" + git push + + - name: Trigger create-puzzle-issue workflow + if: steps.download-part2.outputs.file_path != '' + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ env.GH_TOKEN }} + event-type: create-puzzle-issue + client-payload: '{"year": "${{ steps.extract-info.outputs.year }}", "day": "${{ steps.extract-info.outputs.day }}", "part": "2"}' + + - name: Merge develop into main + run: | + git fetch origin + git checkout main + git merge develop + gh pr merge --squash --auto --delete-branch diff --git a/README.md b/README.md index 866527c..51942e6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ - Each day during advent of code this repo solves the latest puzzle and submits the solution - all without human intervention - All code in this repo was written by `sourcery-ai[bot]` -- Everything was set up by manually writing [five issues](https://github.com/sourcery-ai/autonomous-advent-of-code/labels/inception) and asking `sourcery-ai[bot]` to implement them +- Everything was set up by manually writing [five issues](https://github.com/JohnDaWalka/test-aoc/labels/inception) and asking `sourcery-ai[bot]` to implement them - Since then everything runs fully autonomously There are two main processes that work together to solve the puzzles: @@ -76,7 +76,123 @@ It will be most useful for handling routine maintenance and clearing your backlo If you like the sound of this, [join our waitlist](https://getsourcery.netlify.app/) and tell us anything else you want it to do. -#### Footnotes +## Triggering the `merge-repos` Workflow -[^1]: We run this twelve hours after the puzzles are published to avoid interfering with any leaderboards -[^2]: `sourcery-ai[bot]` cannot log into the advent of code website and read the puzzles and input, so we store them in the repository. The advent of code rules ask you not to publish the puzzle text - so we're ROT13 encoding it +To trigger the `merge-repos` workflow, you need to dispatch a repository event with the event type `merge-repos`. You can do this using the GitHub API or the `gh` CLI tool. + +### Example using `gh` CLI + +```sh +gh api repos/:owner/:repo/dispatches --field event_type=merge-repos --field client_payload='{"source_repo": "source-repo-name", "target_repo": "target-repo-name"}' +``` + +Replace `:owner` with the repository owner, `:repo` with the repository name, `source-repo-name` with the name of the source repository, and `target-repo-name` with the name of the target repository. + +### Example using GitHub API + +```sh +curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token YOUR_GITHUB_TOKEN" https://api.github.com/repos/:owner/:repo/dispatches -d '{"event_type":"merge-repos","client_payload":{"source_repo":"source-repo-name","target_repo":"target-repo-name"}}' +``` + +Replace `YOUR_GITHUB_TOKEN` with your GitHub token, `:owner` with the repository owner, `:repo` with the repository name, `source-repo-name` with the name of the source repository, and `target-repo-name` with the name of the target repository. + +## Rotating `GH_TOKEN` Regularly + +To rotate the `GH_TOKEN` regularly, follow these steps: + +1. Generate a new GitHub token with the required permissions. +2. Update the GitHub Secrets in your repository settings with the new token. +3. Update any local environment variables or configuration files that use the `GH_TOKEN`. +4. Monitor the usage of the new token to ensure it is working correctly. +5. Revoke the old token to minimize the risk of unauthorized access. + +## Monitoring the Usage of `GH_TOKEN` + +To monitor the usage of the `GH_TOKEN`, follow these best practices: + +1. Enable GitHub's security features, such as security alerts and vulnerability scanning, to detect any suspicious activity. +2. Regularly review the audit logs in your GitHub repository to track the usage of the `GH_TOKEN`. +3. Set up notifications for any unusual activity or changes in the repository. +4. Use third-party tools or services to monitor the usage of the `GH_TOKEN` and detect any potential security issues. +5. Periodically review and update the permissions assigned to the `GH_TOKEN` to ensure they are still necessary and appropriate. + +## Merging Code from Different Branches or Repositories + +The repository contains GitHub Actions workflows that automatically merge code from different branches or repositories. These workflows use the `gh pr merge` command with the `--squash`, `--auto`, and `--delete-branch` options. + +### Example using `gh pr merge` + +```sh +gh pr merge --squash --auto --delete-branch +``` + +Replace `` with the number of the pull request you want to merge. + +### Example using GitHub Actions Workflow + +The following GitHub Actions workflow automatically merges pull requests created by the `sourcery-ai[bot]` user: + +```yaml +name: Auto-merge Sourcery AI Bot PRs + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: write + +env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + +jobs: + merge-sourcery-pr: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'sourcery-ai[bot]' + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Merge Pull Request + run: | + gh pr merge "${{ github.event.pull_request.number }}" \ + --squash \ + --auto \ + --delete-branch \ + --repo "${{ github.repository }}" + + - name: Merge develop into main + run: | + git fetch origin + git checkout main + git merge develop + gh pr merge --squash --auto --delete-branch +``` + +## Poker Coach UI + +The Poker Coach UI is an interactive interface for hand analysis and game play. It helps users analyze their poker hands and provides feedback. + +### Installation + +To install the Poker Coach UI, follow these steps: + +1. Ensure you have Python 3.12 or higher installed. +2. Clone the repository: + ```sh + git clone https://github.com/sourcery-ai/test-aoc.git + cd test-aoc + ``` +3. Install the required dependencies: + ```sh + pip install -r requirements.txt + ``` + +### Usage + +To use the Poker Coach UI, run the following command: +```sh +python -m poker_coach.ui +``` + +This will open the Poker Coach UI window where you can enter your poker hand and analyze it. diff --git a/advent_of_code.py b/advent_of_code.py new file mode 100644 index 0000000..74d6afa --- /dev/null +++ b/advent_of_code.py @@ -0,0 +1,231 @@ +import codecs +import os +import re +from pathlib import Path +from typing import Annotated, Optional + +import httpx +import typer +from bs4 import BeautifulSoup +from markdownify import markdownify as md + +app = typer.Typer() + + +def get_session() -> str: + """Get AOC session from environment variable.""" + session = os.getenv("AOC_SESSION") + if not session: + raise typer.BadParameter("AOC_SESSION environment variable not set") + return session + + +def get_headers(session: str) -> dict[str, str]: + """Get request headers with session cookie.""" + return { + "Cookie": f"session={session}", + "User-Agent": "github.com/sourcery-ai/autonomous-advent-of-code", + } + + +def rot13_encode(text: str) -> str: + """ROT13 encode text.""" + return codecs.encode(text, "rot13") + + +def rot13_decode(text: str) -> str: + """ROT13 decode text.""" + return codecs.decode(text, "rot13") + + +def ensure_puzzle_dir(year: int, day: int) -> Path: + """Ensure puzzle directory exists and return path.""" + puzzle_dir = Path(f"puzzles/year_{year}/day_{day:02d}") + puzzle_dir.mkdir(parents=True, exist_ok=True) + return puzzle_dir + + +def extract_parts(html: str) -> tuple[str, Optional[str]]: + """Extract puzzle parts from HTML content.""" + soup = BeautifulSoup(html, "html.parser") + article = soup.find("article") + if not article: + raise typer.BadParameter("Could not find puzzle content") + + # Convert to markdown and split on "--- Part Two ---" + markdown = md(str(article)) + parts = markdown.split("--- Part Two ---") + + part1 = parts[0].strip() + part2 = parts[1].strip() if len(parts) > 1 else None + + return part1, part2 + + +@app.command() +def download( + year: Annotated[int, typer.Argument(help="Year of puzzle")], + day: Annotated[int, typer.Argument(help="Day of puzzle")], +) -> None: + """Download puzzle content and input.""" + session = get_session() + headers = get_headers(session) + + # Fetch puzzle content + url = f"https://adventofcode.com/{year}/day/{day}" + response = httpx.get(url, headers=headers, follow_redirects=True) + response.raise_for_status() + + # Extract parts + part1, part2 = extract_parts(response.text) + + # Create puzzle directory + puzzle_dir = ensure_puzzle_dir(year, day) + + # Save part 1 + part1_path = puzzle_dir / "question_part1_rot13.md" + part1_path.write_text(rot13_encode(part1)) + + # Save part 2 if it exists + if part2: + part2_path = puzzle_dir / "question_part2_rot13.md" + combined = f"{part1}\n\n--- Part Two ---\n\n{part2}" + part2_path.write_text(rot13_encode(combined)) + + # Fetch and save input + input_url = f"{url}/input" + input_response = httpx.get(input_url, headers=headers, follow_redirects=True) + input_response.raise_for_status() + + input_path = puzzle_dir / "input.txt" + input_path.write_text(input_response.text) + + print(2 if part2 else 1) + + +@app.command() +def read( + year: Annotated[int, typer.Argument(help="Year of puzzle")], + day: Annotated[int, typer.Argument(help="Day of puzzle")], + part: Annotated[int, typer.Argument(help="Part number (1 or 2)")], +) -> None: + """Read puzzle content.""" + puzzle_dir = ensure_puzzle_dir(year, day) + question_path = puzzle_dir / f"question_part{part}_rot13.md" + + if not question_path.exists(): + raise typer.BadParameter(f"Question file not found: {question_path}") + + encoded_content = question_path.read_text() + decoded_content = rot13_decode(encoded_content) + print(decoded_content) + + +@app.command() +def submit( + year: Annotated[int, typer.Argument(help="Year of puzzle")], + day: Annotated[int, typer.Argument(help="Day of puzzle")], + part: Annotated[int, typer.Argument(help="Part number (1 or 2)")], +) -> int: + """Submit answer for puzzle.""" + puzzle_dir = ensure_puzzle_dir(year, day) + answer_path = puzzle_dir / f"answer_part{part}.txt" + + if not answer_path.exists(): + raise typer.BadParameter(f"Answer file not found: {answer_path}") + + session = get_session() + headers = get_headers(session) + + answer = answer_path.read_text().strip() + + # Submit answer + url = f"https://adventofcode.com/{year}/day/{day}/answer" + data = {"level": str(part), "answer": answer} + response = httpx.post(url, headers=headers, data=data, follow_redirects=True) + response.raise_for_status() + + # Extract response content + soup = BeautifulSoup(response.text, "html.parser") + article = soup.find("article") + if not article: + raise typer.BadParameter("Could not find response content") + + result_markdown = md(str(article)) + + # Save response + result_path = puzzle_dir / f"result_part{part}.md" + result_path.write_text(result_markdown) + + # Print response + print(result_markdown) + + return 0 if "That's the right answer" in result_markdown else 1 + + +@app.command() +def update_results( + year: Annotated[int, typer.Argument(help="Year of puzzle")], +) -> None: + """Update results table in README.""" + results: dict[int, dict[int, str]] = {} + puzzles_dir = Path(f"puzzles/year_{year}") + + if not puzzles_dir.exists(): + raise typer.BadParameter(f"No puzzles found for year {year}") + + # Collect results for each day + for day_dir in sorted(puzzles_dir.glob("day_*")): + day = int(day_dir.name.replace("day_", "")) + results[day] = {1: "NOT_SUBMITTED", 2: "NOT_SUBMITTED"} + + for part in [1, 2]: + answer_file = day_dir / f"answer_part{part}.txt" + result_file = day_dir / f"result_part{part}.md" + + if answer_file.exists() and result_file.exists(): + result_content = result_file.read_text() + if "That's the right answer" in result_content: + results[day][part] = "CORRECT" + else: + results[day][part] = "INCORRECT" + + # Generate results table + table_lines = [ + "| Day | Part 1 | Part 2 |", + "|-----|--------|--------|", + ] + + max_day = max(results.keys()) + for day in range(1, max_day + 1): + day_results = results.get(day, {1: "NOT_SUBMITTED", 2: "NOT_SUBMITTED"}) + symbols = { + "NOT_SUBMITTED": "", + "INCORRECT": "❌", + "CORRECT": "✅", + } + table_lines.append( + f"| {day:2d} | {symbols[day_results[1]]:8} | {symbols[day_results[2]]:8} |" + ) + + table = "\n".join(table_lines) + + # Update README.md + readme_path = Path("README.md") + if not readme_path.exists(): + readme_path.write_text("") + + content = readme_path.read_text() + pattern = f".*?" + replacement = f"\n{table}\n" + + if re.search(pattern, content, re.DOTALL): + new_content = re.sub(pattern, replacement, content, flags=re.DOTALL) + else: + new_content = f"{content}\n\n{replacement}\n" + + readme_path.write_text(new_content) + + +if __name__ == "__main__": + app() diff --git a/poker_coach/ui.py b/poker_coach/ui.py new file mode 100644 index 0000000..addaa53 --- /dev/null +++ b/poker_coach/ui.py @@ -0,0 +1,38 @@ +import tkinter as tk +from tkinter import messagebox + +class PokerCoachUI: + def __init__(self, root): + self.root = root + self.root.title("Poker Coach") + + self.create_widgets() + + def create_widgets(self): + self.hand_label = tk.Label(self.root, text="Enter your hand:") + self.hand_label.pack() + + self.hand_entry = tk.Entry(self.root) + self.hand_entry.pack() + + self.analyze_button = tk.Button(self.root, text="Analyze Hand", command=self.analyze_hand) + self.analyze_button.pack() + + self.result_label = tk.Label(self.root, text="") + self.result_label.pack() + + def analyze_hand(self): + hand = self.hand_entry.get() + if not hand: + messagebox.showerror("Error", "Please enter a hand.") + return + + # Placeholder for hand analysis logic + result = f"Analysis result for hand: {hand}" + + self.result_label.config(text=result) + +if __name__ == "__main__": + root = tk.Tk() + app = PokerCoachUI(root) + root.mainloop() diff --git a/pyproject.toml b/pyproject.toml index ace9226..2e4e759 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "httpx>=0.28.1", "markdownify>=0.14.1", "typer>=0.15.1", + "types-beautifulsoup4>=4.12.0", ] [dependency-groups] @@ -40,6 +41,8 @@ warn_unreachable = true [tool.ruff] target-version = "py312" line-length = 100 + +[tool.ruff.lint] select = ["ALL"] ignore = ["D203", "D212"] diff --git a/tests/test_advent_of_code.py b/tests/test_advent_of_code.py new file mode 100644 index 0000000..c47b0e2 --- /dev/null +++ b/tests/test_advent_of_code.py @@ -0,0 +1,103 @@ +import os +from pathlib import Path +from typing import Generator + +import pytest +from typer.testing import CliRunner + +from advent_of_code import app, extract_parts, rot13_decode, rot13_encode + +runner = CliRunner() + + +@pytest.fixture +def temp_puzzle_dir(tmp_path: Path) -> Generator[Path, None, None]: + """Create temporary puzzle directory.""" + original_cwd = os.getcwd() + os.chdir(tmp_path) + yield tmp_path + os.chdir(original_cwd) + + +def test_rot13_encode_decode() -> None: + """Test ROT13 encoding and decoding.""" + text = "Hello, World!" + encoded = rot13_encode(text) + assert encoded != text + decoded = rot13_decode(encoded) + assert decoded == text + + +def test_extract_parts_part1_only() -> None: + """Test extracting only part 1 from HTML.""" + html = """ +
+

--- Day 1: Test ---

+

Part 1 content

+
+ """ + part1, part2 = extract_parts(html) + assert "Part 1 content" in part1 + assert part2 is None + + +def test_extract_parts_both_parts() -> None: + """Test extracting both parts from HTML.""" + html = """ +
+

--- Day 1: Test ---

+

Part 1 content

+

--- Part Two ---

+

Part 2 content

+
+ """ + part1, part2 = extract_parts(html) + assert "Part 1 content" in part1 + assert "Part 2 content" in part2 + + +def test_download_command_no_session(temp_puzzle_dir: Path) -> None: + """Test download command fails without session.""" + result = runner.invoke(app, ["download", "2023", "1"]) + assert result.exit_code == 2 + assert "AOC_SESSION environment variable not set" in result.stdout + + +def test_read_command_missing_file(temp_puzzle_dir: Path) -> None: + """Test read command fails with missing file.""" + result = runner.invoke(app, ["read", "2023", "1", "1"]) + assert result.exit_code != 0 + assert "Question file not found" in result.stdout + + +def test_submit_command_missing_answer(temp_puzzle_dir: Path) -> None: + """Test submit command fails with missing answer file.""" + result = runner.invoke(app, ["submit", "2023", "1", "1"]) + assert result.exit_code != 0 + assert "Answer file not found" in result.stdout + + +def test_update_results_command_no_puzzles(temp_puzzle_dir: Path) -> None: + """Test update-results command fails with no puzzles.""" + result = runner.invoke(app, ["update-results", "2023"]) + assert result.exit_code != 0 + assert "No puzzles found for year 2023" in result.stdout + + +def test_update_results_command_creates_table(temp_puzzle_dir: Path) -> None: + """Test update-results command creates results table.""" + # Create puzzle directory with some results + puzzle_dir = temp_puzzle_dir / "puzzles" / "year_2023" / "day_01" + puzzle_dir.mkdir(parents=True) + + # Create answer and result files + (puzzle_dir / "answer_part1.txt").write_text("42") + (puzzle_dir / "result_part1.md").write_text("That's the right answer!") + + result = runner.invoke(app, ["update-results", "2023"]) + assert result.exit_code == 0 + + # Check README.md was created with table + readme = Path("README.md").read_text() + assert "| Day | Part 1 | Part 2 |" in readme + assert "| 1 | ✅" in readme diff --git a/tests/test_poker_coach_ui.py b/tests/test_poker_coach_ui.py new file mode 100644 index 0000000..a613602 --- /dev/null +++ b/tests/test_poker_coach_ui.py @@ -0,0 +1,24 @@ +import pytest +import tkinter as tk +from poker_coach.ui import PokerCoachUI + +@pytest.fixture +def app(): + root = tk.Tk() + app = PokerCoachUI(root) + yield app + root.destroy() + +def test_initial_state(app): + assert app.hand_entry.get() == "" + assert app.result_label.cget("text") == "" + +def test_analyze_hand_empty(app): + app.hand_entry.delete(0, tk.END) + app.analyze_hand() + assert app.result_label.cget("text") == "" + +def test_analyze_hand_valid(app): + app.hand_entry.insert(0, "AS KS QS JS TS") + app.analyze_hand() + assert "Analysis result for hand: AS KS QS JS TS" in app.result_label.cget("text") diff --git a/uv.lock b/uv.lock index e83d582..ef92626 100644 --- a/uv.lock +++ b/uv.lock @@ -303,6 +303,7 @@ dependencies = [ { name = "httpx" }, { name = "markdownify" }, { name = "typer" }, + { name = "types-beautifulsoup4" }, ] [package.dev-dependencies] @@ -318,6 +319,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "markdownify", specifier = ">=0.14.1" }, { name = "typer", specifier = ">=0.15.1" }, + { name = "types-beautifulsoup4", specifier = ">=4.12.0" }, ] [package.metadata.requires-dev] @@ -342,6 +344,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, ] +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20241020" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-html5lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ae/5a7571c649cdd9f3c07d16790467a4fe1191f12a3ad7eecd1097cb8b1d9f/types-beautifulsoup4-4.12.0.20241020.tar.gz", hash = "sha256:158370d08d0cd448bd11b132a50ff5279237a5d4b5837beba074de152a513059", size = 11682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/43/0f96cdf27d7da7dea729af3476b7be997205765209651a42a4e1895bab72/types_beautifulsoup4-4.12.0.20241020-py3-none-any.whl", hash = "sha256:c95e66ce15a4f5f0835f7fbc5cd886321ae8294f977c495424eaf4225307fd30", size = 12170 }, +] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20241018" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/9d/f6fbcc8246f5e46845b4f989c4e17e6fb3ce572f7065b185e515bf8a3be7/types-html5lib-1.1.11.20241018.tar.gz", hash = "sha256:98042555ff78d9e3a51c77c918b1041acbb7eb6c405408d8a9e150ff5beccafa", size = 11370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/7c/f862b1dc31268ef10fe95b43dcdf216ba21a592fafa2d124445cd6b92e93/types_html5lib-1.1.11.20241018-py3-none-any.whl", hash = "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403", size = 17292 }, +] + [[package]] name = "typing-extensions" version = "4.12.2"