1 Commits

Author SHA1 Message Date
herel aa90dbe1f1 bump to version v0.4.0 2025-08-19 08:38:10 +02:00
7 changed files with 192 additions and 685 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: v6.0.0 rev: v3.2.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.12.9 rev: v0.6.8
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.17.1 rev: v1.11.2
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.37.1 rev: v1.35.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.13.0 rev: v0.12.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.11.0.1 rev: v0.10.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.7.0 rev: v0.6.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.1.4" rev: "2.0.0"
hooks: hooks:
- id: poetry-check - id: poetry-check
- id: poetry-lock - id: poetry-lock
+2 -21
View File
@@ -76,34 +76,15 @@ Then open your browser at `http://localhost:8000`. The web interface allows you
- View upcoming games and practices - View upcoming games and practices
- See detailed information about events including player rosters - 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: The web API provides the following endpoints:
- `/schedule` - Get the schedule for a specific account - `/schedule` - Get the schedule for a specific account
- `/game/{game_id}` - Get details for a specific game - `/game/{game_id}` - Get details for a specific game
- `/accounts` - Get a list of available accounts - `/accounts` - Get a list of available accounts
- `/health` - Health check endpoint - `/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. All endpoints (except `/health`) require an Authorization header with a Bearer token.
For development purposes, you can use `abc` as the token.
## mobile functions ## mobile functions
+60 -300
View File
@@ -10,49 +10,28 @@
</head> </head>
<body> <body>
<div id="connectedUser" class="position-fixed top-0 end-0 m-2 p-2 bg-light border rounded" <div class="container mt-4">
style="display: none; z-index: 1000; font-size: 0.9rem;"> <h1 class="mb-4 text-center">MyIce - Games</h1>
<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 class="container-fluid d-flex align-items-center justify-content-center"> <div id="apikeyContainer" class="mb-3"></div>
<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">
<label for="account" class="form-label">Compte</label> <label for="account" class="form-label">Sélectionner un compte</label>
<select id="account" class="form-select"> <select id="account" class="form-select">
<option value="default">Défaut</option> <option value="default">Compte par défaut</option>
</select> </select>
<label for="agegroup" class="form-label">Âge</label> </div>
<div class="mb-3">
<label for="agegroup" class="form-label">Filtrer par groupe d'âge</label>
<select id="agegroup" class="form-select"> <select id="agegroup" class="form-select">
<option value="">Tous</option> <option value="">Tous</option>
</select> </select>
<button id="fetchEvents" class="btn btn-primary" style="margin-top: 1.2rem;">Charger</button>
</div> </div>
<button id="fetchEvents" class="btn btn-primary mb-3">Charger les événements</button>
</div> </div>
<div id="loadingIndicator" class="text-center my-3" style="display: none;"> <div id="eventList" class="row"></div>
<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"
@@ -71,9 +50,7 @@
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const loginContainer = document.getElementById("loginContainer"); const apikeyContainer = document.getElementById("apikeyContainer");
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 accountSelect = document.getElementById("account");
const agegroupSelect = document.getElementById("agegroup"); const agegroupSelect = document.getElementById("agegroup");
@@ -85,174 +62,43 @@
let storedApiKey = localStorage.getItem("apikey"); let storedApiKey = localStorage.getItem("apikey");
let lastFetchedEvents = []; let lastFetchedEvents = [];
let storedAccount = localStorage.getItem("account") || "default"; let storedAccount = localStorage.getItem("account") || "default";
let userInfo = JSON.parse(localStorage.getItem("userInfo") || "null");
// If we have an API key but no userInfo, fetch it from the server function renderApiKeyInput() {
if (storedApiKey && !userInfo) { if (storedApiKey) {
fetch(`${apiBaseUrl}/userinfo`, { apikeyContainer.innerHTML = `<button id="disconnect" class="btn btn-danger">Déconnecter</button>`;
headers: { "Authorization": `Bearer ${storedApiKey}` } document.getElementById("disconnect").addEventListener("click", () => {
})
.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";
updateAccountOptions();
// Don't automatically fetch events on page load
// Wait for user to explicitly select an account or click fetch
} else {
// User is not logged in
loginView.style.display = "flex";
mainContent.style.display = "none";
loginTitle.style.display = "block";
connectedUser.style.display = "none";
oidcLoginSection.style.display = "block";
eventFilters.style.display = "none";
// Remove any existing event listeners to prevent duplicates
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
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("apikey");
localStorage.removeItem("account"); localStorage.removeItem("account");
localStorage.removeItem("userInfo");
storedApiKey = null;
userInfo = null;
location.reload(); location.reload();
});
eventFilters.style.display = "block";
// Load available accounts from server or use predefined ones
updateAccountOptions();
fetchEvents(storedApiKey, storedAccount);
} 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.");
}
} }
function updateAccountOptions() { function updateAccountOptions() {
@@ -260,61 +106,29 @@
fetch(`${apiBaseUrl}/accounts`, { fetch(`${apiBaseUrl}/accounts`, {
headers: { "Authorization": `Bearer ${storedApiKey}` } headers: { "Authorization": `Bearer ${storedApiKey}` }
}) })
.then(response => { .then(response => response.json())
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 => { .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");
}
accountSelect.innerHTML = ''; accountSelect.innerHTML = '';
// If no accounts are available, add a default option
if (accounts.length === 0) {
const option = document.createElement("option");
option.value = "default";
option.textContent = "Default";
accountSelect.appendChild(option);
return;
}
// Add all available accounts
accounts.forEach(account => { accounts.forEach(account => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = account.name; option.value = account.name;
option.textContent = account.label; option.textContent = account.label;
if (account.name === storedAccount) {
option.selected = true;
}
accountSelect.appendChild(option); 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;
}) })
.catch(error => { .catch(error => {
console.error("Erreur lors du chargement des comptes:", 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."); // Fallback to default options
logout(); accountSelect.innerHTML = `
<option value="default" ${storedAccount === "default" ? "selected" : ""}>Compte par défaut</option>
`;
}); });
} }
renderLoginSection(); renderApiKeyInput();
accountSelect.addEventListener("change", () => { accountSelect.addEventListener("change", () => {
const selectedAccount = accountSelect.value; const selectedAccount = accountSelect.value;
@@ -326,7 +140,7 @@
fetchButton.addEventListener("click", () => { fetchButton.addEventListener("click", () => {
if (!storedApiKey) { if (!storedApiKey) {
alert("Veuillez vous connecter"); alert("Veuillez entrer une clé API");
return; return;
} }
const selectedAccount = accountSelect.value; const selectedAccount = accountSelect.value;
@@ -338,75 +152,22 @@
}); });
function fetchEvents(apiKey, account) { 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}`, { fetch(`${apiBaseUrl}/schedule?account=${account}`, {
headers: { "Authorization": `Bearer ${apiKey}` } headers: { "Authorization": `Bearer ${apiKey}` }
}) })
.then(response => { .then(response => response.json())
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 => { .catch(error => console.error("Erreur lors du chargement des événements:", 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)); let agegroups = new Set(events.map(event => `${event.agegroup} ${event.name}`.trim()));
agegroupSelect.innerHTML = '<option value="">Tous</option>'; agegroupSelect.innerHTML = '<option value="">Tous</option>';
Array.from(agegroups).sort().forEach(group => { agegroups.forEach(group => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = group; option.value = group;
option.textContent = group; option.textContent = group;
@@ -417,7 +178,7 @@
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 === selectedAgegroup)); let filteredEvents = events.filter(event => event.event === "Jeu" && (selectedAgegroup === "" || `${event.agegroup} ${event.name}` === selectedAgegroup));
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>";
@@ -430,8 +191,7 @@
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.agegroup} - ${event.name}</h5> <h5 class="card-title">${event.title}</h5>
<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>
+84 -104
View File
@@ -75,29 +75,7 @@ def sanitize_json_response(text):
except json.JSONDecodeError: except json.JSONDecodeError:
# If parsing still fails, try one more aggressive approach # If parsing still fails, try one more aggressive approach
# Remove any remaining control characters that might be causing issues # 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 array_content = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", array_content)
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: try:
data = json.loads(array_content) data = json.loads(array_content)
@@ -114,23 +92,6 @@ def sanitize_json_response(text):
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):
@@ -249,15 +210,12 @@ def select_club(club_id: int = 172):
r.raise_for_status() r.raise_for_status()
def do_login(config_section: str | None = None): def do_login(config_section: str = "default"):
global session global session
global userid global userid
global global_config_section username, password, userid_tmp, token, club_id = get_login(
config_section=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: if userid_tmp is not None:
userid = userid_tmp 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})
@@ -303,9 +261,9 @@ def get_userid():
def wrapper_session(func): def wrapper_session(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
global session, userid, global_config_section global session, userid
# Use the global config_section # Extract config_section from kwargs if present
config_section = global_config_section config_section = kwargs.get("config_section", "default")
session = requests.Session() session = requests.Session()
# session.verify = False # session.verify = False
@@ -313,20 +271,19 @@ def wrapper_session(func):
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(config_section=None) # Use global config_section do_login(config_section=config_section)
save_cookies() save_cookies()
_, _, userid, _, _ = get_login(config_section=config_section) _, _, 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
@wrapper_session @wrapper_session
def get_schedule(num_days: int) -> str: def get_schedule(num_days: int, config_section: str = "default") -> str:
global session global session
global userid global userid
assert session and userid assert session and userid
@@ -353,11 +310,11 @@ def get_schedule(num_days: int) -> str:
# Debug: Save raw response to file for analysis # Debug: Save raw response to file for analysis
# with open("raw_response.txt", "w") as f: # with open("raw_response.txt", "w") as f:
# f.write(r.text) # f.write(r.text)
return json.loads(sanitize_json_response(r.text)) return r.text
@wrapper_session @wrapper_session
def game_pdf(gameid: int, outfile: Path): def game_pdf(gameid: int, outfile: Path, config_section: str = "default"):
global session, userid global session, userid
assert session and userid assert session and userid
r = session.get( r = session.get(
@@ -374,7 +331,7 @@ def game_pdf(gameid: int, outfile: Path):
@wrapper_session @wrapper_session
def practice_pdf(gameid: int, outfile: Path): def practice_pdf(gameid: int, outfile: Path, config_section: str = "default"):
global session, userid global session, userid
assert session and userid assert session and userid
r = session.get( r = session.get(
@@ -399,19 +356,24 @@ def schedule(
), ),
] = None, ] = None,
num_days: Annotated[int, typer.Option("--days")] = 7, num_days: Annotated[int, typer.Option("--days")] = 7,
config_section: Annotated[
str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
): ):
""" """
Fetch schedule as json Fetch schedule as json
""" """
global global_config_section schedule = get_schedule(num_days, config_section=config_section)
schedule = get_schedule(num_days) # 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(json.dumps(schedule)) f.write(sanitized_schedule)
else: else:
import builtins print(sanitized_schedule)
builtins.print(json.dumps(schedule, indent=2))
def os_open(file: str) -> None: def os_open(file: str) -> None:
@@ -443,16 +405,21 @@ def extract_players(pdf_file: Path) -> List[str]:
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, open_file: Annotated[bool, typer.Option("--open", "-o")] = False,
config_section: Annotated[
str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
): ):
""" """
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:
output_filename = tempfile.NamedTemporaryFile().name output_filename = tempfile.NamedTemporaryFile().name
game_pdf(game_id, Path(output_filename)) game_pdf(game_id, Path(output_filename), config_section=config_section)
if open_file: if open_file:
os_open(output_filename) os_open(output_filename)
else: else:
@@ -464,19 +431,24 @@ def get_game_pdf(
@app.command("practice") @app.command("practice")
def get_practice_pdf( def get_practice_pdf(
game_id: Annotated[int, typer.Argument(help="ID of practice to gen pdf for")], game_id: Annotated[int, typer.Argument(help="ID of practice to gen pdf for")],
config_section: Annotated[
str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
): ):
""" """
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), config_section=config_section)
os_open(output_filename) os_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 | 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"),
@@ -484,11 +456,16 @@ def parse_schedule(
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"),
config_section: Annotated[
str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
): ):
""" """
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: try:
with schedule_file.open("r") as f: with schedule_file.open("r") as f:
data = json.load(f) data = json.load(f)
@@ -534,11 +511,16 @@ def check_with_ai(
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"),
config_section: Annotated[
str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
): ):
""" """
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:
@@ -587,14 +569,10 @@ mobile_headers = {
} }
def mobile_login(config_section: str | None = None): def mobile_login(config_section: str = "default"):
global global_config_section
import base64 import base64
# Use provided config_section, or fall back to global one username, password, _, token, club_id = get_login(config_section=config_section)
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: if token and club_id:
return {"id": 0, "token": token, "id_club": club_id} return {"id": 0, "token": token, "id_club": club_id}
@@ -629,50 +607,53 @@ def refresh_data():
# 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") @app.command("mobile-login")
def do_mobile_login(): def do_mobile_login(
global userdata, global_config_section config_section: Annotated[
userdata = mobile_login(config_section=global_config_section) str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
):
global userdata
userdata = mobile_login(config_section=config_section)
print(json.dumps(userdata, indent=2)) print(json.dumps(userdata, indent=2))
@app.command("mobile") @app.command("mobile")
def mobile(): def mobile(
global userdata, global_config_section config_section: Annotated[
userdata = mobile_login(config_section=global_config_section) str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
):
global userdata
userdata = mobile_login(config_section=config_section)
games = [x for x in refresh_data().get("club_games")] games = [x for x in refresh_data().get("club_games")]
# Use built-in print to avoid rich formatting issues print(json.dumps(games, indent=2))
import builtins
builtins.print(json.dumps(games, indent=2, ensure_ascii=False))
@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")],
raw: Annotated[bool, typer.Option(help="display raw output")] = False, raw: Annotated[bool, typer.Option(help="display raw output")] = False,
config_section: Annotated[
str,
typer.Option(
"--config-section", "-c", help="Configuration section to use from INI file"
),
] = "default",
): ):
global userdata, global_config_section global userdata
userdata = mobile_login(config_section=global_config_section) userdata = mobile_login(config_section=config_section)
# 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,
@@ -688,8 +669,7 @@ def mobile_game(
), ),
# verify=False, # verify=False,
) as r: ) as r:
data = json.loads(sanitize_json_response(r.text))["eventData"] data = r.json()["eventData"]
players = data["convocation"]["available"] players = data["convocation"]["available"]
if raw: if raw:
print(data) print(data)
+5 -219
View File
@@ -1,31 +1,12 @@
import logging import logging
import requests import requests
import os
from typing import Annotated from typing import Annotated
from fastapi import FastAPI, Header, HTTPException, status, Request from fastapi import FastAPI, Header, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse from fastapi.responses import FileResponse
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()
@@ -57,64 +38,13 @@ class AuthHeaders(BaseModel):
Authorization: str Authorization: str
def token(self) -> str: def token(self) -> str:
parts = self.Authorization.split(" ") return self.Authorization.split(" ")[1]
if len(parts) == 2:
return parts[1]
return ""
def authorized(self) -> bool: def authorized(self) -> bool:
token = self.token() if self.token() == "abc":
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): class HealthCheck(BaseModel):
"""Response model to validate and return when performing a health check.""" """Response model to validate and return when performing a health check."""
@@ -141,128 +71,6 @@ 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()],
@@ -270,18 +78,9 @@ async def schedule(
): ):
if not headers.authorized(): if not headers.authorized():
raise HTTPException(401, detail="get out") raise HTTPException(401, detail="get out")
try:
username, password, userid, existing_token, club_id = myice.get_login( username, password, userid, existing_token, club_id = myice.get_login(
config_section=account config_section=account
) )
except Exception as e:
raise HTTPException(
400,
detail=f"Configuration error: {str(e)}. Available accounts: isaac, leonard",
)
try:
if existing_token: if existing_token:
myice.userdata = { myice.userdata = {
"id": userid, "id": userid,
@@ -290,20 +89,7 @@ async def schedule(
} }
else: else:
myice.userdata = myice.mobile_login(config_section=account) myice.userdata = myice.mobile_login(config_section=account)
return myice.refresh_data()["club_games"]
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") @app.get("/accounts")
Generated
+11 -11
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. # This file is automatically @generated by Poetry 2.0.0 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) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] 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)"]
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 ; python_version >= \"3.11\""] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
@@ -560,7 +560,7 @@ httpcore = "==1.*"
idna = "*" idna = "*"
[package.extras] [package.extras]
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] brotli = ["brotli", "brotlicffi"]
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 ; python_version < \"3.11\"", "typing_extensions"] doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "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 ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "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 ; python_version >= \"3.9\" and platform_system == \"Windows\""] timezone = ["tzdata"]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
@@ -1755,7 +1755,7 @@ files = [
] ]
[package.extras] [package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
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) ; 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)"] 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)"]
[[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"},
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "myice" name = "myice"
version = "v0.5.4" version = "v0.4.0"
description = "myice parsing" description = "myice parsing"
authors = [ authors = [
{ name = "Rene Luria", "email" = "<rene@luria.ch>"}, { name = "Rene Luria", "email" = "<rene@luria.ch>"},
@@ -35,7 +35,7 @@ priority = "supplemental"
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[project.scripts] [projectscripts]
myice = 'myice.myice:app' myice = 'myice.myice:app'
[tool.poetry.requires-plugins] [tool.poetry.requires-plugins]