diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..515ca62 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI +permissions: + contents: read + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + - name: Lint (errors only) + run: | + pylint -E src || true + - name: Run tests + run: | + pytest -q diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..f14dc5c --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,49 @@ +name: Deploy GitHub Pages + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + - name: Build static site + env: + COMP_URL: ${{ vars.COMP_URL }} + COMP_PAGES: ${{ vars.COMP_PAGES }} + run: | + python scripts/build_site.py + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./site + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 47bbb17..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Python package - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - # latest python minor - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pylint - - name: Install Quarto - run: bash quarto_cli_installation.sh - - name: Lint - run: | - pylint src --fail-under=6 - - name: Test Python code - run: python render.py diff --git a/.gitignore b/.gitignore index e5cbc04..bbaaaf6 100644 --- a/.gitignore +++ b/.gitignore @@ -172,7 +172,8 @@ cython_debug/ ### Quarto files main_files -*.html +# Keep html files tracked when generated into site via CI +# Do not ignore all html globally # Sphinx documentation !docs/**/* \ No newline at end of file diff --git a/README.md b/README.md index a0e03d4..373f820 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # ResultAthle

-[![ci](https://github.com/Kirscher/ResultAthle/actions/workflows/prod.yml/badge.svg)](https://github.com/Kirscher/ResultAthle/actions/workflows/prod.yml) -[![documentation](https://github.com/Kirscher/ResultAthle/actions/workflows/documentation.yml/badge.svg)](https://github.com/Kirscher/ResultAthle/actions/workflows/documentation.yml) +[![ci](https://github.com/Kirscher/ResultAthle/actions/workflows/ci.yml/badge.svg)](https://github.com/Kirscher/ResultAthle/actions/workflows/ci.yml) ResultAthle is a project aimed at making statistical tools more accessible at the amateur level in athletics. It addresses the challenges of data manipulation and the lack of accessible descriptive statistics at the club, race, or individual level. @@ -13,7 +12,8 @@ ResultAthle is a project aimed at making statistical tools more accessible at th - [Installation](#installation) - [Usage](#usage) - [Python package](#python-package) - - [HTML dashboard with Quarto](#html-dashboard-with-quarto) + - [CLI](#cli) + - [GitHub Pages site](#github-pages-site) - [Features Under Development](#features-under-development) - [Contributing](#contributing) - [License](#license) @@ -23,7 +23,7 @@ ResultAthle is a project aimed at making statistical tools more accessible at th To install the necessary dependencies, run the following command: ```sh -pip install -r requirements.txt +pip install -e ".[dev]" ``` ## Usage @@ -39,33 +39,19 @@ header, data = scrape_competition(url, num_pages) Where `url` is the [bases.athle](https://bases.athle.fr/) URL of the competition to scrape and `num_pages` is the number of result pages you want to scrape. +### CLI + You can also use the CLI: ```bash -python -m src.resultathle.cli --out results.csv --json results.json -``` - -### HTML dashboard with Quarto - -Quarto is to be downloaded here: [Quarto URL](https://quarto.org/docs/get-started/). - -To convert the main.ipynb to HTML, run: - -```sh -quarto render .\main.ipynb --to html +python -m src.resultathle.cli --out results.csv --json results.json --site-dir site ``` -To preview the HTML in localhost, run: - -```sh -quarto preview .\main.ipynb -``` +The optional `--site-dir` renders a static `index.html` using the packaged template, which you can serve via any static host (including GitHub Pages). -To host the HTML, run: +### GitHub Pages site -```sh -quarto preview main.ipynb --port 5000 --host 0.0.0.0 --execute -``` +This repository ships with a workflow that builds and deploys a static site to GitHub Pages on each push to `main`. Configure repository variables `COMP_URL` and `COMP_PAGES` to control which competition is scraped at deploy time. The build gracefully falls back to an empty page if scraping fails. ## Features Under Development diff --git a/main.html b/main.html deleted file mode 100644 index 01203dc..0000000 --- a/main.html +++ /dev/null @@ -1,831 +0,0 @@ - - - - - - - - - -ResultAthle: le dashboard de l’athlétisme - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
-
-
-
-
-Example Image -
- - - - - -
-
-
-Analyse des Performances Athlétiques -

Bienvenue sur notre application d’analyse des performances athlétiques ! Que vous soyez un passionné de sport, un entraîneur ou un athlète cherchant à améliorer ses performances, notre plateforme vous offre des outils avancés pour comprendre et évaluer les résultats de vos compétitions.

-Visualisation des Résultats -

Explorez les données de différentes compétitions d’athlétisme à travers des visualisations interactives. Notre application vous permet de filtrer et de comparer les performances des athlètes, de suivre l’évolution des scores au fil du temps et d’identifier les tendances clés.

-Analyse des Performances Individuelles -

Plongez dans les détails en analysant les performances individuelles des athlètes. Découvrez leurs records personnels, leurs progrès au fil des saisons et identifiez les domaines où des améliorations sont possibles.

-Suivi des Classements -

Restez informé des classements actuels et historiques des athlètes dans différentes disciplines. Notre application vous offre une vue d’ensemble des performances des meilleurs athlètes, vous permettant de suivre de près la compétition et d’anticiper les tendances à venir.

-

Que vous soyez un amateur ou un professionnel de l’athlétisme, notre application est votre partenaire idéal pour explorer, comprendre et optimiser les performances athlétiques. Commencez dès maintenant à explorer les possibilités !

-
- - - - - -
-
-
-
-
-
-
-
-
Dernières compétitions
- - - - - - - - - - - -
DateFamilleLibelléLieuURL
Loading... (need help?)
- - -
-
- - - - - -
-
- - -
-
-
-
-
-

Les foulées Valenciennoises
Lieu : VALENCIENNES
Date : 07/04/24
Département : 059
Label : International

- - - - - - - - - - - - - -
AthlèteCatégorieChronoEcartLigueNaissancePerformance
Loading... (need help?)
- - -
-
- - - - - -
-
-
- - - - -
-
- - -
-
-
-
-
-
-
-
- - - - - -
-
-
- - - - -
-
- - - -
-
-
-
-
-
-
-
-
- - - - -
-
-
-
-
-
-
-
-
-
- -
-
-

-Votre classement -

-

-94 -

-
-
-
-
-
-
-
- -
-
-

-Votre temps -

-

-32:41s -

-
-
-
-
-
-
-
- -
-
-

-Votre évolution -

-

-+11% -

-
-
-
-
-
-
-
-
- - - -
- - - - - - - \ No newline at end of file diff --git a/main.ipynb b/main.ipynb deleted file mode 100644 index b7de34f..0000000 --- a/main.ipynb +++ /dev/null @@ -1,649 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "--- \n", - "title: \"ResultAthle: le dashboard de l'athlétisme\"\n", - "format: \n", - " dashboard:\n", - " scrolling: true \n", - " logo: ../src/logo.png\n", - " nav-buttons:\n", - " - icon: github\n", - " href: https://github.com/Kirscher/ResultAthle.git\n", - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Presentation du projet 🏃 {scrolling=\"true\"}\n", - "\"Example" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Analyse des Performances Athlétiques**\n", - "\n", - "Bienvenue sur notre application d'analyse des performances athlétiques ! Que vous soyez un passionné de sport, un entraîneur ou un athlète cherchant à améliorer ses performances, notre plateforme vous offre des outils avancés pour comprendre et évaluer les résultats de vos compétitions. \n", - "\n", - "**Visualisation des Résultats**\n", - "\n", - "Explorez les données de différentes compétitions d'athlétisme à travers des visualisations interactives. Notre application vous permet de filtrer et de comparer les performances des athlètes, de suivre l'évolution des scores au fil du temps et d'identifier les tendances clés. \n", - "\n", - "**Analyse des Performances Individuelles**\n", - "\n", - "Plongez dans les détails en analysant les performances individuelles des athlètes. Découvrez leurs records personnels, leurs progrès au fil des saisons et identifiez les domaines où des améliorations sont possibles. \n", - "\n", - "**Suivi des Classements**\n", - "\n", - "Restez informé des classements actuels et historiques des athlètes dans différentes disciplines. Notre application vous offre une vue d'ensemble des performances des meilleurs athlètes, vous permettant de suivre de près la compétition et d'anticiper les tendances à venir. \n", - "\n", - "Que vous soyez un amateur ou un professionnel de l'athlétisme, notre application est votre partenaire idéal pour explorer, comprendre et optimiser les performances athlétiques. Commencez dès maintenant à explorer les possibilités ! " - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "#| output: false\n", - "from src.resultathle import scraper\n", - "import utils.stat as stat\n", - "import utils.viz as viz\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import pandas as pd\n", - "from itables import show\n", - "from IPython.display import display, Markdown" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#| output: false\n", - "def adapt_dataframe_for_viz(df):\n", - " \"\"\"Adapt new scraper output to old viz format.\"\"\"\n", - " df = df.copy()\n", - " df['Athlète'] = df['Athlete']\n", - " df['duration'] = df['DurationSeconds']\n", - " df['h_duration'] = df['DurationHMS']\n", - " return df\n" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "# Derniers résultats 🏆 {scrolling=\"true\"}" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "#| output: false\n", - "competitions = scraper.scrape_latest_competitions(num_competitions=10)\n", - "competitions_df = pd.DataFrame(competitions).drop(0, axis=0)\n", - "filename = \"dernieres_competitions_FFA\"" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
Dernières compétitions
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "
DateFamilleLibelléLieuURL
Loading... (need help?)
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "title = f\"
Dernières compétitions
\"\n", - "show(competitions_df, buttons = ['copy',{\n", - " 'extend': 'csv',\n", - " 'filename': 'dernieres_competitions',\n", - " 'text': 'Download CSV'},\n", - " {'extend': 'excel',\n", - " 'filename': 'dernieres_competitions',\n", - " 'text': 'Download Excel'},\n", - " {'extend': 'pdf',\n", - " 'filename': 'dernieres_competitions',\n", - " 'text': 'Download PDF'}],\n", - " max_rows=30,\n", - " tags=title,\n", - " scrollCollapse=True,\n", - " paging=True,\n", - " search ={\"regex\": True})" - ] - }, - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "## Row {.tabset} " - ] - }, - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "### Compétition 1" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "#| output: false\n", - "header, data = scraper.scrape_competition(\"https://bases.athle.fr/asp.net/liste.aspx?frmbase=resultats&frmmode=1&frmespace=0&frmcompetition=282742\", 13)\n", - "data = adapt_dataframe_for_viz(data)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "**Compétition:** Les foulées Valenciennoises" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/markdown": [ - "**Lieu:** VALENCIENNES" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/markdown": [ - "**Date:** 07/04/24" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/markdown": [ - "**Dept:** 059" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/markdown": [ - "**Label:** International" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#| output: false\n", - "viz.display_header(header)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "

Les foulées Valenciennoises
Lieu : VALENCIENNES
Date : 07/04/24
Département : 059
Label : International

\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "
AthlèteCatégorieChronoEcartLigueNaissancePerformance
Loading... (need help?)
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "title = f\"

{header['nom']}
Lieu : {header['lieu']}
Date : {header['date']}
Département : {header['dept']}
Label : {header['label']}

\"\n", - "display_df = data.drop([\"Prénom\", \"Nom\", \"hours\", \"minutes\", \"seconds\", \"duration\", \"time_delta\"], axis=1).rename(columns={\"h_duration\": \"Chrono\", \"time_gap\": \"Ecart\"}).sort_index(axis=1)\n", - "show(display_df, buttons = ['copy',{\n", - " 'extend': 'csv',\n", - " 'filename': header['nom'],\n", - " 'text': 'Download CSV'},\n", - " {'extend': 'excel',\n", - " 'filename': header['nom'],\n", - " 'text': 'Download Excel'},\n", - " {'extend': 'pdf',\n", - " 'filename': header['nom'],\n", - " 'text': 'Download PDF'}],\n", - " max_rows=30,\n", - " tags=title,\n", - " scrollCollapse=True,\n", - " paging=True)" - ] - }, - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "## Row {.tabset} " - ] - }, - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "### Compétition 1" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#| label: podium1\n", - "viz.display_podium(data=data, title= \"Podium de la compétition: \" + header['nom'])" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "## Row {.tabset} " - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#| title: Performance de Tristan Kirscher\n", - "#| label: a\n", - "viz.graphePerso(\"tristan\",\"kirscher\", data=data,titre = \"Densité des temps d'arrivée avec performance de Tristan Kirscher\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#| title: Performance de Augustin Cablant\n", - "#| label: b\n", - "viz.graphePerso(\"augustin\",\"cablant\", data=data,titre = \"Densité des temps d'arrivée avec performance d'Augustin Cablant\")" - ] - }, - { - "cell_type": "raw", - "metadata": { - "vscode": { - "languageId": "raw" - } - }, - "source": [ - "# Analyse individuelle 📊 {scrolling=\"true\"}" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "## Rows" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'icon': 'trophy', 'color': 'primary', 'value': 94}" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#| content: valuebox\n", - "#| title: \"Votre classement\"\n", - "dict(\n", - " icon = \"trophy\",\n", - " color = \"primary\",\n", - " value = 94\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'icon': 'stopwatch', 'color': 'secondary', 'value': '32:41s'}" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#| content: valuebox\n", - "#| title: \"Votre temps\"\n", - "dict(\n", - " icon = \"stopwatch\",\n", - " color = \"secondary\",\n", - " value = \"32:41s\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'icon': 'graph-up-arrow', 'color': 'success', 'value': '+11%'}" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#| content: valuebox\n", - "#| title: \"Votre évolution\"\n", - "dict(\n", - " icon = \"graph-up-arrow\",\n", - " color = \"success\",\n", - " value = \"+11%\"\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.2" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6f6d5fa..4df4155 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,8 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "bs4>=0.0.2", + "beautifulsoup4>=4.12.0", "pandas>=2.0.3", - "numpy>=1.24.4", "lxml>=5.2.1", "jinja2>=3.0.0", ] @@ -35,16 +34,6 @@ dev = [ "pytest-cov>=4.0.0", "pylint>=2.0.0", ] -dashboard = [ - "matplotlib>=3.7.5", - "scipy>=1.10.1", - "statsmodels>=0.14.1", - "seaborn>=0.13.2", - "scikit-learn>=1.3.2", - "itables>=2.0.0", - "quarto>=0.1.0", - "plotly>=5.20.0", -] [project.scripts] resultathle = "resultathle.cli:main" diff --git a/quarto_cli_installation.sh b/quarto_cli_installation.sh deleted file mode 100644 index f472e2c..0000000 --- a/quarto_cli_installation.sh +++ /dev/null @@ -1,9 +0,0 @@ -export QUARTO_VERSION="1.4.553" -sudo mkdir -p /opt/quarto/${QUARTO_VERSION} -sudo curl -o quarto.tar.gz -L \ - "https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" -sudo tar -zxvf quarto.tar.gz \ - -C "/opt/quarto/${QUARTO_VERSION}" \ - --strip-components=1 -sudo rm quarto.tar.gz -sudo ln -s /opt/quarto/${QUARTO_VERSION}/bin/quarto /usr/local/bin/quarto \ No newline at end of file diff --git a/render.py b/render.py deleted file mode 100644 index 606a62b..0000000 --- a/render.py +++ /dev/null @@ -1,3 +0,0 @@ -import quarto - -quarto.render('main.ipynb') diff --git a/requirements.txt b/requirements.txt index a555434..d7b8832 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,5 @@ -bs4 >= 0.0.2 -pandas >= 2.0.3 -numpy >= 1.24.4 -matplotlib >= 3.7.5 -scipy >= 1.10.1 -statsmodels >= 0.14.1 -seaborn >= 0.13.2 -lxml >= 5.2.1 -scikit-learn >= 1.3.2 -itables >= 2.0.0 -quarto >= 0.1.0 -plotly >= 5.20.0 \ No newline at end of file +beautifulsoup4>=4.12.0 +pandas>=2.0.3 +lxml>=5.2.1 +jinja2>=3.0.0 +pytest>=7.0.0 \ No newline at end of file diff --git a/scripts/build_site.py b/scripts/build_site.py new file mode 100644 index 0000000..54b9685 --- /dev/null +++ b/scripts/build_site.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import os +from pathlib import Path + +import pandas as pd + +from src.resultathle.scraper import CompetitionHeader, scrape_competition +from src.resultathle.site import render_site + + +def main() -> int: + url = os.getenv( + "COMP_URL", + # Example competition URL; replace via repository variables for real deployments + "https://bases.athle.fr/asp.net/liste.aspx?frmbase=resultats&frmmode=1&frmespace=0&frmcompetition=282742", + ) + pages_str = os.getenv("COMP_PAGES", "1") + try: + pages = int(pages_str) + except ValueError: + pages = 1 + + header: CompetitionHeader + results: pd.DataFrame + try: + header, results = scrape_competition(url, pages) + except Exception: + # Fallback to an empty site if scraping fails (e.g., network/CORS restrictions in CI) + header = CompetitionHeader( + name="ResultAthle", + location="", + date="", + department=None, + label=None, + ) + results = pd.DataFrame( + { + "Athlete": [], + "FirstName": [], + "LastName": [], + "Category": [], + "League": [], + "BirthYear": [], + "Performance": [], + "DurationHMS": [], + } + ) + + output_dir = Path("site") + output_dir.mkdir(parents=True, exist_ok=True) + render_site(output_dir, header, results) + + # Also write JSON for convenience + (output_dir / "results.json").write_text( + results.to_json(orient="records", force_ascii=False) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/resultathle/cli.py b/src/resultathle/cli.py index 7bb01b9..1bab7a1 100644 --- a/src/resultathle/cli.py +++ b/src/resultathle/cli.py @@ -8,6 +8,7 @@ import pandas as pd from .scraper import CompetitionHeader, scrape_competition +from .site import render_site def _write_csv(path: Path, df: pd.DataFrame) -> None: @@ -34,6 +35,7 @@ def build_arg_parser() -> argparse.ArgumentParser: p.add_argument("pages", type=int, help="Number of result pages to scrape") p.add_argument("--out", type=Path, default=Path("output/results.csv"), help="Output CSV path") p.add_argument("--json", type=Path, default=None, help="Optional JSON output path") + p.add_argument("--site-dir", type=Path, default=None, help="Optional directory to render static site") return p @@ -45,6 +47,9 @@ def main(argv: Optional[list[str]] = None) -> int: if args.json is not None: args.json.parent.mkdir(parents=True, exist_ok=True) _write_json(args.json, header, df) + if getattr(args, "site_dir", None) is not None: + args.site_dir.mkdir(parents=True, exist_ok=True) + render_site(args.site_dir, header, df) print(f"Saved {len(df)} rows to {args.out}") return 0