From 01fb3ed3d993e6010b1ec004d543a82183f0e733 Mon Sep 17 00:00:00 2001 From: Jacob Thompson Date: Fri, 6 Dec 2024 19:38:58 -0800 Subject: [PATCH] pypi project format --- .github/workflows/publish_release.yml | 31 +++++ .gitignore | 4 +- main.py | 172 -------------------------- makefile | 11 ++ pyproject.toml | 46 +++++++ src/sportify/__init__.py | 0 src/sportify/main.py | 33 +++++ src/sportify/meta.py | 69 +++++++++++ src/sportify/sportify.py | 73 +++++++++++ 9 files changed, 266 insertions(+), 173 deletions(-) create mode 100644 .github/workflows/publish_release.yml delete mode 100644 main.py create mode 100644 makefile create mode 100644 pyproject.toml create mode 100644 src/sportify/__init__.py create mode 100644 src/sportify/main.py create mode 100644 src/sportify/meta.py create mode 100644 src/sportify/sportify.py diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 0000000..e1ca653 --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,31 @@ +name: Upload release to PyPI + +on: + push: + tags: + - '*' + +jobs: + pypi-publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/sportify/ + permissions: + id-token: write + steps: + - name: Checkout source + uses: actions/checkout@main + - name: Install Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install build tool + run: >- + pip install build + - name: Build package + run: >- + python -m build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index ab47253..0133cff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ __pycache__ -*.pyc \ No newline at end of file +*.pyc +dist +_version.py diff --git a/main.py b/main.py deleted file mode 100644 index 5a5aade..0000000 --- a/main.py +++ /dev/null @@ -1,172 +0,0 @@ -from datetime import datetime -import json - -from dateutil import tz -import requests -from simple_term_menu import TerminalMenu as Menu - -PROJECT = "sportify" -PROJECT_URL = f"https://github.com/jacob-thompson/{PROJECT}" -API_URL = "https://site.api.espn.com/apis/site/v2/sports/" - -ERR = "ERROR:" -REPORT = f"PLEASE SUBMIT AN ISSUE REPORT:\n\t{PROJECT_URL}/issues" -UNEXPECTED = f"{ERR} AN UNEXPECTED ERROR HAS OCCURRED\n{REPORT}" - -API_OK = 200 -EXIT_OK = 0 -EXIT_FAIL = 1 - -SPORTS = { - "football": { - "NFL" - }, - "basketball": { - "NBA" - }, - "baseball": { - "MLB" - }, - "hockey": { - "NHL" - } -} - -MENU_DATA = { - "[1] NFL": [ - "[1] ARI", "[2] ATL", "[3] BAL", "[4] BUF", - "[5] CAR", "[6] CHI", "[7] CIN", "[8] CLE", - "[9] DAL", "[0] DEN", "[a] DET", "[b] GB", - "[c] HOU", "[d] IND", "[e] JAX", "[f] KC", - "[g] LV", "[h] LAC", "[i] LAR", "[j] MIA", - "[k] MIN", "[l] NE", "[m] NO", "[n] NYG", - "[o] NYJ", "[p] PHI", "[q] PIT", "[r] SF", - "[s] SEA", "[t] TB", "[u] TEN", "[v] WSH" - ], - "[2] NBA": [ - "[1] ATL", "[2] BOS", "[3] BKN", "[4] CHA", - "[5] CHI", "[6] CLE", "[7] DAL", "[8] DEN", - "[9] DET", "[0] GS", "[a] HOU", "[b] IND", - "[c] LAC", "[d] LAL", "[e] MEM", "[f] MIA", - "[g] MIL", "[h] MIN", "[i] NO", "[j] NY", - "[k] OKC", "[l] ORL", "[m] PHI", "[n] PHX", - "[o] POR", "[p] SAC", "[q] SA", "[r] TOR", - "[s] UTAH", "[t] WSH" - ], - "[3] MLB": [ - "[1] ARI", "[2] ATL", "[3] BAL", "[4] BOS", - "[5] CHW", "[6] CHC", "[7] CIN", "[8] CLE", - "[9] COL", "[0] DET", "[a] HOU", "[b] KC", - "[c] LAA", "[d] LAD", "[e] MIA", "[f] MIL", - "[g] MIN", "[h] NYY", "[i] NYM", "[j] OAK", - "[k] PHI", "[l] PIT", "[m] SD", "[n] SF", - "[o] SEA", "[p] STL", "[q] TB", "[r] TEX", - "[s] TOR", "[t] WSH" - ], - "[4] NHL": [ - "[1] ANA", "[2] BOS", "[3] BUF", "[4] CGY", - "[5] CAR", "[6] CHI", "[7] COL", "[8] CBJ", - "[9] DAL", "[0] DET", "[a] EDM", "[b] FLA", - "[c] LA", "[d] MIN", "[e] MTL", "[f] NSH", - "[g] NJ", "[h] NYI", "[i] NYR", "[j] OTT", - "[k] PHI", "[l] PIT", "[m] SJ", "[n] SEA", - "[o] STL", "[p] TB", "[q] TOR", "[r] UTAH", - "[s] VAN", "[t] VGK", "[u] WSH", "[v] WPG" - ] -} - -class BadAPIRequest(Exception): - pass - -def convert(time): - parsed = datetime.strptime(time, "%Y-%m-%dT%H:%MZ") - utc = parsed.replace(tzinfo=tz.tzutc()) # represent time in UTC - local = utc.astimezone(tz.tzlocal()) # convert time to local - - local_date = f"{local.date().year}-{local.date().month:02}-{local.date().day:02}" - local_time = f"{local.time().hour:02}:{local.time().minute:02}" - local_datetime = f"{local_date} {local_time}" - - return local_datetime - -def output(data): - link = data["team"]["links"][0]["href"] - print(link) # TODO: expand - - name = data["team"]["displayName"] - print(name, end = " - ") - - standing = data["team"]["standingSummary"] - print(standing, end = " ") - - try: - # be cautious here since a team's record field may be empty during offseason - record = data["team"]["record"]["items"][0]["summary"] - print(f"({record})") # TODO: expand - except KeyError: - print() - - """ - venue = data["team"]["franchise"]["venue"]["fullName"] - address = data["team"]["franchise"]["venue"]["address"] - location = f"{venue}" - for entity in address.values(): - location += f", {entity}" - print(location) - """ - - next_event = data["team"]["nextEvent"][0] - event_name = next_event["name"] - event_time = convert(next_event["date"]) - event_msg = f"{event_name} {event_time}" - print(event_msg) - -def request_data(league, team): - try: - sport = [listed for listed, associations in SPORTS.items() if league in associations] - assert sport != [] - - endpoint = f"{sport[0]}/{league}/teams/{team}/" - response = requests.get(API_URL + endpoint.lower()) - - if response.status_code == API_OK: - return response.json() - else: - raise BadAPIRequest(f"{ERR} API RESPONSE STATUS CODE {response.status_code}") - except requests.exceptions.RequestException as error: - # treat this case as unexpected since RequestException is "ambiguous" - # https://requests.readthedocs.io/en/latest/api/#requests.RequestException - print(f"{UNEXPECTED}\nREQUEST EXCEPTION ENCOUNTERED: {error}") - raise SystemExit(EXIT_FAIL) - -def main(): - print(f"{PROJECT} {PROJECT_URL}") - - leagues = list(MENU_DATA.keys()) - try: # get input & request output - menu = Menu(leagues, title = "LEAGUE") - entry = menu.show() - selected_league = leagues[entry] - - teams = [MENU_DATA[league] for league in leagues if league == selected_league] - assert teams != [] - - submenu = Menu(teams[0], title = "TEAM", clear_screen = True) - entry = submenu.show() - selected_team = teams[0][entry] - - data = request_data(selected_league.split()[1], selected_team.split()[1]) - except BadAPIRequest as error: - print(error) - raise SystemExit(EXIT_FAIL) - except TypeError: # occurs when user presses Escape or Q to quit - raise SystemExit(EXIT_OK) - except AssertionError: # impossible - print(UNEXPECTED) - raise SystemExit(EXIT_FAIL) - - output(data) - raise SystemExit(EXIT_OK) - -if __name__ == "__main__": - main() diff --git a/makefile b/makefile new file mode 100644 index 0000000..5a7a910 --- /dev/null +++ b/makefile @@ -0,0 +1,11 @@ +.PHONY: clean build install run test +DEFAULT: install + +clean: + rm -rf dist .tox +build: clean + python3 -m build +install: build + pipx install dist/*.tar.gz --force +run: + sportify diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..097c030 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +dynamic = ["version"] +name = "sportify" +description = "Fetch sports data from CLI" +readme = "README.md" +requires-python = ">=3.7" +license = {file = "LICENSE"} +keywords = ["sports", "athletics", "data", "executable", "application", "cli", "espn"] +authors = [ + {name = "Jacob A. Thompson", email = "jacobalthompson@gmail.com"} +] +maintainers = [ + {name = "Jacob A. Thompson", email = "jacobalthompson@gmail.com"} +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Topic :: Games/Entertainment", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", +] +dependencies = ["python-dateutil", "requests", "simple_term_menu"] + +[project.urls] +"Homepage" = "https://github.com/jacob-thompson/sportify" +"Bug Reports" = "https://github.com/jacob-thompson/sportify/issues" + +[project.gui-scripts] +sportify = "sportify.main:main" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "_version.py" diff --git a/src/sportify/__init__.py b/src/sportify/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sportify/main.py b/src/sportify/main.py new file mode 100644 index 0000000..dc48de6 --- /dev/null +++ b/src/sportify/main.py @@ -0,0 +1,33 @@ +from .sportify import * + +def main(): + print(f"{PROJECT} {PROJECT_URL}") + + leagues = list(MENU_DATA.keys()) + try: # get input & request output + menu = Menu(leagues, title = "LEAGUE", clear_screen = True) + entry = menu.show() + selected_league = leagues[entry] + + teams = [MENU_DATA[league] for league in leagues if league == selected_league] + assert teams != [] + + submenu = Menu(teams[0], title = "TEAM", clear_screen = True) + entry = submenu.show() + selected_team = teams[0][entry] + + data = request_data(selected_league.split()[1], selected_team.split()[1]) + except BadAPIRequest as error: + print(error) + raise SystemExit(EXIT_FAIL) + except TypeError: # occurs when user presses Escape or Q to quit + raise SystemExit(EXIT_OK) + except AssertionError: # impossible + print(UNEXPECTED) + raise SystemExit(EXIT_FAIL) + + output(data) + raise SystemExit(EXIT_OK) + +if __name__ == "__main__": + main() diff --git a/src/sportify/meta.py b/src/sportify/meta.py new file mode 100644 index 0000000..2ab003a --- /dev/null +++ b/src/sportify/meta.py @@ -0,0 +1,69 @@ +PROJECT = "sportify" +PROJECT_URL = f"https://github.com/jacob-thompson/{PROJECT}" +API_URL = "https://site.api.espn.com/apis/site/v2/sports/" + +ERR = "ERROR:" +REPORT = f"PLEASE SUBMIT AN ISSUE REPORT:\n\t{PROJECT_URL}/issues" +UNEXPECTED = f"{ERR} AN UNEXPECTED ERROR HAS OCCURRED\n{REPORT}" + +SPORTS = { + "football": { + "NFL" + }, + "basketball": { + "NBA" + }, + "baseball": { + "MLB" + }, + "hockey": { + "NHL" + } +} + +MENU_DATA = { + "[1] NFL": [ + "[1] ARI", "[2] ATL", "[3] BAL", "[4] BUF", + "[5] CAR", "[6] CHI", "[7] CIN", "[8] CLE", + "[9] DAL", "[0] DEN", "[a] DET", "[b] GB", + "[c] HOU", "[d] IND", "[e] JAX", "[f] KC", + "[g] LV", "[h] LAC", "[i] LAR", "[j] MIA", + "[k] MIN", "[l] NE", "[m] NO", "[n] NYG", + "[o] NYJ", "[p] PHI", "[q] PIT", "[r] SF", + "[s] SEA", "[t] TB", "[u] TEN", "[v] WSH" + ], + "[2] NBA": [ + "[1] ATL", "[2] BOS", "[3] BKN", "[4] CHA", + "[5] CHI", "[6] CLE", "[7] DAL", "[8] DEN", + "[9] DET", "[0] GS", "[a] HOU", "[b] IND", + "[c] LAC", "[d] LAL", "[e] MEM", "[f] MIA", + "[g] MIL", "[h] MIN", "[i] NO", "[j] NY", + "[k] OKC", "[l] ORL", "[m] PHI", "[n] PHX", + "[o] POR", "[p] SAC", "[q] SA", "[r] TOR", + "[s] UTAH", "[t] WSH" + ], + "[3] MLB": [ + "[1] ARI", "[2] ATL", "[3] BAL", "[4] BOS", + "[5] CHW", "[6] CHC", "[7] CIN", "[8] CLE", + "[9] COL", "[0] DET", "[a] HOU", "[b] KC", + "[c] LAA", "[d] LAD", "[e] MIA", "[f] MIL", + "[g] MIN", "[h] NYY", "[i] NYM", "[j] OAK", + "[k] PHI", "[l] PIT", "[m] SD", "[n] SF", + "[o] SEA", "[p] STL", "[q] TB", "[r] TEX", + "[s] TOR", "[t] WSH" + ], + "[4] NHL": [ + "[1] ANA", "[2] BOS", "[3] BUF", "[4] CGY", + "[5] CAR", "[6] CHI", "[7] COL", "[8] CBJ", + "[9] DAL", "[0] DET", "[a] EDM", "[b] FLA", + "[c] LA", "[d] MIN", "[e] MTL", "[f] NSH", + "[g] NJ", "[h] NYI", "[i] NYR", "[j] OTT", + "[k] PHI", "[l] PIT", "[m] SJ", "[n] SEA", + "[o] STL", "[p] TB", "[q] TOR", "[r] UTAH", + "[s] VAN", "[t] VGK", "[u] WSH", "[v] WPG" + ] +} + +API_OK = 200 +EXIT_OK = 0 +EXIT_FAIL = 1 diff --git a/src/sportify/sportify.py b/src/sportify/sportify.py new file mode 100644 index 0000000..2fd5570 --- /dev/null +++ b/src/sportify/sportify.py @@ -0,0 +1,73 @@ +from datetime import datetime +import json + +from dateutil import tz +import requests +from simple_term_menu import TerminalMenu as Menu + +from .meta import * + +class BadAPIRequest(Exception): + pass + +def convert(time): + parsed = datetime.strptime(time, "%Y-%m-%dT%H:%MZ") + utc = parsed.replace(tzinfo=tz.tzutc()) # represent time in UTC + local = utc.astimezone(tz.tzlocal()) # convert time to local + + local_date = f"{local.date().year}-{local.date().month:02}-{local.date().day:02}" + local_time = f"{local.time().hour:02}:{local.time().minute:02}" + local_datetime = f"{local_date} {local_time}" + + return local_datetime + +def output(data): + link = data["team"]["links"][0]["href"] + print(link) # TODO: expand + + name = data["team"]["displayName"] + print(name, end = " - ") + + standing = data["team"]["standingSummary"] + print(standing, end = " ") + + try: + # be cautious here since a team's record field may be empty during offseason + record = data["team"]["record"]["items"][0]["summary"] + print(f"({record})") # TODO: expand + except KeyError: + print() + + """ + venue = data["team"]["franchise"]["venue"]["fullName"] + address = data["team"]["franchise"]["venue"]["address"] + location = f"{venue}" + for entity in address.values(): + location += f", {entity}" + print(location) + """ + + next_event = data["team"]["nextEvent"][0] + event_name = next_event["name"] + event_time = convert(next_event["date"]) + event_msg = f"{event_name} {event_time}" + print(event_msg) + +def request_data(league, team): + try: + sport = [listed for listed, associations in SPORTS.items() if league in associations] + assert sport != [] + + endpoint = f"{sport[0]}/{league}/teams/{team}/" + response = requests.get(API_URL + endpoint.lower()) + + if response.status_code == API_OK: + return response.json() + else: + raise BadAPIRequest(f"{ERR} API RESPONSE STATUS CODE {response.status_code}") + except requests.exceptions.RequestException as error: + # treat this case as unexpected since RequestException is "ambiguous" + # https://requests.readthedocs.io/en/latest/api/#requests.RequestException + print(f"{UNEXPECTED}\nREQUEST EXCEPTION ENCOUNTERED: {error}") + raise SystemExit(EXIT_FAIL) +