4 Commits

Author SHA1 Message Date
herel 6ad0587246 🟢 LICENSE.txt (New MIT license added)
🛠️ pyproject.toml -> Updated poetry dependencies, authors email changed, added license info
2024-11-01 10:41:39 +01:00
herel 7a19f9acb7 🟢 README.md: Updated command to retrieve schedule and added new commands for searching events by age group and retrieving match details
🛠️ myice/myice.py: Implemented a function to parse the schedule JSON file based on given age groups, improved error handling, enhanced formatting when printing results, added a new command `myice search`
2024-11-01 10:38:43 +01:00
herel 6c502f94f1 chore: add from Python.gitignore 2024-11-01 10:07:04 +01:00
herel e102cfa9c8 initial import 2024-11-01 09:59:45 +01:00
13 changed files with 253 additions and 2060 deletions
-1
View File
@@ -1 +0,0 @@
style "#{File.dirname(__FILE__)}/mdl.rb"
+2 -2
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]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1 rev: v1.35.1
hooks: hooks:
@@ -56,7 +56,7 @@ repos:
- id: misspell - id: misspell
args: ["-i", "charactor"] args: ["-i", "charactor"]
- repo: https://github.com/python-poetry/poetry - repo: https://github.com/python-poetry/poetry
rev: "2.0.0" rev: "1.8.0"
hooks: hooks:
- id: poetry-check - id: poetry-check
- id: poetry-lock - id: poetry-lock
-77
View File
@@ -1,77 +0,0 @@
# Development Guide for Agentic Coding Agents
## Build/Lint/Test Commands
### Setup
```bash
# Install dependencies with Poetry
poetry install
# Install pre-commit hooks
pre-commit install
```
### Linting and Formatting
```bash
# Run all pre-commit checks (linting, formatting, type checking)
pre-commit run --all-files
# Run specific linters
ruff check . # Python linting
ruff format . # Python formatting
mypy . # Type checking
yamllint . # YAML linting
markdownlint . # Markdown linting
```
### Testing
```bash
# Run tests (no specific test framework configured)
# Project uses manual testing with example PDF files in repository
```
## Code Style Guidelines
### Imports
- Standard library imports first, then third-party, then local imports
- Use explicit imports rather than wildcard imports
- Group imports logically with blank lines between groups
### Formatting
- Use ruff-format for automatic formatting
- Follow PEP 8 style guide
- Maximum line length: 88 characters (default ruff setting)
- Use 4 spaces for indentation
### Types
- Use type hints for function parameters and return values
- Prefer built-in types (str, int, list, dict) over typing aliases when possible
- Use typing.Annotated for Typer command options
### Naming Conventions
- Variables and functions: snake_case
- Classes: PascalCase
- Constants: UPPER_SNAKE_CASE
- Private members: prefixed with underscore (_private)
### Error Handling
- Use try/except blocks for expected exceptions
- Raise appropriate HTTPException for API errors
- Include descriptive error messages
- Use sys.exit(1) for command-line tool errors
### Frameworks and Libraries
- Typer for CLI interface
- FastAPI for web API
- requests for HTTP requests
- PyPDF2 for PDF processing
- Use rich for enhanced console output
-20
View File
@@ -1,20 +0,0 @@
FROM python:3.11
RUN install -o www-data -g www-data -d -m 0755 /var/www
USER www-data
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH=/var/www/.local/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
COPY README.md pyproject.toml poetry.lock docker-entrypoint.sh index.html favicon.ico /var/www/
COPY myice /var/www/myice
WORKDIR /var/www
RUN poetry install && . $(poetry env info -p)
EXPOSE 8000
ENTRYPOINT [ "/var/www/docker-entrypoint.sh" ]
+1 -30
View File
@@ -1,24 +1,5 @@
# myice # myice
## intro
Avec tout ça on va aller chercher sur MyIce les planning des gamins et générer
les pdf qu'on veut
## install
with [uv](https://docs.astral.sh/uv/getting-started/installation/):
```shell
uv tool install --extra-index-url https://gitea.parano.ch/api/packages/herel/pypi/simple/ myice
```
with [pipx](https://pipx.pypa.io/stable/installation/):
```shell
pipx install --extra-index-url https://gitea.parano.ch/api/packages/herel/pypi/simple/ myice
```
## récupérer le schedule ## récupérer le schedule
```shell ```shell
@@ -29,7 +10,7 @@ myice schedule -o schedule.json
### listing ### listing
Pour récupérer les event des U13 Elite par example: Pour récupérer les event des U13 Elite par exemple:
```shell ```shell
myice search "U13 (Elite)" myice search "U13 (Elite)"
@@ -76,13 +57,3 @@ et pour la convoc d'un entraînement:
Opening file practice_561855.pdf Opening file practice_561855.pdf
``` ```
### AI
```text
myice ai
> 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.
> 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.
```
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
exec /var/www/.local/bin/poetry run fastapi run myice/webapi.py
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

-187
View File
@@ -1,187 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyIce - Games</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4 text-center">MyIce - Games</h1>
<div id="apikeyContainer" class="mb-3"></div>
<div id="eventFilters" style="display: none;">
<div class="mb-3">
<label for="agegroup" class="form-label">Filtrer par groupe d'âge</label>
<select id="agegroup" class="form-select">
<option value="">Tous</option>
</select>
</div>
<button id="fetchEvents" class="btn btn-primary mb-3">Charger les événements</button>
</div>
<div id="eventList" class="row"></div>
<!-- Modal pour afficher les détails d'un événement -->
<div class="modal fade" id="eventDetailsModal" tabindex="-1" aria-labelledby="eventDetailsLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="eventDetailsLabel">Détails de l'événement</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="eventDetailsContent">Chargement...</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const apikeyContainer = document.getElementById("apikeyContainer");
const eventFilters = document.getElementById("eventFilters");
const agegroupSelect = document.getElementById("agegroup");
const eventList = document.getElementById("eventList");
const fetchButton = document.getElementById("fetchEvents");
const eventDetailsContent = document.getElementById("eventDetailsContent");
const apiBaseUrl = window.location.origin;
let storedApiKey = localStorage.getItem("apikey");
let lastFetchedEvents = [];
function renderApiKeyInput() {
if (storedApiKey) {
apikeyContainer.innerHTML = `<button id="disconnect" class="btn btn-danger">Déconnecter</button>`;
document.getElementById("disconnect").addEventListener("click", () => {
localStorage.removeItem("apikey");
location.reload();
});
eventFilters.style.display = "block";
fetchEvents(storedApiKey);
} else {
apikeyContainer.innerHTML = `
<label for="apikey" class="form-label">API Key</label>
<input type="text" id="apikey" class="form-control" placeholder="Entrez votre API Key">
<button id="validateApiKey" class="btn btn-success mt-2">Valider</button>
`;
eventFilters.style.display = "none";
document.getElementById("validateApiKey").addEventListener("click", saveApiKey);
document.getElementById("apikey").addEventListener("keypress", function (event) {
if (event.key === "Enter") {
saveApiKey();
}
});
}
}
function saveApiKey() {
const key = document.getElementById("apikey").value;
if (key) {
localStorage.setItem("apikey", key);
location.reload();
} else {
alert("Veuillez entrer une clé API valide.");
}
}
renderApiKeyInput();
fetchButton.addEventListener("click", () => {
if (!storedApiKey) {
alert("Veuillez entrer une clé API");
return;
}
fetchEvents(storedApiKey);
});
agegroupSelect.addEventListener("change", () => {
displayEvents(lastFetchedEvents);
});
function fetchEvents(apiKey) {
fetch(`${apiBaseUrl}/schedule`, {
headers: { "Authorization": `Bearer ${apiKey}` }
})
.then(response => response.json())
.then(data => {
lastFetchedEvents = data.filter(event => event.event === "Jeu");
updateAgeGroupOptions(lastFetchedEvents);
displayEvents(lastFetchedEvents);
})
.catch(error => console.error("Erreur lors du chargement des événements:", error));
}
function updateAgeGroupOptions(events) {
let agegroups = new Set(events.map(event => `${event.agegroup} ${event.name}`.trim()));
agegroupSelect.innerHTML = '<option value="">Tous</option>';
agegroups.forEach(group => {
const option = document.createElement("option");
option.value = group;
option.textContent = group;
agegroupSelect.appendChild(option);
});
}
function displayEvents(events) {
eventList.innerHTML = "";
let selectedAgegroup = agegroupSelect.value;
let filteredEvents = events.filter(event => event.event === "Jeu" && (selectedAgegroup === "" || `${event.agegroup} ${event.name}` === selectedAgegroup));
if (filteredEvents.length === 0) {
eventList.innerHTML = "<p class='text-muted'>Aucun événement 'Jeu' trouvé.</p>";
return;
}
filteredEvents.forEach(event => {
const eventCard = document.createElement("div");
eventCard.className = "col-md-4 mb-3";
eventCard.innerHTML = `
<div class="card" style="border-left: 5px solid ${event.color}" data-id="${event.id_event}">
<div class="card-body">
<h5 class="card-title">${event.title}</h5>
<p class="card-text"><strong>Lieu:</strong> ${event.place}</p>
<p class="card-text"><strong>Heure:</strong> ${event.start} - ${event.end}</p>
</div>
</div>
`;
eventCard.addEventListener("click", () => fetchEventDetails(event.id_event));
eventList.appendChild(eventCard);
});
}
function fetchEventDetails(eventId) {
fetch(`${apiBaseUrl}/game/${eventId}`, {
headers: { "Authorization": `Bearer ${storedApiKey}` }
})
.then(response => response.json())
.then(data => {
const sortedPlayers = data.convocation.available
.sort((a, b) => (a.number || 0) - (b.number || 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>
<h6>Joueurs convoqués:</h6>
<ul>${sortedPlayers.map(player => {
let number = player.number ? player.number : "N/A";
let position = player.position ? player.position : "N/A";
return `<li>${number} - ${player.fname} ${player.lname} (${position}, ${player.dob})</li>`;
}).join('')}</ul>
`;
new bootstrap.Modal(document.getElementById('eventDetailsModal')).show();
})
.catch(error => console.error("Erreur lors du chargement des détails de l'événement:", error));
}
});
</script>
</body>
</html>
-2
View File
@@ -1,2 +0,0 @@
all
rule 'MD013', :ignore_code_blocks => true
+23 -376
View File
@@ -9,143 +9,26 @@ import json
import os import os
import re import re
import sys import sys
import tempfile
from enum import Enum 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
import PyPDF2
import requests import requests
import typer import typer
from rich import print
from rl_ai_tools import utils # type: ignore
user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0" user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0"
def sanitize_json_response(text):
"""Sanitize JSON response from MyIce API using our proven fix_schedule_json.py approach"""
try:
# First, try to parse as-is
data = json.loads(text)
return json.dumps(data, ensure_ascii=False, indent=2, separators=(",", ": "))
except json.JSONDecodeError:
# Use the exact same approach as our working fix_schedule_json.py script
# but implemented directly in code
# Split into lines to process
lines = text.split("\n")
# Fix line issues (remove line numbers if present)
fixed_lines = []
for line in lines:
# Remove line numbers prefix if present (from cat -n format)
line = re.sub(r"^\s*\d+\|\s*", "", line)
fixed_lines.append(line)
# Join all lines back together
content = "".join(fixed_lines)
# Apply comprehensive sanitization that we know works
# 1. Escape literal newlines, carriage returns, and tabs everywhere
content = content.replace("\n", "\\n")
content = content.replace("\r", "\\r")
content = content.replace("\t", "\\t")
# 2. Find the main JSON array structure
start = content.find("[")
end = content.rfind("]")
if start != -1 and end != -1 and end > start:
array_content = content[start : end + 1]
else:
# Fallback: try to reconstruct a valid array
array_content = "[" + content + "]"
# 3. Fix common JSON formatting issues
# Remove trailing commas before closing brackets/braces
array_content = re.sub(r",(\s*[}\]])", r"\1", array_content)
# 4. Try to parse the sanitized content
try:
data = json.loads(array_content)
return json.dumps(
data, ensure_ascii=False, indent=2, separators=(",", ": ")
)
except json.JSONDecodeError:
# If parsing still fails, try one more aggressive approach
# Remove any remaining control characters that might be causing issues
array_content = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", array_content)
try:
data = json.loads(array_content)
return json.dumps(
data, ensure_ascii=False, indent=2, separators=(",", ": ")
)
except json.JSONDecodeError:
# Final fallback - return a minimal valid JSON array
return json.dumps(
[], ensure_ascii=False, indent=2, separators=(",", ": ")
)
app = typer.Typer(no_args_is_help=True) app = typer.Typer(no_args_is_help=True)
session: requests.Session session: requests.Session
userid: int userid: int
class AgeGroup(str, Enum): class AgeGroup(str, Enum):
u111 = "U11 (1)"
u13e = "U13 (Elite)" u13e = "U13 (Elite)"
u13t = "U13 (Top)" u13t = "U13 (Top)"
u13a = "U13 (A)" u13a = "U13 (A)"
u13p = "U13 (Prép)" u13p = "U13 (Prép)"
u14ev = "U14 (Elite Vernets)"
u14esmv = "U14 (Elite Sous-Moulin/Vergers)"
u15t = "U15 (Top)" u15t = "U15 (Top)"
u15a = "U15 (A)" u15a = "U15 (A)"
u16e = "U16 (Elite)"
u16t = "U16 (Top GS Ass)"
u18e = "U18 (Elite)"
u18el = "U18 (Elit)"
u21e = "U21 (ELIT)"
def normalize_age_group(value: str) -> AgeGroup | None:
"""Normalize age group string to handle case and spelling variations."""
import re
if not isinstance(value, str):
return None
def normalize_spelling(text: str) -> str:
"""Normalize spelling variations of 'elit' to 'elite'."""
# Replace accented versions first
text = text.replace("élit", "elite")
# Use regex to replace "elit" with "elite" only when it's a complete word
# This avoids replacing "elit" within "elite"
text = re.sub(r"\belit\b", "elite", text)
return text
# Convert to lowercase for case-insensitive comparison
input_lower = value.lower()
input_normalized = normalize_spelling(input_lower)
for member in AgeGroup:
# Convert enum value to lowercase and normalize
member_lower = member.value.lower()
member_normalized = normalize_spelling(member_lower)
# Check for match
if member_normalized == input_normalized:
return member
return None
class EventType(str, Enum):
game = "game"
practice = "practice"
def load_cookies(file: str = "cookies.txt") -> requests.cookies.RequestsCookieJar: def load_cookies(file: str = "cookies.txt") -> requests.cookies.RequestsCookieJar:
@@ -157,13 +40,12 @@ def load_cookies(file: str = "cookies.txt") -> requests.cookies.RequestsCookieJa
return requests.cookies.cookiejar_from_dict(cj_dict) return requests.cookies.cookiejar_from_dict(cj_dict)
def save_cookies(file: str = "cookies.txt"): def save_cookies():
cookie_jar_file = Path(file) with open("cookies.txt", "w") as f:
with cookie_jar_file.open("w") as f:
f.write(json.dumps(requests.utils.dict_from_cookiejar(session.cookies))) f.write(json.dumps(requests.utils.dict_from_cookiejar(session.cookies)))
def get_login(local_file: str = "myice.ini") -> tuple[str, str, int, str]: def get_login(local_file: str = "myice.ini") -> tuple[str, str, int]:
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read( config.read(
[ [
@@ -178,22 +60,19 @@ def get_login(local_file: str = "myice.ini") -> tuple[str, str, int, str]:
username = default_config.get("username") username = default_config.get("username")
password = default_config.get("password") password = default_config.get("password")
userid = default_config.getint("userid") userid = default_config.getint("userid")
token = default_config.get("token")
if not username or not password: if not username or not password:
print( print("Error: please configure username/password in ini file")
"Error: please configure username/password in ini file", file=sys.stderr
)
sys.exit(1) sys.exit(1)
else: else:
print("Error: please configure username/password in ini file", file=sys.stderr) print("Error: please configure username/password in ini file")
sys.exit(1) sys.exit(1)
return username, password, userid, token return username, password, userid
def do_login(): def do_login():
global session global session
global userid global userid
username, password, userid, token = get_login() username, password, userid = get_login()
r = session.get("https://app.myice.hockey/", headers={"User-Agent": user_agent}) r = session.get("https://app.myice.hockey/", headers={"User-Agent": user_agent})
r.raise_for_status() r.raise_for_status()
form_data = { form_data = {
@@ -212,9 +91,6 @@ def do_login():
}, },
) )
r.raise_for_status() r.raise_for_status()
# select the club we want
session.get("https://app.myice.hockey/?cl=172", headers={"User-Agent": user_agent})
r.raise_for_status()
def get_userid(): def get_userid():
@@ -239,16 +115,15 @@ def wrapper_session(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
global session, userid global session, userid
session = requests.Session() session = requests.Session()
# session.verify = False
session.cookies = load_cookies() session.cookies = load_cookies()
session.cookies.clear_expired_cookies() session.cookies.clear_expired_cookies()
if not session.cookies.get("mih_v3_cookname"): if not session.cookies.get("mih_v3_cookname"):
print("login...", file=sys.stderr) print("login...")
do_login() do_login()
save_cookies() save_cookies()
_, _, userid, _ = get_login() _, _, userid = get_login()
if not userid: if not userid:
print("get userid...", file=sys.stderr) print("get userid...")
userid = get_userid() userid = get_userid()
return func(*args, **kwargs) return func(*args, **kwargs)
@@ -256,13 +131,13 @@ def wrapper_session(func):
@wrapper_session @wrapper_session
def get_schedule(num_days: int) -> str: def get_schedule() -> str:
global session global session
global userid global userid
assert session and userid assert session and userid
now = datetime.datetime.now() now = datetime.datetime.now()
date_start = now date_start = now + datetime.timedelta(days=1)
date_end = date_start + datetime.timedelta(days=num_days) date_end = date_start + datetime.timedelta(days=7)
r = session.post( r = session.post(
"https://app.myice.hockey/inc/processclubplanning.php", "https://app.myice.hockey/inc/processclubplanning.php",
data={ data={
@@ -275,14 +150,9 @@ def get_schedule(num_days: int) -> str:
headers={ headers={
"User-Agent": user_agent, "User-Agent": user_agent,
"Referer": "https://app.myice.hockey/players/clubschedule/", "Referer": "https://app.myice.hockey/players/clubschedule/",
"Accept": "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest",
}, },
) )
r.raise_for_status() r.raise_for_status()
# Debug: Save raw response to file for analysis
# with open("raw_response.txt", "w") as f:
# f.write(r.text)
return r.text return r.text
@@ -328,22 +198,20 @@ def schedule(
"--outfile", "-o", help="file to write result to, or stdout if none" "--outfile", "-o", help="file to write result to, or stdout if none"
), ),
] = None, ] = None,
num_days: Annotated[int, typer.Option("--days")] = 7,
): ):
""" """
Fetch schedule as json Fetch schedule as json
""" """
schedule = get_schedule(num_days) schedule = get_schedule()
# Sanitize the JSON response using our proven approach
sanitized_schedule = sanitize_json_response(schedule)
if outfile: if outfile:
with outfile.open("w") as f: with outfile.open("w") as f:
f.write(sanitized_schedule) f.write(schedule)
else: else:
print(sanitized_schedule) print("Schedule:", file=sys.stderr)
print(schedule)
def os_open(file: str) -> None: def open(file: str) -> None:
if os.uname().sysname == "Linux": if os.uname().sysname == "Linux":
os.system(f"xdg-open {file}") os.system(f"xdg-open {file}")
else: else:
@@ -351,42 +219,16 @@ def os_open(file: str) -> None:
os.system(f"open {file}") os.system(f"open {file}")
def extract_players(pdf_file: Path) -> List[str]:
reader = PyPDF2.PdfReader(pdf_file)
page = reader.pages[0]
players = []
def visitor_body(text, cm, tm, fontDict, fontSize):
(x, y) = (tm[4], tm[5])
# print(tm, text)
if x > 79 and x < 80 and y < 741.93:
# and y < 741.93 and y > 741.93 - 585.18:
players.append(text)
page.extract_text(visitor_text=visitor_body)
return players
@app.command("game") @app.command("game")
def get_game_pdf( def get_game_pdf(
game_id: Annotated[int, typer.Argument(help="ID of game to gen pdf for")], game_id: Annotated[int, typer.Argument(help="ID of game to gen pdf for")],
open_file: Annotated[bool, typer.Option("--open", "-o")] = False,
): ):
""" """
Genate the pdf for the game invitation Genate the pdf for the game invitation
""" """
if open_file:
output_filename = f"game_{game_id}.pdf" output_filename = f"game_{game_id}.pdf"
else:
output_filename = tempfile.NamedTemporaryFile().name
game_pdf(game_id, Path(output_filename)) game_pdf(game_id, Path(output_filename))
if open_file: open(output_filename)
os_open(output_filename)
else:
players = extract_players(Path(output_filename))
print("Players:")
print("\n".join(players))
@app.command("practice") @app.command("practice")
@@ -398,16 +240,12 @@ def get_practice_pdf(
""" """
output_filename = f"practice_{game_id}.pdf" output_filename = f"practice_{game_id}.pdf"
practice_pdf(game_id, Path(output_filename)) practice_pdf(game_id, Path(output_filename))
os_open(output_filename) open(output_filename)
@app.command("search") @app.command("search")
def parse_schedule( def parse_schedule(
age_group: Annotated[AgeGroup | None, typer.Option(...)] = None, age_group: Annotated[AgeGroup, typer.Argument(...)],
event_type_filter: Annotated[
EventType | None,
typer.Option("--type", help="Only display events of this type"),
] = None,
schedule_file: Annotated[ schedule_file: Annotated[
Path, typer.Option(help="schedule json file to parse") Path, typer.Option(help="schedule json file to parse")
] = Path("schedule.json"), ] = Path("schedule.json"),
@@ -415,31 +253,11 @@ def parse_schedule(
""" """
Parse schedule.json to look for specific games or practices Parse schedule.json to look for specific games or practices
""" """
try:
with schedule_file.open("r") as f: with schedule_file.open("r") as f:
data = json.load(f) data = json.load(f)
except json.JSONDecodeError: for event in [x for x in data if x["agegroup"] == age_group]:
# If JSON is malformed, try to sanitize it first # print(json.dumps(event, indent=2))
with schedule_file.open("r") as f:
content = f.read()
sanitized_content = sanitize_json_response(content)
data = json.loads(sanitized_content)
# age_group filter
if age_group:
events = [x for x in data if normalize_age_group(x["agegroup"]) == age_group]
else:
events = [x for x in data if normalize_age_group(x["agegroup"]) is not None]
# event_type filter
if event_type_filter:
if event_type_filter.value == EventType.game:
events = [x for x in events if "event" in x and x["event"] == "Jeu"]
else:
events = [x for x in events if "event" not in x or x["event"] == "Jeu"]
for event in events:
if age_group:
raw_title = event["title"].removeprefix(age_group + "\n") raw_title = event["title"].removeprefix(age_group + "\n")
else:
raw_title = event["title"]
title = " ".join(raw_title.split("\n")) title = " ".join(raw_title.split("\n"))
start = datetime.datetime.fromisoformat(event["start"]) start = datetime.datetime.fromisoformat(event["start"])
start_fmt = start.strftime("%H:%M") start_fmt = start.strftime("%H:%M")
@@ -455,176 +273,5 @@ def parse_schedule(
print(f"{event_type}: {title}\n") print(f"{event_type}: {title}\n")
@app.command("ai")
def check_with_ai(
schedule_file: Annotated[
Path, typer.Option(help="schedule json file to parse")
] = Path("schedule.json"),
):
"""
Search through the schedule with natural language using Infomaniak LLM API
"""
if not utils.init():
sys.exit(1)
with schedule_file.open("r") as f:
schedule_data = json.load(f)
schedule_data = [
x for x in schedule_data if normalize_age_group(x["agegroup"]) is not None
]
for event in schedule_data:
event["team"] = event["agegroup"].replace("(", "").replace(")", "")
del event["agegroup"]
when = datetime.datetime.now().strftime("%d-%m-%Y et il est %H:%M")
system = "\n".join(
[
"Tu es une IA connaissant bien les données suivantes, qui décrivent les match et entraînements d'équipes de hockey sur glace.",
f"aujourd'hui, nous sommes le {when}"
"attention: ce qu'il y a entre parenthèse après la catégorie est une catégorie à part entière, example, u13 a = U13 (A) et ça correspond aux agegroup",
"assure-toi de ne pas confondre les catégories d'age dans tes réponses. ne donne pas une réponse pour la mauvaise équipe",
"ne confond pas top, elite, a, prép",
"```json",
json.dumps(schedule_data),
"```",
],
)
history: List[Tuple[str, str]] = []
while True:
try:
question = input("> ")
except EOFError:
break
answer = utils.llm_inference(
question, history, system=system, model="mixtral8x22b"
)
print("<", answer)
history.append((question, answer))
mobile_headers = {
"Accept": "application/json, text/plain, */*",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
"Sec-Fetch-Site": "cross-site",
"Accept-Language": "fr-FR,fr;q=0.9",
"Sec-Fetch-Mode": "cors",
"Origin": "null",
"Sec-Fetch-Dest": "empty",
"Content-Type": "application/x-www-form-urlencoded",
}
def mobile_login():
import base64
username, password, userid, token = get_login()
with requests.post(
"https://app.myice.hockey/api/mobilerest/login",
headers=mobile_headers,
data=f"login_email={base64.b64encode(username.encode()).decode()}&login_password={base64.b64encode(password.encode()).decode()}&language=FR&v=2",
# verify=False,
) as r:
r.raise_for_status()
return {
"id": r.json()["userid"],
"token": r.json()["userinfo"]["token"],
"id_club": r.json()["userinfo"]["id_club"],
}
userdata = {
"id": 0,
"id_club": 0,
"token": "",
}
def refresh_data():
with requests.post(
"https://app.myice.hockey/api/mobilerest/refreshdata",
headers=mobile_headers,
data=f"token={userdata['token']}&id_club=186&language=FR",
# verify=False,
) as r:
r.raise_for_status()
return r.json()
@app.command("mobile")
def mobile(
token: Annotated[str, typer.Option(envvar="MYICE_TOKEN")] = "",
id_club: Annotated[int, typer.Option(envvar="MYICE_CLUB")] = 0,
):
global userdata
if not token:
userdata = mobile_login()
else:
userdata = {
"id": 0,
"id_club": id_club,
"token": token,
}
print(json.dumps(refresh_data(), indent=2))
@app.command("mobile-game")
def mobile_game(
game_id: Annotated[int, typer.Argument(help="game id")],
token: Annotated[str, typer.Option(envvar="MYICE_TOKEN")] = "",
id_club: Annotated[int, typer.Option(envvar="MYICE_CLUB")] = 0,
raw: Annotated[bool, typer.Option(help="display raw output")] = False,
):
global userdata
username, password, userid, existing_token = get_login()
if token:
userdata = {
"id": 0,
"id_club": 186,
"token": token,
}
else:
if existing_token:
userdata = {
"id": userid,
"id_club": 186,
"token": existing_token,
}
else:
userdata = mobile_login()
# data = refresh_data()
with requests.post(
"https://app.myice.hockey/api/mobilerest/getevent",
headers=mobile_headers,
data="&".join(
[
f"token={userdata['token']}",
f"id_event={game_id}",
"type=games",
f"id_player={userdata['id']}",
f"id_club={userdata['id_club']}",
"language=FR",
]
),
# verify=False,
) as r:
data = r.json()["eventData"]
players = data["convocation"]["available"]
if raw:
print(data)
print(
f"Game {game_id}: {data['title']} ({data['type']}, {data['is_away']}) at {data['time_start']}"
)
print(f"{len(players)} players")
for player in sorted(
players,
key=lambda x: (
x["position"] if x["position"] else "none",
x["dob"],
),
):
print(
f"[{player['position']}] {player['fname']} {player['lname']} ({player['dob']})"
)
if __name__ == "__main__": if __name__ == "__main__":
app() app()
-124
View File
@@ -1,124 +0,0 @@
import logging
import requests
from typing import Annotated
from fastapi import FastAPI, Header, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from . import myice
origins = ["*"]
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
block_endpoints = ["/health"]
class LogFilter(logging.Filter):
def filter(self, record):
if record.args and len(record.args) >= 3:
if record.args[2] in block_endpoints:
return False
return True
uvicorn_logger = logging.getLogger("uvicorn.access")
uvicorn_logger.addFilter(LogFilter())
class AuthHeaders(BaseModel):
Authorization: str
def token(self) -> str:
return self.Authorization.split(" ")[1]
def authorized(self) -> bool:
if self.token() == "abc":
return True
return False
class HealthCheck(BaseModel):
"""Response model to validate and return when performing a health check."""
status: str = "OK"
@app.get("/")
async def home():
return FileResponse("index.html")
@app.get(
"/health",
response_model=HealthCheck,
status_code=status.HTTP_200_OK,
)
def get_health() -> HealthCheck:
return HealthCheck(status="OK")
@app.get("/favicon.ico")
async def favico():
return FileResponse("favicon.ico")
@app.get("/schedule")
async def schedule(
headers: Annotated[AuthHeaders, Header()],
):
if not headers.authorized():
raise HTTPException(401, detail="get out")
username, password, userid, existing_token = myice.get_login()
if existing_token:
myice.userdata = {
"id": userid,
"id_club": 186,
"token": existing_token,
}
else:
myice.userdata = myice.mobile_login()
return myice.refresh_data()["club_games"]
@app.get("/game/{game_id}")
async def game(
headers: Annotated[AuthHeaders, Header()],
game_id: int,
):
username, password, userid, existing_token = myice.get_login()
if existing_token:
myice.userdata = {
"id": userid,
"id_club": 186,
"token": existing_token,
}
else:
myice.userdata = myice.mobile_login()
# data = refresh_data()
with requests.post(
"https://app.myice.hockey/api/mobilerest/getevent",
headers=myice.mobile_headers,
data="&".join(
[
f"token={myice.userdata['token']}",
f"id_event={game_id}",
"type=games",
f"id_player={myice.userdata['id']}",
f"id_club={myice.userdata['id_club']}",
"language=FR",
]
),
# verify=False,
) as r:
data = r.json()["eventData"]
return data
Generated
+215 -1207
View File
File diff suppressed because it is too large Load Diff
+8 -27
View File
@@ -1,42 +1,23 @@
[project] [tool.poetry]
name = "myice" name = "myice"
version = "v0.3.2" version = "0.1.0"
description = "myice parsing" description = "myice parsing"
authors = [ authors = ["Rene Luria <rene@luria.ch>"]
{ name = "Rene Luria", "email" = "<rene@luria.ch>"},
]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"requests (>=2.32.3,<2.33.0)",
"typer (>=0.15.1,<0.16.0)",
"pypdf2 (>=3.0.1)",
"rl-ai-tools >=1.9.0",
"fastapi[standard] (>=0.115.11,<0.116.0)",
]
[tool.poetry.dependencies] [tool.poetry.dependencies]
rl-ai-tools = { source = "infomaniak" } python = "^3.12"
requests = "^2.32.3"
typer = {extras = ["all"], version = "^0.12.5"}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ipykernel = "^6.29.5" ipykernel = "^6.29.5"
types-requests = "^2.32.0.20241016"
mypy = "^1.15.0"
[[tool.poetry.source]]
name = "infomaniak"
url = "https://pypi.purple.infomaniak.ch"
priority = "supplemental"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[projectscripts] [tool.poetry.scripts]
myice = 'myice.myice:app' myice = 'myice.myice:app'
[tool.poetry.requires-plugins]
poetry-plugin-export = ">=1.8"