From 47f4f31aadb5a1ea5f4d6dfc9a4e0847e11885e9 Mon Sep 17 00:00:00 2001 From: Conor Cleary Date: Sat, 16 May 2026 23:58:45 -0500 Subject: [PATCH 1/4] PyPI readiness --- .github/workflows/publish.yml | 33 +++++++++++++++++++++ .gitignore | 5 ++++ README.md | 47 +++++++++++++++++------------- mlb_cli.py => app/mlb_cli.py | 14 ++++----- pyproject.toml | 40 ++++++++++++++++++++++++- requirements.txt | 6 ---- tests/test_app_lifecycle.py | 12 ++++---- tests/test_calendar_interaction.py | 10 +++---- tests/test_error_handling.py | 6 ++-- tests/test_navigation_logic.py | 10 +++---- 10 files changed, 130 insertions(+), 53 deletions(-) create mode 100644 .github/workflows/publish.yml rename mlb_cli.py => app/mlb_cli.py (97%) delete mode 100644 requirements.txt diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7e2ea51 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build-n-publish: + name: Build and publish to PyPI + runs-on: ubuntu-latest + permissions: + id-token: write # Mandatory for Trusted Publishing (OIDC) + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/checkout@v4 + with: + python-version: "3.x" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build binary wheel and source tarball + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 5b6424a..882a453 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ **/__pycache__ htmlcov/ .pytest_cache/ +*.egg-info/ +.eggs/ +*.egg +build/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md index 2dfecb8..8ca6078 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MLB Box Score TUI -A high-performance, Python-based Terminal User Interface (TUI) for Major League Baseball (MLB) scores, schedules, and standings. Built with `pytermgui` and the `MLB-StatsAPI`, featuring a modular architecture and 100% test coverage. +A Python-based Terminal User Interface (TUI) for Major League Baseball (MLB) scores, schedules, and standings. Built with `pytermgui` and the `MLB-StatsAPI`, featuring a modular architecture and 100% test coverage. ## Features @@ -13,12 +13,15 @@ A high-performance, Python-based Terminal User Interface (TUI) for Major League ## Installation -### Prerequisites +### From PyPI (Recommended) -- Python 3.10+ -- A terminal with support for box-drawing characters and ANSI colors. +Install the application directly using `pip`: -### Setup +```bash +pip install mlb-cli-py +``` + +### Local Setup (Development) 1. **Clone the repository:** ```bash @@ -32,17 +35,17 @@ A high-performance, Python-based Terminal User Interface (TUI) for Major League source venv/bin/activate ``` -3. **Install dependencies:** +3. **Install in editable mode:** ```bash - pip install -r requirements.txt + pip install -e ".[dev]" ``` ## Usage -Run the application using the main entry point: +After installation, run the application from anywhere in your terminal: ```bash -python3 mlb_cli.py +mlb-cli ``` ## Controls @@ -75,17 +78,17 @@ When in the Calendar view, you can use specialized keys for precise date selecti ## Project Structure -The project follows a strict modular design: +The project follows a strict modular design optimized for distribution: ```text mlb-cli-py/ -├── mlb_cli.py # Main application controller and state management +├── pyproject.toml # Modern build system configuration and metadata ├── app/ +│ ├── mlb_cli.py # Main application controller and TUI entry point │ ├── models/ # Data services (API fetching, caching, date utilities) │ ├── widgets/ # Modular TUI components (Game, Standing, Calendar widgets) │ └── screens/ # Class-based screen definitions (Schedule, Standings, Calendar) ├── tests/ # 100% covered test suite (Unit and integration tests) -├── requirements.txt # Project dependencies └── README.md # You are here! ``` @@ -98,13 +101,7 @@ The project maintains a perfect **10.00/10 Pylint score** and **100% test covera To run the full test suite using `pytest`: ```bash -pytest -``` - -To run tests with coverage reporting: - -```bash -pytest --cov=app --cov=mlb_cli tests/ +PYTHONPATH=. pytest ``` ### Linting @@ -112,9 +109,19 @@ pytest --cov=app --cov=mlb_cli tests/ To verify code quality: ```bash -pylint mlb_cli.py app tests +pylint app tests ``` +## Release Workflow + +This project uses an automated release pipeline to publish updates to PyPI: + +1. **Version Bump:** Update the version in `pyproject.toml`. +2. **GitHub Release:** Create and publish a new Release on GitHub with a version tag (e.g., `v0.1.0`). +3. **Automated Publish:** A GitHub Action is triggered by the release, which: + - Builds the source distribution and wheel. + - Publishes the package to PyPI using **Trusted Publishing (OIDC)**. + ## Acknowledgements This project would not be possible without the [MLB-StatsAPI](https://github.com/toddrob99/MLB-StatsAPI) and [PyTermGUI](https://github.com/bczsalba/pytermgui) projects. diff --git a/mlb_cli.py b/app/mlb_cli.py similarity index 97% rename from mlb_cli.py rename to app/mlb_cli.py index 6303362..8d8c0f5 100644 --- a/mlb_cli.py +++ b/app/mlb_cli.py @@ -4,21 +4,21 @@ """ from datetime import datetime, timedelta import pytermgui as ptg -from app.models.data_service import ( +from .models.data_service import ( fetch_teams, format_date ) -from app.screens import ( +from .screens import ( ScheduleScreen, StandingsScreen, CalendarScreen, ErrorScreen ) -from app.widgets import CalendarWidget, slide_transition -from app.config import STATIC_WIDTH, INITIAL_HEIGHT -from app.state import ApplicationState -from app.exceptions import APIError -from app.logger import get_logger +from .widgets import CalendarWidget, slide_transition +from .config import STATIC_WIDTH, INITIAL_HEIGHT +from .state import ApplicationState +from .exceptions import APIError +from .logger import get_logger logger = get_logger(__name__) diff --git a/pyproject.toml b/pyproject.toml index d032614..f8a51b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,40 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "mlb-cli-py" +version = "0.1.0" +description = "A modern TUI for MLB scores, standings, and schedules" +readme = "README.md" +authors = [{ name = "Conor Cleary" }] +license = { file = "LICENSE" } +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Environment :: Console", + "Topic :: Terminals", +] +dependencies = [ + "pytermgui==7.7.4", + "MLB-StatsAPI==1.9.0", + "redis==5.0.4", +] + +[project.optional-dependencies] +dev = [ + "pytest==8.2.0", + "pytest-cov==5.0.0", + "fakeredis==2.23.2", +] + +[project.scripts] +mlb-cli = "app.mlb_cli:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["app*"] + [tool.pylint.messages_control] -disable = ["fixme"] \ No newline at end of file +disable = ["fixme"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 327d3c7..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pytermgui==7.7.4 -MLB-StatsAPI==1.9.0 -pytest==8.2.0 -pytest-cov==5.0.0 -redis==5.0.4 -fakeredis==2.23.2 diff --git a/tests/test_app_lifecycle.py b/tests/test_app_lifecycle.py index 7f646c3..6f39018 100644 --- a/tests/test_app_lifecycle.py +++ b/tests/test_app_lifecycle.py @@ -4,7 +4,7 @@ import unittest from unittest.mock import patch, MagicMock import pytermgui as ptg -from mlb_cli import MLBApp, main +from app.mlb_cli import MLBApp, main class TestAppLifecycle(unittest.TestCase): @@ -13,8 +13,8 @@ class TestAppLifecycle(unittest.TestCase): def setUp(self): """Initialize MLBApp with mocked WindowManager.""" - with patch('mlb_cli.fetch_teams'), \ - patch('mlb_cli.ptg.WindowManager') as mock_manager: + with patch('app.mlb_cli.fetch_teams'), \ + patch('app.mlb_cli.ptg.WindowManager') as mock_manager: self.app = MLBApp() self.app.manager = mock_manager.return_value self.app.manager.terminal.width = 100 @@ -52,7 +52,7 @@ def test_set_window_data_same_page_with_callback(self): self.app.set_window_data([], "Title", "page1", on_finish=mock_finish) mock_finish.assert_called_once() - @patch('mlb_cli.slide_transition') + @patch('app.mlb_cli.slide_transition') def test_set_window_data_transition(self, mock_transition): """Test set_window_data triggers slide_transition for subsequent calls.""" self.app.is_initialized = True @@ -69,7 +69,7 @@ def test_exit_app(self): self.app.exit_app() self.app.manager.stop.assert_called_once() - @patch('mlb_cli.ptg.Window') + @patch('app.mlb_cli.ptg.Window') def test_run(self, _mock_window): """Test main run loop setup.""" # Mock run to exit immediately @@ -82,7 +82,7 @@ def test_run(self, _mock_window): mock_update.assert_called_once() self.app.manager.run.assert_called_once() - @patch('mlb_cli.MLBApp') + @patch('app.mlb_cli.MLBApp') def test_main(self, mock_app_class): """Test the main() entry point.""" mock_app_instance = mock_app_class.return_value diff --git a/tests/test_calendar_interaction.py b/tests/test_calendar_interaction.py index 849b5fd..a1778d4 100644 --- a/tests/test_calendar_interaction.py +++ b/tests/test_calendar_interaction.py @@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock from datetime import datetime import pytermgui as ptg -from mlb_cli import MLBApp +from app.mlb_cli import MLBApp from app.widgets import CalendarWidget @@ -15,8 +15,8 @@ class TestCalendarInteraction(unittest.TestCase): def setUp(self): """Initialize MLBApp with mocked WindowManager.""" - with patch('mlb_cli.fetch_teams'), \ - patch('mlb_cli.ptg.WindowManager') as mock_manager: + with patch('app.mlb_cli.fetch_teams'), \ + patch('app.mlb_cli.ptg.WindowManager') as mock_manager: self.app = MLBApp() self.app.manager = mock_manager.return_value self.app.manager.terminal.width = 100 @@ -92,7 +92,7 @@ def test_focus_current_date_in_calendar_targets(self): self.assertTrue(self.app._focus_current_date_in_calendar(target="last")) self.assertEqual(self.app.manager.focused, btn_june30) - @patch('mlb_cli.CalendarScreen.get_widgets') + @patch('app.mlb_cli.CalendarScreen.get_widgets') def test_update_to_calendar(self, mock_get): """Test transition to calendar.""" mock_get.return_value = ([], "Calendar") @@ -103,7 +103,7 @@ def test_update_to_calendar(self, mock_get): _, kwargs = mock_set.call_args self.assertTrue(callable(kwargs['on_finish'])) - @patch('mlb_cli.CalendarScreen.get_widgets') + @patch('app.mlb_cli.CalendarScreen.get_widgets') def test_update_to_calendar_from_standings(self, mock_get): """Test that date is reset when returning from standings.""" mock_get.return_value = ([], "Calendar") diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 10ae989..bee2286 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -4,7 +4,7 @@ import unittest from unittest.mock import patch, MagicMock import pytermgui as ptg -from mlb_cli import MLBApp +from app.mlb_cli import MLBApp from app.exceptions import APIError from app.screens import ErrorScreen @@ -14,8 +14,8 @@ class TestErrorHandling(unittest.TestCase): def setUp(self): """Initialize MLBApp with mocked WindowManager.""" - with patch('mlb_cli.fetch_teams'), \ - patch('mlb_cli.ptg.WindowManager') as mock_manager: + with patch('app.mlb_cli.fetch_teams'), \ + patch('app.mlb_cli.ptg.WindowManager') as mock_manager: self.app = MLBApp() self.app.manager = mock_manager.return_value self.app.manager.terminal.width = 100 diff --git a/tests/test_navigation_logic.py b/tests/test_navigation_logic.py index 8ae99f8..a648c2c 100644 --- a/tests/test_navigation_logic.py +++ b/tests/test_navigation_logic.py @@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock from datetime import datetime, timedelta import pytermgui as ptg -from mlb_cli import MLBApp +from app.mlb_cli import MLBApp class TestNavigationLogic(unittest.TestCase): @@ -14,8 +14,8 @@ class TestNavigationLogic(unittest.TestCase): def setUp(self): """Initialize MLBApp with mocked WindowManager.""" - with patch('mlb_cli.fetch_teams'), \ - patch('mlb_cli.ptg.WindowManager') as mock_manager: + with patch('app.mlb_cli.fetch_teams'), \ + patch('app.mlb_cli.ptg.WindowManager') as mock_manager: self.app = MLBApp() self.app.manager = mock_manager.return_value self.app.manager.terminal.width = 100 @@ -92,7 +92,7 @@ def test_go_to_next_page_boundary(self): self.assertTrue(self.app.go_to_next_page()) self.assertEqual(self.app.state.calendar_page, 0) - @patch('mlb_cli.ScheduleScreen.get_widgets') + @patch('app.mlb_cli.ScheduleScreen.get_widgets') def test_update_to_schedule(self, mock_get): """Test transition to schedule.""" mock_get.return_value = ([], "Schedule") @@ -124,7 +124,7 @@ def test_go_to_today(self): datetime.now().strftime('%Y-%m-%d') ) - @patch('mlb_cli.StandingsScreen.get_widgets') + @patch('app.mlb_cli.StandingsScreen.get_widgets') def test_toggle_standings(self, mock_get): """Test transition to standings and back.""" mock_get.return_value = ([], "Standings") From 178e42303564167ca23ebe6f663367976db7d36a Mon Sep 17 00:00:00 2001 From: Conor Cleary Date: Sun, 17 May 2026 00:20:58 -0500 Subject: [PATCH 2/4] fix --- .github/workflows/publish.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7e2ea51..6a9eae9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/checkout@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" @@ -31,3 +31,4 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 +ublish@release/v1 From bbedf378bb65c0042a0af7d163e5aaa63b573a77 Mon Sep 17 00:00:00 2001 From: Conor Cleary Date: Sun, 17 May 2026 00:24:17 -0500 Subject: [PATCH 3/4] Fix github workflows to account for requirements.txt deletion --- .github/workflows/pylint.yml | 6 +++--- .github/workflows/test.yml | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index d65e4aa..520b4a8 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -17,14 +17,14 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install pylint - pip install -r requirements.txt + pip install ".[dev]" - name: Analysing the code with pylint run: | - pylint $(git ls-files '*.py') + pylint app tests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba03d0e..ab663ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,16 +9,19 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install ".[dev]" - name: Run tests with pytest run: | - pytest --cov=app --cov-fail-under=100 + PYTHONPATH=. pytest --cov=app --cov-fail-under=100 From f1b97586e5f4ae09c7c212875a2f9fa24bdb8fb6 Mon Sep 17 00:00:00 2001 From: Conor Cleary Date: Sun, 17 May 2026 00:27:38 -0500 Subject: [PATCH 4/4] fix syntax error in publish workflow --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6a9eae9..3b6fc67 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,4 +31,3 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 -ublish@release/v1