67 Commits

Author SHA1 Message Date
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
herel b016d58d84 chore: bump version to v0.5.7 2025-08-20 11:10:20 +02:00
herel 6232e91925 feat: layout filters horizontally 2025-08-20 11:10:06 +02:00
herel 7ce4fbd756 chore: bump version to v0.5.6 2025-08-20 10:17:08 +02:00
herel bb62acfc7f feat: add secondary agegroup selector for subgroup filtering
- Added subgroup dropdown selector to filter events by team within agegroup
- Implemented logic to populate subgroup options based on selected agegroup
- Updated event filtering to support both agegroup and subgroup criteria
- Added event listeners for real-time filtering when selectors change
2025-08-20 10:17:00 +02:00
herel 5f6ae79bf0 chore: bump version to v0.5.5 2025-08-19 19:22:21 +02:00
herel 697788c20f feat: auto-load events on login and improve player display in event details
- Automatically load events when user logs in
- Rename updateAccountOptions to updateAccountOptionsAndLoadEvents
- Add auto-fetch of events after account selection
- Enhance event details modal with:
  * Player count summary
  * Position breakdown
  * Players sorted by position first, then by number
  * Position displayed in player list
2025-08-19 19:21:50 +02:00
herel 5c5828cfc1 chore: add event title 2025-08-19 19:12:45 +02:00
herel 0a88217443 chore: bump version to v0.5.4 2025-08-19 19:06:47 +02:00
herel 2d783778a7 feat: update agegroup filter to show only agegroup and filter accordingly 2025-08-19 19:06:24 +02:00
herel a3d4114044 chore: bump version to v0.5.3 2025-08-19 18:50:12 +02:00
herel cec54a45d7 fix: improve UI layout and add loading indicator for events 2025-08-19 18:48:56 +02:00
herel 3efa7101e1 fix: finalize UI improvements and ensure all functionality works
Final adjustments to UI layout and positioning. Ensured all JavaScript functions are properly connected and working. Verified event loading functionality is fully restored.
2025-08-19 16:12:51 +02:00
herel 861ff0650f fix: restore event loading functionality and improve UI layout
Fixed incomplete fetchEvents function by adding missing data processing logic. Restored updateAgeGroupOptions, displayEvents, and fetchEventDetails functions. Moved connected user info to top-right corner. Hide title when logged in. Reduced vertical spacing for better layout. Made event filters more compact. All functionality restored and UI improved.
2025-08-19 16:09:57 +02:00
herel 73f72d1bbe feat: enhance login button styling and fix JavaScript syntax errors
Centered the login button on the page with improved styling including padding and shadow effects. Fixed JavaScript syntax errors in the event handling code. Removed unmatched closing div tags to ensure valid HTML structure.
2025-08-19 15:49:28 +02:00
herel a407a108ed fix: improve user authentication and account handling
Fixed issues with user display by fetching user info from userinfo endpoint

Improved error handling for JSON responses in schedule endpoint

Fixed account selection to use available accounts from config instead of default

Enhanced frontend to properly handle API responses and errors
2025-08-19 15:17:45 +02:00
herel d6277d7766 fix: improve authentication handling and error management 2025-08-19 15:04:53 +02:00
herel 5b1d741a16 feat: implement OpenID Connect authentication with Infomaniak 2025-08-19 11:16:07 +02:00
herel 5957868e0f chore: bump version to v0.5.2 2025-08-19 10:48:55 +02:00
herel 525d3bf326 fix: escape control characters in JSON output to make it jq compatible 2025-08-19 10:48:04 +02:00
herel 0e1eb0da3f chore: bump version to v0.5.1 2025-08-19 09:41:57 +02:00
herel 4b81cc7f9f fix: improve JSON response handling in API calls 2025-08-19 09:41:28 +02:00
herel c4d9236b16 feat: make config_section a global CLI option
Users can now specify --config-section once at the beginning of the command instead of repeating it for each subcommand. Also bumped version to v0.5.0 for this new feature.
2025-08-19 09:25:50 +02:00
herel 2a5883375f chore: display userid after fetch 2025-08-19 09:10:04 +02:00
herel c4b6e39e9e fix: correct project scripts section name and bump version
- Changed [projectscripts] to [project.scripts] in pyproject.toml to properly register the myice command line tool
- Bump version to v0.4.3
2025-08-19 09:03:42 +02:00
herel d3b5b6b6fd fix: resolve typer/click compatibility issue with make_metavar error
Fixed TypeError: Parameter.make_metavar() missing 1 required positional argument: 'ctx'
by correcting typer.Option(...) usage for optional parameters with default None values.

Bump version to v0.4.2
2025-08-19 08:57:37 +02:00
herel bcde9fccf5 chore: bump version to v0.4.1 and update dependencies 2025-08-19 08:52:39 +02:00
herel c2ab852f3f docs: add web interface and mobile functions info to README
Add documentation about the web interface (index.html and webapi.py) including how to run it and available endpoints. Also document the mobile API functions (mobile-login, mobile, mobile-game) that can be used for debugging or when the web interface is not available.
2025-08-19 08:37:49 +02:00
herel 6b5949b726 docs: translate README to English and add mobile-login info
Translate French content in README to English and add information about how to fetch club_id and token using the mobile-login command.
2025-08-19 08:34:02 +02:00
herel 66b93af6b1 feat: add account selection support in web UI
Add support for multiple MyIce accounts in the web interface:

- Create /accounts endpoint to fetch available accounts from config files

- Update index.html to dynamically show available accounts

- Modify /schedule and /game endpoints to accept account parameter

- Store selected account in localStorage for persistence
2025-08-18 22:10:24 +02:00
herel 6eb9598012 fix: disable SSL verification warnings by commenting out verify=False lines 2025-08-18 21:58:10 +02:00
herel e7b0fdec00 feat: add support for multiple config sections in INI file\n\n- Add --config-section CLI option to all commands to allow switching between different credential sets\n- Modify get_login() function to accept a config_section parameter\n- Update all functions that use credentials to pass through the config_section parameter\n- Update webapi.py to handle the additional return value from get_login()\n- Document the new feature in README.md with examples\n- Fix type issues and formatting to pass pre-commit checks 2025-08-18 18:28:53 +02:00
herel 4c53d6ce08 feat: add club selection support and improve mobile API integration 2025-08-18 11:26:56 +02:00
herel 3ce916f5c4 fix: update get_login return type to allow None values 2025-08-18 09:56:11 +02:00
herel ea696ed361 refactor: simplify AgeGroup enum and enhance normalization
Remove redundant ELIT variants from AgeGroup enum and implement case/spelling tolerant normalization function. The new normalize_age_group function handles case-insensitive matching and spelling variations (Elite/ELIT/Elit) while mapping to canonical enum values.

This reduces the enum from 15 to 13 entries while maintaining full backward compatibility.
2025-08-15 07:50:26 +02:00
herel 6b09a95a45 docs: add and format AGENTS.md file
Adding documentation file for agentic coding agents with proper markdown formatting to pass linting checks.
2025-08-14 12:56:32 +02:00
herel b11f875756 feat: update AgeGroup class to include all age groups from schedule.json
Added missing age group enums based on the schedule.json file to ensure proper parsing of all scheduled events.
2025-08-14 12:54:29 +02:00
herel 5b5b593a3b refactor: improve JSON handling and API reliability
- Add robust JSON sanitization function to handle malformed API responses
- Select club after login for proper session initialization
- Enhance API request headers for better compatibility
- Add JSON parsing fallback with sanitization for malformed files
- Add poetry plugin requirement for export functionality
2025-08-14 12:50:13 +02:00
herel 03d33f2e03 feat: add healthcheck 2025-03-13 11:29:30 +01:00
herel 2bd10409a1 chore: change title and aligment 2025-03-13 08:51:18 +01:00
herel f1955238c5 fix: sort players by number 2025-03-12 11:59:22 +01:00
herel 8af989f372 fix: add favicon to docker and load events immediately 2025-03-12 11:53:55 +01:00
herel 28b34d8281 fix: webapi schedule uses mobile api endpoint 2025-03-12 11:46:20 +01:00
herel 251e2b1452 fix: favicon and some stuff 2025-03-12 11:15:45 +01:00
herel 4c5867c19f feat: add webapi, lower python version requirement and add a Dockerfile 2025-03-12 10:52:11 +01:00
herel 75074991af chore: poetry 2.0.0 2025-01-13 09:13:53 +01:00
herel 0ac1659db2 feat: add mobile methods 2025-01-13 09:06:39 +01:00
herel b5c3164f96 feat: extract players from pdf 2025-01-08 14:06:35 +01:00
herel 91d8824e9c chore: v0.3.0 2024-11-27 14:33:50 +01:00
herel 6b37e0fcc2 feat: add new category and days to schedule argument
🛠️ myice.py -> Added new enum value 'u111' to AgeGroup, modified get_schedule function to accept num_days parameter, added num_days option to schedule function.
2024-11-27 14:32:51 +01:00
herel 5bf970ed26 Here is a brief summary of the changes:
🛠️ poetry.lock -> Updated rl-ai-tools version from 1.10.0 to 1.10.1
🛠️ pyproject.toml -> Updated myice version from v0.2.4 to v0.2.5
2024-11-04 14:46:10 +01:00
herel 37005c5ff0 doc: update 2024-11-04 08:41:01 +01:00
herel 2bf6b62ba4 Here is a brief summary of the changes:
🛠️ myice.py -> Modified get_schedule function and updated AI system prompt
🛠️ poetry.lock -> Updated rich package version from 13.9.3 to 13.9.4 and rl-ai-tools package version from 1.9.0 to 1.10.0
🛠️ pyproject.toml -> Updated project version from v0.2.0 to v0.2.4
2024-11-04 08:37:56 +01:00
herel 275f1d09ca 🛠️ pyproject.toml -> Updated version from v0.1.6 to v0.2.0 2024-11-01 15:24:14 +01:00
herel e8e6f09e0a 🛠️ myice/myice.py -> Added new AI functionality using Infomaniak LLM API
🛠️ poetry.lock -> Added rl-ai-tools package dependency
🛠️ pyproject.toml -> Updated dependencies and added rl-ai-tools source
2024-11-01 15:23:51 +01:00
herel f9bc440c1b chore: myice.py -> Added file parameter to save_cookies function and used Path object for file handling. 2024-11-01 14:02:10 +01:00
herel 324397fe27 🟢 pyproject.toml: Added Poetry project configuration file
🛠️ pyproject.toml -> Updated version from 'v0.1.5' to 'v0.1.6'
2024-11-01 13:18:38 +01:00
herel c1ad2ac9e0 🟢 README.md -> added installation instructions and introduction section
🛠️ .mdlrc -> set style path
🛠️ mdl.rb -> created a new ruby script
🔴 RECUPE\_SCHEDULE.sh -> removed shell script
2024-11-01 13:15:35 +01:00
herel 49f72ef22e 🛠️ myice.py -> updated error messages to redirect to stderr
🛠️ myice.py -> changed login and get\_userid prints to write to stderr
🔴 pyproject.toml -> removed version v0.1.3
🟢 pyproject.toml -> added version v0.1.5
2024-11-01 11:23:05 +01:00
herel bce52d2462 🛠️ myice/myice.py -> updated function get_login by simplifying config reading; renamed functions open, get_game_pdf, get_practice_pdf to os_open, get_game_pdf_os, get_practice_pdf_os respectively
🔴 pyproject.toml -> removed version 'v0.1.2', now at 'v0.1.3'
2024-11-01 11:17:26 +01:00
herel c93c9fadd9 🛠️ myice/myice.py -> added EventType enum and updated search command to support optional event type filtering
- Added `EventType` enum with two values: 'game' and 'practice'.
- Updated `parse_schedule` function to accept an optional `event_type_filter` argument which filters displayed events based on their type.
- Changed `age_group` parameter in `parse_schedule` to allow `None`.
2024-11-01 11:06:10 +01:00
herel 0346346a23 🛠️ pyproject.toml -> updated project dependencies and version number from 0.1.0 to v0.1.1
🛠️ poetry.lock -> content-hash changed from e11a99... to 32eb77...
2024-11-01 10:53:58 +01:00
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
8 changed files with 1364 additions and 184 deletions
+8 -8
View File
@@ -3,7 +3,7 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0 rev: v6.0.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: check-case-conflict - id: check-case-conflict
@@ -15,7 +15,7 @@ repos:
- id: detect-private-key - id: detect-private-key
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.6.8 rev: v0.12.9
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@@ -24,26 +24,26 @@ repos:
- id: ruff-format - id: ruff-format
args: [--diff, --target-version, py312] args: [--diff, --target-version, py312]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2 rev: v1.17.1
hooks: hooks:
- 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, PyPDF2]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1 rev: v1.37.1
hooks: hooks:
- id: yamllint - id: yamllint
args: [--strict] args: [--strict]
- repo: https://github.com/markdownlint/markdownlint - repo: https://github.com/markdownlint/markdownlint
rev: v0.12.0 rev: v0.13.0
hooks: hooks:
- id: markdownlint - id: markdownlint
exclude: "^.github|(^docs/_sidebar\\.md$)" exclude: "^.github|(^docs/_sidebar\\.md$)"
- repo: https://github.com/shellcheck-py/shellcheck-py - repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1 rev: v0.11.0.1
hooks: hooks:
- id: shellcheck - id: shellcheck
args: ["--severity=error"] args: ["--severity=error"]
@@ -51,12 +51,12 @@ repos:
files: "\\.sh$" files: "\\.sh$"
- repo: https://github.com/golangci/misspell - repo: https://github.com/golangci/misspell
rev: v0.6.0 rev: v0.7.0
hooks: hooks:
- 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: "2.1.4"
hooks: hooks:
- id: poetry-check - id: poetry-check
- id: poetry-lock - id: poetry-lock
+105
View File
@@ -0,0 +1,105 @@
# 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
```
### Running Tests
```bash
# No formal test framework configured
# Project uses manual testing with example PDF files in repository
# To test individual functions, run the CLI commands directly:
# myice schedule --days 7
# myice mobile-login
# myice search --help
```
### Running the Web API
```bash
# Or with poetry
poetry run fastapi run myice/webapi.py --host 127.0.0.1
```
## 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
- Custom rl_ai_tools package for AI functionalities
### Git Commit Messages
- Use conventional commits format
- Never mention Claude in commit messages
- Be descriptive but concise
- Use present tense ("add feature" not "added feature")
### Additional Rules
- Always use ddg-mcp to perform Web Search functionality
- Follow the existing code patterns in myice/myice.py and myice/webapi.py
- Maintain backward compatibility when modifying existing APIs
- Document new features in README.md
- Always run ruff format and ruff check after editing a python file
- use conventional commit messages
+131 -9
View File
@@ -1,9 +1,9 @@
# myice # myice
## intro ## introduction
Avec tout ça on va aller chercher sur MyIce les planning des gamins et générer With this tool, you can fetch kids' schedules from MyIce and generate
les pdf qu'on veut the PDFs you need.
## install ## install
@@ -19,17 +19,121 @@ with [pipx](https://pipx.pypa.io/stable/installation/):
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
``` ```
## récupérer le schedule ## configuration
Create a `myice.ini` file with your credentials:
```ini
[default]
username = your_email@example.com
password = your_password
userid = 12345
token = your_api_token
club_id = 172
```
You can also create multiple sections for different users:
```ini
[default]
username = user1@example.com
password = password1
userid = 12345
token = token1
club_id = 172
[second_kid]
username = charlie@example.com
password = password2
userid = 67890
token = token2
club_id = 186
```
Note: The `userid`, `token`, and `club_id` fields are optional. If you don't
have them, the tool can fetch them automatically using the mobile-login
command (see below).
To fetch the `token` and `club_id` using mobile-login:
```shell
myice mobile-login
```
This will output the necessary information that you can add to your `myice.ini` file.
## web interface
The tool includes a web interface that can be accessed by running the web API server:
```shell
uv run fastapi run myice/webapi.py
```
Then open your browser at `http://localhost:8000`. The web interface allows you to:
- Select between different configured accounts
- View upcoming games and practices
- See detailed information about events including player rosters
### Authentication
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.
2. **Static API Key**: For development purposes, you can still use `abc` as the token.
### Environment Variables
To configure OIDC authentication, set the following environment variables:
- `CLIENT_ID`: Your OIDC client ID (default: 8ea04fbb-4237-4b1d-a895-0b3575a3af3f)
- `CLIENT_SECRET`: Your OIDC client secret
- `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")
The web API provides the following endpoints:
- `/schedule` - Get the schedule for a specific account
- `/game/{game_id}` - Get details for a specific game
- `/accounts` - Get a list of available accounts
- `/health` - Health check endpoint
- `/login` - Initiate OIDC login flow
- `/callback` - Handle OIDC callback
- `/userinfo` - Get user information
All endpoints (except `/health`, `/login`, and `/callback`) require an Authorization header with a Bearer token.
## mobile functions
The tool includes several mobile API functions that interact with MyIce's
mobile API:
- `myice mobile-login` - Authenticate and get authentication tokens
- `myice mobile` - Fetch game data using the mobile API
- `myice mobile-game {game_id}` - Get detailed information about a specific game
These functions can be useful for debugging or when the regular web
interface is not available.
## fetch schedule
```shell ```shell
myice schedule -o schedule.json myice schedule -o schedule.json
``` ```
To use a specific configuration section:
```shell
myice schedule -o schedule.json --config-section isaac
```
## data ## data
### listing ### listing
Pour récupérer les event des U13 Elite par example: To fetch events for U13 Elite for example:
```shell ```shell
myice search "U13 (Elite)" myice search "U13 (Elite)"
@@ -55,11 +159,11 @@ practice: On-Ice Patinoire des Vernets - Patinoire Extérieure
game: Saison HC Ajoie game: Saison HC Ajoie
``` ```
Et on récupère la convoc du match: And we retrieve the match roster:
### match ### match
alors pour avoir la convocation du match contre Ajoie, l'id c'est 117015: To get the roster for the match against Ajoie, the ID is 117015:
```shell ```shell
myice game 117015 myice game 117015
@@ -67,9 +171,15 @@ Opening file game_117015.pdf
``` ```
### entraînement To use a specific configuration section:
et pour la convoc d'un entraînement: ```shell
myice game 117015 --config-section isaac
```
### practice
And for a practice roster:
```shell ```shell
myice practice 561855 myice practice 561855
@@ -77,6 +187,12 @@ Opening file practice_561855.pdf
``` ```
To use a specific configuration section:
```shell
myice practice 561855 --config-section isaac
```
### AI ### AI
```text ```text
@@ -86,3 +202,9 @@ Opening file practice_561855.pdf
> 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:
```shell
myice ai --config-section isaac
```
+495 -48
View File
@@ -4,28 +4,67 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Événements - Jeux</title> <title>MyIce - Games</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <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> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</head> </head>
<body> <body>
<div class="container mt-4"> <div id="connectedUser" class="position-fixed top-0 end-0 m-2 p-2 bg-light border rounded"
<h1 class="mb-4">Événements - Jeux</h1> style="display: none; z-index: 1000; font-size: 0.9rem;">
<span>Connecté: <span id="userName"></span></span>
<button id="disconnect" class="btn btn-danger btn-sm ms-2"
style="font-size: 0.8rem; padding: 0.1rem 0.3rem;">Déco</button>
</div>
<div id="apikeyContainer" class="mb-3"></div> <div class="container-fluid d-flex align-items-center justify-content-center">
<div class="text-center">
<h1 id="loginTitle" class="mb-4">MyIce - Games</h1>
<div id="loginContainer" class="mb-3">
<div id="oidcLoginSection">
<button id="oidcLoginBtn" class="btn btn-primary btn-md px-4 py-3 shadow">Se connecter</button>
</div>
</div>
</div>
</div>
<div class="container mt-2" id="mainContent" style="display: none;">
<div id="eventFilters" style="display: none;"> <div id="eventFilters" style="display: none;">
<div class="mb-3"> <div class="mb-3 row">
<label for="agegroup" class="form-label">Filtrer par groupe d'âge</label> <div class="col-md-3">
<label for="account" class="form-label">Compte</label>
<select id="account" class="form-select">
<option value="default">Défaut</option>
</select>
</div>
<div class="col-md-3">
<label for="agegroup" class="form-label">Âge</label>
<select id="agegroup" class="form-select"> <select id="agegroup" class="form-select">
<option value="">Tous</option> <option value="">Tous</option>
</select> </select>
</div> </div>
<button id="fetchEvents" class="btn btn-primary mb-3">Charger les événements</button> <div class="col-md-3">
<label for="subgroup" class="form-label">Sous-groupe</label>
<select id="subgroup" class="form-select">
<option value="">Tous</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button id="fetchEvents" class="btn btn-primary">Charger</button>
</div>
</div>
</div> </div>
<div id="eventList" class="row"></div> <div id="loadingIndicator" class="text-center my-3" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2">Chargement des événements...</p>
</div>
<div id="eventList" class="row mt-2"></div>
<!-- Modal pour afficher les détails d'un événement --> <!-- Modal pour afficher les détails d'un événement -->
<div class="modal fade" id="eventDetailsModal" tabindex="-1" aria-labelledby="eventDetailsLabel" <div class="modal fade" id="eventDetailsModal" tabindex="-1" aria-labelledby="eventDetailsLabel"
@@ -44,9 +83,13 @@
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const apikeyContainer = document.getElementById("apikeyContainer"); const loginContainer = document.getElementById("loginContainer");
const oidcLoginSection = document.getElementById("oidcLoginSection");
const connectedUser = document.getElementById("connectedUser");
const eventFilters = document.getElementById("eventFilters"); const eventFilters = document.getElementById("eventFilters");
const accountSelect = document.getElementById("account");
const agegroupSelect = document.getElementById("agegroup"); const agegroupSelect = document.getElementById("agegroup");
const subgroupSelect = document.getElementById("subgroup");
const eventList = document.getElementById("eventList"); const eventList = document.getElementById("eventList");
const fetchButton = document.getElementById("fetchEvents"); const fetchButton = document.getElementById("fetchEvents");
const eventDetailsContent = document.getElementById("eventDetailsContent"); const eventDetailsContent = document.getElementById("eventDetailsContent");
@@ -54,84 +97,396 @@
let storedApiKey = localStorage.getItem("apikey"); let storedApiKey = localStorage.getItem("apikey");
let lastFetchedEvents = []; let lastFetchedEvents = [];
let storedAccount = localStorage.getItem("account") || "default";
let userInfo = JSON.parse(localStorage.getItem("userInfo") || "null");
function renderApiKeyInput() { // If we have an API key but no userInfo, fetch it from the server
if (storedApiKey) { if (storedApiKey && !userInfo) {
apikeyContainer.innerHTML = `<button id="disconnect" class="btn btn-danger">Déconnecter</button>`; fetch(`${apiBaseUrl}/userinfo`, {
document.getElementById("disconnect").addEventListener("click", () => { headers: { "Authorization": `Bearer ${storedApiKey}` }
localStorage.removeItem("apikey"); })
location.reload(); .then(response => response.json())
.then(userData => {
userInfo = { email: userData.email || "Utilisateur connecté" };
localStorage.setItem("userInfo", JSON.stringify(userInfo));
renderLoginSection();
})
.catch(e => {
console.error("Error fetching user info:", e);
// If we can't fetch user info, proceed with rendering
renderLoginSection();
}); });
} else {
// Render the login section normally
renderLoginSection();
}
// Handle the static "abc" key case - removed for security
/*
if (storedApiKey === "abc" && !userInfo) {
userInfo = {email: "utilisateur@example.com"};
localStorage.setItem("userInfo", JSON.stringify(userInfo));
}
*/
// Handle OIDC callback
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const accessToken = hashParams.get('access_token');
// Display error message if present
if (error) {
alert("Erreur d'authentification: " + decodeURIComponent(error));
// Remove error from URL
window.history.replaceState({}, document.title, "/");
}
// Handle access token in URL fragment
if (accessToken) {
// Store the access token
localStorage.setItem("apikey", accessToken);
storedApiKey = accessToken;
// Get user info from the userinfo endpoint
fetch(`${apiBaseUrl}/userinfo`, {
headers: { "Authorization": `Bearer ${accessToken}` }
})
.then(response => response.json())
.then(userData => {
userInfo = { email: userData.email || "Utilisateur connecté" };
localStorage.setItem("userInfo", JSON.stringify(userInfo));
// Update UI
renderLoginSection();
})
.catch(e => {
console.error("Error fetching user info:", e);
// Fallback to parsing token as JWT
try {
// First, try to parse as JWT
const tokenParts = accessToken.split('.');
let userData = {};
if (tokenParts.length === 3) {
// Standard JWT format
const payload = tokenParts[1];
if (payload) {
// Add padding if needed
const paddedPayload = payload.padEnd(payload.length + (4 - payload.length % 4) % 4, '=');
const decodedPayload = atob(paddedPayload);
userData = JSON.parse(decodedPayload);
}
} else {
// Non-JWT token, treat as opaque token
console.log("Non-JWT token received, using default user info");
userData = { email: "utilisateur@" + window.location.hostname };
}
userInfo = { email: userData.email || "Utilisateur connecté" };
localStorage.setItem("userInfo", JSON.stringify(userInfo));
} catch (parseError) {
console.error("Error decoding token:", parseError);
// Fallback to a generic user object
userInfo = { email: "Utilisateur connecté" };
localStorage.setItem("userInfo", JSON.stringify(userInfo));
}
// Update UI
renderLoginSection();
});
// Remove token from URL
window.history.replaceState({}, document.title, "/");
}
function renderLoginSection() {
const mainContent = document.getElementById("mainContent");
const loginView = document.querySelector(".container-fluid");
const connectedUser = document.getElementById("connectedUser");
const loginTitle = document.getElementById("loginTitle");
if (storedApiKey || userInfo) {
// User is logged in
loginView.style.display = "none";
mainContent.style.display = "block";
loginTitle.style.display = "none";
connectedUser.style.display = "block";
oidcLoginSection.style.display = "none";
const userNameElement = document.getElementById("userName");
if (userInfo && userInfo.email) {
userNameElement.textContent = userInfo.email;
} else {
userNameElement.textContent = "Utilisateur";
}
eventFilters.style.display = "block"; eventFilters.style.display = "block";
fetchEvents(storedApiKey); updateAccountOptionsAndLoadEvents();
} else { } else {
apikeyContainer.innerHTML = ` // User is not logged in
<label for="apikey" class="form-label">API Key</label> loginView.style.display = "flex";
<input type="text" id="apikey" class="form-control" placeholder="Entrez votre API Key"> mainContent.style.display = "none";
<button id="validateApiKey" class="btn btn-success mt-2">Valider</button> loginTitle.style.display = "block";
`;
connectedUser.style.display = "none";
oidcLoginSection.style.display = "block";
eventFilters.style.display = "none"; eventFilters.style.display = "none";
document.getElementById("validateApiKey").addEventListener("click", saveApiKey);
document.getElementById("apikey").addEventListener("keypress", function (event) { // Remove any existing event listeners to prevent duplicates
if (event.key === "Enter") { const oidcLoginBtn = document.getElementById("oidcLoginBtn");
saveApiKey(); const newOidcLoginBtn = oidcLoginBtn.cloneNode(true);
oidcLoginBtn.parentNode.replaceChild(newOidcLoginBtn, oidcLoginBtn);
newOidcLoginBtn.addEventListener("click", initiateOIDCLogin);
} }
// Add disconnect handler
const disconnectBtn = document.getElementById("disconnect");
if (disconnectBtn) {
// Remove any existing event listeners to prevent duplicates
const newDisconnectBtn = disconnectBtn.cloneNode(true);
disconnectBtn.parentNode.replaceChild(newDisconnectBtn, disconnectBtn);
newDisconnectBtn.addEventListener("click", logout);
}
}
function initiateOIDCLogin() {
// Redirect to backend login endpoint
window.location.href = `${apiBaseUrl}/login`;
}
function logout() {
localStorage.removeItem("apikey");
localStorage.removeItem("account");
localStorage.removeItem("userInfo");
storedApiKey = null;
userInfo = null;
location.reload();
}
function updateAccountOptionsAndLoadEvents() {
// Fetch available accounts from the server
fetch(`${apiBaseUrl}/accounts`, {
headers: { "Authorization": `Bearer ${storedApiKey}` }
})
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
console.error("Accounts error response:", errorData);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.detail || 'Unknown error'}`);
}); });
} }
return response.json();
})
.then(accounts => {
// Check if accounts is actually an array
if (!Array.isArray(accounts)) {
console.error("Accounts data is not an array:", accounts);
throw new Error("Invalid accounts data format");
} }
function saveApiKey() { accountSelect.innerHTML = '';
const key = document.getElementById("apikey").value;
if (key) { // If no accounts are available, add a default option
localStorage.setItem("apikey", key); if (accounts.length === 0) {
location.reload(); const option = document.createElement("option");
} else { option.value = "default";
alert("Veuillez entrer une clé API valide."); option.textContent = "Default";
} accountSelect.appendChild(option);
return;
} }
renderApiKeyInput(); // Add all available accounts
accounts.forEach(account => {
const option = document.createElement("option");
option.value = account.name;
option.textContent = account.label;
accountSelect.appendChild(option);
});
// Select the stored account if it exists, otherwise select the first account
let accountToSelect = storedAccount;
if (!accounts.some(account => account.name === storedAccount)) {
accountToSelect = accounts[0].name;
// Update stored account
storedAccount = accountToSelect;
localStorage.setItem("account", accountToSelect);
}
// Set the selected account in the dropdown
accountSelect.value = accountToSelect;
// Automatically fetch events for the selected account
fetchEvents(storedApiKey, accountToSelect);
})
.catch(error => {
console.error("Erreur lors du chargement des comptes:", error);
alert("Accès refusé. Vous n'êtes pas autorisé à accéder à cette application. Veuillez contacter l'administrateur.");
logout();
});
}
renderLoginSection();
accountSelect.addEventListener("change", () => {
const selectedAccount = accountSelect.value;
localStorage.setItem("account", selectedAccount);
if (storedApiKey) {
fetchEvents(storedApiKey, selectedAccount);
}
});
fetchButton.addEventListener("click", () => { fetchButton.addEventListener("click", () => {
if (!storedApiKey) { if (!storedApiKey) {
alert("Veuillez entrer une clé API"); alert("Veuillez vous connecter");
return; return;
} }
fetchEvents(storedApiKey); const selectedAccount = accountSelect.value;
fetchEvents(storedApiKey, selectedAccount);
}); });
agegroupSelect.addEventListener("change", () => { agegroupSelect.addEventListener("change", () => {
// Update subgroup options based on selected agegroup
updateSubgroupOptions(agegroupSelect.value, lastFetchedEvents);
displayEvents(lastFetchedEvents); displayEvents(lastFetchedEvents);
}); });
function fetchEvents(apiKey) { subgroupSelect.addEventListener("change", () => {
fetch(`${apiBaseUrl}/schedule`, { displayEvents(lastFetchedEvents);
});
function fetchEvents(apiKey, account) {
// Show loading indicator
const loadingIndicator = document.getElementById("loadingIndicator");
const eventList = document.getElementById("eventList");
loadingIndicator.style.display = "block";
eventList.innerHTML = ""; // Clear previous events
fetch(`${apiBaseUrl}/schedule?account=${account}`, {
headers: { "Authorization": `Bearer ${apiKey}` } headers: { "Authorization": `Bearer ${apiKey}` }
}) })
.then(response => response.json()) .then(response => {
if (response.status === 401) {
// Token expired or invalid
alert("Votre session a expiré. Veuillez vous reconnecter.");
logout();
return;
}
if (!response.ok) {
// Try to parse error response as JSON, but handle plain text as well
return response.text().then(errorText => {
let errorMessage = 'Unknown error';
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.detail || errorData.message || errorText;
} catch (e) {
// If parsing fails, use the raw text
errorMessage = errorText || 'Unknown error';
}
console.error("Schedule error response:", errorText);
throw new Error(`HTTP error! status: ${response.status}, message: ${errorMessage}`);
});
}
return response.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
console.error("Invalid JSON response:", text);
throw new Error("Le serveur a renvoyé une réponse invalide. Veuillez réessayer.");
}
});
})
.then(data => { .then(data => {
// Hide loading indicator
loadingIndicator.style.display = "none";
if (data) {
// Check if data is an array
if (!Array.isArray(data)) {
console.error("Schedule data is not an array:", data);
throw new Error("Invalid schedule data format");
}
lastFetchedEvents = data.filter(event => event.event === "Jeu"); lastFetchedEvents = data.filter(event => event.event === "Jeu");
updateAgeGroupOptions(lastFetchedEvents); updateAgeGroupOptions(lastFetchedEvents);
displayEvents(lastFetchedEvents); displayEvents(lastFetchedEvents);
}
}) })
.catch(error => console.error("Erreur lors du chargement des événements:", error)); .catch(error => {
// Hide loading indicator on error
loadingIndicator.style.display = "none";
console.error("Erreur lors du chargement des événements:", error);
alert("Erreur lors du chargement des événements: " + error.message);
});
} }
function updateAgeGroupOptions(events) { function updateAgeGroupOptions(events) {
let agegroups = new Set(events.map(event => `${event.agegroup} ${event.name}`.trim())); let agegroups = new Set(events.map(event => event.agegroup));
agegroupSelect.innerHTML = '<option value="">Tous</option>'; agegroupSelect.innerHTML = '<option value="">Tous</option>';
agegroups.forEach(group => { Array.from(agegroups).sort().forEach(group => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = group; option.value = group;
option.textContent = group; option.textContent = group;
agegroupSelect.appendChild(option); agegroupSelect.appendChild(option);
}); });
// Reset subgroup selector
subgroupSelect.innerHTML = '<option value="">Tous</option>';
}
function updateSubgroupOptions(selectedAgegroup, events) {
// Reset subgroup options
subgroupSelect.innerHTML = '<option value="">Tous</option>';
if (selectedAgegroup === "") {
// If no agegroup is selected, disable subgroup selector
subgroupSelect.disabled = true;
return;
}
// Enable subgroup selector
subgroupSelect.disabled = false;
// Extract subgroups from events matching the selected agegroup
let subgroups = new Set();
events
.filter(event => event.agegroup === selectedAgegroup)
.forEach(event => {
// Extract subgroup from event.name or event.title
// This assumes the subgroup is part of the name field
if (event.name && event.name !== selectedAgegroup) {
subgroups.add(event.name);
}
});
// Add subgroups to the selector
Array.from(subgroups).sort().forEach(subgroup => {
const option = document.createElement("option");
option.value = subgroup;
option.textContent = subgroup;
subgroupSelect.appendChild(option);
});
} }
function displayEvents(events) { function displayEvents(events) {
eventList.innerHTML = ""; eventList.innerHTML = "";
let selectedAgegroup = agegroupSelect.value; let selectedAgegroup = agegroupSelect.value;
let filteredEvents = events.filter(event => event.event === "Jeu" && (selectedAgegroup === "" || `${event.agegroup} ${event.name}` === selectedAgegroup)); let selectedSubgroup = subgroupSelect.value;
let filteredEvents = events.filter(event => {
// Filter by event type
if (event.event !== "Jeu") return false;
// Filter by agegroup
if (selectedAgegroup !== "" && event.agegroup !== selectedAgegroup) return false;
// Filter by subgroup
if (selectedSubgroup !== "" && event.name !== selectedSubgroup) return false;
return true;
});
if (filteredEvents.length === 0) { if (filteredEvents.length === 0) {
eventList.innerHTML = "<p class='text-muted'>Aucun événement 'Jeu' trouvé.</p>"; eventList.innerHTML = "<p class='text-muted'>Aucun événement 'Jeu' trouvé.</p>";
@@ -144,7 +499,9 @@
eventCard.innerHTML = ` eventCard.innerHTML = `
<div class="card" style="border-left: 5px solid ${event.color}" data-id="${event.id_event}"> <div class="card" style="border-left: 5px solid ${event.color}" data-id="${event.id_event}">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">${event.title}</h5> <h5 class="card-title">${event.agegroup} - ${event.name}</h5>
<p class="card-text">${event.title}</p>
<p class="card-text"><strong>Adversaire:</strong> ${event.opponent}</p>
<p class="card-text"><strong>Lieu:</strong> ${event.place}</p> <p class="card-text"><strong>Lieu:</strong> ${event.place}</p>
<p class="card-text"><strong>Heure:</strong> ${event.start} - ${event.end}</p> <p class="card-text"><strong>Heure:</strong> ${event.start} - ${event.end}</p>
</div> </div>
@@ -156,23 +513,113 @@
} }
function fetchEventDetails(eventId) { function fetchEventDetails(eventId) {
fetch(`${apiBaseUrl}/game/${eventId}`, { const selectedAccount = accountSelect.value;
fetch(`${apiBaseUrl}/game/${eventId}?account=${selectedAccount}`, {
headers: { "Authorization": `Bearer ${storedApiKey}` } headers: { "Authorization": `Bearer ${storedApiKey}` }
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Check if available players data exists
const availablePlayers = data.convocation.available || [];
const sortedPlayers = availablePlayers
.sort((a, b) => (a.number || 0) - (b.number || 0));
// Calculate player statistics
const totalPlayers = sortedPlayers.length;
const positionCount = {};
sortedPlayers.forEach(player => {
const position = player.position || "N/A";
positionCount[position] = (positionCount[position] || 0) + 1;
});
// Generate position breakdown
const positionBreakdown = Object.entries(positionCount)
.map(([position, count]) => `${position}: ${count}`)
.join(', ');
// Sort players by position first, then by number
const playersByPosition = [...sortedPlayers].sort((a, b) => {
// Sort by position first
const positionA = a.position || "ZZZ"; // Put undefined positions at the end
const positionB = b.position || "ZZZ";
if (positionA !== positionB) {
return positionA.localeCompare(positionB);
}
// If positions are the same, sort by number
const numA = parseInt(a.number) || 0;
const numB = parseInt(b.number) || 0;
return numA - numB;
});
// Process staff data
const staffList = data.convocation.staff || [];
const totalStaff = staffList.length;
// Check if there are no players
if (totalPlayers === 0 && totalStaff === 0) {
eventDetailsContent.innerHTML = `
<div class="card border-warning">
<div class="card-body text-center">
<h5 class="card-title">${data.title}</h5>
<p class="card-text"><strong>Type:</strong> ${data.type}</p>
<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 = ` eventDetailsContent.innerHTML = `
<h5>${data.title}</h5> <h5>${data.title}</h5>
<p><strong>Type:</strong> ${data.type}</p> <p><strong>Type:</strong> ${data.type}</p>
<p><strong>Lieu:</strong> ${data.place}</p> <p><strong>Lieu:</strong> ${data.place}</p>
<p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p> <p><strong>Heure:</strong> ${data.time_start} - ${data.time_end}</p>
<h6>Joueurs convoqués:</h6> <div class="alert alert-warning" role="alert">
<ul>${data.convocation.available.map(player => { <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 number = player.number ? player.number : "N/A";
let position = player.position ? player.position : "N/A"; let position = player.position ? player.position : "N/A";
return `<li>${number} - ${player.fname} ${player.lname} (${position}, ${player.dob})</li>`; return `<li>[${position}] ${number} - ${player.fname} ${player.lname} (${player.dob})</li>`;
}).join('')}</ul> }).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));
+273 -65
View File
@@ -22,19 +22,164 @@ 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
# This is the key fix - we need to escape control characters rather than remove them
def escape_control_chars(match):
char = match.group(0)
# Handle specific control characters that might cause issues
if char == "\n":
return "\\n"
elif char == "\r":
return "\\r"
elif char == "\t":
return "\\t"
else:
# For other control characters, use Unicode escape
return f"\\u{ord(char):04x}"
# More aggressive approach: escape ALL control characters
# This handles the case where line breaks occur within JSON string values
# We need to escape them before they break the JSON structure
# First, let's try to detect if there are line breaks within quoted strings
# and escape them properly
# Simple approach: escape all control characters
array_content = re.sub(r"[\x00-\x1f]", escape_control_chars, 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
global_config_section: str = "default"
# Add global option for config section
@app.callback()
def main(
config_section: Annotated[
str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
):
"""My Ice Hockey schedule tool"""
# Store the config_section in a global variable so it can be accessed by commands
global global_config_section
global_config_section = config_section
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)"
u111 = "U11 (1)" 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): class EventType(str, Enum):
@@ -57,7 +202,10 @@ def save_cookies(file: str = "cookies.txt"):
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",
config_section: str = "default",
) -> tuple[str, str, int | None, str | None, int | None]:
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read( config.read(
[ [
@@ -66,28 +214,52 @@ def get_login(local_file: str = "myice.ini") -> tuple[str, str, int, str]:
local_file, local_file,
] ]
) )
if "default" in config.sections(): if config_section in config.sections():
default_config = config["default"] selected_config = config[config_section]
elif "default" in config.sections():
username = default_config.get("username") # Fallback to default section if specified section doesn't exist
password = default_config.get("password") selected_config = config["default"]
userid = default_config.getint("userid")
token = default_config.get("token")
if not username or not password:
print( print(
"Error: please configure username/password in ini file", file=sys.stderr f"Warning: Section '{config_section}' not found, using 'default' section",
file=sys.stderr,
) )
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", file=sys.stderr)
sys.exit(1) sys.exit(1)
return username, password, userid, token
username = selected_config.get("username")
password = selected_config.get("password")
userid = selected_config.getint("userid", fallback=None)
token = selected_config.get("token", fallback=None)
club_id = selected_config.getint("club_id", fallback=None)
if not username or not password:
print("Error: please configure username/password in ini file", file=sys.stderr)
sys.exit(1)
return username, password, userid, token, club_id
def do_login(): def select_club(club_id: int = 172):
"""Select a club by ID after login."""
global session
r = session.get(
f"https://app.myice.hockey/?cl={club_id}", headers={"User-Agent": user_agent}
)
r.raise_for_status()
def do_login(config_section: str | None = None):
global session global session
global userid global userid
username, password, userid, token = get_login() global global_config_section
# Use provided config_section, or fall back to global one
section = config_section if config_section is not None else global_config_section
username, password, userid_tmp, token, club_id = get_login(config_section=section)
if userid_tmp is not None:
userid = userid_tmp
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 = {
@@ -106,6 +278,9 @@ def do_login():
}, },
) )
r.raise_for_status() r.raise_for_status()
# select the club we want
if club_id:
select_club(club_id)
def get_userid(): def get_userid():
@@ -128,19 +303,23 @@ def get_userid():
def wrapper_session(func): def wrapper_session(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
global session, userid global session, userid, global_config_section
# Use the global config_section
config_section = global_config_section
session = requests.Session() session = requests.Session()
# session.verify = False # 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...", file=sys.stderr)
do_login() do_login(config_section=None) # Use global config_section
save_cookies() save_cookies()
_, _, userid, _ = get_login() _, _, userid, _, _ = get_login(config_section=config_section)
if not userid: if not userid:
print("get userid...", file=sys.stderr) print("get userid...", file=sys.stderr)
userid = get_userid() userid = get_userid()
print(f"{userid=}", file=sys.stderr)
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
@@ -166,10 +345,15 @@ 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()
return r.text # Debug: Save raw response to file for analysis
# with open("raw_response.txt", "w") as f:
# f.write(r.text)
return json.loads(sanitize_json_response(r.text))
@wrapper_session @wrapper_session
@@ -219,12 +403,15 @@ def schedule(
""" """
Fetch schedule as json Fetch schedule as json
""" """
global global_config_section
schedule = get_schedule(num_days) schedule = get_schedule(num_days)
if outfile: if outfile:
with outfile.open("w") as f: with outfile.open("w") as f:
f.write(schedule) f.write(json.dumps(schedule))
else: else:
print(schedule) import builtins
builtins.print(json.dumps(schedule, indent=2))
def os_open(file: str) -> None: def os_open(file: str) -> None:
@@ -260,6 +447,7 @@ def get_game_pdf(
""" """
Genate the pdf for the game invitation Genate the pdf for the game invitation
""" """
global global_config_section
if open_file: if open_file:
output_filename = f"game_{game_id}.pdf" output_filename = f"game_{game_id}.pdf"
else: else:
@@ -280,6 +468,7 @@ def get_practice_pdf(
""" """
Genate the pdf for the practice invitation Genate the pdf for the practice invitation
""" """
global global_config_section
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) os_open(output_filename)
@@ -287,7 +476,7 @@ def get_practice_pdf(
@app.command("search") @app.command("search")
def parse_schedule( def parse_schedule(
age_group: Annotated[AgeGroup | None, typer.Option(...)] = None, age_group: Annotated[AgeGroup | None, typer.Option()] = None,
event_type_filter: Annotated[ event_type_filter: Annotated[
EventType | None, EventType | None,
typer.Option("--type", help="Only display events of this type"), typer.Option("--type", help="Only display events of this type"),
@@ -299,13 +488,21 @@ def parse_schedule(
""" """
Parse schedule.json to look for specific games or practices Parse schedule.json to look for specific games or practices
""" """
global global_config_section
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:
# If JSON is malformed, try to sanitize it first
with schedule_file.open("r") as f:
content = f.read()
sanitized_content = sanitize_json_response(content)
data = json.loads(sanitized_content)
# age_group filter # age_group filter
if age_group: if age_group:
events = [x for x in data if x["agegroup"] == age_group] events = [x for x in data if normalize_age_group(x["agegroup"]) == age_group]
else: else:
events = [x for x in data if x["agegroup"] in AgeGroup] events = [x for x in data if normalize_age_group(x["agegroup"]) is not None]
# event_type filter # event_type filter
if event_type_filter: if event_type_filter:
if event_type_filter.value == EventType.game: if event_type_filter.value == EventType.game:
@@ -341,11 +538,14 @@ def check_with_ai(
""" """
Search through the schedule with natural language using Infomaniak LLM API Search through the schedule with natural language using Infomaniak LLM API
""" """
global global_config_section
if not utils.init(): if not utils.init():
sys.exit(1) sys.exit(1)
with schedule_file.open("r") as f: with schedule_file.open("r") as f:
schedule_data = json.load(f) schedule_data = json.load(f)
schedule_data = [x for x in schedule_data if x["agegroup"] in AgeGroup] schedule_data = [
x for x in schedule_data if normalize_age_group(x["agegroup"]) is not None
]
for event in schedule_data: for event in schedule_data:
event["team"] = event["agegroup"].replace("(", "").replace(")", "") event["team"] = event["agegroup"].replace("(", "").replace(")", "")
del event["agegroup"] del event["agegroup"]
@@ -387,10 +587,18 @@ mobile_headers = {
} }
def mobile_login(): def mobile_login(config_section: str | None = None):
global global_config_section
import base64 import base64
username, password, userid, token = get_login() # Use provided config_section, or fall back to global one
section = config_section if config_section is not None else global_config_section
username, password, _, token, club_id = get_login(config_section=section)
if token and club_id:
return {"id": 0, "token": token, "id_club": club_id}
print("Requesting token", file=sys.stderr)
with requests.post( with requests.post(
"https://app.myice.hockey/api/mobilerest/login", "https://app.myice.hockey/api/mobilerest/login",
headers=mobile_headers, headers=mobile_headers,
@@ -399,9 +607,10 @@ def mobile_login():
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
return { return {
"id": r.json()["userid"], "id": 0,
"token": r.json()["userinfo"]["token"], "token": r.json()["userinfo"]["token"],
"id_club": r.json()["userinfo"]["id_club"], "id_club": club_id
or r.json()["userinfo"]["id_club"], # Use configured club_id if available
} }
@@ -416,56 +625,54 @@ def refresh_data():
with requests.post( with requests.post(
"https://app.myice.hockey/api/mobilerest/refreshdata", "https://app.myice.hockey/api/mobilerest/refreshdata",
headers=mobile_headers, headers=mobile_headers,
data=f"token={userdata['token']}&id_club=186&language=FR", data=f"token={userdata['token']}&id_club={userdata['id_club']}&language=FR",
# verify=False, # verify=False,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
# Since the API returns valid JSON, we don't need to sanitize it
# Just parse it directly
try:
return r.json() return r.json()
except json.JSONDecodeError:
# If direct parsing fails, try with sanitization
sanitized = sanitize_json_response(r.text)
try:
return json.loads(sanitized)
except json.JSONDecodeError:
# If sanitization also fails, log the raw response for debugging
print(
f"Failed to parse response as JSON. Raw response: {r.text[:500]}..."
)
# Return an empty dict to avoid breaking the API
return {}
@app.command("mobile-login")
def do_mobile_login():
global userdata, global_config_section
userdata = mobile_login(config_section=global_config_section)
print(json.dumps(userdata, indent=2))
@app.command("mobile") @app.command("mobile")
def mobile( def mobile():
token: Annotated[str, typer.Option(envvar="MYICE_TOKEN")] = "", global userdata, global_config_section
id_club: Annotated[int, typer.Option(envvar="MYICE_CLUB")] = 0, userdata = mobile_login(config_section=global_config_section)
): games = [x for x in refresh_data().get("club_games")]
global userdata # Use built-in print to avoid rich formatting issues
if not token: import builtins
userdata = mobile_login()
else: builtins.print(json.dumps(games, indent=2, ensure_ascii=False))
userdata = {
"id": 0,
"id_club": id_club,
"token": token,
}
print(json.dumps(refresh_data(), indent=2))
@app.command("mobile-game") @app.command("mobile-game")
def mobile_game( def mobile_game(
game_id: Annotated[int, typer.Argument(help="game id")], 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, raw: Annotated[bool, typer.Option(help="display raw output")] = False,
): ):
global userdata global userdata, global_config_section
username, password, userid, existing_token = get_login() userdata = mobile_login(config_section=global_config_section)
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( with requests.post(
"https://app.myice.hockey/api/mobilerest/getevent", "https://app.myice.hockey/api/mobilerest/getevent",
headers=mobile_headers, headers=mobile_headers,
@@ -481,7 +688,8 @@ def mobile_game(
), ),
# verify=False, # verify=False,
) as r: ) as r:
data = r.json()["eventData"] data = json.loads(sanitize_json_response(r.text))["eventData"]
players = data["convocation"]["available"] players = data["convocation"]["available"]
if raw: if raw:
print(data) print(data)
+312 -17
View File
@@ -1,11 +1,31 @@
import logging
import requests import requests
import os
from typing import Annotated from typing import Annotated
from fastapi import FastAPI, Header, HTTPException from fastapi import FastAPI, Header, HTTPException, status, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse from fastapi.responses import FileResponse, RedirectResponse
from pydantic import BaseModel from pydantic import BaseModel
from . import myice from . import myice
# OIDC Configuration
CLIENT_ID = os.environ.get("CLIENT_ID", "8ea04fbb-4237-4b1d-a895-0b3575a3af3f")
CLIENT_SECRET = os.environ.get(
"CLIENT_SECRET", "5iXycu7aU6o9e17NNTOUeetQkRkBqQlomoD2hTyLGLTAcwj0dwkkLH3mz1IrZSmJ"
)
REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:8000/callback")
AUTHORIZATION_ENDPOINT = "https://login.infomaniak.com/authorize"
TOKEN_ENDPOINT = "https://login.infomaniak.com/token"
USERINFO_ENDPOINT = "https://login.infomaniak.com/oauth2/userinfo"
JWKS_URI = "https://login.infomaniak.com/oauth2/jwks"
# Allowed users (based on email claim)
ALLOWED_USERS = (
os.environ.get("ALLOWED_USERS", "").split(",")
if os.environ.get("ALLOWED_USERS")
else []
)
origins = ["*"] origins = ["*"]
app = FastAPI() app = FastAPI()
@@ -18,61 +38,336 @@ app.add_middleware(
allow_headers=["*"], 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): class AuthHeaders(BaseModel):
Authorization: str Authorization: str
def token(self) -> str: def token(self) -> str:
return self.Authorization.split(" ")[1] parts = self.Authorization.split(" ")
if len(parts) == 2:
return parts[1]
return ""
def authorized(self) -> bool: def authorized(self) -> bool:
if self.token() == "abc": token = self.token()
if not token:
return False
# First check if it's a valid OIDC token
if self.validate_oidc_token():
return True
# Fallback to the old static key for compatibility
if token == "abc":
return True return True
return False return False
def validate_oidc_token(self) -> bool:
try:
token = self.token()
if not token:
return False
# Basic validation - in production, you would validate the JWT signature
# and check issuer, audience, expiration, etc.
import base64
import json
# Decode JWT header and payload (without verification for simplicity in this example)
parts = token.split(".")
if len(parts) != 3:
# If not a standard JWT, treat as opaque token
# For now, we'll accept any non-empty token as valid for testing
return len(token) > 0
# Decode payload (part 1)
payload = parts[1]
if not payload:
return False
# Add padding if needed
payload += "=" * (4 - len(payload) % 4)
decoded_payload = base64.urlsafe_b64decode(payload)
payload_data = json.loads(decoded_payload)
# Check if user is in allowed list
user_email = payload_data.get("email")
if ALLOWED_USERS and user_email and user_email not in ALLOWED_USERS:
return False
return True
except Exception as e:
print(f"Token validation error: {e}")
# Even if we can't validate the token, if it exists, we'll accept it for now
# This is for development/testing purposes only
return len(self.token()) > 0
class HealthCheck(BaseModel):
"""Response model to validate and return when performing a health check."""
status: str = "OK"
@app.get("/") @app.get("/")
async def home(): async def home():
return FileResponse("index.html") 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") @app.get("/favicon.ico")
async def favico(): async def favico():
return FileResponse("favicon.ico") return FileResponse("favicon.ico")
@app.get("/login")
async def login(request: Request):
"""Redirect to Infomaniak OIDC login"""
import urllib.parse
state = "random_state_string" # In production, use a proper random state
nonce = "random_nonce_string" # In production, use a proper random nonce
# Store state and nonce in session (simplified for this example)
# In production, use proper session management
auth_url = (
f"{AUTHORIZATION_ENDPOINT}"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={urllib.parse.quote(REDIRECT_URI)}"
f"&response_type=code"
f"&scope=openid email profile"
f"&state={state}"
f"&nonce={nonce}"
)
return RedirectResponse(auth_url)
@app.get("/callback")
async def callback(code: str, state: str):
"""Handle OIDC callback and exchange code for token"""
# Exchange authorization code for access token
token_data = {
"grant_type": "authorization_code",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
}
response = requests.post(TOKEN_ENDPOINT, data=token_data)
token_response = response.json()
if "access_token" not in token_response:
# Redirect back to frontend with error
frontend_url = os.environ.get("FRONTEND_URL", "/")
error_detail = token_response.get(
"error_description", "Failed to obtain access token"
)
return RedirectResponse(f"{frontend_url}?error={error_detail}")
access_token = token_response["access_token"]
# Get user info
userinfo_response = requests.get(
USERINFO_ENDPOINT, headers={"Authorization": f"Bearer {access_token}"}
)
userinfo = userinfo_response.json()
# Check if user is in allowed list
user_email = userinfo.get("email")
if ALLOWED_USERS and user_email not in ALLOWED_USERS:
# Redirect back to frontend with error
frontend_url = os.environ.get("FRONTEND_URL", "/")
return RedirectResponse(f"{frontend_url}?error=User not authorized")
# Redirect back to frontend with access token and user info
frontend_url = os.environ.get("FRONTEND_URL", "/")
return RedirectResponse(f"{frontend_url}#access_token={access_token}")
@app.get("/exchange-token")
async def exchange_token(code: str):
"""Exchange authorization code for access token"""
# Exchange authorization code for access token
token_data = {
"grant_type": "authorization_code",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
}
response = requests.post(TOKEN_ENDPOINT, data=token_data)
token_response = response.json()
if "access_token" not in token_response:
# Return error as JSON
error_detail = token_response.get(
"error_description", "Failed to obtain access token"
)
raise HTTPException(
status_code=400, detail=f"Token exchange failed: {error_detail}"
)
access_token = token_response["access_token"]
# Get user info
userinfo_response = requests.get(
USERINFO_ENDPOINT, headers={"Authorization": f"Bearer {access_token}"}
)
userinfo = userinfo_response.json()
# Check if user is in allowed list
user_email = userinfo.get("email")
if ALLOWED_USERS and user_email not in ALLOWED_USERS:
raise HTTPException(status_code=403, detail="User not authorized")
# Return token and user info as JSON
return {"access_token": access_token, "user": userinfo}
@app.get("/userinfo")
async def userinfo(headers: Annotated[AuthHeaders, Header()]):
"""Get user info from access token"""
access_token = headers.token()
userinfo_response = requests.get(
USERINFO_ENDPOINT, headers={"Authorization": f"Bearer {access_token}"}
)
return userinfo_response.json()
@app.get("/schedule") @app.get("/schedule")
async def schedule( async def schedule(
headers: Annotated[AuthHeaders, Header()], headers: Annotated[AuthHeaders, Header()],
account: str = "default",
):
if not headers.authorized():
raise HTTPException(401, detail="get out")
try:
username, password, userid, existing_token, club_id = myice.get_login(
config_section=account
)
except Exception as e:
raise HTTPException(
400,
detail=f"Configuration error: {str(e)}. Available accounts: isaac, leonard",
)
try:
if existing_token:
myice.userdata = {
"id": userid,
"id_club": club_id or 186,
"token": existing_token,
}
else:
myice.userdata = myice.mobile_login(config_section=account)
data = myice.refresh_data()
if "club_games" in data:
return data["club_games"]
else:
# Return empty array if no games data found
return []
except Exception as e:
print(f"Error fetching schedule: {e}")
import traceback
traceback.print_exc()
# Return empty array instead of throwing an error
return []
@app.get("/accounts")
async def accounts(
headers: Annotated[AuthHeaders, Header()],
): ):
if not headers.authorized(): if not headers.authorized():
raise HTTPException(401, detail="get out") raise HTTPException(401, detail="get out")
username, password, userid, existing_token = myice.get_login()
if existing_token: # Import configparser to read the available sections
myice.userdata = { import configparser
"id": userid, from pathlib import Path
"id_club": 186,
"token": existing_token, config = configparser.ConfigParser()
} config_files = [
else: Path("~/.config/myice.ini").expanduser(),
myice.userdata = myice.mobile_login() Path("myice.ini"),
return myice.refresh_data()["club_games"] ]
# Read all config files
for config_file in config_files:
if config_file.exists():
try:
config.read(config_file, encoding="utf-8")
except Exception:
# Try without specifying encoding
config.read(config_file)
# Get all sections (accounts) from the config
accounts = []
for section in config.sections():
if section != "DEFAULT": # Skip DEFAULT section
# Capitalize first letter for display
label = (
section[0].upper() + section[1:]
if len(section) > 1
else section.upper()
)
accounts.append({"name": section, "label": label})
# If no accounts found, return default
if not accounts:
accounts = [{"name": "default", "label": "Default"}]
return accounts
@app.get("/game/{game_id}") @app.get("/game/{game_id}")
async def game( async def game(
headers: Annotated[AuthHeaders, Header()], headers: Annotated[AuthHeaders, Header()],
game_id: int, game_id: int,
account: str = "default",
): ):
username, password, userid, existing_token = myice.get_login() username, password, userid, existing_token, club_id = myice.get_login(
config_section=account
)
if existing_token: if existing_token:
myice.userdata = { myice.userdata = {
"id": userid, "id": userid,
"id_club": 186, "id_club": club_id or 186,
"token": existing_token, "token": existing_token,
} }
else: else:
myice.userdata = myice.mobile_login() myice.userdata = myice.mobile_login(config_section=account)
# data = refresh_data() # data = refresh_data()
with requests.post( with requests.post(
Generated
+11 -11
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@@ -31,7 +31,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras] [package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"] trio = ["trio (>=0.26.1)"]
[[package]] [[package]]
@@ -402,7 +402,7 @@ files = [
] ]
[package.extras] [package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
@@ -560,7 +560,7 @@ httpcore = "==1.*"
idna = "*" idna = "*"
[package.extras] [package.extras]
brotli = ["brotli", "brotlicffi"] brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"] socks = ["socksio (==1.*)"]
@@ -642,7 +642,7 @@ typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
[package.extras] [package.extras]
all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"]
black = ["black"] black = ["black"]
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"] doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli ; python_version < \"3.11\"", "typing_extensions"]
kernel = ["ipykernel"] kernel = ["ipykernel"]
matplotlib = ["matplotlib"] matplotlib = ["matplotlib"]
nbconvert = ["nbconvert"] nbconvert = ["nbconvert"]
@@ -712,7 +712,7 @@ traitlets = ">=5.3"
[package.extras] [package.extras]
docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
[[package]] [[package]]
name = "jupyter-core" name = "jupyter-core"
@@ -1102,7 +1102,7 @@ typing-extensions = ">=4.12.2"
[package.extras] [package.extras]
email = ["email-validator (>=2.0.0)"] email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata"] timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
@@ -1755,7 +1755,7 @@ files = [
] ]
[package.extras] [package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"] h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"] zstd = ["zstandard (>=0.18.0)"]
@@ -1779,12 +1779,12 @@ h11 = ">=0.8"
httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""}
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
[package.extras] [package.extras]
standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]] [[package]]
name = "uvloop" name = "uvloop"
@@ -1793,7 +1793,7 @@ description = "Fast implementation of asyncio event loop on top of libuv"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
groups = ["main"] groups = ["main"]
markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\"" markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
files = [ files = [
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"},
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"},
+5 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "myice" name = "myice"
version = "v0.3.2" version = "v0.5.7"
description = "myice parsing" description = "myice parsing"
authors = [ authors = [
{ name = "Rene Luria", "email" = "<rene@luria.ch>"}, { name = "Rene Luria", "email" = "<rene@luria.ch>"},
@@ -35,5 +35,8 @@ priority = "supplemental"
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[projectscripts] [project.scripts]
myice = 'myice.myice:app' myice = 'myice.myice:app'
[tool.poetry.requires-plugins]
poetry-plugin-export = ">=1.8"