7 Commits

Author SHA1 Message Date
herel c21afdebc0 chore: use python image 3.13-slim instead of 3.13-alpine 2025-10-01 16:22:48 +02:00
herel 11d9aa0290 feat: slim down Docker image size by 8x using Alpine Linux base and multi-stage build 2025-09-30 09:22:26 +02:00
herel 33d3dee358 chore: update fastapi and dependencies to latest versions 2025-09-30 08:37:56 +02:00
herel 8ae1c33b3a chore: migrate to python 3.13 and update dependencies
Migrate from Python 3.11 to 3.13 with updated dependencies. Switch from PyPDF2 to pypdf library for better PDF processing. Add new U14 age groups and extract-pdf utility script.
2025-09-29 23:05:48 +02:00
herel ce42f489bf refactor: migrate to distroless multi-stage Docker build 2025-09-29 22:07:11 +02:00
herel e7615de98b feat: display staff information and improve empty state handling in game details modal 2025-09-29 20:17:23 +02:00
herel 394d71f59c doc: fix markdownlint issues in AGENTS.md 2025-09-23 09:33:44 +02:00
10 changed files with 1405 additions and 932 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ repos:
- id: mypy - id: mypy
exclude: ^(docs/|example-plugin/) exclude: ^(docs/|example-plugin/)
args: [--ignore-missing-imports] args: [--ignore-missing-imports]
additional_dependencies: [types-requests, PyPDF2] additional_dependencies: [types-requests, pypdf]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1 rev: v1.37.1
hooks: hooks:
+14 -2
View File
@@ -3,6 +3,7 @@
## Build/Lint/Test Commands ## Build/Lint/Test Commands
### Setup ### Setup
```bash ```bash
# Install dependencies with Poetry # Install dependencies with Poetry
poetry install poetry install
@@ -12,6 +13,7 @@ pre-commit install
``` ```
### Linting and Formatting ### Linting and Formatting
```bash ```bash
# Run all pre-commit checks (linting, formatting, type checking) # Run all pre-commit checks (linting, formatting, type checking)
pre-commit run --all-files pre-commit run --all-files
@@ -25,6 +27,7 @@ markdownlint . # Markdown linting
``` ```
### Running Tests ### Running Tests
```bash ```bash
# No formal test framework configured # No formal test framework configured
# Project uses manual testing with example PDF files in repository # Project uses manual testing with example PDF files in repository
@@ -35,6 +38,7 @@ markdownlint . # Markdown linting
``` ```
### Running the Web API ### Running the Web API
```bash ```bash
# Or with poetry # Or with poetry
poetry run fastapi run myice/webapi.py --host 127.0.0.1 poetry run fastapi run myice/webapi.py --host 127.0.0.1
@@ -43,51 +47,59 @@ poetry run fastapi run myice/webapi.py --host 127.0.0.1
## Code Style Guidelines ## Code Style Guidelines
### Imports ### Imports
- Standard library imports first, then third-party, then local imports - Standard library imports first, then third-party, then local imports
- Use explicit imports rather than wildcard imports - Use explicit imports rather than wildcard imports
- Group imports logically with blank lines between groups - Group imports logically with blank lines between groups
### Formatting ### Formatting
- Use ruff-format for automatic formatting - Use ruff-format for automatic formatting
- Follow PEP 8 style guide - Follow PEP 8 style guide
- Maximum line length: 88 characters (default ruff setting) - Maximum line length: 88 characters (default ruff setting)
- Use 4 spaces for indentation - Use 4 spaces for indentation
### Types ### Types
- Use type hints for function parameters and return values - Use type hints for function parameters and return values
- Prefer built-in types (str, int, list, dict) over typing aliases when possible - Prefer built-in types (str, int, list, dict) over typing aliases when possible
- Use typing.Annotated for Typer command options - Use typing.Annotated for Typer command options
### Naming Conventions ### Naming Conventions
- Variables and functions: snake_case - Variables and functions: snake_case
- Classes: PascalCase - Classes: PascalCase
- Constants: UPPER_SNAKE_CASE - Constants: UPPER_SNAKE_CASE
- Private members: prefixed with underscore (_private) - Private members: prefixed with underscore (_private)
### Error Handling ### Error Handling
- Use try/except blocks for expected exceptions - Use try/except blocks for expected exceptions
- Raise appropriate HTTPException for API errors - Raise appropriate HTTPException for API errors
- Include descriptive error messages - Include descriptive error messages
- Use sys.exit(1) for command-line tool errors - Use sys.exit(1) for command-line tool errors
### Frameworks and Libraries ### Frameworks and Libraries
- Typer for CLI interface - Typer for CLI interface
- FastAPI for web API - FastAPI for web API
- requests for HTTP requests - requests for HTTP requests
- PyPDF2 for PDF processing - pypdf for PDF processing
- Use rich for enhanced console output - Use rich for enhanced console output
- Custom rl_ai_tools package for AI functionalities - Custom rl_ai_tools package for AI functionalities
### Git Commit Messages ### Git Commit Messages
- Use conventional commits format - Use conventional commits format
- Never mention Claude in commit messages - Never mention Claude in commit messages
- Be descriptive but concise - Be descriptive but concise
- Use present tense ("add feature" not "added feature") - Use present tense ("add feature" not "added feature")
### Additional Rules ### Additional Rules
- Always use ddg-mcp to perform Web Search functionality - Always use ddg-mcp to perform Web Search functionality
- Follow the existing code patterns in myice/myice.py and myice/webapi.py - Follow the existing code patterns in myice/myice.py and myice/webapi.py
- Maintain backward compatibility when modifying existing APIs - Maintain backward compatibility when modifying existing APIs
- Document new features in README.md - Document new features in README.md
- Always run ruff format and ruff check after editing a python file - Always run ruff format and ruff check after editing a python file
- use conventional commit messages - use conventional commit messages
+28 -10
View File
@@ -1,20 +1,38 @@
FROM python:3.11 # Multi-stage build to create a minimal image
FROM python:3.13-slim AS builder
RUN install -o www-data -g www-data -d -m 0755 /var/www # Create working directory
WORKDIR /app
USER www-data # poetry export -f requirements.txt --output requirements.txt --without-hashes
# Copy dependency files
COPY requirements.txt ./
RUN curl -sSL https://install.python-poetry.org | python3 - # Install dependencies to a target directory
RUN pip install --no-cache-dir --no-deps --disable-pip-version-check --target=/app/site-packages -r requirements.txt
ENV PATH=/var/www/.local/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # Use Alpine as the base image for a much smaller footprint
FROM python:3.13-slim
COPY README.md pyproject.toml poetry.lock docker-entrypoint.sh index.html favicon.ico /var/www/ # Copy installed packages from builder stage
COPY myice /var/www/myice COPY --from=builder /app/site-packages /app/site-packages
WORKDIR /var/www # Copy application code
COPY index.html favicon.ico /app/
COPY myice /app/myice
RUN poetry install && . $(poetry env info -p) # Set PYTHONPATH so Python can find our installed packages
ENV PYTHONPATH=/app/site-packages
# Set working directory
WORKDIR /app
# Create a non-root user for security
RUN useradd --home-dir /app --no-create-home --uid 1000 myice
USER myice
# Expose port
EXPOSE 8000 EXPOSE 8000
ENTRYPOINT [ "/var/www/docker-entrypoint.sh" ] # Run the application
ENTRYPOINT ["python", "-m", "uvicorn", "myice.webapi:app", "--host", "0.0.0.0", "--port", "8000"]
+23 -9
View File
@@ -10,13 +10,17 @@ the PDFs you need.
with [uv](https://docs.astral.sh/uv/getting-started/installation/): with [uv](https://docs.astral.sh/uv/getting-started/installation/):
```shell ```shell
uv tool install --extra-index-url https://gitea.parano.ch/api/packages/herel/pypi/simple/ myice uv tool install \
--extra-index-url https://gitea.parano.ch/api/packages/herel/pypi/simple/ \
myice
``` ```
with [pipx](https://pipx.pypa.io/stable/installation/): with [pipx](https://pipx.pypa.io/stable/installation/):
```shell ```shell
pipx install --extra-index-url https://gitea.parano.ch/api/packages/herel/pypi/simple/ myice pipx install \
--extra-index-url https://gitea.parano.ch/api/packages/herel/pypi/simple/ \
myice
``` ```
## configuration ## configuration
@@ -80,9 +84,11 @@ Then open your browser at `http://localhost:8000`. The web interface allows you
The web interface supports two authentication methods: The web interface supports two authentication methods:
1. **Infomaniak OpenID Connect (Recommended)**: Click the "Se connecter avec Infomaniak" button to authenticate using Infomaniak's OIDC provider. Only users in the allowed list will be granted access. 1. **Infomaniak OpenID Connect (Recommended)**: Click the "Se connecter avec
Infomaniak" button to authenticate using Infomaniak's OIDC provider. Only
users in the allowed list will be granted access.
2. **Static API Key**: For development purposes, you can still use `abc` as the token. 1. **Static API Key**: For development purposes, you can still use `abc` as the token.
### Environment Variables ### Environment Variables
@@ -90,8 +96,8 @@ To configure OIDC authentication, set the following environment variables:
- `CLIENT_ID`: Your OIDC client ID (default: 8ea04fbb-4237-4b1d-a895-0b3575a3af3f) - `CLIENT_ID`: Your OIDC client ID (default: 8ea04fbb-4237-4b1d-a895-0b3575a3af3f)
- `CLIENT_SECRET`: Your OIDC client secret - `CLIENT_SECRET`: Your OIDC client secret
- `REDIRECT_URI`: The redirect URI (default: http://localhost:8000/callback) - `REDIRECT_URI`: The redirect URI (default: `http://localhost:8000/callback`)
- `ALLOWED_USERS`: Comma-separated list of allowed email addresses (e.g., "user1@example.com,user2@example.com") - `ALLOWED_USERS`: Comma-separated list of allowed email addresses (e.g., `"user1@example.com,user2@example.com"`)
The web API provides the following endpoints: The web API provides the following endpoints:
@@ -103,7 +109,8 @@ The web API provides the following endpoints:
- `/callback` - Handle OIDC callback - `/callback` - Handle OIDC callback
- `/userinfo` - Get user information - `/userinfo` - Get user information
All endpoints (except `/health`, `/login`, and `/callback`) require an Authorization header with a Bearer token. All endpoints (except `/health`, `/login`, and `/callback`) require an
Authorization header with a Bearer token.
## mobile functions ## mobile functions
@@ -198,9 +205,16 @@ To use a specific configuration section:
```text ```text
myice ai myice ai
> prochain match u13 top ? > prochain match u13 top ?
< Le prochain match de l'équipe U13 Top se déroulera le dimanche 10 novembre 2024 contre HC Ajoie à la Raffeisen Arena de Porrentruy. Le match débutera à 14h00 et se terminera à 16h15. < Le prochain match de l'équipe U13 Top se déroulera le dimanche 10 novembre
2024 contre HC Ajoie à la Raffeisen Arena de Porrentruy. Le match débutera
à 14h00 et se terminera à 16h15.
> et les u13 a ? > et les u13 a ?
< Le prochain match de l'équipe U13 A se déroulera le samedi 9 novembre 2024 contre HC Vallorbe à P. du Frézillon, 1337 Vallorbe VD. Le match débutera à 13h00 et se terminera à 15h00. Le prochain match à domicile de l'équipe U13 A se déroulera le dimanche 10 novembre 2024 contre CP Meyrin à Les Vernets, Glace extérieure, 1227 Les Acacias GE. Le match débutera à 13h00 et se terminera à 15h00. < Le prochain match de l'équipe U13 A se déroulera le samedi 9 novembre 2024
contre HC Vallorbe à P. du Frézillon, 1337 Vallorbe VD. Le match débutera à
13h00 et se terminera à 15h00. Le prochain match à domicile de l'équipe
U13 A se déroulera le dimanche 10 novembre 2024 contre CP Meyrin à
Les Vernets, Glace extérieure, 1227 Les Acacias GE. Le match débutera
à 13h00 et se terminera à 15h00.
``` ```
To use a specific configuration section: To use a specific configuration section:
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
exec /var/www/.local/bin/poetry run fastapi run myice/webapi.py
+72 -16
View File
@@ -518,9 +518,11 @@
headers: { "Authorization": `Bearer ${storedApiKey}` } headers: { "Authorization": `Bearer ${storedApiKey}` }
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const sortedPlayers = data.convocation.available // Check if available players data exists
.sort((a, b) => (a.number || 0) - (b.number || 0)); const availablePlayers = data.convocation.available || [];
const sortedPlayers = availablePlayers
.sort((a, b) => (a.number || 0) - (b.number || 0));
// Calculate player statistics // Calculate player statistics
const totalPlayers = sortedPlayers.length; const totalPlayers = sortedPlayers.length;
@@ -551,19 +553,73 @@
return numA - numB; return numA - numB;
}); });
eventDetailsContent.innerHTML = ` // Process staff data
<h5>${data.title}</h5> const staffList = data.convocation.staff || [];
<p><strong>Type:</strong> ${data.type}</p> const totalStaff = staffList.length;
<p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p> // Check if there are no players
<p><strong>Joueurs convoqués:</strong> ${totalPlayers} joueur${totalPlayers > 1 ? 's' : ''} (${positionBreakdown})</p> if (totalPlayers === 0 && totalStaff === 0) {
<h6>Liste des joueurs:</h6> eventDetailsContent.innerHTML = `
<ul>${playersByPosition.map(player => { <div class="card border-warning">
let number = player.number ? player.number : "N/A"; <div class="card-body text-center">
let position = player.position ? player.position : "N/A"; <h5 class="card-title">${data.title}</h5>
return `<li>[${position}] ${number} - ${player.fname} ${player.lname} (${player.dob})</li>`; <p class="card-text"><strong>Type:</strong> ${data.type}</p>
}).join('')}</ul> <p class="card-text"><strong>Lieu:</strong> ${data.place}</p>
`; <p class="card-text"><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
<div class="alert alert-warning" role="alert">
<h6 class="alert-heading">Aucun joueur ni personnel convoqué</h6>
<p>Il n'y a actuellement aucun joueur ni personnel convoqué pour ce match.</p>
</div>
</div>
</div>
`;
} else {
let staffHtml = '';
if (totalStaff > 0) {
staffHtml = `
<h6>Personnel (${totalStaff}):</h6>
<ul>${staffList.map(staff => {
return `<li><strong>${staff.role}:</strong> ${staff.fname} ${staff.lname}</li>`;
}).join('')}</ul>
`;
} else {
staffHtml = `
<div class="alert alert-info" role="alert">
<h6>Aucun personnel convoqué</h6>
<p>Il n'y a actuellement aucun personnel convoqué pour ce match.</p>
</div>
`;
}
if (totalPlayers === 0) {
eventDetailsContent.innerHTML = `
<h5>${data.title}</h5>
<p><strong>Type:</strong> ${data.type}</p>
<p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
<div class="alert alert-warning" role="alert">
<h6 class="alert-heading">Aucun joueur convoqué</h6>
<p>Il n'y a actuellement aucun joueur convoqué pour ce match.</p>
</div>
${staffHtml}
`;
} else {
eventDetailsContent.innerHTML = `
<h5>${data.title}</h5>
<p><strong>Type:</strong> ${data.type}</p>
<p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
<p><strong>Joueurs convoqués:</strong> ${totalPlayers} joueur${totalPlayers > 1 ? 's' : ''} (${positionBreakdown})</p>
<h6>Liste des joueurs:</h6>
<ul>${playersByPosition.map(player => {
let number = player.number ? player.number : "N/A";
let position = player.position ? player.position : "N/A";
return `<li>[${position}] ${number} - ${player.fname} ${player.lname} (${player.dob})</li>`;
}).join('')}</ul>
${staffHtml}
`;
}
}
new bootstrap.Modal(document.getElementById('eventDetailsModal')).show(); new bootstrap.Modal(document.getElementById('eventDetailsModal')).show();
}) })
.catch(error => console.error("Erreur lors du chargement des détails de l'événement:", error)); .catch(error => console.error("Erreur lors du chargement des détails de l'événement:", error));
+10 -5
View File
@@ -14,7 +14,7 @@ from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Annotated from typing import Annotated
from typing import List, Tuple from typing import List, Tuple
import PyPDF2 import pypdf
import requests import requests
import typer import typer
from rich import print from rich import print
@@ -148,6 +148,9 @@ class AgeGroup(str, Enum):
u18e = "U18 (Elite)" u18e = "U18 (Elite)"
u18el = "U18 (Elit)" u18el = "U18 (Elit)"
u21e = "U21 (ELIT)" u21e = "U21 (ELIT)"
u14t = "U14 (Top)"
u14t1 = "U14 (Top1)"
u14t2 = "U14 (Top2)"
def normalize_age_group(value: str) -> AgeGroup | None: def normalize_age_group(value: str) -> AgeGroup | None:
@@ -423,19 +426,21 @@ def os_open(file: str) -> None:
def extract_players(pdf_file: Path) -> List[str]: def extract_players(pdf_file: Path) -> List[str]:
reader = PyPDF2.PdfReader(pdf_file) reader = pypdf.PdfReader(pdf_file)
page = reader.pages[0] page = reader.pages[0]
players = [] players = []
def visitor_body(text, cm, tm, fontDict, fontSize): def visitor_body(text, cm, tm, fontDict, fontSize):
global last_text
if text:
last_text = text
(x, y) = (tm[4], tm[5]) (x, y) = (tm[4], tm[5])
# print(tm, text)
if x > 79 and x < 80 and y < 741.93: if x > 79 and x < 80 and y < 741.93:
# and y < 741.93 and y > 741.93 - 585.18: # and y < 741.93 and y > 741.93 - 585.18:
players.append(text) players.append(last_text.strip())
page.extract_text(visitor_text=visitor_body) page.extract_text(visitor_text=visitor_body, extraction_mode="plain")
return players return players
Generated
+1205 -880
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,20 +1,20 @@
[project] [project]
name = "myice" name = "myice"
version = "v0.5.7" version = "v0.5.8"
description = "myice parsing" description = "myice parsing"
authors = [ authors = [
{ name = "Rene Luria", "email" = "<rene@luria.ch>"}, { name = "Rene Luria", "email" = "<rene@luria.ch>"},
] ]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.13"
dependencies = [ dependencies = [
"requests (>=2.32.3,<2.33.0)", "requests (>=2.32.3)",
"typer (>=0.15.1,<0.16.0)", "typer (>=0.15.1)",
"pypdf2 (>=3.0.1)", "pypdf (>=6.0.0)",
"rl-ai-tools >=1.9.0", "rl-ai-tools >=1.9.0",
"fastapi[standard] (>=0.115.11,<0.116.0)", "fastapi[standard] (>=0.115.11)",
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
+46
View File
@@ -0,0 +1,46 @@
--extra-index-url https://pypi.purple.infomaniak.ch
annotated-types==0.7.0 ; python_version >= "3.13"
anyio==4.11.0 ; python_version >= "3.13"
certifi==2025.8.3 ; python_version >= "3.13"
charset-normalizer==3.4.3 ; python_version >= "3.13"
click==8.1.8 ; python_version >= "3.13"
colorama==0.4.6 ; (platform_system == "Windows" or sys_platform == "win32") and python_version >= "3.13"
dnspython==2.8.0 ; python_version >= "3.13"
email-validator==2.3.0 ; python_version >= "3.13"
fastapi-cli==0.0.13 ; python_version >= "3.13"
fastapi-cloud-cli==0.2.1 ; python_version >= "3.13"
fastapi==0.118.0 ; python_version >= "3.13"
h11==0.16.0 ; python_version >= "3.13"
httpcore==1.0.9 ; python_version >= "3.13"
httptools==0.6.4 ; python_version >= "3.13"
httpx==0.28.1 ; python_version >= "3.13"
idna==3.10 ; python_version >= "3.13"
jinja2==3.1.6 ; python_version >= "3.13"
markdown-it-py==4.0.0 ; python_version >= "3.13"
markupsafe==3.0.3 ; python_version >= "3.13"
mdurl==0.1.2 ; python_version >= "3.13"
pydantic-core==2.33.2 ; python_version >= "3.13"
pydantic==2.11.9 ; python_version >= "3.13"
pygments==2.19.2 ; python_version >= "3.13"
pypdf==6.1.1 ; python_version >= "3.13"
python-dotenv==1.1.1 ; python_version >= "3.13"
python-multipart==0.0.20 ; python_version >= "3.13"
pyyaml==6.0.3 ; python_version >= "3.13"
requests==2.32.5 ; python_version >= "3.13"
rich-toolkit==0.15.1 ; python_version >= "3.13"
rich==14.1.0 ; python_version >= "3.13"
rignore==0.6.4 ; python_version >= "3.13"
rl-ai-tools==1.15.0 ; python_version >= "3.13"
sentry-sdk==2.39.0 ; python_version >= "3.13"
shellingham==1.5.4 ; python_version >= "3.13"
sniffio==1.3.1 ; python_version >= "3.13"
starlette==0.48.0 ; python_version >= "3.13"
typer==0.15.4 ; python_version >= "3.13"
typing-extensions==4.15.0 ; python_version >= "3.13"
typing-inspection==0.4.1 ; python_version >= "3.13"
urllib3==2.5.0 ; python_version >= "3.13"
uvicorn==0.37.0 ; python_version >= "3.13"
uvloop==0.21.0 ; sys_platform != "win32" and sys_platform != "cygwin" and platform_python_implementation != "PyPy" and python_version >= "3.13"
watchfiles==1.1.0 ; python_version >= "3.13"
websockets==15.0.1 ; python_version >= "3.13"