From 3598c7d6b602bd6841dd97bbbfbb1ab96062b22c Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Sun, 15 Mar 2026 12:25:11 +0100 Subject: [PATCH 01/13] gstaudit: Refactor WebSocket handling into shared hook Split WebSocket management into a separate useWebSocket hook that can be shared across multiple components (useCallbackRegistry, useBusRegistry, etc.). This refactoring: - Extracts WebSocket singleton logic into useWebSocket.ts - Simplifies useCallbackRegistry.ts to focus on callback execution - Maintains proper TypeScript types and linter compliance - Enables multiple hooks to share the same WebSocket connection per session - Fixes require() usage in connection-manager.ts to use proper ES6 imports --- .../app/api/callbacks/[callbackId]/route.ts | 10 +- gstaudit/hooks/index.ts | 1 + gstaudit/hooks/useCallbackRegistry.ts | 391 ++++-------------- gstaudit/hooks/useWebSocket.ts | 348 ++++++++++++++++ gstaudit/lib/callbacks.ts | 3 + gstaudit/lib/server/connection-manager.ts | 16 + gstaudit/lib/server/websocket-handler.ts | 3 +- 7 files changed, 451 insertions(+), 321 deletions(-) create mode 100644 gstaudit/hooks/useWebSocket.ts diff --git a/gstaudit/app/api/callbacks/[callbackId]/route.ts b/gstaudit/app/api/callbacks/[callbackId]/route.ts index 4b3fb8a..ce0260d 100644 --- a/gstaudit/app/api/callbacks/[callbackId]/route.ts +++ b/gstaudit/app/api/callbacks/[callbackId]/route.ts @@ -35,9 +35,13 @@ export async function POST( console.log(`[API] Callback args:`, JSON.stringify(args, null, 2)); - // Check if this is a client-side callback (has sessionId) - if (sessionId) { - // CLIENT-SIDE CALLBACK: Broadcast via WebSocket + // Determine if this is a server-side or client-side callback + // Server sessionIds start with "gstaudit-" (connection IDs) + // Browser sessionIds are random UUIDs + const isServerCallback = !sessionId || sessionId.startsWith('gstaudit-'); + + if (!isServerCallback) { + // CLIENT-SIDE CALLBACK: Broadcast via WebSocket to browser console.log(`[API] Client callback - broadcasting to session ${sessionId}`); const wsManager = getWebSocketManager(); diff --git a/gstaudit/hooks/index.ts b/gstaudit/hooks/index.ts index d4a4118..28a4536 100644 --- a/gstaudit/hooks/index.ts +++ b/gstaudit/hooks/index.ts @@ -1 +1,2 @@ +export { useWebSocket } from './useWebSocket'; export { useCallbackRegistry } from './useCallbackRegistry'; \ No newline at end of file diff --git a/gstaudit/hooks/useCallbackRegistry.ts b/gstaudit/hooks/useCallbackRegistry.ts index 6372dd0..4fa339b 100644 --- a/gstaudit/hooks/useCallbackRegistry.ts +++ b/gstaudit/hooks/useCallbackRegistry.ts @@ -1,21 +1,22 @@ /** * Callback Registry Hook * - * Manages a SHARED WebSocket connection for receiving real-time callbacks. - * Multiple components can use this hook with the same sessionId, - * and they will share a single WebSocket connection. + * Manages callback execution for GIRest callbacks invoked from the server. + * Uses the shared WebSocket connection to receive callback invocations + * and send results back. * * Works with ClientCallbackHandler to invoke callbacks registered in the browser. */ -import { useEffect, useRef, useCallback, useState } from 'react'; +import { useCallback, useRef, useEffect } from 'react'; import { getCallbackHandler } from '@/lib/gst'; import type { ClientCallbackHandler } from '@/lib/callbacks'; +import { useWebSocket, WebSocketMessage } from './useWebSocket'; -interface CallbackMessage { - type: string; +interface CallbackMessage extends WebSocketMessage { + type: 'callback'; callbackId: string; - invocationId?: string; // Unique ID for this specific invocation + invocationId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any args: any; } @@ -34,343 +35,101 @@ interface CallbackRegistryReturn { reconnect: () => void; } -// ============================================================================ -// Shared WebSocket Manager (Singleton per sessionId) -// ============================================================================ - -interface WebSocketListener { - onConnect?: () => void; - onDisconnect?: () => void; - onError?: (error: Error) => void; -} - -interface WebSocketInstance { - ws: WebSocket | null; - refCount: number; - isConnected: boolean; - listeners: Set; - reconnectTimeout?: NodeJS.Timeout; -} - -// Global map: sessionId → WebSocket instance -const wsInstances = new Map(); - -function getOrCreateWebSocket( - sessionId: string, - wsUrl: string, - autoConnect: boolean -): WebSocketInstance { - let instance = wsInstances.get(sessionId); - - if (!instance) { - console.log(`[WebSocketManager] Creating new WebSocket instance for session: ${sessionId}`); +/** + * Hook for managing callback registry via WebSocket. + * + * This hook: + * 1. Connects to the WebSocket server + * 2. Listens for 'callback' messages + * 3. Executes the registered callback function + * 4. Sends the result back via WebSocket + */ +export function useCallbackRegistry( + options: UseCallbackRegistryOptions +): CallbackRegistryReturn { + const { + sessionId, + wsUrl = 'ws://localhost:3000/ws', + autoConnect = true, + onConnect, + onDisconnect, + onError, + } = options; - instance = { - ws: null, - refCount: 0, - isConnected: false, - listeners: new Set(), - }; + // Use a ref to store the sendMessage function to avoid circular dependency + const sendMessageRef = useRef<((message: unknown) => void) | null>(null); - wsInstances.set(sessionId, instance); + // Handle callback messages - this callback is stable + const handleMessage = useCallback(async (message: WebSocketMessage) => { + // Only process 'callback' messages + if (message.type !== 'callback') return; - if (autoConnect) { - console.log(`[WebSocketManager] Auto-connecting to: ${wsUrl}`); - connectWebSocket(sessionId, wsUrl); - } else { - console.log(`[WebSocketManager] Not auto-connecting (autoConnect=false)`); + const callbackMsg = message as CallbackMessage; + const invocationId = callbackMsg.invocationId || callbackMsg.callbackId; + + console.log(`[CallbackRegistry] Received callback: ${invocationId} (registration: ${callbackMsg.callbackId})`); + + // Get the callback handler + const handler = getCallbackHandler(); + if (!handler) { + console.error('[CallbackRegistry] No callback handler configured'); + return; } - } - - return instance; -} - -function connectWebSocket(sessionId: string, wsUrl: string) { - const instance = wsInstances.get(sessionId); - if (!instance) return; - - if (instance.ws?.readyState === WebSocket.OPEN) { - console.log(`[WebSocketManager] WebSocket already connected for session: ${sessionId}`); - return; - } - - // Build WebSocket URL with sessionId parameter - const url = new URL(wsUrl); - url.searchParams.set('sessionId', sessionId); - const fullWsUrl = url.toString(); - - console.log(`[WebSocketManager] Connecting to: ${fullWsUrl}`); - - try { - const ws = new WebSocket(fullWsUrl); - instance.ws = ws; - - ws.onopen = () => { - console.log(`[WebSocketManager] WebSocket connected for session: ${sessionId}`); - instance.isConnected = true; - // Notify all listeners - instance.listeners.forEach(listener => listener.onConnect?.()); - }; - - ws.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - console.log(`[WebSocketManager] Received message:`, message.type); - switch (message.type) { - case 'connected': - case 'registered': - console.log(`[WebSocketManager] Session registered: ${sessionId}`); - break; - - case 'callback': - handleCallbackMessage(message, ws); - break; - - case 'pong': - // Heartbeat response - break; - - default: - console.warn(`[WebSocketManager] Unknown message type:`, message.type); - } - } catch (error) { - console.error(`[WebSocketManager] Error parsing message:`, error); - } - }; - - ws.onerror = (event) => { - console.error(`[WebSocketManager] WebSocket error:`, event); - const error = new Error('WebSocket error'); - instance.listeners.forEach(listener => listener.onError?.(error)); - }; - - ws.onclose = () => { - console.log(`[WebSocketManager] WebSocket disconnected for session: ${sessionId}`); - instance.isConnected = false; - // Notify all listeners - instance.listeners.forEach(listener => listener.onDisconnect?.()); - - // Auto-reconnect after 5 seconds if there are still active listeners - if (instance.refCount > 0) { - console.log(`[WebSocketManager] Reconnecting in 5 seconds... (${instance.refCount} active users)`); - instance.reconnectTimeout = setTimeout(() => { - connectWebSocket(sessionId, wsUrl); - }, 5000); - } - }; - } catch (error) { - console.error(`[WebSocketManager] Error creating WebSocket:`, error); - instance.listeners.forEach(listener => listener.onError?.(error as Error)); - } -} -function handleCallbackMessage(message: CallbackMessage, ws: WebSocket) { - const invocationId = message.invocationId || message.callbackId; - console.log(`[WebSocketManager] Received callback: ${invocationId} (registration: ${message.callbackId})`); - - // Get the callback handler - const handler = getCallbackHandler(); - if (!handler) { - console.error('[WebSocketManager] No callback handler configured'); - return; - } - - // Look up the callback in ClientCallbackHandler using the registration callbackId - const callbackInfo = (handler as ClientCallbackHandler).getCallback?.(message.callbackId); - if (!callbackInfo) { - console.warn(`[WebSocketManager] Callback not found: ${message.callbackId}`); - return; - } + // Look up the callback in ClientCallbackHandler using the registration callbackId + const callbackInfo = (handler as ClientCallbackHandler).getCallback?.(callbackMsg.callbackId); + if (!callbackInfo) { + console.warn(`[CallbackRegistry] Callback not found: ${callbackMsg.callbackId}`); + return; + } - // Execute callback and send result back to server via WebSocket - (async () => { + // Execute callback and send result back let result = null; try { // Apply converter function to transform raw args to typed args - // Converter may be async, so await it - const convertedArgs = await callbackInfo.converter(message.args); - console.log(`[WebSocketManager] Invoking callback ${invocationId}`); + const convertedArgs = await callbackInfo.converter(callbackMsg.args); + console.log(`[CallbackRegistry] Invoking callback ${invocationId}`); // Invoke the actual callback function and capture result result = await callbackInfo.func(...convertedArgs); - console.log(`[WebSocketManager] Callback ${invocationId} returned:`, result); + console.log(`[CallbackRegistry] Callback ${invocationId} returned:`, result); } catch (error) { - console.error(`[WebSocketManager] Error invoking callback ${invocationId}:`, error); + console.error(`[CallbackRegistry] Error invoking callback ${invocationId}:`, error); result = null; } - // Send result back to server via WebSocket with invocationId + // Send result back to server via WebSocket using the ref try { - ws.send(JSON.stringify({ + sendMessageRef.current?.({ type: 'callback-response', - invocationId: invocationId, // Use unique invocation ID - callbackId: message.callbackId, // Keep original for reference + invocationId: invocationId, + callbackId: callbackMsg.callbackId, result: result - })); - console.log(`[WebSocketManager] Callback response sent for ${invocationId}`); + }); + console.log(`[CallbackRegistry] Callback response sent for ${invocationId}`); } catch (error) { - console.error(`[WebSocketManager] Error sending callback response:`, error); - } - })(); -} - -function addListener( - sessionId: string, - listener: { - onConnect?: () => void; - onDisconnect?: () => void; - onError?: (error: Error) => void; - } -) { - const instance = wsInstances.get(sessionId); - if (instance) { - instance.listeners.add(listener); - instance.refCount++; - } -} - -function removeListener( - sessionId: string, - listener: WebSocketListener -) { - const instance = wsInstances.get(sessionId); - if (instance) { - instance.listeners.delete(listener); - instance.refCount--; - // If no more listeners, close the WebSocket after a delay - if (instance.refCount === 0) { - console.log(`[WebSocketManager] No more listeners for session ${sessionId}, closing in 10s`); - setTimeout(() => { - const inst = wsInstances.get(sessionId); - if (inst && inst.refCount === 0) { - console.log(`[WebSocketManager] Closing WebSocket for session: ${sessionId}`); - inst.ws?.close(); - wsInstances.delete(sessionId); - } - }, 10000); + console.error(`[CallbackRegistry] Error sending callback response:`, error); } - } -} + }, []); -// ============================================================================ -// React Hook -// ============================================================================ - -/** - * Hook for managing WebSocket connection and executing callbacks. - * Multiple components can use this hook with the same sessionId. - */ -export function useCallbackRegistry( - options: UseCallbackRegistryOptions -): CallbackRegistryReturn { - const { + // Use shared WebSocket with message handler + const ws = useWebSocket({ sessionId, - wsUrl = 'ws://localhost:3000/ws', // Match the WebSocket path from server.js - autoConnect = true, + wsUrl, + autoConnect, onConnect, onDisconnect, - onError - } = options; - - const [isConnected, setIsConnected] = useState(false); - const listenerRef = useRef(null); - const prevAutoConnectRef = useRef(autoConnect); - const prevWsUrlRef = useRef(wsUrl); - - // Use refs for callbacks to avoid recreating listener - const onConnectRef = useRef(onConnect); - const onDisconnectRef = useRef(onDisconnect); - const onErrorRef = useRef(onError); - - // Update refs when callbacks change - useEffect(() => { - onConnectRef.current = onConnect; - onDisconnectRef.current = onDisconnect; - onErrorRef.current = onError; - }, [onConnect, onDisconnect, onError]); - - // Effect 1: Set up listener (only depends on sessionId) - useEffect(() => { - // Don't connect if sessionId is empty (SSR or initial render) - if (!sessionId) { - console.log('[WebSocketManager] Skipping WebSocket setup - no sessionId'); - return; - } - - console.log('[WebSocketManager] Setting up listener for session:', sessionId); - - // Create listener object with stable callbacks via refs - const listener = { - onConnect: () => { - setIsConnected(true); - onConnectRef.current?.(); - }, - onDisconnect: () => { - setIsConnected(false); - onDisconnectRef.current?.(); - }, - onError: (error: Error) => { - onErrorRef.current?.(error); - } - }; - listenerRef.current = listener; - - // Get or create shared WebSocket instance (but don't auto-connect yet) - const instance = getOrCreateWebSocket(sessionId, wsUrl, false); - - // Register this component as a listener - addListener(sessionId, listener); - - // Set initial connection state - setIsConnected(instance.isConnected); + onError, + onMessage: handleMessage, + }); - // Cleanup on unmount only - return () => { - if (listenerRef.current) { - console.log('[WebSocketManager] Removing listener for session:', sessionId); - removeListener(sessionId, listenerRef.current); - } - }; - }, [sessionId]); // Only re-run if sessionId changes - - // Effect 2: Handle connection when autoConnect or wsUrl changes + // Update the ref when ws.sendMessage changes useEffect(() => { - if (!sessionId || !autoConnect) { - return; - } - - // Check if wsUrl or autoConnect actually changed - const wsUrlChanged = prevWsUrlRef.current !== wsUrl; - const autoConnectChanged = prevAutoConnectRef.current !== autoConnect; - - if (wsUrlChanged || autoConnectChanged) { - console.log('[WebSocketManager] Connection params changed:', { - wsUrl, - autoConnect, - wsUrlChanged, - autoConnectChanged - }); - - // Connect or reconnect with new URL - connectWebSocket(sessionId, wsUrl); - - prevWsUrlRef.current = wsUrl; - prevAutoConnectRef.current = autoConnect; - } else if (autoConnect) { - // Initial auto-connect - console.log('[WebSocketManager] Initial auto-connect to:', wsUrl); - connectWebSocket(sessionId, wsUrl); - prevWsUrlRef.current = wsUrl; - prevAutoConnectRef.current = autoConnect; - } - }, [sessionId, wsUrl, autoConnect]); - - const reconnect = useCallback(() => { - connectWebSocket(sessionId, wsUrl); - }, [sessionId, wsUrl]); + sendMessageRef.current = ws.sendMessage; + }, [ws.sendMessage]); return { - isConnected, - reconnect + isConnected: ws.isConnected, + reconnect: ws.reconnect, }; } diff --git a/gstaudit/hooks/useWebSocket.ts b/gstaudit/hooks/useWebSocket.ts new file mode 100644 index 0000000..c72afe2 --- /dev/null +++ b/gstaudit/hooks/useWebSocket.ts @@ -0,0 +1,348 @@ +/** + * WebSocket Hook - Shared WebSocket connection management + * + * Provides a single WebSocket connection per sessionId that can be shared + * across multiple hooks (useCallbackRegistry, useBus, etc.). + * + * Multiple components can use this hook with the same sessionId, + * and they will share a single WebSocket connection. + */ + +import { useEffect, useRef, useCallback, useState } from 'react'; + +export interface WebSocketMessage { + type: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +export type MessageHandler = (message: WebSocketMessage) => void; + +interface UseWebSocketOptions { + sessionId: string; + wsUrl?: string; + autoConnect?: boolean; + onConnect?: () => void; + onDisconnect?: () => void; + onError?: (error: Error) => void; + onMessage?: MessageHandler; +} + +interface UseWebSocketReturn { + isConnected: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendMessage: (message: any) => void; + reconnect: () => void; +} + +// ============================================================================ +// Shared WebSocket Manager (Singleton per sessionId) +// ============================================================================ + +interface WebSocketListener { + onConnect?: () => void; + onDisconnect?: () => void; + onError?: (error: Error) => void; +} + +interface WebSocketInstance { + ws: WebSocket | null; + refCount: number; + isConnected: boolean; + listeners: Set; + messageHandlers: Set; + reconnectTimeout?: NodeJS.Timeout; +} + +// Global map: sessionId → WebSocket instance +const wsInstances = new Map(); + +function getOrCreateWebSocket( + sessionId: string, + wsUrl: string, + autoConnect: boolean +): WebSocketInstance { + let instance = wsInstances.get(sessionId); + + if (!instance) { + console.log(`[WebSocketManager] Creating new WebSocket instance for session: ${sessionId}`); + + instance = { + ws: null, + refCount: 0, + isConnected: false, + listeners: new Set(), + messageHandlers: new Set(), + }; + + wsInstances.set(sessionId, instance); + + if (autoConnect) { + console.log(`[WebSocketManager] Auto-connecting to: ${wsUrl}`); + connectWebSocket(sessionId, wsUrl); + } else { + console.log(`[WebSocketManager] Not auto-connecting (autoConnect=false)`); + } + } else { + // Instance already exists - connect if requested and not already connected + if (autoConnect && !instance.isConnected && (!instance.ws || instance.ws.readyState !== WebSocket.OPEN)) { + console.log(`[WebSocketManager] Instance exists but not connected, connecting to: ${wsUrl}`); + connectWebSocket(sessionId, wsUrl); + } + } + + return instance; +} + +function connectWebSocket(sessionId: string, wsUrl: string) { + const instance = wsInstances.get(sessionId); + if (!instance) return; + + if (instance.ws?.readyState === WebSocket.OPEN) { + console.log(`[WebSocketManager] WebSocket already connected for session: ${sessionId}`); + return; + } + + // Build WebSocket URL with sessionId parameter + const url = new URL(wsUrl); + url.searchParams.set('sessionId', sessionId); + const fullWsUrl = url.toString(); + + console.log(`[WebSocketManager] Connecting to: ${fullWsUrl}`); + + try { + const ws = new WebSocket(fullWsUrl); + instance.ws = ws; + + ws.onopen = () => { + console.log(`[WebSocketManager] WebSocket connected for session: ${sessionId}`); + instance.isConnected = true; + // Notify all listeners + instance.listeners.forEach(listener => listener.onConnect?.()); + }; + + ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + console.log(`[WebSocketManager] Received message:`, message.type); + + // Handle common/system messages internally + switch (message.type) { + case 'connected': + console.log(`[WebSocketManager] WebSocket connection established`); + return; // Don't forward to handlers + + case 'registered': + console.log(`[WebSocketManager] Session registered: ${sessionId}`); + return; // Don't forward to handlers + + case 'pong': + // Heartbeat response - no action needed + return; // Don't forward to handlers + + default: + // Forward all other messages to application handlers + instance.messageHandlers.forEach(handler => { + try { + handler(message); + } catch (error) { + console.error(`[WebSocketManager] Error in message handler:`, error); + } + }); + } + } catch (error) { + console.error(`[WebSocketManager] Error parsing message:`, error); + } + }; + + ws.onerror = (event) => { + console.error(`[WebSocketManager] WebSocket error:`, event); + const error = new Error('WebSocket error'); + instance.listeners.forEach(listener => listener.onError?.(error)); + }; + + ws.onclose = () => { + console.log(`[WebSocketManager] WebSocket disconnected for session: ${sessionId}`); + instance.isConnected = false; + // Notify all listeners + instance.listeners.forEach(listener => listener.onDisconnect?.()); + + // Auto-reconnect after 5 seconds if there are still active listeners + if (instance.refCount > 0) { + console.log(`[WebSocketManager] Reconnecting in 5 seconds... (${instance.refCount} active users)`); + instance.reconnectTimeout = setTimeout(() => { + connectWebSocket(sessionId, wsUrl); + }, 5000); + } + }; + } catch (error) { + console.error(`[WebSocketManager] Error creating WebSocket:`, error); + instance.listeners.forEach(listener => listener.onError?.(error as Error)); + } +} + +function addListener( + sessionId: string, + listener: { + onConnect?: () => void; + onDisconnect?: () => void; + onError?: (error: Error) => void; + } +) { + const instance = wsInstances.get(sessionId); + if (instance) { + instance.listeners.add(listener); + instance.refCount++; + } +} + +function removeListener( + sessionId: string, + listener: WebSocketListener +) { + const instance = wsInstances.get(sessionId); + if (instance) { + instance.listeners.delete(listener); + instance.refCount--; + // If no more listeners, close the WebSocket after a delay + if (instance.refCount === 0) { + console.log(`[WebSocketManager] No more listeners for session ${sessionId}, closing in 10s`); + setTimeout(() => { + const inst = wsInstances.get(sessionId); + if (inst && inst.refCount === 0) { + console.log(`[WebSocketManager] Closing WebSocket for session: ${sessionId}`); + inst.ws?.close(); + wsInstances.delete(sessionId); + } + }, 10000); + } + } +} + +function addMessageHandler(sessionId: string, handler: MessageHandler) { + const instance = wsInstances.get(sessionId); + if (instance) { + instance.messageHandlers.add(handler); + } +} + +function removeMessageHandler(sessionId: string, handler: MessageHandler) { + const instance = wsInstances.get(sessionId); + if (instance) { + instance.messageHandlers.delete(handler); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function sendMessage(sessionId: string, message: any) { + const instance = wsInstances.get(sessionId); + if (instance?.ws?.readyState === WebSocket.OPEN) { + instance.ws.send(JSON.stringify(message)); + } else { + console.warn(`[WebSocketManager] Cannot send message, WebSocket not connected`); + } +} + +// ============================================================================ +// React Hook +// ============================================================================ + +/** + * Hook for managing a shared WebSocket connection. + * Multiple components can use this hook with the same sessionId. + */ +export function useWebSocket( + options: UseWebSocketOptions +): UseWebSocketReturn { + const { + sessionId, + wsUrl = 'ws://localhost:3000/ws', + autoConnect = true, + onConnect, + onDisconnect, + onError, + onMessage, + } = options; + + const [isConnected, setIsConnected] = useState(false); + const listenerRef = useRef(null); + const messageHandlerRef = useRef(null); + + console.log('[useWebSocket] Hook called:', { + sessionId, + wsUrl, + autoConnect, + hasSessionId: !!sessionId + }); + + // Initialize WebSocket instance + useEffect(() => { + console.log('!!!!! [useWebSocket] Effect running:', { sessionId, autoConnect }); + if (!sessionId) { + console.log('!!!!! [useWebSocket] No sessionId, returning early'); + return; + } + + const instance = getOrCreateWebSocket(sessionId, wsUrl, autoConnect); + + // Create listener object + const listener = { + onConnect: () => { + setIsConnected(true); + onConnect?.(); + }, + onDisconnect: () => { + setIsConnected(false); + onDisconnect?.(); + }, + onError: (error: Error) => { + onError?.(error); + }, + }; + + listenerRef.current = listener; + addListener(sessionId, listener); + + // Update connected state + setIsConnected(instance.isConnected); + + return () => { + if (listenerRef.current) { + removeListener(sessionId, listenerRef.current); + listenerRef.current = null; + } + }; + }, [sessionId, wsUrl, autoConnect, onConnect, onDisconnect, onError]); + + // Register message handler + useEffect(() => { + if (!sessionId || !onMessage) return; + + messageHandlerRef.current = onMessage; + addMessageHandler(sessionId, onMessage); + + return () => { + if (messageHandlerRef.current) { + removeMessageHandler(sessionId, messageHandlerRef.current); + messageHandlerRef.current = null; + } + }; + }, [sessionId, onMessage]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const send = useCallback((message: any) => { + sendMessage(sessionId, message); + }, [sessionId]); + + const reconnect = useCallback(() => { + if (sessionId) { + connectWebSocket(sessionId, wsUrl); + } + }, [sessionId, wsUrl]); + + return { + isConnected, + sendMessage: send, + reconnect, + }; +} diff --git a/gstaudit/lib/callbacks.ts b/gstaudit/lib/callbacks.ts index d62f91c..8d504d6 100644 --- a/gstaudit/lib/callbacks.ts +++ b/gstaudit/lib/callbacks.ts @@ -243,7 +243,10 @@ export class ServerCallbackHandler implements ICallbackHandler { console.log(`[Server] Registered callback: ${callbackId}`, metadata); // Build callback URL pointing to Next.js API route + // IMPORTANT: Include server sessionId so the callback route can distinguish + // between server-side callbacks (connectionId) and browser callbacks (tab sessionId) const url = new URL(`/api/callbacks/${callbackId}`, this.baseUrl); + url.searchParams.set('sessionId', this.sessionId); return { callbackUrl: url.toString(), diff --git a/gstaudit/lib/server/connection-manager.ts b/gstaudit/lib/server/connection-manager.ts index 97195fe..ce8570d 100644 --- a/gstaudit/lib/server/connection-manager.ts +++ b/gstaudit/lib/server/connection-manager.ts @@ -11,6 +11,7 @@ */ import { ServerCallbackHandler } from '@/lib/callbacks'; +import { setApiConfig, setCallbackHandler } from '@/lib/gst'; export interface ConnectionConfig { host: string; @@ -64,6 +65,16 @@ class ConnectionManager { let handler = this.handlers.get(connectionId); if (!handler) { + // Configure the API to point to this gstaudit-server + // Note: This is global config, so it assumes single connection at a time + // For multiple connections, we'd need per-connection API instances + setApiConfig({ + host: config.host, + port: config.port, + basePath: 'girest' + }); + console.log(`[ConnectionManager] Configured API for ${config.host}:${config.port}/girest`); + // Create new handler for this gstaudit-server connection handler = new ServerCallbackHandler({ baseUrl: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000', @@ -71,6 +82,11 @@ class ConnectionManager { callbackSecret: process.env.CALLBACK_SECRET || 'dev-secret' }); + // Set the callback handler for gst.ts API calls + // This is needed for any API calls that register callbacks (e.g., GLibThread.new()) + setCallbackHandler(handler); + console.log(`[ConnectionManager] Set callback handler for connection: ${connectionId}`); + this.handlers.set(connectionId, handler); this.configs.set(connectionId, config); diff --git a/gstaudit/lib/server/websocket-handler.ts b/gstaudit/lib/server/websocket-handler.ts index 2093514..9ef3d85 100644 --- a/gstaudit/lib/server/websocket-handler.ts +++ b/gstaudit/lib/server/websocket-handler.ts @@ -237,7 +237,6 @@ export function initializeWebSocketServer(wss: WebSocketServer): void { handleCallbackResponse(invocationId, message.result); } - } catch (error) { console.error('Error handling WebSocket message:', error); } @@ -245,7 +244,7 @@ export function initializeWebSocketServer(wss: WebSocketServer): void { ws.on('close', () => { console.log(`WebSocket client disconnected: ${sessionId}`); - + // Unregister from WebSocketManager const wsManager = getWebSocketManager(); wsManager.unregister(sessionId); From a806b75a62c078a6c74c1aa382b1a909eb58003f Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Sun, 15 Mar 2026 12:26:04 +0100 Subject: [PATCH 02/13] ci: Fix pre-commit eslint hook to only check staged files Change pass_filenames from false to true so that eslint only checks the files being committed, not all files in the workspace. This prevents pre-commit failures due to linter errors in unstaged files. --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b4b284..53e3340 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: eslint name: eslint (gstaudit) entry: bash -c 'cd gstaudit && ([ -d node_modules ] || npm install) && npm - run lint -- --fix' + run lint -- --fix "${@#gstaudit/}"' -- language: system files: ^gstaudit/.*\.(ts|tsx|js|jsx)$ - pass_filenames: false + pass_filenames: true From 2497d740943e71c23a0695394e70eebbd6265cfe Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Sun, 15 Mar 2026 21:35:04 +0100 Subject: [PATCH 03/13] girest: Remove the dangling callbacks endpoint --- girest/girest/main.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/girest/girest/main.py b/girest/girest/main.py index 9326915..ba09fb4 100644 --- a/girest/girest/main.py +++ b/girest/girest/main.py @@ -42,22 +42,6 @@ def __init__(self, ns, ns_version): self.repo.require(dep_ns, dep_version, 0) self.namespaces.append((dep_ns, dep_version)) self.namespaces.append((ns, ns_version)) - # Generate the generic callback endpoint - # TODO define all callbacks as events as defined in - # https://spec.openapis.org/oas/v3.2.0.html#server-sent-event-streams - operation = { - "summary": "Callback emitters", - "description": "", - "operationId": "GIRest--callbacks", - "tags": ["GIRest"], - "responses": { - "200": { - "description": "Success", - "content": {"text/event-stream": {"schema": {"$ref": "#/components/schemas/Event"}}}, - } - }, - } - self.spec.path(path="/GIRest/callbacks", operations={"get": operation}) def _get_container_element_type_schema(self, container_type_info): """ From feaec44513ee8fd4e67c2505ba1cfd33ecd277ea Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Sun, 15 Mar 2026 21:35:18 +0100 Subject: [PATCH 04/13] girest: Be more verbose about missing namespaces --- girest/girest/resolvers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/girest/girest/resolvers.py b/girest/girest/resolvers.py index 493817b..e7f8a95 100644 --- a/girest/girest/resolvers.py +++ b/girest/girest/resolvers.py @@ -871,6 +871,11 @@ def _find_function_info(self, namespace, class_name, method_name): # operation_id format: {namespace}_{object_name}_{method_name} # or {namespace}__{function_name} for standalone functions + # Check if namespace is loaded + if not self.repo.is_registered(namespace, None): + logger.warning(f"Namespace '{namespace}' not loaded, cannot resolve method {method_name}") + return None + # Search through the repository n_infos = self.repo.get_n_infos(namespace) for i in range(n_infos): @@ -1542,6 +1547,10 @@ def get_function_from_operation(self, operation): # Find the struct info struct_info = None + # Check if namespace is loaded + if not self.repo.is_registered(namespace, None): + logger.warning(f"Namespace '{namespace}' not loaded, skipping field operation for {method_name}") + return None n_infos = self.repo.get_n_infos(namespace) for i in range(n_infos): info = self.repo.get_info(namespace, i) @@ -1590,6 +1599,10 @@ def get_function_from_operation(self, operation): elif method_name in ["new", "free", "get_type", "ref", "unref"]: # Try to find the info (struct, object, enum, or flags) type_info = None + # Check if namespace is loaded + if not self.repo.is_registered(namespace, None): + logger.warning(f"Namespace '{namespace}' not loaded, skipping artificial method {method_name}") + return None n_infos = self.repo.get_n_infos(namespace) for i in range(n_infos): info = self.repo.get_info(namespace, i) From 8241fa9942c9f2bf904f739afa82696afb2f8c4d Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 11:03:53 +0100 Subject: [PATCH 05/13] gstaudit: Use the proper constructor --- gstaudit/app/pipeline/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gstaudit/app/pipeline/page.tsx b/gstaudit/app/pipeline/page.tsx index c9c663d..df93e21 100644 --- a/gstaudit/app/pipeline/page.tsx +++ b/gstaudit/app/pipeline/page.tsx @@ -88,7 +88,7 @@ export default function PipelinePage() { const allPipelines: { name: string; pipeline: GstPipeline }[] = []; for (const pipelineData of pipelinesData) { - const pipeline = new GstPipeline(pipelineData.ptr, 'none'); + const pipeline = await GstPipeline.create(pipelineData.ptr, 'none'); allPipelines.push({ name: pipelineData.name, pipeline }); } From ed95b2716125446ae19d3a4f7896b3ca393e6c1d Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 11:04:32 +0100 Subject: [PATCH 06/13] gstaudit: Pass the proper args to the converter --- .../app/api/callbacks/[callbackId]/route.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/gstaudit/app/api/callbacks/[callbackId]/route.ts b/gstaudit/app/api/callbacks/[callbackId]/route.ts index ce0260d..c85efc5 100644 --- a/gstaudit/app/api/callbacks/[callbackId]/route.ts +++ b/gstaudit/app/api/callbacks/[callbackId]/route.ts @@ -31,9 +31,18 @@ export async function POST( console.log(`[API] Callback: ${callbackId}, Session: ${sessionId}`); // Parse the callback arguments from the request body - const args = await request.json(); + const body = await request.json(); - console.log(`[API] Callback args:`, JSON.stringify(args, null, 2)); + console.log(`[API] Callback body:`, JSON.stringify(body, null, 2)); + + // Extract the actual callback arguments + // gstaudit-server always sends: { sessionId, callbackName, args: {...}, invocationNumber, timestamp } + // where args contains the actual parameter values + const callbackArgs = body.args; + if (!callbackArgs) { + console.error(`[API] Missing 'args' in callback body`); + return NextResponse.json({ error: 'Invalid callback payload' }, { status: 400 }); + } // Determine if this is a server-side or client-side callback // Server sessionIds start with "gstaudit-" (connection IDs) @@ -63,11 +72,8 @@ export async function POST( return NextResponse.json({ result: null }); } - // Extract the args (actual callback arguments) - // girest/gstaudit-server sends: { sessionId, callbackName, args: {...}, invocationNumber, timestamp } - // args is now a dict with parameter names as keys - const callbackArgs = args.args || args; - const invocationNumber = args.invocationNumber || 0; + // Extract invocation number for creating unique invocation ID + const invocationNumber = body.invocationNumber || 0; // Create unique invocation ID by combining callbackId with invocationNumber // This allows multiple concurrent invocations of the same callback registration @@ -123,7 +129,7 @@ export async function POST( } // Execute the callback and return the result (for sync callbacks) - const result = await handler.executeCallback(callbackId, args); + const result = await handler.executeCallback(callbackId, callbackArgs); console.log(`[API] Server callback ${callbackId} executed with result:`, result); From 22b24300eb3e2cfcfb5f78c811555ec646a91b66 Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 11:09:32 +0100 Subject: [PATCH 07/13] gstaudit: Implement a new log component --- gstaudit/app/pipeline/page.tsx | 26 ++- .../components/logs/LogCategorySelector.tsx | 201 ++++++++++++++++++ gstaudit/components/logs/LogViewer.tsx | 133 ++++++++++++ gstaudit/components/logs/LogWatcher.tsx | 86 ++++++++ gstaudit/components/logs/index.ts | 5 + gstaudit/hooks/index.ts | 4 +- gstaudit/hooks/useLogRegistry.ts | 183 ++++++++++++++++ gstaudit/lib/server/websocket-handler.ts | 86 ++++++++ 8 files changed, 722 insertions(+), 2 deletions(-) create mode 100644 gstaudit/components/logs/LogCategorySelector.tsx create mode 100644 gstaudit/components/logs/LogViewer.tsx create mode 100644 gstaudit/components/logs/LogWatcher.tsx create mode 100644 gstaudit/components/logs/index.ts create mode 100644 gstaudit/hooks/useLogRegistry.ts diff --git a/gstaudit/app/pipeline/page.tsx b/gstaudit/app/pipeline/page.tsx index df93e21..c37365e 100644 --- a/gstaudit/app/pipeline/page.tsx +++ b/gstaudit/app/pipeline/page.tsx @@ -8,6 +8,7 @@ import { ElementTreeManager, FactoryTreeManager } from '@/lib'; import { GstPipeline, Gst } from '@/lib/gst'; import { useSession } from '@/lib/SessionContext'; import { PipelineGraph, PipelineTreeView, StatusBar, PipelineSelector, ObjectDetails, FactoriesTreeView, FactoryDetail, PipelineControl } from '@/components'; +import { LogWatcher } from '@/components/logs'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { Button, Box, Typography, Tabs, Tab } from '@mui/material'; import AccountTreeIcon from '@mui/icons-material/AccountTree'; @@ -248,7 +249,7 @@ export default function PipelinePage() { ) : (
- +
@@ -355,6 +356,29 @@ export default function PipelinePage() { + + + + {/* Watchers Panel - Horizontal, resizable, above status bar */} + +
+ {/* Tabs */} +
+ + {/* Future tabs: Bus, etc. */} +
+ + {/* Content */} +
+ +
+
+
+ diff --git a/gstaudit/components/logs/LogCategorySelector.tsx b/gstaudit/components/logs/LogCategorySelector.tsx new file mode 100644 index 0000000..b372087 --- /dev/null +++ b/gstaudit/components/logs/LogCategorySelector.tsx @@ -0,0 +1,201 @@ +/** + * LogCategorySelector Component + * + * Displays and manages GStreamer debug log categories. + * Allows setting log levels per category. + * + * Communication: + * - REST API: Fetch categories from server + * - WebSocket: Send level changes to server + * - WebSocket: Receive current category states from server (TODO) + */ + +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Gst, GstDebugCategory, type GstDebugLevelValue } from '@/lib/gst'; +import { useSession } from '@/lib/SessionContext'; +import { useLogRegistry } from '@/hooks/useLogRegistry'; +import { Select, MenuItem, Button, SelectChangeEvent } from '@mui/material'; + +const DEBUG_LEVELS: GstDebugLevelValue[] = [ + "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" +]; + +export interface CategoryData { + ptr: string; + name: string; + description: string; + level: GstDebugLevelValue; +} + +interface LogCategorySelectorProps { + onCategoryLevelChange?: (categoryPtr: string, level: GstDebugLevelValue) => void; +} + +export function LogCategorySelector({ + onCategoryLevelChange +}: LogCategorySelectorProps) { + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState(''); + + // Get session from context + const { sessionId, callbackSecret, connection } = useSession(); + + // Get setCategoryLevel from log registry + const { setCategoryLevel: sendCategoryLevel } = useLogRegistry({ + sessionId, + callbackSecret, + onLog: () => {}, // We don't need logs here, just the setCategoryLevel function + }); + + const fetchCategories = useCallback(async () => { + try { + setLoading(true); + setStatus('Fetching debug categories...'); + + // Fetch the linked list of categories (GLibSList) + let list = await Gst.debug_get_all_categories(); + const fetchedCategories: CategoryData[] = []; + + // Traverse the list + while (list && list.ptr && list.ptr !== '0x0') { + try { + const dataPtr = await list.get_data(); + if (dataPtr && dataPtr !== '0x0') { + // Instantiate the category from the pointer + const cat = await GstDebugCategory.create(dataPtr, 'none'); + // Fetch details in parallel + const [name, desc, level] = await Promise.all([ + cat.get_name(), + cat.get_description(), + cat.get_threshold() + ]); + fetchedCategories.push({ + ptr: dataPtr, + name, + description: desc, + level + }); + } + const next = await list.get_next(); + // Break if there is no valid next node + if (!next || !next.ptr || next.ptr === '0x0') break; + list = next; + } catch (e) { + console.warn("Error fetching category node", e); + break; + } + } + + fetchedCategories.sort((a, b) => a.name.localeCompare(b.name)); + setCategories(fetchedCategories); + setStatus(`Loaded ${fetchedCategories.length} categories`); + } catch (error) { + console.error('Error fetching categories:', error); + setStatus('Error loading categories'); + } finally { + setLoading(false); + } + }, []); + + // Fetch categories only on first mount + useEffect(() => { + if (categories.length === 0) { + fetchCategories(); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const handleLevelChange = async (ptr: string, newLevel: GstDebugLevelValue) => { + // Update UI optimistically + setCategories(prev => prev.map(c => + c.ptr === ptr ? { ...c, level: newLevel } : c + )); + + // Send to server via WebSocket + sendCategoryLevel(ptr, newLevel); + + // Notify parent (for any additional logic) + onCategoryLevelChange?.(ptr, newLevel); + }; + + const handleSelectChange = (ptr: string) => (event: SelectChangeEvent) => { + handleLevelChange(ptr, event.target.value as GstDebugLevelValue); + }; + + return ( +
+ {/* Header */} +
+
+
+

Categories ({categories.length})

+ {status &&

{status}

} +
+ +
+
+ + {/* Categories list */} +
+ {categories.length === 0 && !loading && ( +
+ No categories found. Click Refresh. +
+ )} + {categories.map((cat) => ( +
+
+
+ {cat.name} +
+
+ {cat.description} +
+
+ +
+ ))} +
+
+ ); +} diff --git a/gstaudit/components/logs/LogViewer.tsx b/gstaudit/components/logs/LogViewer.tsx new file mode 100644 index 0000000..23ca3bd --- /dev/null +++ b/gstaudit/components/logs/LogViewer.tsx @@ -0,0 +1,133 @@ +/** + * LogViewer Component + * + * Displays live logs from a GStreamer connection. + * Connects via WebSocket and receives logs in real-time. + */ + +'use client'; + +import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { useLogRegistry, type LogEntry } from '@/hooks/useLogRegistry'; +import { useSession } from '@/lib/SessionContext'; + +interface LogViewerProps { + onStartLogging?: () => void; + onStopLogging?: () => void; +} + +export interface LogViewerHandle { + clearLogs: () => void; +} + +interface LogEntryWithObjectName extends LogEntry { + objectName?: string | null; +} + +export const LogViewer = forwardRef(({ + onStartLogging, + onStopLogging +}, ref) => { + const [logs, setLogs] = useState([]); + const [isLogging, setIsLogging] = useState(false); + const logsEndRef = useRef(null); + + // Get session from context + const { sessionId, callbackSecret, connection } = useSession(); + + // Handle incoming logs + const handleLog = async (log: LogEntry) => { + // Fetch object name if GstObject exists + let objectName: string | null | undefined; + if (log.object) { + try { + objectName = await log.object.get_name(); + } catch (error) { + console.error('Failed to get object name:', error); + objectName = undefined; + } + } + + setLogs(prev => { + const updated = [...prev, { ...log, objectName }]; + // Keep last 500 logs + return updated.slice(-500); + }); + }; + + // Connect to log stream + const { isConnected } = useLogRegistry({ + sessionId, + callbackSecret, + onLog: handleLog, + }); + + // Auto-scroll to bottom when new logs arrive + useEffect(() => { + if (isLogging && logsEndRef.current) { + logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [logs, isLogging]); + + const handleToggleLogging = () => { + if (isLogging) { + setIsLogging(false); + onStopLogging?.(); + } else { + setIsLogging(true); + onStartLogging?.(); + } + }; + + const handleClear = () => { + setLogs([]); + }; + + // Expose clearLogs method to parent via ref + useImperativeHandle(ref, () => ({ + clearLogs: handleClear + })); + + return ( +
+ {/* Log output */} +
+ {logs.length > 0 ? ( +
+ {logs.map((log, idx) => ( +
+ + {log.level.toUpperCase().padEnd(8)} + + {log.category.padEnd(20)} + {log.file}:{log.line}:{log.function} + {log.objectName && <{log.objectName}>} + {log.message} +
+ ))} +
+
+ ) : ( +
+ + + +

No logs captured.

+
+ )} +
+
+ ); +}); + +LogViewer.displayName = 'LogViewer'; diff --git a/gstaudit/components/logs/LogWatcher.tsx b/gstaudit/components/logs/LogWatcher.tsx new file mode 100644 index 0000000..c80d97b --- /dev/null +++ b/gstaudit/components/logs/LogWatcher.tsx @@ -0,0 +1,86 @@ +/** + * LogWatcher Component + * + * Wrapper component that contains LogViewer and LogCategorySelector. + * Provides a header with controls for clearing logs and showing categories. + */ + +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { LogViewer, type LogViewerHandle } from './LogViewer'; +import { LogCategorySelector } from './LogCategorySelector'; +import { Panel, PanelGroup, PanelResizeHandle, ImperativePanelHandle } from 'react-resizable-panels'; +import { IconButton } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import SettingsIcon from '@mui/icons-material/Settings'; + +export function LogWatcher() { + const [showCategories, setShowCategories] = useState(false); + const logViewerRef = useRef(null); + const categoriesPanelRef = useRef(null); + + const handleClear = () => { + logViewerRef.current?.clearLogs(); + }; + + const toggleCategories = () => { + setShowCategories(!showCategories); + }; + + // Collapse/expand the panel when showCategories changes + useEffect(() => { + if (categoriesPanelRef.current) { + if (showCategories) { + categoriesPanelRef.current.expand(); + } else { + categoriesPanelRef.current.collapse(); + } + } + }, [showCategories]); + + return ( +
+ {/* Header with icons */} +
+ + + + + + +
+ + {/* Content area with LogViewer and optional LogCategorySelector */} +
+ + + + + + + + + +
+
+ ); +} diff --git a/gstaudit/components/logs/index.ts b/gstaudit/components/logs/index.ts new file mode 100644 index 0000000..41bd5cf --- /dev/null +++ b/gstaudit/components/logs/index.ts @@ -0,0 +1,5 @@ +export { LogViewer } from './LogViewer'; +export { LogCategorySelector } from './LogCategorySelector'; +export { LogWatcher } from './LogWatcher'; +export type { CategoryData } from './LogCategorySelector'; +export type { LogViewerHandle } from './LogViewer'; diff --git a/gstaudit/hooks/index.ts b/gstaudit/hooks/index.ts index 28a4536..01a2e34 100644 --- a/gstaudit/hooks/index.ts +++ b/gstaudit/hooks/index.ts @@ -1,2 +1,4 @@ export { useWebSocket } from './useWebSocket'; -export { useCallbackRegistry } from './useCallbackRegistry'; \ No newline at end of file +export { useCallbackRegistry } from './useCallbackRegistry'; +export { useLogRegistry } from './useLogRegistry'; +export type { LogEntry } from './useLogRegistry'; \ No newline at end of file diff --git a/gstaudit/hooks/useLogRegistry.ts b/gstaudit/hooks/useLogRegistry.ts new file mode 100644 index 0000000..269be5f --- /dev/null +++ b/gstaudit/hooks/useLogRegistry.ts @@ -0,0 +1,183 @@ +/** + * Log Registry Hook + * + * Manages log streaming from the server via WebSocket. + * Similar architecture to useCallbackRegistry. + * + * Communication flow: + * 1. Connects to WebSocket server + * 2. Subscribes to logs on mount + * 3. Receives 'log' messages from server + * 4. Invokes callback for each log received + * 5. Unsubscribes on unmount + */ + +import { useCallback, useRef, useEffect } from 'react'; +import { useWebSocket, WebSocketMessage } from './useWebSocket'; +import { GstObject, type GstDebugLevelValue } from '@/lib/gst'; + +export interface LogEntry { + timestamp: number; + category: string; + level: GstDebugLevelValue; + file: string; + function: string; + line: number; + object: GstObject | null; // GObject (converted from pointer, properly ref'd) + message: string; +} + +interface RawLogEntry { + timestamp: number; + category: string; + level: GstDebugLevelValue; + file: string; + function: string; + line: number; + object: string | null; // Raw pointer from server + message: string; +} + +interface LogMessage extends WebSocketMessage { + type: 'log'; + sessionId: string; + data: RawLogEntry; +} + +interface UseLogRegistryOptions { + sessionId: string; + callbackSecret: string; + onLog: (log: LogEntry) => void; + wsUrl?: string; + autoConnect?: boolean; + onConnect?: () => void; + onDisconnect?: () => void; + onError?: (error: Error) => void; +} + +interface LogRegistryReturn { + isConnected: boolean; + reconnect: () => void; + setCategoryLevel: (categoryPtr: string, level: GstDebugLevelValue) => void; +} + +/** + * Hook for receiving logs via WebSocket + * + * This hook: + * 1. Connects to the WebSocket server + * 2. Subscribes to logs on mount (auto-starts logging on server) + * 3. Listens for 'log' messages + * 4. Invokes onLog callback for each received log + * 5. Unsubscribes on unmount (stops logging on server) + */ +export function useLogRegistry( + options: UseLogRegistryOptions +): LogRegistryReturn { + const { + sessionId, + callbackSecret, + onLog, + wsUrl = 'ws://localhost:3000/ws', + autoConnect = true, + onConnect, + onDisconnect, + onError, + } = options; + + // Use a ref to store the sendMessage function + const sendMessageRef = useRef<((message: unknown) => void) | null>(null); + + // Handle log messages + const handleMessage = useCallback(async (message: WebSocketMessage) => { + // Only process 'log' messages for our connection + if (message.type !== 'log') return; + + const logMsg = message as LogMessage; + + // Check if this log is for our session + if (logMsg.sessionId !== sessionId) return; + + console.log(`[LogRegistry] Received log for session ${sessionId}:`, logMsg.data); + + // Convert raw log entry to proper LogEntry with GstObject + try { + const rawLog = logMsg.data; + let gstObject: GstObject | null = null; + + // Convert pointer to GstObject if present + if (rawLog.object) { + gstObject = await GstObject.create(rawLog.object, 'none'); + // GC will unref automatically with transfer: 'none' + } + + const logEntry: LogEntry = { + ...rawLog, + object: gstObject + }; + + // Invoke the callback with the converted log data + onLog(logEntry); + } catch (error) { + console.error(`[LogRegistry] Error processing log:`, error); + } + }, [sessionId, onLog]); + + // Use shared WebSocket with message handler + const ws = useWebSocket({ + sessionId, + wsUrl, + autoConnect, + onConnect, + onDisconnect, + onError, + onMessage: handleMessage, + }); + + // Update the ref when ws.sendMessage changes + useEffect(() => { + sendMessageRef.current = ws.sendMessage; + }, [ws.sendMessage]); + + // Subscribe to logs when connected + useEffect(() => { + if (!ws.isConnected || !sendMessageRef.current) return; + + console.log(`[LogRegistry] Subscribing to logs for session ${sessionId}`); + + // Send subscription message to server (server will auto-start logging) + sendMessageRef.current({ + type: 'subscribe-logs', + callbackSecret + }); + + // Cleanup: unsubscribe on unmount (server will stop logging) + return () => { + console.log(`[LogRegistry] Unsubscribing from logs for session ${sessionId}`); + sendMessageRef.current?.({ + type: 'unsubscribe-logs' + }); + }; + }, [ws.isConnected, sessionId, callbackSecret]); + + // Function to set category level + const setCategoryLevel = useCallback((categoryPtr: string, level: GstDebugLevelValue) => { + if (!sendMessageRef.current) { + console.warn('[LogRegistry] Cannot set category level - not connected'); + return; + } + + console.log(`[LogRegistry] Setting category ${categoryPtr} to level ${level}`); + sendMessageRef.current({ + type: 'set-category-level', + categoryPtr, + level + }); + }, []); + + return { + isConnected: ws.isConnected, + reconnect: ws.reconnect, + setCategoryLevel, + }; +} diff --git a/gstaudit/lib/server/websocket-handler.ts b/gstaudit/lib/server/websocket-handler.ts index 9ef3d85..8c8560a 100644 --- a/gstaudit/lib/server/websocket-handler.ts +++ b/gstaudit/lib/server/websocket-handler.ts @@ -14,6 +14,7 @@ import { WebSocketServer, WebSocket } from 'ws'; import { IncomingMessage } from 'http'; import { getConnectionManager } from './connection-manager'; import { handleCallbackResponse } from '@/lib/server/callback-manager'; +import { getLogManager } from './log-manager'; // Callback response handler is now imported from callback-manager // This ensures it's always available regardless of API route reloads @@ -237,6 +238,83 @@ export function initializeWebSocketServer(wss: WebSocketServer): void { handleCallbackResponse(invocationId, message.result); } + + // Handle log subscription + else if (message.type === 'subscribe-logs') { + const { callbackSecret } = message; + if (!callbackSecret) { + console.error('[WebSocket] Missing callbackSecret in subscribe-logs message'); + return; + } + + console.log(`[WebSocket] Client ${sessionId} subscribing to logs`); + + const logManager = getLogManager(); + const wsConnection = wsManager.getConnection(sessionId); + if (!wsConnection) { + console.error(`[WebSocket] No connection found for session ${sessionId}`); + return; + } + + logManager.startLogging(sessionId, connectionId, callbackSecret, wsConnection) + .then(() => { + console.log(`[WebSocket] Successfully subscribed ${sessionId} to logs`); + ws.send(JSON.stringify({ + type: 'logs-subscribed' + })); + }) + .catch((error: Error) => { + console.error(`[WebSocket] Failed to subscribe to logs:`, error); + ws.send(JSON.stringify({ + type: 'error', + message: `Failed to subscribe to logs: ${error.message}` + })); + }); + } + + // Handle log unsubscription + else if (message.type === 'unsubscribe-logs') { + console.log(`[WebSocket] Client ${sessionId} unsubscribing from logs`); + + const logManager = getLogManager(); + logManager.stopLogging(sessionId) + .then(() => { + console.log(`[WebSocket] Successfully unsubscribed ${sessionId} from logs`); + }) + .catch((error: Error) => { + console.error(`[WebSocket] Failed to unsubscribe from logs:`, error); + }); + } + + // Handle set category level + else if (message.type === 'set-category-level') { + const { categoryPtr, level } = message; + if (!categoryPtr || !level) { + console.error('[WebSocket] Missing categoryPtr or level in set-category-level message'); + return; + } + + console.log(`[WebSocket] Client ${sessionId} setting category ${categoryPtr} to ${level}`); + + const logManager = getLogManager(); + logManager.setCategoryLevel(categoryPtr, level) + .then(() => { + console.log(`[WebSocket] Successfully set category level`); + ws.send(JSON.stringify({ + type: 'category-level-set', + categoryPtr, + level + })); + }) + .catch((error: Error) => { + console.error(`[WebSocket] Failed to set category level:`, error); + ws.send(JSON.stringify({ + type: 'error', + message: `Failed to set category level: ${error.message}` + })); + }); + } + } catch (error) { console.error('Error handling WebSocket message:', error); } @@ -245,6 +323,14 @@ export function initializeWebSocketServer(wss: WebSocketServer): void { ws.on('close', () => { console.log(`WebSocket client disconnected: ${sessionId}`); + // Stop logging if active + const logManager = getLogManager(); + if (logManager.isLogging(sessionId)) { + logManager.stopLogging(sessionId).catch((error: Error) => { + console.error('[WebSocket] Error stopping logging on disconnect:', error); + }); + } + // Unregister from WebSocketManager const wsManager = getWebSocketManager(); wsManager.unregister(sessionId); From a4a94351d1c14589965caec4903cad4ad30f61e0 Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 13:36:55 +0100 Subject: [PATCH 08/13] gstaudit-server: Add an endpoint for logs This new approach will reduce the latency on the app server communication. Include aiohttp now so we can call the callback in async mode --- gstaudit-server/gstaudit_server/app.py | 309 +++++++- gstaudit-server/gstaudit_server/script.js | 154 ++++ gstaudit-server/poetry.lock | 926 +++++++++++++++++++++- gstaudit-server/pyproject.toml | 1 + 4 files changed, 1380 insertions(+), 10 deletions(-) diff --git a/gstaudit-server/gstaudit_server/app.py b/gstaudit-server/gstaudit_server/app.py index b14c689..c1ab8e6 100644 --- a/gstaudit-server/gstaudit_server/app.py +++ b/gstaudit-server/gstaudit_server/app.py @@ -58,7 +58,57 @@ async def get_pipelines(*args, **kwargs): pipelines = _get_pipelines() return [p for p in pipelines if p["name"].isascii()] - return get_pipelines + async def register_logs(body, **kwargs): + """Endpoint for registering a log callback.""" + from connexion import request + + callback_url = body.get("url") + if not callback_url: + return {"error": "Missing 'url' parameter"}, 400 + + # Get session-id and callback-secret from headers + session_id = request.headers.get("session-id") + callback_secret = request.headers.get("callback-secret") + + if not session_id: + return {"error": "Missing 'session-id' header"}, 400 + + try: + callback_id = _add_log_callback( + {"url": callback_url, "session_id": session_id, "secret": callback_secret} + ) + + return {"success": True, "callbackId": callback_id}, 200 + except Exception as e: + logger.error(f"Error registering log callback: {e}") + return {"error": str(e)}, 500 + + async def unregister_logs(body, **kwargs): + """Endpoint for unregistering a log callback.""" + callback_id = body.get("callbackId") + if not callback_id: + return {"error": "Missing 'callbackId' parameter"}, 400 + + try: + removed = _remove_log_callback(callback_id) + + if removed: + return {"success": True, "message": "Callback unregistered successfully"}, 200 + else: + return {"error": "Callback ID not found"}, 404 + except Exception as e: + logger.error(f"Error unregistering log callback: {e}") + return {"error": str(e)}, 500 + + # Route to the appropriate handler + if operation_id == "get_pipelines": + return get_pipelines + elif operation_id == "register_logs": + return register_logs + elif operation_id == "unregister_logs": + return unregister_logs + + return None def _add_pipeline(pipeline_data: dict): @@ -89,6 +139,147 @@ def _get_pipelines() -> list: return list(pipelines) +def _add_log_callback(callback_data: dict) -> str: + """ + Add a log callback to the list of registered callbacks. + Returns the callback ID. + + Args: + callback_data: Dictionary containing url, session_id, secret + + Returns: + Unique callback ID + """ + import uuid + + with log_callbacks_lock: + callback_id = str(uuid.uuid4()) + callback_data["id"] = callback_id + log_callbacks.append(callback_data) + + # Register with Frida if this is the first callback + if len(log_callbacks) == 1: + logger.info("First log callback registered, enabling GStreamer logging") + try: + # Access the custom script (script.js is loaded as scripts[1], after girest.js) + result = resolver.scripts[1].exports_sync.register_log_function() + logger.info(f"Frida log registration result: {result}") + except Exception as e: + logger.error(f"Failed to register log function with Frida: {e}") + log_callbacks.remove(callback_data) + raise + + return callback_id + + +def _remove_log_callback(callback_id: str) -> bool: + """ + Remove a log callback by ID. + Returns True if removed, False if not found. + + Args: + callback_id: The callback ID to remove + + Returns: + True if removed successfully + """ + with log_callbacks_lock: + for callback in log_callbacks: + if callback.get("id") == callback_id: + log_callbacks.remove(callback) + + # Unregister from Frida if this was the last callback + if len(log_callbacks) == 0: + logger.info("Last log callback removed, disabling GStreamer logging") + try: + # Access the custom script (script.js is loaded as scripts[1], after girest.js) + result = resolver.scripts[1].exports_sync.unregister_log_function() + logger.info(f"Frida log unregistration result: {result}") + except Exception as e: + logger.error(f"Failed to unregister log function from Frida: {e}") + + return True + return False + + +def _broadcast_log(log_data: dict): + """ + Broadcast a log message to all registered callbacks asynchronously. + + Args: + log_data: The log data to broadcast + """ + import asyncio + import hashlib + import hmac + import json + from datetime import datetime, timezone + + import aiohttp + + def create_signature(payload: dict, secret: str, timestamp: str) -> str: + """Create HMAC-SHA256 signature for callback authentication""" + if not secret: + return "" + + secret_bytes = secret.encode() if isinstance(secret, str) else secret + canonical_json = json.dumps(payload, sort_keys=True, separators=(",", ":")) + message = f"{timestamp}.{canonical_json}" + signature = hmac.new(secret_bytes, message.encode("utf-8"), hashlib.sha256).hexdigest() + return signature + + async def post_to_callback(callback): + """Post log data to a single callback""" + try: + payload = { + "sessionId": callback["session_id"], + "callbackName": "log", + "args": log_data, + "timestamp": log_data.get("timestamp"), + } + + # Create headers with HMAC signature if secret is provided + timestamp = datetime.now(timezone.utc).isoformat() + headers = { + "Content-Type": "application/json", + "X-Callback-Timestamp": timestamp, + } + + if callback.get("secret"): + signature = create_signature(payload, callback["secret"], timestamp) + headers["X-Callback-Signature"] = signature + + async with aiohttp.ClientSession() as session: + async with session.post( + callback["url"], json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=5) + ) as response: + if response.status != 200: + logger.warning(f"Log callback returned status {response.status}") + except Exception as e: + logger.debug(f"Error posting log to callback: {e}") + + async def broadcast(): + """Post to all callbacks concurrently""" + with log_callbacks_lock: + callbacks = list(log_callbacks) # Copy to avoid lock during HTTP calls + + if callbacks: + tasks = [post_to_callback(cb) for cb in callbacks] + await asyncio.gather(*tasks, return_exceptions=True) + + # Run async broadcast in background thread + def run_async(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(broadcast()) + loop.close() + + import threading + + thread = threading.Thread(target=run_async, daemon=True) + thread.start() + + def _on_log(level, message): """Handle the console from js""" levels = { @@ -107,6 +298,17 @@ def _on_message(message, data): payload = message.get("payload", {}) kind = payload.get("kind") + # Handle log messages + if kind == "log": + log_data = payload.get("data") + if log_data: + logger.info( + f"Received log from Frida: {log_data.get('category')} - {log_data.get('level')} - {log_data.get('message')[:50]}" + ) + logger.info(f"Broadcasting to {len(log_callbacks)} callbacks") + _broadcast_log(log_data) + return + # Handle pipeline discovery messages if kind == "pipeline": _add_pipeline(payload["data"]) @@ -154,6 +356,10 @@ def _on_message(message, data): pipelines = [] # List of discovered pipelines pipelines_lock = threading.Lock() +# Log callback tracking +log_callbacks = [] # List of registered log callbacks {id, url, session_id, secret} +log_callbacks_lock = threading.Lock() + # Process the mode and get the PID pid = None spawned_process = None @@ -202,21 +408,110 @@ def _on_message(message, data): openapi_version="3.0.2", ) -operation = { - "summary": "", - "description": "", +# Pipelines endpoint +pipelines_operation = { + "summary": "Get GStreamer pipelines", + "description": "Get the GstPipelines available in the process", "operationId": "get_pipelines", "tags": ["GstAudit"], "parameters": [], "responses": { "200": { - "description": "Get the GstPipelines available in the process", - "content": {"application/json": {"schema": {"type": "array", "items": {"type": "integer"}}}}, + "description": "List of GStreamer pipelines", + "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object"}}}}, } }, } -gstaudit_spec.path(path="/GstAudit/pipelines", operations={"get": operation}) +# Log registration endpoint +register_logs_operation = { + "summary": "Register log callback", + "description": "Register a callback URL to receive GStreamer log messages", + "operationId": "register_logs", + "tags": ["GstAudit"], + "parameters": [ + { + "name": "session-id", + "in": "header", + "required": True, + "schema": {"type": "string"}, + "description": "Session identifier for routing", + }, + { + "name": "callback-secret", + "in": "header", + "required": False, + "schema": {"type": "string"}, + "description": "Shared secret for HMAC authentication", + }, + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {"url": {"type": "string", "description": "Callback URL to POST log messages to"}}, + "required": ["url"], + } + } + }, + }, + "responses": { + "200": { + "description": "Registration successful", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {"success": {"type": "boolean"}, "callbackId": {"type": "string"}}, + } + } + }, + }, + "400": {"description": "Invalid request"}, + "500": {"description": "Server error"}, + }, +} + +# Log unregistration endpoint +unregister_logs_operation = { + "summary": "Unregister log callback", + "description": "Unregister a previously registered log callback", + "operationId": "unregister_logs", + "tags": ["GstAudit"], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {"callbackId": {"type": "string", "description": "The callback ID to unregister"}}, + "required": ["callbackId"], + } + } + }, + }, + "responses": { + "200": { + "description": "Unregistration successful", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {"success": {"type": "boolean"}, "message": {"type": "string"}}, + } + } + }, + }, + "404": {"description": "Callback ID not found"}, + "500": {"description": "Server error"}, + }, +} + +gstaudit_spec.path(path="/GstAudit/pipelines", operations={"get": pipelines_operation}) +gstaudit_spec.path(path="/GstAudit/logs/register", operations={"post": register_logs_operation}) +gstaudit_spec.path(path="/GstAudit/logs/unregister", operations={"post": unregister_logs_operation}) app.add_api(gstaudit_spec.to_dict(), resolver=GstAuditResolver(), base_path="/gstaudit") diff --git a/gstaudit-server/gstaudit_server/script.js b/gstaudit-server/gstaudit_server/script.js index 972e2a4..10b2965 100644 --- a/gstaudit-server/gstaudit_server/script.js +++ b/gstaudit-server/gstaudit_server/script.js @@ -158,4 +158,158 @@ function shutdown() rpc.exports = { 'init': init, 'shutdown': shutdown, + 'registerLogFunction': register_log_function, + 'unregisterLogFunction': unregister_log_function, }; + +// ============================================================================ +// Logging Support +// ============================================================================ + +let logFunctionId = null; +let startTime = null; + +function register_log_function() { + if (logFunctionId !== null) { + console.log("Log function already registered"); + return { success: true, alreadyRegistered: true }; + } + + console.log("Registering log function"); + + // Find required GStreamer functions + let gst_debug_add_log_function = null; + let gst_debug_remove_log_function_by_data = null; + let gst_util_get_timestamp = null; + let gst_debug_category_get_name = null; + let gst_debug_level_get_name = null; + let gst_object_get_name = null; + let gst_debug_message_get = null; + + Process.enumerateModules().some(m => { + gst_debug_add_log_function = m.findExportByName('gst_debug_add_log_function'); + gst_debug_remove_log_function_by_data = m.findExportByName('gst_debug_remove_log_function_by_data'); + gst_util_get_timestamp = m.findExportByName('gst_util_get_timestamp'); + gst_debug_category_get_name = m.findExportByName('gst_debug_category_get_name'); + gst_debug_level_get_name = m.findExportByName('gst_debug_level_get_name'); + gst_object_get_name = m.findExportByName('gst_object_get_name'); + gst_debug_message_get = m.findExportByName('gst_debug_message_get'); + + if (gst_debug_add_log_function && gst_debug_remove_log_function_by_data && + gst_util_get_timestamp && gst_debug_category_get_name && + gst_debug_level_get_name && gst_object_get_name && gst_debug_message_get) { + return true; + } + return false; + }); + + if (!gst_debug_add_log_function) { + console.error("Could not find required GStreamer functions"); + return { success: false, error: "GStreamer functions not found" }; + } + + // Capture start time (like gst_init does) + const get_timestamp = new NativeFunction(gst_util_get_timestamp, 'uint64', []); + startTime = get_timestamp(); + console.log(`Log start time: ${startTime}`); + + // Create native functions + const add_log_function = new NativeFunction(gst_debug_add_log_function, 'void', ['pointer', 'pointer']); + const remove_log_function = new NativeFunction(gst_debug_remove_log_function_by_data, 'uint', ['pointer']); + const category_get_name = new NativeFunction(gst_debug_category_get_name, 'pointer', ['pointer']); + const level_get_name = new NativeFunction(gst_debug_level_get_name, 'pointer', ['int']); + const object_get_name = new NativeFunction(gst_object_get_name, 'pointer', ['pointer']); + const message_get = new NativeFunction(gst_debug_message_get, 'pointer', ['pointer']); + + // Create the log callback + // GstLogFunction signature: void (*GstLogFunction)(GstDebugCategory *category, GstDebugLevel level, + // const gchar *file, const gchar *function, + // gint line, GObject *object, + // GstDebugMessage *message, gpointer user_data) + const logCallback = new NativeCallback(function(category, level, file, func, line, object, message, userData) { + try { + // Get current timestamp and calculate diff + const now = get_timestamp(); + const timestamp = now - startTime; + + // Extract category name + const categoryNamePtr = category_get_name(category); + const categoryName = categoryNamePtr.readCString(); + + // Extract level name and normalize it + // GStreamer returns padded uppercase strings like "DEBUG ", "ERROR " + // Normalize to lowercase trimmed to match GstDebugLevelValue type + const levelNamePtr = level_get_name(level); + const levelName = levelNamePtr.readCString().trim().toLowerCase(); + + // Extract file, function + const fileName = file.readCString(); + const functionName = func.readCString(); + + // Extract object name (may be NULL) + let objectName = null; + if (!object.isNull()) { + const objectNamePtr = object_get_name(object); + if (!objectNamePtr.isNull()) { + objectName = objectNamePtr.readCString(); + } + } + + // Extract message + const messagePtr = message_get(message); + const messageText = messagePtr.readCString(); + + // Send to Python + send({ + kind: 'log', + data: { + timestamp: timestamp.toString(), + category: categoryName, + level: levelName, + file: fileName, + function: functionName, + line: line, + object: objectName, + message: messageText + } + }); + } catch (error) { + console.error(`Error in log callback: ${error}`); + } + }, 'void', ['pointer', 'int', 'pointer', 'pointer', 'int', 'pointer', 'pointer', 'pointer']); + + // Register with GStreamer + add_log_function(logCallback, ptr(0)); + logFunctionId = logCallback; + + console.log("Log function registered successfully"); + return { success: true }; +} + +function unregister_log_function() { + if (logFunctionId === null) { + console.log("No log function to unregister"); + return { success: true, notRegistered: true }; + } + + console.log("Unregistering log function"); + + // Find the remove function + let gst_debug_remove_log_function_by_data = null; + Process.enumerateModules().some(m => { + gst_debug_remove_log_function_by_data = m.findExportByName('gst_debug_remove_log_function_by_data'); + return gst_debug_remove_log_function_by_data !== null; + }); + + if (gst_debug_remove_log_function_by_data) { + const remove_log_function = new NativeFunction(gst_debug_remove_log_function_by_data, 'uint', ['pointer']); + const removed = remove_log_function(ptr(0)); + console.log(`Removed ${removed} log function(s)`); + } + + logFunctionId = null; + startTime = null; + + console.log("Log function unregistered successfully"); + return { success: true }; +} diff --git a/gstaudit-server/poetry.lock b/gstaudit-server/poetry.lock index 36b16e1..36e624a 100644 --- a/gstaudit-server/poetry.lock +++ b/gstaudit-server/poetry.lock @@ -15,6 +15,177 @@ files = [ [package.dependencies] typing_extensions = {version = "*", markers = "python_version < \"3.11\""} +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"}, + {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"}, + {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"}, + {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"}, + {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"}, + {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"}, + {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"}, + {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"}, + {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"}, + {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"}, + {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"}, + {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"}, + {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"}, + {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + [[package]] name = "anyio" version = "4.11.0" @@ -76,6 +247,19 @@ typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + [[package]] name = "attrs" version = "25.4.0" @@ -112,6 +296,18 @@ files = [ {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] +[[package]] +name = "cfgv" +version = "3.5.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, + {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -298,6 +494,18 @@ mock = ["jsf (>=0.10.0)"] swagger-ui = ["swagger-ui-bundle (>=1.1.0)"] uvicorn = ["uvicorn[standard] (>=0.17.6)"] +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -317,6 +525,18 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "filelock" +version = "3.25.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}, + {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}, +] + [[package]] name = "flask" version = "3.1.2" @@ -369,6 +589,146 @@ files = [ [package.dependencies] typing_extensions = {version = "*", markers = "python_version < \"3.11\""} +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + [[package]] name = "girest" version = "0.0.1" @@ -502,6 +862,21 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "identify" +version = "2.6.18" +description = "File identification library for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737"}, + {file = "identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.11" @@ -695,6 +1070,177 @@ files = [ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, +] + [[package]] name = "packaging" version = "25.0" @@ -707,6 +1253,169 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "platformdirs" +version = "4.9.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77"}, + {file = "pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "propcache" +version = "0.4.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, + {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, + {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, + {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, + {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, + {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, + {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, + {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, + {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, + {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, + {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, + {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, + {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, + {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, + {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, + {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, + {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, + {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, + {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, + {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, + {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, + {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, + {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, + {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, + {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, + {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, + {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, +] + [[package]] name = "pycairo" version = "1.29.0" @@ -753,6 +1462,26 @@ pycairo = ">=1.16" dev = ["flake8", "pytest", "pytest-cov"] docs = ["sphinx (>=4.0,<5.0)", "sphinx-rtd-theme (>=0.5,<2.0)"] +[[package]] +name = "python-discovery" +version = "1.1.3" +description = "Python interpreter discovery" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e"}, + {file = "python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -786,7 +1515,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -1027,6 +1756,34 @@ files = [ {file = "rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359"}, ] +[[package]] +name = "ruff" +version = "0.15.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"}, + {file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"}, + {file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"}, + {file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"}, + {file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"}, + {file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"}, + {file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1079,11 +1836,12 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +markers = {dev = "python_version == \"3.10\""} [[package]] name = "uritemplate" @@ -1207,6 +1965,25 @@ dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=25.3.0,<25.4.0)", "pycodestyle (>=2.11.0,<2.12.0)"] +[[package]] +name = "virtualenv" +version = "21.2.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"}, + {file = "virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} +platformdirs = ">=3.9.1,<5" +python-discovery = ">=1" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} + [[package]] name = "watchfiles" version = "1.1.1" @@ -1426,7 +2203,150 @@ MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] +[[package]] +name = "yarl" +version = "1.23.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6"}, + {file = "yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d"}, + {file = "yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb"}, + {file = "yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2"}, + {file = "yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5"}, + {file = "yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46"}, + {file = "yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34"}, + {file = "yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d"}, + {file = "yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e"}, + {file = "yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543"}, + {file = "yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957"}, + {file = "yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3"}, + {file = "yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5"}, + {file = "yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595"}, + {file = "yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090"}, + {file = "yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe"}, + {file = "yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169"}, + {file = "yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70"}, + {file = "yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4"}, + {file = "yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4"}, + {file = "yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2"}, + {file = "yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25"}, + {file = "yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f"}, + {file = "yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "38d2605d17a14915b65c47897b66d49b9129391a0b5edac8fdc75353bc8881b6" +content-hash = "813882644eff475ea2e45082440521dab12d417a4c6b01a88fc8c085b1dfe789" diff --git a/gstaudit-server/pyproject.toml b/gstaudit-server/pyproject.toml index da8fc11..e56890c 100644 --- a/gstaudit-server/pyproject.toml +++ b/gstaudit-server/pyproject.toml @@ -10,6 +10,7 @@ include = ["script.js"] python = "^3.10" connexion = { version = "^3.3.0", extras = ["flask", "swagger-ui", "uvicorn"] } girest = { path = "../girest", develop = true } +aiohttp = "^3.13.3" [tool.poetry.group.dev.dependencies] ruff = "*" From 421dbbbda6dc482a46d306364eebbda2b6943b0c Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 13:38:19 +0100 Subject: [PATCH 09/13] gstaudit: Make the logs travel through the backend This will reduce the latency in the communication --- gstaudit/app/api/logs/[callbackId]/route.ts | 74 +++++++++++++++++++ .../components/logs/LogCategorySelector.tsx | 33 ++++----- gstaudit/components/logs/LogViewer.tsx | 25 ++----- gstaudit/hooks/useLogRegistry.ts | 60 +++------------ gstaudit/lib/server/websocket-handler.ts | 29 -------- 5 files changed, 104 insertions(+), 117 deletions(-) create mode 100644 gstaudit/app/api/logs/[callbackId]/route.ts diff --git a/gstaudit/app/api/logs/[callbackId]/route.ts b/gstaudit/app/api/logs/[callbackId]/route.ts new file mode 100644 index 0000000..8dda274 --- /dev/null +++ b/gstaudit/app/api/logs/[callbackId]/route.ts @@ -0,0 +1,74 @@ +/** + * Log Callback Handler + * + * Dedicated endpoint for receiving log callbacks from gstaudit-server. + * Uses fire-and-forget pattern - broadcasts to WebSocket immediately without waiting for response. + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { getWebSocketManager } from '@/lib/server/websocket-handler'; + +/** + * POST /api/logs/[callbackId]?sessionId=xxx + * Receives log callback invocation from gstaudit-server + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ callbackId: string }> } +) { + try { + // Next.js 15: params must be awaited + const { callbackId } = await params; + + // Get sessionId from query params + const url = new URL(request.url); + const sessionId = url.searchParams.get('sessionId'); + + if (!sessionId) { + console.error(`[API Log] Missing sessionId for callback ${callbackId}`); + return NextResponse.json({ error: 'Missing sessionId' }, { status: 400 }); + } + + console.log(`[API Log] Callback: ${callbackId}, Session: ${sessionId}`); + + // Parse the callback arguments from the request body + const body = await request.json(); + + console.log(`[API Log] Callback body:`, JSON.stringify(body, null, 2)); + + // Extract the actual log data + // gstaudit-server sends: { sessionId, callbackName, args: {...}, timestamp } + const logData = body.args; + if (!logData) { + console.error(`[API Log] Missing 'args' in callback body`); + return NextResponse.json({ error: 'Invalid log payload' }, { status: 400 }); + } + + // Get WebSocket connection and broadcast log + const wsManager = getWebSocketManager(); + const connection = wsManager.getConnection(sessionId); + + if (!connection) { + console.warn(`[API Log] No WebSocket connection for session: ${sessionId}`); + // Return success anyway - logs are fire-and-forget + return NextResponse.json({ success: true }); + } + + // Broadcast log to browser (no response expected) + connection.sendMessage({ + type: 'log', + sessionId: sessionId, + data: logData + }); + + console.log(`[API Log] Broadcast to session ${sessionId}`); + return NextResponse.json({ success: true }); + + } catch (error) { + console.error('[API Log] Error handling log callback:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/gstaudit/components/logs/LogCategorySelector.tsx b/gstaudit/components/logs/LogCategorySelector.tsx index b372087..6277b92 100644 --- a/gstaudit/components/logs/LogCategorySelector.tsx +++ b/gstaudit/components/logs/LogCategorySelector.tsx @@ -14,8 +14,6 @@ import { useState, useEffect, useCallback } from 'react'; import { Gst, GstDebugCategory, type GstDebugLevelValue } from '@/lib/gst'; -import { useSession } from '@/lib/SessionContext'; -import { useLogRegistry } from '@/hooks/useLogRegistry'; import { Select, MenuItem, Button, SelectChangeEvent } from '@mui/material'; const DEBUG_LEVELS: GstDebugLevelValue[] = [ @@ -40,16 +38,6 @@ export function LogCategorySelector({ const [loading, setLoading] = useState(false); const [status, setStatus] = useState(''); - // Get session from context - const { sessionId, callbackSecret, connection } = useSession(); - - // Get setCategoryLevel from log registry - const { setCategoryLevel: sendCategoryLevel } = useLogRegistry({ - sessionId, - callbackSecret, - onLog: () => {}, // We don't need logs here, just the setCategoryLevel function - }); - const fetchCategories = useCallback(async () => { try { setLoading(true); @@ -113,11 +101,22 @@ export function LogCategorySelector({ c.ptr === ptr ? { ...c, level: newLevel } : c )); - // Send to server via WebSocket - sendCategoryLevel(ptr, newLevel); - - // Notify parent (for any additional logic) - onCategoryLevelChange?.(ptr, newLevel); + try { + // Call REST API directly to set the category level + const category = await GstDebugCategory.create(ptr, 'none'); + await category.set_threshold(newLevel); + + console.log(`[LogCategorySelector] Set category ${ptr} to level ${newLevel}`); + + // Notify parent (for any additional logic) + onCategoryLevelChange?.(ptr, newLevel); + } catch (error) { + console.error(`[LogCategorySelector] Failed to set category level:`, error); + // Revert optimistic update on error + setCategories(prev => prev.map(c => + c.ptr === ptr ? { ...c, level: categories.find(cat => cat.ptr === ptr)?.level || newLevel } : c + )); + } }; const handleSelectChange = (ptr: string) => (event: SelectChangeEvent) => { diff --git a/gstaudit/components/logs/LogViewer.tsx b/gstaudit/components/logs/LogViewer.tsx index 23ca3bd..2d21e68 100644 --- a/gstaudit/components/logs/LogViewer.tsx +++ b/gstaudit/components/logs/LogViewer.tsx @@ -20,36 +20,21 @@ export interface LogViewerHandle { clearLogs: () => void; } -interface LogEntryWithObjectName extends LogEntry { - objectName?: string | null; -} - export const LogViewer = forwardRef(({ onStartLogging, onStopLogging }, ref) => { - const [logs, setLogs] = useState([]); + const [logs, setLogs] = useState([]); const [isLogging, setIsLogging] = useState(false); const logsEndRef = useRef(null); // Get session from context const { sessionId, callbackSecret, connection } = useSession(); - // Handle incoming logs - const handleLog = async (log: LogEntry) => { - // Fetch object name if GstObject exists - let objectName: string | null | undefined; - if (log.object) { - try { - objectName = await log.object.get_name(); - } catch (error) { - console.error('Failed to get object name:', error); - objectName = undefined; - } - } - + // Handle incoming logs - data is already fully formatted! + const handleLog = (log: LogEntry) => { setLogs(prev => { - const updated = [...prev, { ...log, objectName }]; + const updated = [...prev, log]; // Keep last 500 logs return updated.slice(-500); }); @@ -111,7 +96,7 @@ export const LogViewer = forwardRef(({ {log.category.padEnd(20)} {log.file}:{log.line}:{log.function} - {log.objectName && <{log.objectName}>} + {log.object && <{log.object}>} {log.message}
))} diff --git a/gstaudit/hooks/useLogRegistry.ts b/gstaudit/hooks/useLogRegistry.ts index 269be5f..38839ac 100644 --- a/gstaudit/hooks/useLogRegistry.ts +++ b/gstaudit/hooks/useLogRegistry.ts @@ -14,34 +14,23 @@ import { useCallback, useRef, useEffect } from 'react'; import { useWebSocket, WebSocketMessage } from './useWebSocket'; -import { GstObject, type GstDebugLevelValue } from '@/lib/gst'; +import type { GstDebugLevelValue } from '@/lib/gst'; export interface LogEntry { - timestamp: number; + timestamp: string; // Nanoseconds since start as string category: string; level: GstDebugLevelValue; file: string; function: string; line: number; - object: GstObject | null; // GObject (converted from pointer, properly ref'd) - message: string; -} - -interface RawLogEntry { - timestamp: number; - category: string; - level: GstDebugLevelValue; - file: string; - function: string; - line: number; - object: string | null; // Raw pointer from server + object: string | null; message: string; } interface LogMessage extends WebSocketMessage { type: 'log'; sessionId: string; - data: RawLogEntry; + data: LogEntry; } interface UseLogRegistryOptions { @@ -58,7 +47,6 @@ interface UseLogRegistryOptions { interface LogRegistryReturn { isConnected: boolean; reconnect: () => void; - setCategoryLevel: (categoryPtr: string, level: GstDebugLevelValue) => void; } /** @@ -89,7 +77,7 @@ export function useLogRegistry( const sendMessageRef = useRef<((message: unknown) => void) | null>(null); // Handle log messages - const handleMessage = useCallback(async (message: WebSocketMessage) => { + const handleMessage = useCallback((message: WebSocketMessage) => { // Only process 'log' messages for our connection if (message.type !== 'log') return; @@ -100,26 +88,12 @@ export function useLogRegistry( console.log(`[LogRegistry] Received log for session ${sessionId}:`, logMsg.data); - // Convert raw log entry to proper LogEntry with GstObject + // Data is already fully formatted from gstaudit-server! + // No conversion needed - just pass it through try { - const rawLog = logMsg.data; - let gstObject: GstObject | null = null; - - // Convert pointer to GstObject if present - if (rawLog.object) { - gstObject = await GstObject.create(rawLog.object, 'none'); - // GC will unref automatically with transfer: 'none' - } - - const logEntry: LogEntry = { - ...rawLog, - object: gstObject - }; - - // Invoke the callback with the converted log data - onLog(logEntry); + onLog(logMsg.data); } catch (error) { - console.error(`[LogRegistry] Error processing log:`, error); + console.error(`[LogRegistry] Error in onLog callback:`, error); } }, [sessionId, onLog]); @@ -160,24 +134,8 @@ export function useLogRegistry( }; }, [ws.isConnected, sessionId, callbackSecret]); - // Function to set category level - const setCategoryLevel = useCallback((categoryPtr: string, level: GstDebugLevelValue) => { - if (!sendMessageRef.current) { - console.warn('[LogRegistry] Cannot set category level - not connected'); - return; - } - - console.log(`[LogRegistry] Setting category ${categoryPtr} to level ${level}`); - sendMessageRef.current({ - type: 'set-category-level', - categoryPtr, - level - }); - }, []); - return { isConnected: ws.isConnected, reconnect: ws.reconnect, - setCategoryLevel, }; } diff --git a/gstaudit/lib/server/websocket-handler.ts b/gstaudit/lib/server/websocket-handler.ts index 8c8560a..096fffa 100644 --- a/gstaudit/lib/server/websocket-handler.ts +++ b/gstaudit/lib/server/websocket-handler.ts @@ -286,35 +286,6 @@ export function initializeWebSocketServer(wss: WebSocketServer): void { }); } - // Handle set category level - else if (message.type === 'set-category-level') { - const { categoryPtr, level } = message; - if (!categoryPtr || !level) { - console.error('[WebSocket] Missing categoryPtr or level in set-category-level message'); - return; - } - - console.log(`[WebSocket] Client ${sessionId} setting category ${categoryPtr} to ${level}`); - - const logManager = getLogManager(); - logManager.setCategoryLevel(categoryPtr, level) - .then(() => { - console.log(`[WebSocket] Successfully set category level`); - ws.send(JSON.stringify({ - type: 'category-level-set', - categoryPtr, - level - })); - }) - .catch((error: Error) => { - console.error(`[WebSocket] Failed to set category level:`, error); - ws.send(JSON.stringify({ - type: 'error', - message: `Failed to set category level: ${error.message}` - })); - }); - } - } catch (error) { console.error('Error handling WebSocket message:', error); } From 6dcbe3533a0cbdee9a1e9ac739e29abcf1579734 Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 13:40:49 +0100 Subject: [PATCH 10/13] gstaudit: Remove old logs page --- gstaudit/app/logs/page.tsx | 373 ------------------------------------- 1 file changed, 373 deletions(-) delete mode 100644 gstaudit/app/logs/page.tsx diff --git a/gstaudit/app/logs/page.tsx b/gstaudit/app/logs/page.tsx deleted file mode 100644 index abe82af..0000000 --- a/gstaudit/app/logs/page.tsx +++ /dev/null @@ -1,373 +0,0 @@ -'use client'; - -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { useSession } from '@/lib/SessionContext'; -import { - Gst, - GstDebugCategory, - GstDebugMessage, - GObjectObject, - type GstDebugLevelValue, - type GstLogFunction -} from '@/lib/gst'; - -const DEBUG_LEVELS: GstDebugLevelValue[] = [ - "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" -]; - -interface CategoryData { - ptr: string; - name: string; - description: string; - level: GstDebugLevelValue; -} - -interface LogEntry { - timestamp: number; - category: string; - level: GstDebugLevelValue; - file: string; - function: string; - line: number; - message: string; -} - -export default function LogsPage() { - const [categories, setCategories] = useState([]); - const [loading, setLoading] = useState(false); - const [logs, setLogs] = useState([]); - const [isLogging, setIsLogging] = useState(false); - const [status, setStatus] = useState(''); - const logsEndRef = useRef(null); - const logFunctionIdRef = useRef(null); - const logsBufferRef = useRef([]); - const flushIntervalRef = useRef(null); - - const router = useRouter(); - const { connection } = useSession(); - - // Redirect to home if not connected to a server - useEffect(() => { - if (!connection) { - console.log('[Logs] No connection configured, redirecting to home'); - router.push('/'); - } - }, [connection, router]); - - // Función para vaciar el buffer y actualizar el estado - const flushLogs = useCallback(() => { - // 1. Capturamos el contenido actual del buffer (copia síncrona inmediata) - const bufferToFlush = logsBufferRef.current; - - // 2. Si está vacío, no hacemos nada - if (bufferToFlush.length === 0) return; - - // 3. Vaciamos el buffer GLOBAL inmediatamente para que los nuevos logs entren en un array limpio - // (Importante: asignamos un nuevo array vacío, no usamos .length = 0 para no mutar lo que pasamos a React) - logsBufferRef.current = []; - - // 4. Actualizamos el estado usando la COPIA capturada (bufferToFlush) - setLogs(prev => { - const combined = [...prev, ...bufferToFlush]; - return combined.slice(-500); - }); - }, []); - - // Configurar el intervalo de flush cuando se activa el logging - useEffect(() => { - if (isLogging) { - // Flush cada 100ms para balance entre rendimiento y actualización visual - flushIntervalRef.current = setInterval(flushLogs, 100); - } else { - if (flushIntervalRef.current) { - clearInterval(flushIntervalRef.current); - flushIntervalRef.current = null; - } - // Flush final al detener - flushLogs(); - } - - return () => { - if (flushIntervalRef.current) { - clearInterval(flushIntervalRef.current); - } - }; - }, [isLogging, flushLogs]); - - const fetchCategories = useCallback(async () => { - try { - setLoading(true); - setStatus('Fetching debug categories...'); - // Fetch the linked list of categories (GLibSList) - let list = await Gst.debug_get_all_categories(); - - const fetchedCategories: CategoryData[] = []; - // Traverse the list (this can be slow if there are many categories, optimizable on the backend) - // Note: In a real implementation, it would be ideal to have an endpoint that returns the entire JSON array - while (list && list.ptr && list.ptr !== '0x0') { - try { - const dataPtr = await list.get_data(); - if (dataPtr && dataPtr !== '0x0') { - // Instantiate the category from the pointer - const cat = new GstDebugCategory(dataPtr, 'none'); - // Fetch details in parallel - const [name, desc, level] = await Promise.all([ - cat.get_name(), - cat.get_description(), - cat.get_threshold() - ]); - fetchedCategories.push({ - ptr: dataPtr, - name, - description: desc, - level - }); - } - const next = await list.get_next(); - // Break if there is no valid next node - if (!next || !next.ptr || next.ptr === '0x0') break; - list = next; - } catch (e) { - console.warn("Error fetching category node", e); - break; - } - } - fetchedCategories.sort((a, b) => a.name.localeCompare(b.name)); - setCategories(fetchedCategories); - setStatus(`Loaded ${fetchedCategories.length} categories`); - } catch (error) { - console.error('Error fetching categories:', error); - setStatus('Error loading categories'); - } finally { - setLoading(false); - } - }, []); - - // Fetch categories on mount - useEffect(() => { - fetchCategories(); - }, [fetchCategories]); - - const handleLevelChange = async (ptr: string, newLevel: GstDebugLevelValue) => { - try { - // Re-instantiate the category by pointer to change its level - const cat = new GstDebugCategory(ptr, 'none'); - await cat.set_threshold(newLevel); - - setCategories(prev => prev.map(c => - c.ptr === ptr ? { ...c, level: newLevel } : c - )); - } catch (error) { - console.error(`Error setting threshold for ${ptr}:`, error); - alert('Failed to update debug level'); - } - }; - - const toggleLogging = async () => { - if (isLogging) { - setIsLogging(false); - setStatus('Logging stopped'); - // Unregister log function if active - if (logFunctionIdRef.current !== null) { - try { - // Note: debug_remove_log_function might need implementation details - logFunctionIdRef.current = null; - } catch (error) { - console.error('Error removing log function:', error); - } - } - } else { - try { - // Register the log function callback - const logFunction: GstLogFunction = ( - category: GstDebugCategory, - level: GstDebugLevelValue, - file: string, - function_: string, - line: number, - object: GObjectObject, - message: GstDebugMessage, - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - user_data: any - ) => { - // Process asynchronously but don't block the callback - (async () => { - try { - // Fetch the message text and category name - const [messageText, categoryName] = await Promise.all([ - message.get(), - category.get_name() - ]); - - const logEntry: LogEntry = { - timestamp: Date.now(), - category: categoryName, - level, - file, - function: function_, - line, - message: messageText - }; - - // Ingesta rápida: solo agregar al buffer, sin actualizar estado - logsBufferRef.current.push(logEntry); - } catch (e) { - console.error('Error processing log entry:', e); - } - })(); - }; - - const callbackId = await Gst.debug_add_log_function(logFunction); - logFunctionIdRef.current = callbackId; - setIsLogging(true); - setStatus('Logging active...'); - } catch (error) { - console.error('Error starting logger:', error); - alert('Failed to register log function'); - } - } - }; - - // Auto-scroll optimizado: solo cuando hay nuevos logs y el logging está activo - useEffect(() => { - if (logsEndRef.current && isLogging && logs.length > 0) { - // Usar requestAnimationFrame para optimizar el scroll - requestAnimationFrame(() => { - logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }); - } - }, [logs, isLogging]); - - // Don't render if not connected (will be redirected by useEffect above) - if (!connection) { - return null; - } - - return ( -
-
-
-

GStreamer Debug Logs

-

Configure levels and view output

-
-
- - Back to Home - - -
-
- -
- {/* Left panel: List of categories */} -
-
-

Categories ({categories.length})

-

{status}

-
-
- {categories.length === 0 && !loading && ( -
- No categories found. Click Refresh. -
- )} - {categories.map((cat) => ( -
-
-
- {cat.name} -
-
- {cat.description} -
-
- -
- ))} -
-
- - {/* Right panel: Live logs */} -
-
-

Live Output

-
- - -
-
-
- {logs.length > 0 ? ( -
- {logs.map((log, idx) => ( -
- - [{log.level}] - - {log.category} - {log.file}:{log.line} - {log.message} -
- ))} -
-
- ) : ( -
- -

No logs captured.

-

Click "Start" to begin logging.

-
- )} -
-
-
-
- ); -} \ No newline at end of file From 00f3a83bb4ba5aee90da158cfd4b8b89fe9da957 Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 16:20:02 +0100 Subject: [PATCH 11/13] gstaudit: Improve the logging system --- .../components/logs/LogCategorySelector.tsx | 228 +++++++++++++----- gstaudit/components/logs/LogViewer.tsx | 43 +++- gstaudit/components/logs/LogWatcher.tsx | 117 ++++++++- 3 files changed, 315 insertions(+), 73 deletions(-) diff --git a/gstaudit/components/logs/LogCategorySelector.tsx b/gstaudit/components/logs/LogCategorySelector.tsx index 6277b92..e5aff63 100644 --- a/gstaudit/components/logs/LogCategorySelector.tsx +++ b/gstaudit/components/logs/LogCategorySelector.tsx @@ -12,14 +12,34 @@ 'use client'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { Gst, GstDebugCategory, type GstDebugLevelValue } from '@/lib/gst'; -import { Select, MenuItem, Button, SelectChangeEvent } from '@mui/material'; +import { IconButton, TextField, ToggleButton, ToggleButtonGroup } from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; const DEBUG_LEVELS: GstDebugLevelValue[] = [ "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" ]; +// Color mapping for debug levels +const LEVEL_COLORS: Record = { + none: '#94a3b8', // slate-400 + error: '#ef4444', // red-500 + warning: '#f97316', // orange-500 + fixme: '#eab308', // yellow-500 + info: '#3b82f6', // blue-500 + debug: '#22c55e', // green-500 + log: '#06b6d4', // cyan-500 + trace: '#a855f7', // purple-500 + memdump: '#ec4899', // pink-500 + count: '#94a3b8', // slate-400 (fallback, shouldn't be used) +}; + +// Get level index for comparison +const getLevelIndex = (level: GstDebugLevelValue): number => { + return DEBUG_LEVELS.indexOf(level); +}; + export interface CategoryData { ptr: string; name: string; @@ -36,12 +56,33 @@ export function LogCategorySelector({ }: LogCategorySelectorProps) { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(false); - const [status, setStatus] = useState(''); + const [showActiveOnly, setShowActiveOnly] = useState(false); + const [filterText, setFilterText] = useState(''); + + // Computed filtered categories + const filteredCategories = useMemo(() => { + let result = categories; + + // Filter by active status (level !== 'none') + if (showActiveOnly) { + result = result.filter(cat => cat.level !== 'none'); + } + + // Filter by text (name or description) + if (filterText) { + const search = filterText.toLowerCase(); + result = result.filter(cat => + cat.name.toLowerCase().includes(search) || + cat.description.toLowerCase().includes(search) + ); + } + + return result; + }, [categories, showActiveOnly, filterText]); const fetchCategories = useCallback(async () => { try { setLoading(true); - setStatus('Fetching debug categories...'); // Fetch the linked list of categories (GLibSList) let list = await Gst.debug_get_all_categories(); @@ -79,10 +120,8 @@ export function LogCategorySelector({ fetchedCategories.sort((a, b) => a.name.localeCompare(b.name)); setCategories(fetchedCategories); - setStatus(`Loaded ${fetchedCategories.length} categories`); } catch (error) { console.error('Error fetching categories:', error); - setStatus('Error loading categories'); } finally { setLoading(false); } @@ -119,82 +158,149 @@ export function LogCategorySelector({ } }; - const handleSelectChange = (ptr: string) => (event: SelectChangeEvent) => { - handleLevelChange(ptr, event.target.value as GstDebugLevelValue); - }; - return (
- {/* Header */} -
-
-
-

Categories ({categories.length})

- {status &&

{status}

} -
- -
-
- {/* Categories list */} -
- {categories.length === 0 && !loading && ( -
- No categories found. Click Refresh. +
+ {filteredCategories.length === 0 && !loading && ( +
+ {categories.length === 0 + ? 'No categories found. Click Refresh.' + : 'No categories match the current filter.' + }
)} - {categories.map((cat) => ( + {filteredCategories.map((cat) => (
-
-
+ {/* Category name and description */} +
+
{cat.name}
-
- {cat.description} -
+ {cat.description && ( +
+ {cat.description} +
+ )}
- +
))}
+ + {/* Status bar with filters */} +
+
+ {/* Toggle: All / Active Only */} + { + if (value !== null) { + setShowActiveOnly(value === 'active'); + } + }} + size="small" + sx={{ + height: '22px', + '& .MuiToggleButton-root': { + fontSize: '9px', + padding: '2px 8px', + lineHeight: 1, + textTransform: 'none', + border: '1px solid', + borderColor: 'divider', + } + }} + > + All + Active + + + {/* Text filter */} + setFilterText(e.target.value)} + size="small" + sx={{ + flex: 1, + '& .MuiInputBase-root': { + height: '22px', + fontSize: '10px', + }, + '& .MuiInputBase-input': { + padding: '2px 6px', + } + }} + /> + + {/* Count indicator and refresh button */} + + {filteredCategories.length} / {categories.length} + + + + +
+
); } diff --git a/gstaudit/components/logs/LogViewer.tsx b/gstaudit/components/logs/LogViewer.tsx index 2d21e68..aaeabc4 100644 --- a/gstaudit/components/logs/LogViewer.tsx +++ b/gstaudit/components/logs/LogViewer.tsx @@ -7,13 +7,20 @@ 'use client'; -import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { useState, useRef, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react'; import { useLogRegistry, type LogEntry } from '@/hooks/useLogRegistry'; import { useSession } from '@/lib/SessionContext'; +import type { GstDebugLevelValue } from '@/lib/gst'; + +const DEBUG_LEVELS: GstDebugLevelValue[] = [ + "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" +]; interface LogViewerProps { onStartLogging?: () => void; onStopLogging?: () => void; + filterText?: string; + enabledLevels?: Set; } export interface LogViewerHandle { @@ -22,7 +29,9 @@ export interface LogViewerHandle { export const LogViewer = forwardRef(({ onStartLogging, - onStopLogging + onStopLogging, + filterText = '', + enabledLevels = new Set(DEBUG_LEVELS), }, ref) => { const [logs, setLogs] = useState([]); const [isLogging, setIsLogging] = useState(false); @@ -31,6 +40,30 @@ export const LogViewer = forwardRef(({ // Get session from context const { sessionId, callbackSecret, connection } = useSession(); + // Filter logs based on text and level + const filteredLogs = useMemo(() => { + return logs.filter(log => { + // Filter by level + if (!enabledLevels.has(log.level)) { + return false; + } + + // Filter by text (search in category, file, function, object, message) + if (filterText) { + const search = filterText.toLowerCase(); + return ( + log.category.toLowerCase().includes(search) || + log.file.toLowerCase().includes(search) || + log.function.toLowerCase().includes(search) || + log.message.toLowerCase().includes(search) || + (log.object && log.object.toLowerCase().includes(search)) + ); + } + + return true; + }); + }, [logs, filterText, enabledLevels]); + // Handle incoming logs - data is already fully formatted! const handleLog = (log: LogEntry) => { setLogs(prev => { @@ -77,9 +110,9 @@ export const LogViewer = forwardRef(({
{/* Log output */}
- {logs.length > 0 ? ( + {filteredLogs.length > 0 ? (
- {logs.map((log, idx) => ( + {filteredLogs.map((log, idx) => (
(({ -

No logs captured.

+

{logs.length === 0 ? 'No logs captured.' : 'No logs match the current filter.'}

)}
diff --git a/gstaudit/components/logs/LogWatcher.tsx b/gstaudit/components/logs/LogWatcher.tsx index c80d97b..a66f75d 100644 --- a/gstaudit/components/logs/LogWatcher.tsx +++ b/gstaudit/components/logs/LogWatcher.tsx @@ -11,12 +11,21 @@ import { useState, useRef, useEffect } from 'react'; import { LogViewer, type LogViewerHandle } from './LogViewer'; import { LogCategorySelector } from './LogCategorySelector'; import { Panel, PanelGroup, PanelResizeHandle, ImperativePanelHandle } from 'react-resizable-panels'; -import { IconButton } from '@mui/material'; +import { IconButton, TextField, ToggleButton, ToggleButtonGroup } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import SettingsIcon from '@mui/icons-material/Settings'; +import type { GstDebugLevelValue } from '@/lib/gst'; + +const DEBUG_LEVELS: GstDebugLevelValue[] = [ + "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" +]; export function LogWatcher() { const [showCategories, setShowCategories] = useState(false); + const [filterText, setFilterText] = useState(''); + const [enabledLevels, setEnabledLevels] = useState>( + new Set(DEBUG_LEVELS) + ); const logViewerRef = useRef(null); const categoriesPanelRef = useRef(null); @@ -28,6 +37,14 @@ export function LogWatcher() { setShowCategories(!showCategories); }; + const toggleAllLevels = () => { + if (enabledLevels.size === DEBUG_LEVELS.length) { + setEnabledLevels(new Set()); + } else { + setEnabledLevels(new Set(DEBUG_LEVELS)); + } + }; + // Collapse/expand the panel when showCategories changes useEffect(() => { if (categoriesPanelRef.current) { @@ -41,8 +58,9 @@ export function LogWatcher() { return (
- {/* Header with icons */} -
+ {/* Header with filter controls */} +
+ {/* Clear button */} + + {/* Text filter */} + setFilterText(e.target.value)} + size="small" + sx={{ + flex: 1, + '& .MuiInputBase-root': { + height: '28px', + fontSize: '11px', + }, + '& .MuiInputBase-input': { + padding: '4px 8px', + } + }} + /> + + {/* Level filter toggle buttons */} + { + // Prevent empty selection - always need at least one level + if (newLevels.length > 0) { + setEnabledLevels(new Set(newLevels)); + } + }} + size="small" + sx={{ + height: '28px', + '& .MuiToggleButton-root': { + fontSize: '10px', + padding: '4px 8px', + lineHeight: 1, + textTransform: 'capitalize', + border: '1px solid', + borderColor: 'divider', + minWidth: '60px', + '&.Mui-selected': { + fontWeight: 600, + }, + } + }} + > + {DEBUG_LEVELS.filter(l => l !== 'count').map((level) => ( + + {level.charAt(0).toUpperCase() + level.slice(1)} + + ))} + + + {/* ALL toggle button */} + + ALL + + + {/* Categories toggle button */} - - + + From a8578bc38029741192932b6b19ce037153c06b6f Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 19:30:47 +0100 Subject: [PATCH 12/13] gstaudit: Properly sync the object among components --- gstaudit/app/pipeline/page.tsx | 36 ++++- gstaudit/components/PipelineGraph.tsx | 10 ++ gstaudit/components/logs/LogViewer.tsx | 23 ++- gstaudit/components/logs/LogWatcher.tsx | 51 ++++++- gstaudit/lib/server/log-manager.ts | 184 ++++++++++++++++++++++++ 5 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 gstaudit/lib/server/log-manager.ts diff --git a/gstaudit/app/pipeline/page.tsx b/gstaudit/app/pipeline/page.tsx index c37365e..a74b637 100644 --- a/gstaudit/app/pipeline/page.tsx +++ b/gstaudit/app/pipeline/page.tsx @@ -70,6 +70,37 @@ export default function PipelinePage() { } }; + // Handle element selection by name (from logs) + const handleElementSelectByName = async (elementName: string | null) => { + // Handle deselection + if (!elementName) { + setSelectedElement(null); + setSelectedFactory(null); + return; + } + + if (!elementTreeManagerRef.current) return; + + // Find element by name in the tree + const findElementByName = (node: ElementTree): ElementTree | null => { + if (node.name === elementName) return node; + for (const child of node.children) { + const found = findElementByName(child); + if (found) return found; + } + return null; + }; + + const root = elementTreeManagerRef.current.getRoot(); + if (root) { + const found = findElementByName(root); + if (found) { + await handleElementSelect(found); + return; + } + } + }; + const fetchPipelines = async () => { if (!connection) { console.error('No connection configured'); @@ -374,7 +405,10 @@ export default function PipelinePage() { {/* Content */}
- +
diff --git a/gstaudit/components/PipelineGraph.tsx b/gstaudit/components/PipelineGraph.tsx index 571496f..70ab830 100644 --- a/gstaudit/components/PipelineGraph.tsx +++ b/gstaudit/components/PipelineGraph.tsx @@ -99,6 +99,16 @@ const PipelineGraphInner: React.FC = ({ treeManager, selecte } }, [selectedElement, nodes, fitView]); + // Update node selection state when selectedElement changes + useEffect(() => { + setNodes((nds) => + nds.map((node) => ({ + ...node, + selected: selectedElement ? node.id === selectedElement.id : false, + })) + ); + }, [selectedElement, setNodes]); + // Handle node click const handleNodeClick = (_event: React.MouseEvent, node: Node) => { if (onElementSelect) { diff --git a/gstaudit/components/logs/LogViewer.tsx b/gstaudit/components/logs/LogViewer.tsx index aaeabc4..69347aa 100644 --- a/gstaudit/components/logs/LogViewer.tsx +++ b/gstaudit/components/logs/LogViewer.tsx @@ -20,7 +20,9 @@ interface LogViewerProps { onStartLogging?: () => void; onStopLogging?: () => void; filterText?: string; + objectFilter?: string | null; enabledLevels?: Set; + onObjectClick?: (objectName: string) => void; } export interface LogViewerHandle { @@ -31,7 +33,9 @@ export const LogViewer = forwardRef(({ onStartLogging, onStopLogging, filterText = '', + objectFilter = null, enabledLevels = new Set(DEBUG_LEVELS), + onObjectClick, }, ref) => { const [logs, setLogs] = useState([]); const [isLogging, setIsLogging] = useState(false); @@ -40,13 +44,18 @@ export const LogViewer = forwardRef(({ // Get session from context const { sessionId, callbackSecret, connection } = useSession(); - // Filter logs based on text and level + // Filter logs based on text, object, and level const filteredLogs = useMemo(() => { return logs.filter(log => { // Filter by level if (!enabledLevels.has(log.level)) { return false; } + + // Filter by object + if (objectFilter && log.object !== objectFilter) { + return false; + } // Filter by text (search in category, file, function, object, message) if (filterText) { @@ -62,7 +71,7 @@ export const LogViewer = forwardRef(({ return true; }); - }, [logs, filterText, enabledLevels]); + }, [logs, filterText, objectFilter, enabledLevels]); // Handle incoming logs - data is already fully formatted! const handleLog = (log: LogEntry) => { @@ -129,7 +138,15 @@ export const LogViewer = forwardRef(({ {log.category.padEnd(20)} {log.file}:{log.line}:{log.function} - {log.object && <{log.object}>} + {log.object && ( + onObjectClick?.(log.object!)} + title="Click to select this element" + > + {' '}<{log.object}> + + )} {log.message}
))} diff --git a/gstaudit/components/logs/LogWatcher.tsx b/gstaudit/components/logs/LogWatcher.tsx index a66f75d..63164c8 100644 --- a/gstaudit/components/logs/LogWatcher.tsx +++ b/gstaudit/components/logs/LogWatcher.tsx @@ -11,24 +11,38 @@ import { useState, useRef, useEffect } from 'react'; import { LogViewer, type LogViewerHandle } from './LogViewer'; import { LogCategorySelector } from './LogCategorySelector'; import { Panel, PanelGroup, PanelResizeHandle, ImperativePanelHandle } from 'react-resizable-panels'; -import { IconButton, TextField, ToggleButton, ToggleButtonGroup } from '@mui/material'; +import { IconButton, TextField, ToggleButton, ToggleButtonGroup, Chip, InputAdornment } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import SettingsIcon from '@mui/icons-material/Settings'; +import CloseIcon from '@mui/icons-material/Close'; import type { GstDebugLevelValue } from '@/lib/gst'; const DEBUG_LEVELS: GstDebugLevelValue[] = [ "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" ]; -export function LogWatcher() { +interface LogWatcherProps { + selectedElementName?: string | null; + onElementSelect?: (elementName: string | null) => void; +} + +export function LogWatcher({ selectedElementName, onElementSelect }: LogWatcherProps = {}) { const [showCategories, setShowCategories] = useState(false); const [filterText, setFilterText] = useState(''); + const [objectFilter, setObjectFilter] = useState(null); const [enabledLevels, setEnabledLevels] = useState>( new Set(DEBUG_LEVELS) ); const logViewerRef = useRef(null); const categoriesPanelRef = useRef(null); + // Sync objectFilter with selectedElementName from parent + useEffect(() => { + if (selectedElementName !== undefined) { + setObjectFilter(selectedElementName); + } + }, [selectedElementName]); + const handleClear = () => { logViewerRef.current?.clearLogs(); }; @@ -37,6 +51,17 @@ export function LogWatcher() { setShowCategories(!showCategories); }; + const handleObjectFilterRemove = () => { + setObjectFilter(null); + // Also notify parent to clear selection + onElementSelect?.(null); + }; + + const handleObjectClick = (objectName: string) => { + setObjectFilter(objectName); + onElementSelect?.(objectName); + }; + const toggleAllLevels = () => { if (enabledLevels.size === DEBUG_LEVELS.length) { setEnabledLevels(new Set()); @@ -86,8 +111,28 @@ export function LogWatcher() { padding: '4px 8px', } }} + InputProps={{ + startAdornment: objectFilter ? ( + + } + sx={{ + height: '22px', + fontSize: '10px', + '& .MuiChip-deleteIcon': { + fontSize: '14px', + } + }} + /> + + ) : undefined, + }} /> + {/* Level filter toggle buttons */} diff --git a/gstaudit/lib/server/log-manager.ts b/gstaudit/lib/server/log-manager.ts new file mode 100644 index 0000000..8e6847d --- /dev/null +++ b/gstaudit/lib/server/log-manager.ts @@ -0,0 +1,184 @@ +/** + * Log Manager - Server-side log streaming manager + * + * Manages GStreamer debug log streaming via gstaudit-server custom endpoints. + * Uses async push architecture for zero-blocking performance. + * + * Architecture: + * 1. Browser requests start logging via WebSocket + * 2. LogManager registers callback URL with gstaudit-server + * 3. gstaudit-server extracts log strings natively in Frida + * 4. gstaudit-server POSTs formatted logs asynchronously to Next.js + * 5. LogManager broadcasts logs to browser via WebSocket + * + * Flow: + * Browser → Next.js WebSocket → LogManager → /gstaudit/logs/register → gstaudit-server + * ↑ ↓ + * ←──────── Async HTTP POST ───────────────┘ + */ + +import type { GstDebugLevelValue } from '@/lib/gst'; +import type { WebSocketConnection } from './websocket-handler'; + +export interface LogEntry { + timestamp: string; // Nanoseconds since start (as string from native) + category: string; + level: string; // 'info', 'debug', etc. (from gst_debug_level_get_name) + file: string; + function: string; + line: number; + object: string | null; // Object name (already extracted from pointer) + message: string; +} + +interface LogSession { + sessionId: string; + connectionId: string; + callbackSecret: string; + callbackId: string; // ID from gstaudit-server for unregister + isActive: boolean; +} + +/** + * LogManager - Singleton that manages log streaming sessions + */ +class LogManager { + private static instance: LogManager; + private sessions: Map; // sessionId -> LogSession + + private constructor() { + this.sessions = new Map(); + } + + static getInstance(): LogManager { + const globalWithManager = global as typeof global & { __logManager?: LogManager }; + if (!globalWithManager.__logManager) { + console.log('[LogManager] Creating new singleton instance'); + globalWithManager.__logManager = new LogManager(); + } + return globalWithManager.__logManager; + } + + /** + * Start logging for a session + */ + async startLogging( + sessionId: string, + connectionId: string, + callbackSecret: string, + wsConnection: WebSocketConnection + ): Promise { + if (this.sessions.has(sessionId)) { + console.log(`[LogManager] Session ${sessionId} already logging`); + return; + } + + console.log(`[LogManager] Starting logging for session ${sessionId}`); + + // Get gstaudit-server config from connectionId + const { getConnectionManager } = await import('./connection-manager'); + const connManager = getConnectionManager(); + const config = connManager.getConfig(connectionId); + + if (!config) { + throw new Error(`Connection config not found for ${connectionId}`); + } + + const gstauditServerUrl = `http://${config.host}:${config.port}`; + console.log(`[LogManager] Using gstaudit-server URL: ${gstauditServerUrl}`); + + // Build callback URL - use dedicated log endpoint + // Include sessionId as query parameter (consistent with other callbacks) + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; + const callbackUrl = `${baseUrl}/api/logs/${sessionId}?sessionId=${sessionId}`; + + // Register with gstaudit-server + const response = await fetch( + `${gstauditServerUrl}/gstaudit/GstAudit/logs/register`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'session-id': sessionId, + 'callback-secret': callbackSecret + }, + body: JSON.stringify({ url: callbackUrl }) + } + ); + + if (!response.ok) { + throw new Error(`Failed to register logs: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + const callbackId = result.callbackId; + + console.log(`[LogManager] Registered callback with ID: ${callbackId}`); + + this.sessions.set(sessionId, { + sessionId, + connectionId, + callbackSecret, + callbackId, + isActive: true + }); + + console.log(`[LogManager] Logging started for session ${sessionId}`); + } + + /** + * Stop logging for a session + */ + async stopLogging(sessionId: string): Promise { + const session = this.sessions.get(sessionId); + if (!session) { + console.log(`[LogManager] Session ${sessionId} not logging`); + return; + } + + console.log(`[LogManager] Stopping logging for session ${sessionId}`); + + // Mark as inactive + session.isActive = false; + + // Get gstaudit-server config from connectionId + const { getConnectionManager } = await import('./connection-manager'); + const connManager = getConnectionManager(); + const config = connManager.getConfig(session.connectionId); + + if (config) { + const gstauditServerUrl = `http://${config.host}:${config.port}`; + + // Unregister from gstaudit-server + try { + await fetch( + `${gstauditServerUrl}/gstaudit/GstAudit/logs/unregister`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ callbackId: session.callbackId }) + } + ); + } catch (error) { + console.error('[LogManager] Error unregistering log callback:', error); + } + } + + // Cleanup + this.sessions.delete(sessionId); + + console.log(`[LogManager] Logging stopped for session ${sessionId}`); + } + + /** + * Check if a session is logging + */ + isLogging(sessionId: string): boolean { + return this.sessions.has(sessionId); + } +} + +// Export singleton instance +export const getLogManager = () => LogManager.getInstance(); From e5b5e8870ab8bc6e30e76ca8ad9b83c7b109781b Mon Sep 17 00:00:00 2001 From: Jorge Zapata Date: Mon, 16 Mar 2026 19:38:53 +0100 Subject: [PATCH 13/13] gstaudit: Share the debug level seletor UI component --- .../components/logs/DebugLevelSelector.tsx | 183 ++++++++++++++++++ .../components/logs/LogCategorySelector.tsx | 67 ++----- gstaudit/components/logs/LogWatcher.tsx | 45 ++--- gstaudit/components/logs/index.ts | 2 + 4 files changed, 209 insertions(+), 88 deletions(-) create mode 100644 gstaudit/components/logs/DebugLevelSelector.tsx diff --git a/gstaudit/components/logs/DebugLevelSelector.tsx b/gstaudit/components/logs/DebugLevelSelector.tsx new file mode 100644 index 0000000..34917c4 --- /dev/null +++ b/gstaudit/components/logs/DebugLevelSelector.tsx @@ -0,0 +1,183 @@ +/** + * DebugLevelSelector Component + * + * A reusable component for selecting GStreamer debug levels using ToggleButtons. + * Supports both single-select (exclusive) and multi-select modes. + * Can use either a primary theme color or per-level custom colors. + */ + +'use client'; + +import { ToggleButton, ToggleButtonGroup } from '@mui/material'; +import type { GstDebugLevelValue } from '@/lib/gst'; + +const DEBUG_LEVELS: GstDebugLevelValue[] = [ + "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" +]; + +// Color mapping for debug levels +const LEVEL_COLORS: Record = { + none: '#94a3b8', // slate-400 + error: '#ef4444', // red-500 + warning: '#f97316', // orange-500 + fixme: '#eab308', // yellow-500 + info: '#3b82f6', // blue-500 + debug: '#22c55e', // green-500 + log: '#06b6d4', // cyan-500 + trace: '#a855f7', // purple-500 + memdump: '#ec4899', // pink-500 + count: '#94a3b8', // slate-400 (fallback, shouldn't be used) +}; + +export interface DebugLevelSelectorProps { + /** + * Current selected level(s). + * - For single-select (exclusive): a single GstDebugLevelValue + * - For multi-select: an array of GstDebugLevelValue + */ + value: GstDebugLevelValue | GstDebugLevelValue[]; + + /** + * Callback when selection changes. + * - For single-select: receives single level or null + * - For multi-select: receives array of levels + */ + onChange: (value: GstDebugLevelValue | GstDebugLevelValue[] | null) => void; + + /** + * If true, only one level can be selected at a time (exclusive mode). + * If false, multiple levels can be selected (multi-select mode). + * @default false + */ + exclusive?: boolean; + + /** + * If true, uses per-level colors for selected buttons. + * If false, uses the primary theme color. + * @default false + */ + useColoredButtons?: boolean; + + /** + * Size of the toggle buttons. + * @default 'small' + */ + size?: 'small' | 'medium' | 'large'; + + /** + * Custom height for the button group (in pixels or CSS string). + */ + height?: string | number; + + /** + * Custom font size for the buttons (in pixels or CSS string). + */ + fontSize?: string | number; + + /** + * Custom padding for the buttons (CSS string). + */ + padding?: string; + + /** + * Minimum width for each button (in pixels or CSS string). + */ + minWidth?: string | number; + + /** + * Additional sx props for the ToggleButtonGroup. + */ + sx?: object; + + /** + * If true, prevents empty selection in multi-select mode. + * @default false + */ + preventEmptySelection?: boolean; +} + +export function DebugLevelSelector({ + value, + onChange, + exclusive = false, + useColoredButtons = false, + size = 'small', + height, + fontSize, + padding, + minWidth, + sx = {}, + preventEmptySelection = false, +}: DebugLevelSelectorProps) { + + const handleChange = (_event: React.MouseEvent, newValue: GstDebugLevelValue | GstDebugLevelValue[] | null) => { + // Handle multi-select mode + if (!exclusive) { + // Prevent empty selection if configured + if (preventEmptySelection && (!newValue || (Array.isArray(newValue) && newValue.length === 0))) { + return; + } + } + + onChange(newValue); + }; + + // Prepare value for ToggleButtonGroup + const buttonGroupValue = exclusive ? value : (Array.isArray(value) ? value : [value]); + + return ( + + {DEBUG_LEVELS.map((level) => ( + + {level.charAt(0).toUpperCase() + level.slice(1)} + + ))} + + ); +} + +// Export constants for convenience +export { DEBUG_LEVELS, LEVEL_COLORS }; diff --git a/gstaudit/components/logs/LogCategorySelector.tsx b/gstaudit/components/logs/LogCategorySelector.tsx index e5aff63..88f0a37 100644 --- a/gstaudit/components/logs/LogCategorySelector.tsx +++ b/gstaudit/components/logs/LogCategorySelector.tsx @@ -16,24 +16,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { Gst, GstDebugCategory, type GstDebugLevelValue } from '@/lib/gst'; import { IconButton, TextField, ToggleButton, ToggleButtonGroup } from '@mui/material'; import RefreshIcon from '@mui/icons-material/Refresh'; - -const DEBUG_LEVELS: GstDebugLevelValue[] = [ - "none", "error", "warning", "fixme", "info", "debug", "log", "trace", "memdump" -]; - -// Color mapping for debug levels -const LEVEL_COLORS: Record = { - none: '#94a3b8', // slate-400 - error: '#ef4444', // red-500 - warning: '#f97316', // orange-500 - fixme: '#eab308', // yellow-500 - info: '#3b82f6', // blue-500 - debug: '#22c55e', // green-500 - log: '#06b6d4', // cyan-500 - trace: '#a855f7', // purple-500 - memdump: '#ec4899', // pink-500 - count: '#94a3b8', // slate-400 (fallback, shouldn't be used) -}; +import { DebugLevelSelector, DEBUG_LEVELS } from './DebugLevelSelector'; // Get level index for comparison const getLevelIndex = (level: GstDebugLevelValue): number => { @@ -188,49 +171,21 @@ export function LogCategorySelector({
{/* Level toggle buttons */} - { - if (newLevel !== null) { + onChange={(newLevel) => { + if (newLevel !== null && !Array.isArray(newLevel)) { handleLevelChange(cat.ptr, newLevel as GstDebugLevelValue); } }} + exclusive={true} + useColoredButtons={true} size="small" - sx={{ - height: '24px', - '& .MuiToggleButton-root': { - fontSize: '9px', - padding: '2px 6px', - lineHeight: 1, - textTransform: 'capitalize', - border: '1px solid', - borderColor: 'divider', - minWidth: '48px', - } - }} - > - {DEBUG_LEVELS.map((level) => ( - - {level.charAt(0).toUpperCase() + level.slice(1)} - - ))} - + height="24px" + fontSize="9px" + padding="2px 6px" + minWidth="48px" + />
))}
diff --git a/gstaudit/components/logs/LogWatcher.tsx b/gstaudit/components/logs/LogWatcher.tsx index 63164c8..9664c0b 100644 --- a/gstaudit/components/logs/LogWatcher.tsx +++ b/gstaudit/components/logs/LogWatcher.tsx @@ -10,8 +10,9 @@ import { useState, useRef, useEffect } from 'react'; import { LogViewer, type LogViewerHandle } from './LogViewer'; import { LogCategorySelector } from './LogCategorySelector'; +import { DebugLevelSelector } from './DebugLevelSelector'; import { Panel, PanelGroup, PanelResizeHandle, ImperativePanelHandle } from 'react-resizable-panels'; -import { IconButton, TextField, ToggleButton, ToggleButtonGroup, Chip, InputAdornment } from '@mui/material'; +import { IconButton, TextField, ToggleButton, Chip, InputAdornment } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import SettingsIcon from '@mui/icons-material/Settings'; import CloseIcon from '@mui/icons-material/Close'; @@ -134,41 +135,21 @@ export function LogWatcher({ selectedElementName, onElementSelect }: LogWatcherP {/* Level filter toggle buttons */} - { - // Prevent empty selection - always need at least one level - if (newLevels.length > 0) { + onChange={(newLevels) => { + if (Array.isArray(newLevels) && newLevels.length > 0) { setEnabledLevels(new Set(newLevels)); } }} - size="small" - sx={{ - height: '28px', - '& .MuiToggleButton-root': { - fontSize: '10px', - padding: '4px 8px', - lineHeight: 1, - textTransform: 'capitalize', - border: '1px solid', - borderColor: 'divider', - minWidth: '60px', - '&.Mui-selected': { - fontWeight: 600, - }, - } - }} - > - {DEBUG_LEVELS.filter(l => l !== 'count').map((level) => ( - - {level.charAt(0).toUpperCase() + level.slice(1)} - - ))} - + exclusive={false} + useColoredButtons={false} + height="28px" + fontSize="10px" + padding="4px 8px" + minWidth="60px" + preventEmptySelection={true} + /> {/* ALL toggle button */}