From 5f3300cd16f5f838b45bdabb477276d0f4b60c87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:24:53 +0000 Subject: [PATCH 1/4] Initial plan From 89627ae6a16059f16ee0a0f40feb06a5684983ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:34:59 +0000 Subject: [PATCH 2/4] Add SQL Server support with loader, tests, frontend, and integration - Create api/loaders/sqlserver_loader.py following MySQL/Postgres pattern - Add pymssql dependency to pyproject.toml - Register SQL Server loader in schema_loader.py and text2sql.py - Update DatabaseSpecificQuoter for SQL Server bracket quoting - Update SQLIdentifierQuoter to handle bracket-quoted identifiers - Add SQL Server option to frontend DatabaseModal.tsx - Add unit tests in test_sqlserver_loader.py (11 tests) - Update spellcheck wordlist Agent-Logs-Url: https://github.com/FalkorDB/QueryWeaver/sessions/cb38384c-67c5-45c2-a553-8993ac42376b Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> --- .github/wordlist.txt | 3 + api/core/schema_loader.py | 4 + api/core/text2sql.py | 3 + api/loaders/sqlserver_loader.py | 615 ++++++++++++++++++++ api/sql_utils/sql_sanitizer.py | 28 +- app/src/components/modals/DatabaseModal.tsx | 14 +- pyproject.toml | 1 + tests/test_sqlserver_loader.py | 157 +++++ uv.lock | 31 + 9 files changed, 847 insertions(+), 9 deletions(-) create mode 100644 api/loaders/sqlserver_loader.py create mode 100644 tests/test_sqlserver_loader.py diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 5eacb0df..af8192d5 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -121,3 +121,6 @@ pytest Radix Zod Dependabot +pymssql +sqlserver +SQLServerLoader diff --git a/api/core/schema_loader.py b/api/core/schema_loader.py index b1568514..0f59c4d6 100644 --- a/api/core/schema_loader.py +++ b/api/core/schema_loader.py @@ -14,6 +14,7 @@ from api.loaders.postgres_loader import PostgresLoader from api.loaders.mysql_loader import MySQLLoader from api.loaders.snowflake_loader import SnowflakeLoader +from api.loaders.sqlserver_loader import SQLServerLoader # Use the same delimiter as in the JavaScript frontend for streaming chunks MESSAGE_DELIMITER = "|||FALKORDB_MESSAGE_BOUNDARY|||" @@ -48,6 +49,9 @@ def _step_detect_db_type(steps_counter: int, url: str) -> tuple[type[BaseLoader] elif url.startswith("snowflake://"): db_type = "snowflake" loader = SnowflakeLoader + elif url.startswith("sqlserver://"): + db_type = "sqlserver" + loader = SQLServerLoader else: raise InvalidArgumentError("Invalid database URL format") diff --git a/api/core/text2sql.py b/api/core/text2sql.py index 65bc0b4f..76f380af 100644 --- a/api/core/text2sql.py +++ b/api/core/text2sql.py @@ -21,6 +21,7 @@ from api.loaders.postgres_loader import PostgresLoader from api.loaders.mysql_loader import MySQLLoader from api.loaders.snowflake_loader import SnowflakeLoader +from api.loaders.sqlserver_loader import SQLServerLoader from api.memory.graphiti_tool import MemoryTool from api.sql_utils import SQLIdentifierQuoter, DatabaseSpecificQuoter @@ -87,6 +88,8 @@ def get_database_type_and_loader(db_url: str): return 'mysql', MySQLLoader if db_url_lower.startswith('snowflake://'): return 'snowflake', SnowflakeLoader + if db_url_lower.startswith('sqlserver://'): + return 'sqlserver', SQLServerLoader # Default to PostgresLoader for backward compatibility return 'postgresql', PostgresLoader diff --git a/api/loaders/sqlserver_loader.py b/api/loaders/sqlserver_loader.py new file mode 100644 index 00000000..fdc73ef7 --- /dev/null +++ b/api/loaders/sqlserver_loader.py @@ -0,0 +1,615 @@ +"""SQL Server loader for loading database schemas into FalkorDB graphs.""" + +import datetime +import decimal +import logging +import re +from typing import AsyncGenerator, Dict, Any, List, Tuple + +import tqdm +import pymssql + +from api.loaders.base_loader import BaseLoader +from api.loaders.graph_loader import load_to_graph + + +class SQLServerQueryError(Exception): + """Exception raised for SQL Server query execution errors.""" + + +class SQLServerConnectionError(Exception): + """Exception raised for SQL Server connection errors.""" + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") + + +class SQLServerLoader(BaseLoader): + """ + Loader for SQL Server databases that connects and extracts schema information. + """ + + # DDL operations that modify database schema # pylint: disable=duplicate-code + SCHEMA_MODIFYING_OPERATIONS = { + 'CREATE', 'ALTER', 'DROP', 'RENAME', 'TRUNCATE' + } + + # More specific patterns for schema-affecting operations + SCHEMA_PATTERNS = [ # pylint: disable=duplicate-code + r'^\s*CREATE\s+TABLE', + r'^\s*CREATE\s+INDEX', + r'^\s*CREATE\s+UNIQUE\s+INDEX', + r'^\s*ALTER\s+TABLE', + r'^\s*DROP\s+TABLE', + r'^\s*DROP\s+INDEX', + r'^\s*RENAME\s+TABLE', + r'^\s*TRUNCATE\s+TABLE', + r'^\s*CREATE\s+VIEW', + r'^\s*DROP\s+VIEW', + r'^\s*CREATE\s+SCHEMA', + r'^\s*DROP\s+SCHEMA', + ] + + @staticmethod + def _execute_sample_query( + cursor, table_name: str, col_name: str, sample_size: int = 3 + ) -> List[Any]: + """ + Execute query to get random sample values for a column. + SQL Server implementation using TABLESAMPLE or TOP with NEWID() for random sampling. + """ + query = ( + f"SELECT DISTINCT TOP {int(sample_size)} [{col_name}]" + f" FROM [{table_name}]" + f" WHERE [{col_name}] IS NOT NULL" + f" ORDER BY NEWID()" + ) + cursor.execute(query) + + sample_results = cursor.fetchall() + return [row[0] for row in sample_results if row[0] is not None] + + @staticmethod + def _serialize_value(value): + """ + Convert non-JSON serializable values to JSON serializable format. + + Args: + value: The value to serialize + + Returns: + JSON serializable version of the value + """ + if isinstance(value, (datetime.date, datetime.datetime)): + return value.isoformat() + if isinstance(value, datetime.time): + return value.isoformat() + if isinstance(value, decimal.Decimal): + return float(value) + if isinstance(value, bytes): + return value.hex() + if value is None: + return None + return value + + @staticmethod + def _parse_sqlserver_url(connection_url: str) -> Dict[str, Any]: + """ + Parse SQL Server connection URL into components. + + Args: + connection_url: SQL Server connection URL in format: + sqlserver://username:password@host:port/database + + Returns: + Dict with connection parameters + """ + # Remove sqlserver:// prefix + if connection_url.startswith('sqlserver://'): + url = connection_url[len('sqlserver://'):] + else: + raise ValueError( + "Invalid SQL Server URL format. Expected " + "sqlserver://username:password@host:port/database" + ) + + # Parse components + if '@' not in url: + raise ValueError("SQL Server URL must include username and host") + + credentials, host_db = url.split('@', 1) + + if ':' in credentials: + username, password = credentials.split(':', 1) + else: + username = credentials + password = "" + + if '/' not in host_db: + raise ValueError("SQL Server URL must include database name") + + host_port, database = host_db.split('/', 1) + + # Handle query parameters + if '?' in database: + database = database.split('?')[0] + + if ':' in host_port: + host, port_str = host_port.split(':', 1) + port = int(port_str) + else: + host = host_port + port = 1433 + + return { + 'server': host, + 'port': port, + 'user': username, + 'password': password, + 'database': database + } + + @staticmethod + async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, str], None]: + """ + Load the graph data from a SQL Server database into the graph database. + + Args: + connection_url: SQL Server connection URL in format: + sqlserver://username:password@host:port/database + + Returns: + Tuple[bool, str]: Success status and message + """ + try: + # Parse connection URL + conn_params = SQLServerLoader._parse_sqlserver_url(connection_url) + + # Connect to SQL Server database + conn = pymssql.connect(**conn_params) # pylint: disable=no-member + cursor = conn.cursor(as_dict=True) + + # Get database name + db_name = conn_params['database'] + + # Get all table information + yield True, "Extracting table information..." + entities = SQLServerLoader.extract_tables_info(cursor, db_name) + + # Get all relationship information + yield True, "Extracting relationship information..." + relationships = SQLServerLoader.extract_relationships(cursor, db_name) + + # Close database connection + cursor.close() + conn.close() + + # Load data into graph + yield True, "Loading data into graph..." + await load_to_graph(f"{prefix}_{db_name}", entities, relationships, + db_name=db_name, db_url=connection_url) + + yield True, (f"SQL Server schema loaded successfully. " + f"Found {len(entities)} tables.") + + except pymssql.Error as e: + logging.error("SQL Server connection error: %s", e) + yield False, "Failed to connect to SQL Server database" + except Exception as e: # pylint: disable=broad-exception-caught + logging.error("Error loading SQL Server schema: %s", e) + yield False, "Failed to load SQL Server database schema" + + @staticmethod + def extract_tables_info(cursor, db_name: str) -> Dict[str, Any]: # pylint: disable=unused-argument + """ + Extract table and column information from SQL Server database. + + Args: + cursor: Database cursor + db_name: Database name + + Returns: + Dict containing table information + """ + entities = {} + + # Get all tables in the database + cursor.execute(""" + SELECT + t.name AS table_name, + ISNULL(ep.value, '') AS table_comment + FROM sys.tables t + LEFT JOIN sys.extended_properties ep + ON ep.major_id = t.object_id + AND ep.minor_id = 0 + AND ep.name = 'MS_Description' + WHERE t.is_ms_shipped = 0 + ORDER BY t.name; + """) + + tables = cursor.fetchall() + + for table_info in tqdm.tqdm(tables, desc="Extracting table information"): + table_name = table_info['table_name'] + table_comment = table_info['table_comment'] + + # Get column information for this table + columns_info = SQLServerLoader.extract_columns_info(cursor, db_name, table_name) + + # Get foreign keys for this table + foreign_keys = SQLServerLoader.extract_foreign_keys(cursor, db_name, table_name) + + # Generate table description + table_description = table_comment if table_comment else f"Table: {table_name}" + + # Get column descriptions for batch embedding + col_descriptions = [col_info['description'] for col_info in columns_info.values()] + + entities[table_name] = { + 'description': table_description, + 'columns': columns_info, + 'foreign_keys': foreign_keys, + 'col_descriptions': col_descriptions + } + + return entities + + @staticmethod + def extract_columns_info(cursor, db_name: str, table_name: str) -> Dict[str, Any]: # pylint: disable=unused-argument + """ + Extract column information for a specific table. + + Args: + cursor: Database cursor + db_name: Database name + table_name: Name of the table + + Returns: + Dict containing column information + """ + cursor.execute(""" + SELECT + c.name AS column_name, + tp.name AS data_type, + c.is_nullable, + dc.definition AS column_default, + CASE + WHEN pk.column_id IS NOT NULL THEN 'PRI' + WHEN fk.parent_column_id IS NOT NULL THEN 'MUL' + WHEN uc.column_id IS NOT NULL THEN 'UNI' + ELSE '' + END AS column_key, + ISNULL(ep.value, '') AS column_comment + FROM sys.columns c + JOIN sys.types tp ON c.user_type_id = tp.user_type_id + JOIN sys.tables t ON c.object_id = t.object_id + LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id + LEFT JOIN ( + SELECT ic.object_id, ic.column_id + FROM sys.index_columns ic + JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id + WHERE i.is_primary_key = 1 + ) pk ON c.object_id = pk.object_id AND c.column_id = pk.column_id + LEFT JOIN sys.foreign_key_columns fk + ON fk.parent_object_id = c.object_id AND fk.parent_column_id = c.column_id + LEFT JOIN ( + SELECT ic.object_id, ic.column_id + FROM sys.index_columns ic + JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id + WHERE i.is_unique = 1 AND i.is_primary_key = 0 + ) uc ON c.object_id = uc.object_id AND c.column_id = uc.column_id + LEFT JOIN sys.extended_properties ep + ON ep.major_id = c.object_id + AND ep.minor_id = c.column_id + AND ep.name = 'MS_Description' + WHERE t.name = %s + ORDER BY c.column_id; + """, (table_name,)) + + columns = cursor.fetchall() + columns_info = {} + + for col_info in columns: + col_name = col_info['column_name'] + data_type = col_info['data_type'] + is_nullable = 'YES' if col_info['is_nullable'] else 'NO' + column_default = col_info['column_default'] + column_key = col_info['column_key'] + column_comment = col_info['column_comment'] + + # Determine key type + if column_key == 'PRI': + key_type = 'PRIMARY KEY' + elif column_key == 'MUL': + key_type = 'FOREIGN KEY' + elif column_key == 'UNI': + key_type = 'UNIQUE KEY' + else: + key_type = 'NONE' + + # Generate column description + description_parts = [] + if column_comment: + description_parts.append(str(column_comment)) + else: + description_parts.append(f"Column {col_name} of type {data_type}") + + if key_type != 'NONE': + description_parts.append(f"({key_type})") + + if is_nullable == 'NO': + description_parts.append("(NOT NULL)") + + if column_default is not None: + description_parts.append(f"(Default: {column_default})") + + # Extract sample values for the column (stored separately, not in description) + sample_values = SQLServerLoader.extract_sample_values_for_column( + cursor, table_name, col_name + ) + + columns_info[col_name] = { + 'type': data_type, + 'null': is_nullable, + 'key': key_type, + 'description': ' '.join(description_parts), + 'default': column_default, + 'sample_values': sample_values + } + + return columns_info + + @staticmethod + def extract_foreign_keys(cursor, db_name: str, table_name: str) -> List[Dict[str, str]]: # pylint: disable=unused-argument + """ + Extract foreign key information for a specific table. + + Args: + cursor: Database cursor + db_name: Database name + table_name: Name of the table + + Returns: + List of foreign key dictionaries + """ + cursor.execute(""" + SELECT + fk.name AS constraint_name, + cp.name AS column_name, + rt.name AS referenced_table_name, + cr.name AS referenced_column_name + FROM sys.foreign_keys fk + JOIN sys.foreign_key_columns fkc + ON fk.object_id = fkc.constraint_object_id + JOIN sys.columns cp + ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id + JOIN sys.tables rt + ON fkc.referenced_object_id = rt.object_id + JOIN sys.columns cr + ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id + JOIN sys.tables pt + ON fkc.parent_object_id = pt.object_id + WHERE pt.name = %s + ORDER BY fk.name; + """, (table_name,)) + + foreign_keys = [] + for fk_info in cursor.fetchall(): + foreign_keys.append({ + 'constraint_name': fk_info['constraint_name'], + 'column': fk_info['column_name'], + 'referenced_table': fk_info['referenced_table_name'], + 'referenced_column': fk_info['referenced_column_name'] + }) + + return foreign_keys + + @staticmethod + def extract_relationships(cursor, db_name: str) -> Dict[str, List[Dict[str, str]]]: # pylint: disable=unused-argument + """ + Extract all relationship information from the database. + + Args: + cursor: Database cursor + db_name: Database name + + Returns: + Dict containing relationship information + """ + cursor.execute(""" + SELECT + pt.name AS table_name, + fk.name AS constraint_name, + cp.name AS column_name, + rt.name AS referenced_table_name, + cr.name AS referenced_column_name + FROM sys.foreign_keys fk + JOIN sys.foreign_key_columns fkc + ON fk.object_id = fkc.constraint_object_id + JOIN sys.columns cp + ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id + JOIN sys.tables pt + ON fkc.parent_object_id = pt.object_id + JOIN sys.tables rt + ON fkc.referenced_object_id = rt.object_id + JOIN sys.columns cr + ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id + ORDER BY pt.name, fk.name; + """) + + relationships = {} + for rel_info in cursor.fetchall(): + constraint_name = rel_info['constraint_name'] + + if constraint_name not in relationships: + relationships[constraint_name] = [] + + relationships[constraint_name].append({ + 'from': rel_info['table_name'], + 'to': rel_info['referenced_table_name'], + 'source_column': rel_info['column_name'], + 'target_column': rel_info['referenced_column_name'], + 'note': f'Foreign key constraint: {constraint_name}' + }) + + return relationships + + @staticmethod + def is_schema_modifying_query(sql_query: str) -> Tuple[bool, str]: + """ + Check if a SQL query modifies the database schema. + + Args: + sql_query: The SQL query to check + + Returns: + Tuple of (is_schema_modifying, operation_type) + """ + if not sql_query or not sql_query.strip(): + return False, "" + + # Clean and normalize the query + normalized_query = sql_query.strip().upper() + + # Check for basic DDL operations + first_word = normalized_query.split()[0] if normalized_query.split() else "" + if first_word in SQLServerLoader.SCHEMA_MODIFYING_OPERATIONS: + # Additional pattern matching for more precise detection + for pattern in SQLServerLoader.SCHEMA_PATTERNS: + if re.match(pattern, normalized_query, re.IGNORECASE): + return True, first_word + + # If it's a known DDL operation but doesn't match specific patterns, + # still consider it schema-modifying (better safe than sorry) + return True, first_word + + return False, "" + + @staticmethod + async def refresh_graph_schema(graph_id: str, db_url: str) -> Tuple[bool, str]: + """ + Refresh the graph schema by clearing existing data and reloading from the database. + + Args: + graph_id: The graph ID to refresh + db_url: Database connection URL + + Returns: + Tuple of (success, message) + """ + try: + logging.info("Schema modification detected. Refreshing graph schema.") + + # Import here to avoid circular imports + from api.extensions import db # pylint: disable=import-error,import-outside-toplevel + + # Clear existing graph data + # Drop current graph before reloading + graph = db.select_graph(graph_id) + await graph.delete() + + # Extract prefix from graph_id (remove database name part) + # graph_id format is typically "prefix_database_name" + parts = graph_id.split('_') + if len(parts) >= 2: + # Reconstruct prefix by joining all parts except the last one + prefix = '_'.join(parts[:-1]) + else: + prefix = graph_id + + # Reuse the existing load method to reload the schema + success, message = await SQLServerLoader.load(prefix, db_url) + + if success: + logging.info("Graph schema refreshed successfully.") + return True, message + + logging.error("Schema refresh failed") + return False, "Failed to reload schema" + + except Exception as e: # pylint: disable=broad-exception-caught + # Log the error and return failure + logging.error("Error refreshing graph schema: %s", str(e)) + error_msg = "Error refreshing graph schema" + logging.error(error_msg) + return False, error_msg + + @staticmethod + def execute_sql_query(sql_query: str, db_url: str) -> List[Dict[str, Any]]: + """ + Execute a SQL query on the SQL Server database and return the results. + + Args: + sql_query: The SQL query to execute + db_url: SQL Server connection URL in format: + sqlserver://username:password@host:port/database + + Returns: + List of dictionaries containing the query results + """ + try: + # Parse connection URL + conn_params = SQLServerLoader._parse_sqlserver_url(db_url) + + # Connect to SQL Server database + conn = pymssql.connect(**conn_params) # pylint: disable=no-member + cursor = conn.cursor(as_dict=True) + + # Execute the SQL query + cursor.execute(sql_query) + + # Check if the query returns results (SELECT queries) + if cursor.description is not None: + # This is a SELECT query or similar that returns rows + results = cursor.fetchall() + result_list = [] + for row in results: + # Serialize each value to ensure JSON compatibility + serialized_row = { + key: SQLServerLoader._serialize_value(value) + for key, value in row.items() + } + result_list.append(serialized_row) + else: + # This is an INSERT, UPDATE, DELETE, or other non-SELECT query + # Return information about the operation + affected_rows = cursor.rowcount + sql_type = sql_query.strip().split()[0].upper() + + if sql_type in ['INSERT', 'UPDATE', 'DELETE']: + result_list = [{ + "operation": sql_type, + "affected_rows": affected_rows, + "status": "success" + }] + else: + # For other types of queries (CREATE, DROP, etc.) + result_list = [{ + "operation": sql_type, + "status": "success" + }] + + # Commit the transaction for write operations + conn.commit() + + # Close database connection + cursor.close() + conn.close() + + return result_list + + except pymssql.Error as e: + # Rollback in case of error + if 'conn' in locals(): + conn.rollback() + cursor.close() + conn.close() + logging.error("SQL Server query execution error: %s", e) + raise SQLServerQueryError(f"SQL Server query execution error: {str(e)}") from e + except Exception as e: + # Rollback in case of error + if 'conn' in locals(): + conn.rollback() + cursor.close() + conn.close() + logging.error("Error executing SQL query: %s", e) + raise SQLServerQueryError(f"Error executing SQL query: {str(e)}") from e diff --git a/api/sql_utils/sql_sanitizer.py b/api/sql_utils/sql_sanitizer.py index 6f7d127e..c1a304fa 100644 --- a/api/sql_utils/sql_sanitizer.py +++ b/api/sql_utils/sql_sanitizer.py @@ -24,6 +24,17 @@ class SQLIdentifierQuoter: 'EXCEPT', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'CAST', 'ASC', 'DESC' } + # Quote pairs recognized as already-quoted identifiers + _QUOTE_PAIRS = [('"', '"'), ('`', '`'), ('[', ']')] + + @classmethod + def _is_already_quoted(cls, identifier: str) -> bool: + """Check if an identifier is already quoted.""" + return any( + identifier.startswith(start) and identifier.endswith(end) + for start, end in cls._QUOTE_PAIRS + ) + @classmethod def needs_quoting(cls, identifier: str) -> bool: """ @@ -36,8 +47,7 @@ def needs_quoting(cls, identifier: str) -> bool: True if the identifier needs quoting, False otherwise """ # Already quoted - if (identifier.startswith('"') and identifier.endswith('"')) or \ - (identifier.startswith('`') and identifier.endswith('`')): + if cls._is_already_quoted(identifier): return False # Check if it's a SQL keyword @@ -54,7 +64,8 @@ def quote_identifier(identifier: str, quote_char: str = '"') -> str: Args: identifier: The identifier to quote - quote_char: The quote character to use (default: " for PostgreSQL/standard SQL) + quote_char: The quote character to use (default: " for PostgreSQL/standard SQL, + use ` for MySQL, [ for SQL Server) Returns: Quoted identifier @@ -62,10 +73,13 @@ def quote_identifier(identifier: str, quote_char: str = '"') -> str: identifier = identifier.strip() # Don't double-quote - if (identifier.startswith('"') and identifier.endswith('"')) or \ - (identifier.startswith('`') and identifier.endswith('`')): + if SQLIdentifierQuoter._is_already_quoted(identifier): return identifier + # SQL Server uses bracket pairs: [identifier] + if quote_char == '[': + return f'[{identifier}]' + return f'{quote_char}{identifier}{quote_char}' @classmethod @@ -167,5 +181,7 @@ def get_quote_char(db_type: str) -> str: """ if db_type.lower() in ['mysql', 'mariadb']: return '`' - # PostgreSQL, SQLite, SQL Server (standard SQL) use double quotes + if db_type.lower() in ['sqlserver', 'mssql']: + return '[' + # PostgreSQL, SQLite use double quotes (standard SQL) return '"' diff --git a/app/src/components/modals/DatabaseModal.tsx b/app/src/components/modals/DatabaseModal.tsx index e3a7f47a..26d7cb6b 100644 --- a/app/src/components/modals/DatabaseModal.tsx +++ b/app/src/components/modals/DatabaseModal.tsx @@ -134,7 +134,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { builtUrl.searchParams.set('warehouse', warehouse); dbUrl = builtUrl.toString(); } else { - const protocol = selectedDatabase === 'mysql' ? 'mysql' : 'postgresql'; + const protocol = selectedDatabase === 'mysql' ? 'mysql' : selectedDatabase === 'sqlserver' ? 'sqlserver' : 'postgresql'; const builtUrl = new URL(`${protocol}://${host}:${port}/${database}`); builtUrl.username = username; builtUrl.password = password; @@ -301,7 +301,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { Connect to Database - Connect to PostgreSQL, MySQL, or Snowflake database using a connection URL or manual entry.{" "} + Connect to PostgreSQL, MySQL, Snowflake, or SQL Server database using a connection URL or manual entry.{" "} { Snowflake + +
+
+ SQL Server +
+
@@ -385,6 +391,8 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { ? 'postgresql://username:password@host:5432/database' : selectedDatabase === 'mysql' ? 'mysql://username:password@host:3306/database' + : selectedDatabase === 'sqlserver' + ? 'sqlserver://username:password@host:1433/database' : 'snowflake://username:password@account/database/schema?warehouse=warehouse_name' } value={connectionUrl} @@ -551,7 +559,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { setPort(e.target.value)} className="bg-muted border-border focus-visible:ring-purple-500" diff --git a/pyproject.toml b/pyproject.toml index 407e5258..079bf7c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "falkordb~=1.6.0", "psycopg2-binary~=2.9.11", "pymysql~=1.1.0", + "pymssql~=2.3.13", "authlib~=1.6.4", "itsdangerous~=2.2.0", "jsonschema~=4.26.0", diff --git a/tests/test_sqlserver_loader.py b/tests/test_sqlserver_loader.py new file mode 100644 index 00000000..1abf3b8f --- /dev/null +++ b/tests/test_sqlserver_loader.py @@ -0,0 +1,157 @@ +"""Tests for SQL Server loader functionality.""" +# pylint: disable=protected-access + +import asyncio +import datetime +import decimal +from unittest.mock import patch, MagicMock + +import pytest + +from api.loaders.sqlserver_loader import SQLServerLoader + + +async def _consume_loader(loader_gen): + """Consume an async generator loader and return the final result.""" + last_success, last_message = False, "" + async for success, message in loader_gen: + last_success, last_message = success, message + return last_success, last_message + + +class TestSQLServerLoader: + """Test cases for SQLServerLoader class.""" + + def test_parse_sqlserver_url_valid(self): + """Test parsing a valid SQL Server URL.""" + url = "sqlserver://testuser:testpass@localhost:1433/testdb" + result = SQLServerLoader._parse_sqlserver_url(url) + expected = { + 'server': 'localhost', + 'port': 1433, + 'user': 'testuser', + 'password': 'testpass', + 'database': 'testdb' + } + assert result == expected + + def test_parse_sqlserver_url_default_port(self): + """Test parsing SQL Server URL without port (should default to 1433).""" + url = "sqlserver://testuser:testpass@localhost/testdb" + result = SQLServerLoader._parse_sqlserver_url(url) + assert result['port'] == 1433 + assert result['server'] == 'localhost' + + def test_parse_sqlserver_url_no_password(self): + """Test parsing SQL Server URL without password.""" + url = "sqlserver://testuser@localhost:1433/testdb" + result = SQLServerLoader._parse_sqlserver_url(url) + assert result['password'] == "" + assert result['user'] == 'testuser' + + def test_parse_sqlserver_url_with_query_params(self): + """Test parsing SQL Server URL with query parameters.""" + url = "sqlserver://testuser:testpass@localhost:1433/testdb?charset=utf8" + result = SQLServerLoader._parse_sqlserver_url(url) + assert result['database'] == 'testdb' # Should strip query params + + def test_parse_sqlserver_url_invalid_format(self): + """Test parsing invalid SQL Server URL format.""" + with pytest.raises(ValueError, match="Invalid SQL Server URL format"): + SQLServerLoader._parse_sqlserver_url("postgresql://user@host/db") + + def test_parse_sqlserver_url_missing_host(self): + """Test parsing SQL Server URL without host.""" + with pytest.raises(ValueError, match="SQL Server URL must include username and host"): + SQLServerLoader._parse_sqlserver_url("sqlserver://") + + def test_parse_sqlserver_url_missing_database(self): + """Test parsing SQL Server URL without database.""" + with pytest.raises(ValueError, match="SQL Server URL must include database name"): + SQLServerLoader._parse_sqlserver_url("sqlserver://user@host") + + def test_serialize_value(self): + """Test value serialization for JSON compatibility.""" + # Test datetime + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + assert SQLServerLoader._serialize_value(dt) == "2023-01-01T12:00:00" + + # Test date + d = datetime.date(2023, 1, 1) + assert SQLServerLoader._serialize_value(d) == "2023-01-01" + + # Test time + t = datetime.time(12, 0, 0) + assert SQLServerLoader._serialize_value(t) == "12:00:00" + + # Test decimal + dec = decimal.Decimal("123.45") + assert SQLServerLoader._serialize_value(dec) == 123.45 + + # Test bytes + b = b'\x00\x01\x02' + assert SQLServerLoader._serialize_value(b) == "000102" + + # Test None + assert SQLServerLoader._serialize_value(None) is None + + # Test regular value + assert SQLServerLoader._serialize_value("test") == "test" + + def test_is_schema_modifying_query(self): + """Test detection of schema-modifying queries.""" + # Schema-modifying queries + assert SQLServerLoader.is_schema_modifying_query("CREATE TABLE test (id INT)")[0] is True + assert SQLServerLoader.is_schema_modifying_query("DROP TABLE test")[0] is True + assert SQLServerLoader.is_schema_modifying_query( + "ALTER TABLE test ADD COLUMN name VARCHAR(50)")[0] is True + assert SQLServerLoader.is_schema_modifying_query( + " CREATE INDEX idx_name ON test(name)")[0] is True + + # Non-schema-modifying queries + assert SQLServerLoader.is_schema_modifying_query("SELECT * FROM test")[0] is False + assert SQLServerLoader.is_schema_modifying_query("INSERT INTO test VALUES (1)")[0] is False + assert SQLServerLoader.is_schema_modifying_query("UPDATE test SET name = 'test'")[0] is False + assert SQLServerLoader.is_schema_modifying_query("DELETE FROM test WHERE id = 1")[0] is False + + # Edge cases + assert SQLServerLoader.is_schema_modifying_query("")[0] is False + assert SQLServerLoader.is_schema_modifying_query("")[0] is False + + @patch('pymssql.connect') + def test_connection_error(self, mock_connect): + """Test handling of SQL Server connection errors.""" + # Mock connection failure + mock_connect.side_effect = Exception("Connection failed") + + success, message = asyncio.run( + _consume_loader(SQLServerLoader.load("test_prefix", "sqlserver://user:pass@host:1433/db")) + ) + + assert success is False + assert "Failed to load SQL Server database schema" in message + + @patch('pymssql.connect') + @patch('api.loaders.sqlserver_loader.load_to_graph') + def test_successful_load(self, mock_load_to_graph, mock_connect): + """Test successful SQL Server schema loading.""" + # Mock database connection and cursor + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_cursor.fetchall.return_value = [ + {'table_name': 'users', 'table_comment': 'User table'} + ] + mock_conn.cursor.return_value = mock_cursor + mock_connect.return_value = mock_conn + + # Mock the extract methods to return minimal data + with patch.object(SQLServerLoader, 'extract_tables_info', + return_value={'users': {'description': 'User table'}}): + with patch.object(SQLServerLoader, 'extract_relationships', return_value={}): + success, message = asyncio.run(_consume_loader(SQLServerLoader.load( + "test_prefix", "sqlserver://user:pass@localhost:1433/testdb" + ))) + + assert success is True + assert "SQL Server schema loaded successfully" in message + mock_load_to_graph.assert_called_once() diff --git a/uv.lock b/uv.lock index 3c664f51..8c1763eb 100644 --- a/uv.lock +++ b/uv.lock @@ -1920,6 +1920,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, ] +[[package]] +name = "pymssql" +version = "2.3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/cc/843c044b7f71ee329436b7327c578383e2f2499313899f88ad267cdf1f33/pymssql-2.3.13.tar.gz", hash = "sha256:2137e904b1a65546be4ccb96730a391fcd5a85aab8a0632721feb5d7e39cfbce", size = 203153, upload-time = "2026-02-14T05:00:36.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/60/a2e8a8a38f7be21d54402e2b3365cd56f1761ce9f2706c97f864e8aa8300/pymssql-2.3.13-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cf4f32b4a05b66f02cb7d55a0f3bcb0574a6f8cf0bee4bea6f7b104038364733", size = 3158689, upload-time = "2026-02-14T04:59:46.982Z" }, + { url = "https://files.pythonhosted.org/packages/43/9e/0cf0ffb9e2f73238baf766d8e31d7237b5bee3cc1bb29a376b404610994a/pymssql-2.3.13-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:2b056eb175955f7fb715b60dc1c0c624969f4d24dbdcf804b41ab1e640a2b131", size = 2960018, upload-time = "2026-02-14T04:59:48.668Z" }, + { url = "https://files.pythonhosted.org/packages/93/ea/bc27354feaca717faa4626911f6b19bb62985c87dda28957c63de4de5895/pymssql-2.3.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:319810b89aa64b99d9c5c01518752c813938df230496fa2c4c6dda0603f04c4c", size = 3065719, upload-time = "2026-02-14T04:59:50.369Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7a/8028681c96241fb5fc850b87c8959402c353e4b83c6e049a99ffa67ded54/pymssql-2.3.13-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0ea72641cb0f8bce7ad8565dbdbda4a7437aa58bce045f2a3a788d71af2e4be", size = 3190567, upload-time = "2026-02-14T04:59:52.202Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f1/ab5b76adbbd6db9ce746d448db34b044683522e7e7b95053f9dd0165297b/pymssql-2.3.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1493f63d213607f708a5722aa230776ada726ccdb94097fab090a1717a2534e0", size = 3710481, upload-time = "2026-02-14T04:59:54.01Z" }, + { url = "https://files.pythonhosted.org/packages/59/aa/2fa0951475cd0a1829e0b8bfbe334d04ece4bce11546a556b005c4100689/pymssql-2.3.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb3275985c23479e952d6462ae6c8b2b6993ab6b99a92805a9c17942cf3d5b3d", size = 3453789, upload-time = "2026-02-14T04:59:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/78/08/8cd2af9003f9fc03912b658a64f5a4919dcd68f0dd3bbc822b49a3d14fd9/pymssql-2.3.13-cp312-cp312-win_amd64.whl", hash = "sha256:a930adda87bdd8351a5637cf73d6491936f34e525a5e513068a6eac742f69cdb", size = 1994709, upload-time = "2026-02-14T04:59:58.972Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4f/ee15b1f6b11e7c3accdc7da7840a019b63f12ba09eaa008acc601182f516/pymssql-2.3.13-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:30918bb044242865c01838909777ef5e0f1b9ecd7f5882346aefa57f4414b29c", size = 3156333, upload-time = "2026-02-14T05:00:01.21Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/aea5c77bad4a52649a1d9f786a1d9ce1c83d50f1a75df288e292737b6d80/pymssql-2.3.13-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:1c6d0b2d7961f159a07e4f0d8cc81f70ceab83f5e7fd1e832a2d069e1d67ee4e", size = 2957990, upload-time = "2026-02-14T05:00:03.11Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f8/30ac16fba32ff066b05f12c392d7b812fe11f06cb62d1d86ca5177c50a8b/pymssql-2.3.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16c5957a3c9e51a03276bfd76a22431e2bc4c565e2e95f2cbb3559312edda230", size = 3065264, upload-time = "2026-02-14T05:00:05.377Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/7568447bf85921d21453fd56e19b6c9591d595fde0546c5a569f3ae937a8/pymssql-2.3.13-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fddd24efe9d18bbf174fab7c6745b0927773718387f5517cf8082241f721a68", size = 3190039, upload-time = "2026-02-14T05:00:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/35/f1/4d9d275ebaac42cdd49d40d504ccb648f27710660c8b60cc427752438c09/pymssql-2.3.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:123c55ee41bc7a82c76db12e2eb189b50d0d7a11222b4f8789206d1cda3b33b9", size = 3710151, upload-time = "2026-02-14T05:00:08.424Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bd/a5cc6244fd27d3ea0cc82f12a7d38a24d7fd90b0022afd250014e8bfba15/pymssql-2.3.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e053b443e842f9e1698fcb2b23a4bff1ff3d410894d880064e754ad823d541e5", size = 3453156, upload-time = "2026-02-14T05:00:09.978Z" }, + { url = "https://files.pythonhosted.org/packages/26/d0/c20ff0bbffd18db528bcc7b0c68b25c12ad563ed67c56ceca87c58f7399e/pymssql-2.3.13-cp313-cp313-win_amd64.whl", hash = "sha256:5c045c0f1977a679cc30d5acd9da3f8aeb2dc6e744895b26444b4a2f20dad9a0", size = 1995236, upload-time = "2026-02-14T05:00:11.495Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5f/6b64f78181d680f655ab40ba7b34cb68c045a2f4e04a10a70d768cd383b7/pymssql-2.3.13-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fc5482969c813b0a45ce51c41844ae5bfa8044ad5ef8b4820ef6de7d4545b7f2", size = 3158377, upload-time = "2026-02-14T05:00:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/ff/24/155dbb0992c431496d440f47fb9d587cd0059ee20baf65e3d891794d862a/pymssql-2.3.13-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:ff5be7ab1d643dbce2ee3424d2ef9ae8e4146cf75bd20946bc7a6108e3ad1e47", size = 2959039, upload-time = "2026-02-14T05:00:15.883Z" }, + { url = "https://files.pythonhosted.org/packages/c9/89/b453dd1b1188779621fb974ac715ab2e738f4a0b69f7291ab014298bd80d/pymssql-2.3.13-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d66ce0a249d2e3b57369048d71e1f00d08dfb90a758d134da0250ae7bc739c1", size = 3063862, upload-time = "2026-02-14T05:00:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/02/e5/96f57c78162013678ecc3f3f7e5fb52c83ee07beef26906d0870770c3ef6/pymssql-2.3.13-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d663c908414a6a032f04d17628138b1782af916afc0df9fefac4751fa394c3ac", size = 3188155, upload-time = "2026-02-14T05:00:19.011Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a2/4bee9484734ae0c55d10a2f6ff82dd4e416f52420755161b8760c817ad64/pymssql-2.3.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa5e07eff7e6e8bd4ba22c30e4cb8dd073e138cd272090603609a15cc5dbc75b", size = 3709344, upload-time = "2026-02-14T05:00:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/37/cf/3520d96afa213c88db4f4a1988199db476d869a62afdd5d9c4635c184631/pymssql-2.3.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db77da1a3fc9b5b5c5400639d79d7658ba7ad620957100c5b025be608b562193", size = 3451799, upload-time = "2026-02-14T05:00:22.504Z" }, + { url = "https://files.pythonhosted.org/packages/25/50/4be9bd9cf4b43208a7175117a533ece200cfe4131a39f9909bdc7560ddeb/pymssql-2.3.13-cp314-cp314-win_amd64.whl", hash = "sha256:7d7037d2b5b907acc7906d0479924db2935a70c720450c41339146a4ada2b93d", size = 2049139, upload-time = "2026-02-14T05:00:23.951Z" }, +] + [[package]] name = "pymysql" version = "1.1.2" @@ -2146,6 +2175,7 @@ dependencies = [ { name = "jsonschema" }, { name = "litellm" }, { name = "psycopg2-binary" }, + { name = "pymssql" }, { name = "pymysql" }, { name = "python-dotenv" }, { name = "python-multipart" }, @@ -2176,6 +2206,7 @@ requires-dist = [ { name = "jsonschema", specifier = "~=4.26.0" }, { name = "litellm", specifier = ">=1.83.0" }, { name = "psycopg2-binary", specifier = "~=2.9.11" }, + { name = "pymssql", specifier = "~=2.3.13" }, { name = "pymysql", specifier = "~=1.1.0" }, { name = "python-dotenv", specifier = "~=1.1.0" }, { name = "python-multipart", specifier = "~=0.0.10" }, From 259b2def2d315e7ca1196e8cc40da454259b466a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:37:41 +0000 Subject: [PATCH 3/4] Address code review: fix async generator consumption in refresh_graph_schema and deduplicate test assertion Agent-Logs-Url: https://github.com/FalkorDB/QueryWeaver/sessions/cb38384c-67c5-45c2-a553-8993ac42376b Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> --- api/loaders/sqlserver_loader.py | 4 +++- tests/test_sqlserver_loader.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/loaders/sqlserver_loader.py b/api/loaders/sqlserver_loader.py index fdc73ef7..e5d9518b 100644 --- a/api/loaders/sqlserver_loader.py +++ b/api/loaders/sqlserver_loader.py @@ -517,7 +517,9 @@ async def refresh_graph_schema(graph_id: str, db_url: str) -> Tuple[bool, str]: prefix = graph_id # Reuse the existing load method to reload the schema - success, message = await SQLServerLoader.load(prefix, db_url) + success, message = False, "" + async for progress in SQLServerLoader.load(prefix, db_url): + success, message = progress if success: logging.info("Graph schema refreshed successfully.") diff --git a/tests/test_sqlserver_loader.py b/tests/test_sqlserver_loader.py index 1abf3b8f..22e9ed58 100644 --- a/tests/test_sqlserver_loader.py +++ b/tests/test_sqlserver_loader.py @@ -116,7 +116,7 @@ def test_is_schema_modifying_query(self): # Edge cases assert SQLServerLoader.is_schema_modifying_query("")[0] is False - assert SQLServerLoader.is_schema_modifying_query("")[0] is False + assert SQLServerLoader.is_schema_modifying_query(" ")[0] is False @patch('pymssql.connect') def test_connection_error(self, mock_connect): From 940c8fd745b17a2b921fc6a0bacfdb7fd2360dd5 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Tue, 21 Apr 2026 13:18:37 +0300 Subject: [PATCH 4/4] Staging-->Main (#455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: update @falkordb/canvas version to 0.0.34 in package.json and package-lock.json * fix: update @falkordb/canvas version to 0.0.35 in package.json and package-lock.json * fix: update @falkordb/canvas version to 0.0.36 in package.json and package-lock.json * Initial plan * Fix flaky Playwright e2e tests for database connection verification - Add waitForGraphPresent() polling helper to apiCalls.ts to retry getGraphs() until expected graph appears instead of one-shot calls - Add connectDatabaseWithRetry() helper to retry streaming connection on transient errors with diagnostic logging - Enhance parseStreamingResponse() to log error message details - Update all database.spec.ts tests to use scoped test.setTimeout(120000/180000) - Increase waitForDatabaseConnection timeout to 90s in all DB connection tests - Replace bare getGraphs() calls with waitForGraphPresent() polling - Add console.log diagnostics throughout for easier CI debugging Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Bump playwright from 1.57.0 to 1.58.0 Bumps [playwright](https://github.com/microsoft/playwright-python) from 1.57.0 to 1.58.0. - [Release notes](https://github.com/microsoft/playwright-python/releases) - [Commits](https://github.com/microsoft/playwright-python/compare/v1.57.0...v1.58.0) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.58.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Address review feedback: try/catch in retry, finalMessage guards, specific DB predicates, polling for deletion - connectDatabaseWithRetry: wrap per-attempt logic in try/catch so network/parse exceptions don't abort retries; log with attempt# via console.error; backoff delay behaviour unchanged - Add expect(messages.length).toBeGreaterThan(0) guard before accessing finalMessage in all 4 caller blocks (PostgreSQL API, MySQL API, PostgreSQL delete, MySQL delete) - Fix UI-to-API test predicates from generic 'graphs.length > 0' to 'testdb'/'_testdb' match, avoiding false positives on pre-existing graphs - Replace wait(1000)+getGraphs() in both delete tests with waitForGraphPresent polling until the deleted graphId is absent Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Rename waitForGraphPresent to waitForGraphs; make database tests serial - Rename waitForGraphPresent -> waitForGraphs in apiCalls.ts (more neutral name since it's used for both presence and absence checks) - Update all 10 call sites in database.spec.ts accordingly - Change outer test.describe -> test.describe.serial to prevent cross-test interference on local multi-worker runs (CI is already single-worker via workers: CI ? 1 : undefined in playwright.config.ts) Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Tighten testdb_delete predicate: use === / endsWith instead of includes Replace id.includes('testdb_delete') with id === 'testdb_delete' || id.endsWith('_testdb_delete') in both delete test predicates and find() calls so only the exact graph forms ('testdb_delete' or '{userId}_testdb_delete') match, preventing accidental matches on unrelated graph names. Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Bump fastapi, uvicorn, litellm, playwright, and globals (#439) Update dependency versions: - fastapi: ~=0.131.0 → ~=0.133.0 - uvicorn: ~=0.40.0 → ~=0.41.0 - litellm: ~=1.80.9 → ~=1.81.15 - playwright: ~=1.57.0 → ~=1.58.0 - globals (npm): ^15.15.0 → ^17.3.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update @falkordb/canvas version to 0.0.40 in package.json and package-lock.json * fix: update @falkordb/canvas version to 1.51.1 in package-lock.json * Fix SPA catch-all route not serving index.html (#433) * Return generic 400 for RequestValidationError instead of Pydantic details Add a global RequestValidationError exception handler that returns {"detail": "Bad request"} with status 400, preventing internal Pydantic validation details from leaking to clients. This primarily affects the SPA catch-all proxy route when accessed without the expected path parameter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scope validation handler to SPA catch-all, add logging, fix tests Address PR review feedback: - Scope the generic 400 handler to only the SPA catch-all route (query._full_path errors) so API consumers still get useful 422 responses with field-level detail - Add logging.warning of validation details for server-side debugging - Make test assertions unconditional instead of guarding behind status-code checks - Add test verifying API routes preserve 422 with field-level info Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix SPA catch-all route parameter name mismatch The function parameter `_full_path` didn't match the URL template `{full_path:path}`, causing FastAPI to treat it as a required query parameter and return 422 for every non-API route. Co-Authored-By: Claude Opus 4.6 * Remove validation error handler workaround The handler was masking a parameter name mismatch in the catch-all route. Now that the root cause is fixed, the handler, its import, pylint suppression, and test file are no longer needed. Co-Authored-By: Claude Opus 4.6 * Suppress pylint unused-argument for catch-all route parameter The parameter name must match the URL template to avoid validation errors, but the function body doesn't use it. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 * Fix: Add CSRF protection via double-submit cookie pattern (#432) * Add CSRF protection via double-submit cookie pattern Add CSRFMiddleware to protect all state-changing endpoints (POST, PUT, DELETE, PATCH) against cross-site request forgery attacks. Backend: - New CSRFMiddleware in app_factory.py sets a csrf_token cookie (non-HttpOnly, readable by JS) on every response - State-changing requests must echo the token via X-CSRF-Token header - Uses hmac.compare_digest for timing-safe validation - Exempts Bearer token auth (not CSRF-vulnerable), login/signup/OAuth flows, and MCP endpoints Frontend: - New app/src/lib/csrf.ts utility reads the cookie and builds headers - All service files (auth, tokens, database, chat) now include the X-CSRF-Token header on every state-changing fetch call Fixes: - CSRF on POST /tokens/generate (API token hijack) - CSRF on POST /logout (forced session termination) - Missing CSRF protection on all other mutating endpoints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback on CSRF protection - Set CSRF cookie on 403 rejection responses so clients can retry - Add max_age (14 days) to CSRF cookie matching session cookie lifetime - Guard document access in csrf.ts for SSR/Node compatibility - Add console.warn when CSRF cookie is missing for easier debugging - Add comment clarifying MCP exempt prefix pattern - Add comprehensive unit tests for CSRF middleware (12 test cases) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix E2E tests: seed CSRF token in API request helpers The E2E API helpers (postRequest, deleteRequest, patchRequest) now make a lightweight GET to /auth-status first to obtain the csrf_token cookie, then include it as X-CSRF-Token header on the actual request. This ensures E2E tests pass with the new CSRF middleware. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #432 review: fix missing CSRF headers and improvements - Add csrfHeaders() to POST /graphs/{id}/refresh in Index.tsx - Add csrfHeaders() to POST /database in DatabaseModal.tsx - Refactor CSRFMiddleware.dispatch() to single return path - Change console.warn to console.debug in csrf.ts - Cache CSRF token per APIRequestContext in E2E helpers - Add DELETE/PATCH and secure-flag tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update @falkordb/canvas version to v0.0.40 (#440) * fix: update @falkordb/canvas version to 0.0.34 in package.json and package-lock.json * fix: update @falkordb/canvas version to 0.0.35 in package.json and package-lock.json * fix: update @falkordb/canvas version to 0.0.36 in package.json and package-lock.json * fix: update @falkordb/canvas version to 0.0.40 in package.json and package-lock.json * fix: update @falkordb/canvas version to 1.51.1 in package-lock.json --------- Co-authored-by: Guy Korland * fix(e2e): pass authenticated request context to API calls and browser pages - Add defaultRequestContext field to ApiCalls class, set via constructor - All API methods now use the default context for auth (session cookies + CSRF) - Tests use Playwright's request fixture which inherits storageState from config - Pass storageState path to BrowserWrapper.createNewPage for authenticated browser sessions - Revert outer test.describe.serial() to test.describe() to prevent cascade failures (inner Database Deletion Tests remain serial as needed) Fixes unauthenticated API requests that caused 401 errors in Firefox E2E tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update @falkordb/canvas version to 0.0.41 in package.json and package-lock.json * Bump fastapi from 0.133.0 to 0.135.0 (#446) * Bump fastapi from 0.133.0 to 0.135.0 Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.133.0 to 0.135.0. - [Release notes](https://github.com/fastapi/fastapi/releases) - [Commits](https://github.com/fastapi/fastapi/compare/0.133.0...0.135.0) --- updated-dependencies: - dependency-name: fastapi dependency-version: 0.135.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * fix(e2e): read CSRF token from storageState when Set-Cookie is absent When the Playwright request fixture is initialised from a storageState that already carries a csrf_token cookie, the server does not emit a new Set-Cookie header. getCsrfToken() would then return undefined, causing every state-changing API call to fail with 403 'CSRF token missing or invalid'. Fall back to reading the token from the context's storageState() when the Set-Cookie header does not contain it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump actions/upload-artifact from 6 to 7 (#444) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * Bump the npm-minor-patch group in /app with 5 updates (#443) * Bump the npm-minor-patch group in /app with 5 updates Bumps the npm-minor-patch group in /app with 5 updates: | Package | From | To | | --- | --- | --- | | [@falkordb/canvas](https://github.com/FalkorDB/falkordb-canvas) | `0.0.40` | `0.0.41` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.19` | `5.90.21` | | [preact](https://github.com/preactjs/preact) | `10.28.3` | `10.28.4` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.71.1` | `7.71.2` | | [autoprefixer](https://github.com/postcss/autoprefixer) | `10.4.23` | `10.4.27` | Updates `@falkordb/canvas` from 0.0.40 to 0.0.41 - [Release notes](https://github.com/FalkorDB/falkordb-canvas/releases) - [Commits](https://github.com/FalkorDB/falkordb-canvas/compare/v0.0.40...v0.0.41) Updates `@tanstack/react-query` from 5.90.19 to 5.90.21 - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.21/packages/react-query) Updates `preact` from 10.28.3 to 10.28.4 - [Release notes](https://github.com/preactjs/preact/releases) - [Commits](https://github.com/preactjs/preact/compare/10.28.3...10.28.4) Updates `react-hook-form` from 7.71.1 to 7.71.2 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.71.1...v7.71.2) Updates `autoprefixer` from 10.4.23 to 10.4.27 - [Release notes](https://github.com/postcss/autoprefixer/releases) - [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/autoprefixer/compare/10.4.23...10.4.27) --- updated-dependencies: - dependency-name: "@falkordb/canvas" dependency-version: 0.0.41 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: "@tanstack/react-query" dependency-version: 5.90.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: preact dependency-version: 10.28.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: react-hook-form dependency-version: 7.71.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: autoprefixer dependency-version: 10.4.27 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch ... Signed-off-by: dependabot[bot] * Update root package-lock.json for app dependency bumps The root package-lock.json must be kept in sync with app/package.json changes since root package.json references app via file: protocol. Without this update, npm ci at the root fails with lockfile mismatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(ci): accelerate Playwright CI from ~16min to ~5min (#448) * perf(ci): accelerate Playwright CI from ~16min to ~5min - Increase CI workers from 1 to 4 (matches ubuntu-latest vCPUs) - Skip Firefox in CI, run Chromium only (halves test count) - Reduce retries from 2 to 1 (still catches transient failures) - Add pip, npm, and Playwright browser caching - Replace hardcoded sleep 20 with health-check polling - Install only Chromium browser (not Firefox) in CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ci): fix YAML indentation and use docker compose --wait Replace inline Python health-check with docker compose --wait flag which natively waits for service healthchecks to pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ci): remove pip cache (incompatible with pipenv setup) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ci): keep 2 retries for flaky AI-dependent chat tests Chat tests that interact with the AI processing endpoint need 2 retries to handle intermittent timeouts, especially under parallel execution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ci): key npm cache on both root and app lockfiles The setup-node npm cache was only keyed on the root package-lock.json. Add cache-dependency-path to include app/package-lock.json so the cache invalidates when frontend dependencies change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ci): add pip caching with Pipfile.lock dependency path The setup-python cache: 'pip' was removed earlier because it failed without cache-dependency-path (defaults to requirements*.txt). Re-add it with cache-dependency-path: Pipfile.lock so pip downloads are cached between runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update comment to reflect hard-coded worker count The comment said 'Use all available vCPUs' but the config hard-codes 4 workers. Update to accurately describe the intentional pinning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump litellm from 1.81.15 to 1.82.0 (#445) Bumps [litellm](https://github.com/BerriAI/litellm) from 1.81.15 to 1.82.0. - [Release notes](https://github.com/BerriAI/litellm/releases) - [Commits](https://github.com/BerriAI/litellm/commits) --- updated-dependencies: - dependency-name: litellm dependency-version: 1.82.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * Bump the npm_and_yarn group across 1 directory with 2 updates (#447) * Bump the npm_and_yarn group across 1 directory with 2 updates Bumps the npm_and_yarn group with 2 updates in the /app directory: [minimatch](https://github.com/isaacs/minimatch) and [rollup](https://github.com/rollup/rollup). Updates `minimatch` from 3.1.2 to 3.1.5 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `rollup` from 4.55.1 to 4.59.0 - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.55.1...v4.59.0) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: rollup dependency-version: 4.59.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] * ci: retrigger CI after transient test failure The previous Playwright test run had database connectivity issues in CI (Docker container readiness timing). All infrastructure steps passed but database connection tests returned success:false. Retriggering to verify. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump version from 0.0.14 to 0.1.0 (#450) * Initial plan * chore: bump version from 0.0.14 to 0.1.0 Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> Co-authored-by: Guy Korland * Change npm install to npm ci for consistency (#454) Change npm install to npm ci for consistency * fix: update @falkordb/canvas version to 0.0.44 in package.json and package-lock.json * Implement feature X to enhance user experience and fix bug Y in module Z * chore: update dependencies and remove unused packages - Removed unused Rollup binaries for various platforms. - Updated `minimatch` from version 9.0.5 to 9.0.9. - Updated `@types/estree` to version 1.0.8. - Updated `ajv` to version 6.14.0 and added its dependencies. - Removed several unused packages including `balanced-match`, `concat-map`, `fast-deep-equal`, and others. - Added new Rollup binaries for version 4.59.0 across multiple platforms. * Move the project from Pipfile to pyproject (#409) * Initial plan * Migrate from Pipfile to pyproject.toml with uv - Created pyproject.toml with all dependencies from Pipfile - Updated Makefile to use uv instead of pipenv - Updated GitHub Actions workflows (tests.yml, pylint.yml, playwright.yml) - Updated Dockerfile to use uv - Updated setup_e2e_tests.sh script - Updated README.md documentation - Updated .github/copilot-instructions.md Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Generate uv.lock and remove Pipfile - Generated uv.lock file with all dependencies - Removed Pipfile and Pipfile.lock - Updated pyproject.toml with hatch build config for api package Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Fix CI: use 'python -m' for pytest and pylint with uv - Updated .github/workflows/tests.yml to use 'uv run python -m pytest' - Updated .github/workflows/pylint.yml to use 'uv run python -m pylint' - Updated Makefile test and lint commands to use 'python -m' syntax - Updated README.md and copilot-instructions.md with correct commands - Updated setup_e2e_tests.sh with correct pytest command This fixes the CI failures where uv couldn't find pytest/pylint executables. Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Add 'uv' and 'pyproject' to spellcheck wordlist Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix test_simple_integration: update conftest.py to use uv instead of pipenv The test fixture was still launching the app via 'pipenv run uvicorn', causing FileNotFoundError in CI where pipenv is no longer installed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Playwright CI: stop background server before post-job cleanup The background 'uv run uvicorn' process was staying alive during setup-uv's cache pruning step, causing a 5-minute timeout and exit code 2 despite all 33 tests passing. Save the server PID via GITHUB_OUTPUT and terminate it in the cleanup step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Allow GHSA-w8v5-vhqr-4h9v in dependency review (no fix available) diskcache@5.6.3 is a transitive dependency via graphiti-core. CVE-2025-69872 (unsafe pickle deserialization) has no patched version upstream yet. Allow-listed to unblock the PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update dependabot to use uv package ecosystem Replace 'pip' with 'uv' so Dependabot manages pyproject.toml + uv.lock natively instead of looking for Pipfile/requirements.txt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Upgrade graphiti-core to 0.28.1 and remove diskcache workaround graphiti-core 0.28.1 no longer depends on diskcache, which had a moderate vulnerability (GHSA-w8v5-vhqr-4h9v). Switch from the git fork to the PyPI release and remove the dependency-review allow-list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Docker build: copy README.md for hatchling metadata hatchling requires README.md during 'uv sync' to validate the project metadata. Copy it alongside pyproject.toml and uv.lock. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> Co-authored-by: Guy Korland Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move project to pyproject (#471) * Initial plan * Migrate from Pipfile to pyproject.toml with uv - Created pyproject.toml with all dependencies from Pipfile - Updated Makefile to use uv instead of pipenv - Updated GitHub Actions workflows (tests.yml, pylint.yml, playwright.yml) - Updated Dockerfile to use uv - Updated setup_e2e_tests.sh script - Updated README.md documentation - Updated .github/copilot-instructions.md Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Generate uv.lock and remove Pipfile - Generated uv.lock file with all dependencies - Removed Pipfile and Pipfile.lock - Updated pyproject.toml with hatch build config for api package Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Fix CI: use 'python -m' for pytest and pylint with uv - Updated .github/workflows/tests.yml to use 'uv run python -m pytest' - Updated .github/workflows/pylint.yml to use 'uv run python -m pylint' - Updated Makefile test and lint commands to use 'python -m' syntax - Updated README.md and copilot-instructions.md with correct commands - Updated setup_e2e_tests.sh with correct pytest command This fixes the CI failures where uv couldn't find pytest/pylint executables. Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Add 'uv' and 'pyproject' to spellcheck wordlist Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix test_simple_integration: update conftest.py to use uv instead of pipenv The test fixture was still launching the app via 'pipenv run uvicorn', causing FileNotFoundError in CI where pipenv is no longer installed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Playwright CI: stop background server before post-job cleanup The background 'uv run uvicorn' process was staying alive during setup-uv's cache pruning step, causing a 5-minute timeout and exit code 2 despite all 33 tests passing. Save the server PID via GITHUB_OUTPUT and terminate it in the cleanup step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Allow GHSA-w8v5-vhqr-4h9v in dependency review (no fix available) diskcache@5.6.3 is a transitive dependency via graphiti-core. CVE-2025-69872 (unsafe pickle deserialization) has no patched version upstream yet. Allow-listed to unblock the PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update dependabot to use uv package ecosystem Replace 'pip' with 'uv' so Dependabot manages pyproject.toml + uv.lock natively instead of looking for Pipfile/requirements.txt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Upgrade graphiti-core to 0.28.1 and remove diskcache workaround graphiti-core 0.28.1 no longer depends on diskcache, which had a moderate vulnerability (GHSA-w8v5-vhqr-4h9v). Switch from the git fork to the PyPI release and remove the dependency-review allow-list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Docker build: copy README.md for hatchling metadata hatchling requires README.md during 'uv sync' to validate the project metadata. Copy it alongside pyproject.toml and uv.lock. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Docker: add .dockerignore and venv PATH for uvicorn - Add .dockerignore to prevent host .venv from being copied into the container (which overwrites the uv-installed packages). - Add /app/.venv/bin to PATH so uvicorn and other entry points installed by uv sync are available to start.sh. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clean up remaining pipenv/poetry references after uv migration (#472) - Remove 'pipenv'/'Pipenv' from spellcheck wordlist (no longer used) - Update docs/postgres_loader.md: replace poetry/pip install with uv add Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: Configurable model usage (#386) * init * merge-staging * update-model-usage * fix: resolve pylint lint errors across PR files Strip trailing whitespace from agents, config, and settings modules. Suppress too-many-statements in app_factory.py factory function. Fix no-else-return, too-many-return-statements, and unused argument in settings.py route handler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: extract shared LLM completion helper to eliminate duplicate code Extract run_completion() into utils.py and use it across all agent modules. Resolves pylint R0801 duplicate-code warning that was causing CI build failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: resolve review issues and add Ollama/Cohere provider support - Add LLM_PROVIDER field to replace binary AZURE_FLAG for provider routing - Add Ollama and Cohere provider support (cherry-picked from #452) - Add _with_prefix() helper to eliminate duplicated prefix logic - Fix Anthropic embedding fallback: fail-fast instead of silent Azure fallback - Fix double-prefix bug in chat.ts (gemini/gemini/... no longer happens) - Fix useEffect clobbering saved model name on Settings page load - Forward custom credentials through confirm flow for destructive operations - Add localStorage persistence for vendor/model settings (API key stays session-only) - Remove stack trace leak from /validate-api-key error responses - Add ollama/cohere to supported vendors in text2sql and settings routes - Replace unnecessary hasattr with direct Pydantic field access - Update .env.example with precedence docs, Ollama and Cohere examples - Remove duplicate SettingsModal (consolidated into Settings page) Co-authored-by: Udaykumar Dhokia Co-authored-by: Claude Opus 4.6 * fix: resolve pylint and CodeQL CI failures - Rename SUPPORTED_VENDORS to snake_case (pylint C0103) - Break long line in settings.py (pylint C0301) - Remove exception details from log to prevent info exposure (CodeQL) - Sanitize vendor in log message to prevent log injection Co-Authored-By: Claude Opus 4.6 * fix: sanitize user input in log to prevent log injection Add _sanitize_for_log() helper that strips \r, \n, and \t from user-provided values before logging, preventing log injection attacks. Co-Authored-By: Claude Opus 4.6 * Fix review comments: CSRF headers, buildApiUrl, type safety, docs - useApiKeyValidation: add csrfHeaders() and buildApiUrl(), strip double-prefix from model name before sending to backend - ChatInterface: use ConfirmRequest type instead of `any` - Settings: use `import type` for AIVendor - README: update provider priority to include Ollama/Cohere, fix Anthropic embeddings note (ValueError, not Azure fallback) - .env.example: fix misleading fallback comments Co-Authored-By: Claude Opus 4.6 * Add Ollama and Cohere to spellcheck wordlist Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Guy Korland Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Udaykumar Dhokia Co-authored-by: Claude Opus 4.6 * chore: consolidate Dependabot PRs and resolve staging conflicts (#473) * Initial plan * Consolidate all 10 open Dependabot PRs into a single update targeting staging Covers: - Bump fastapi from ~=0.124.0 to ~=0.135.1 (PR #468) - Bump @hookform/resolvers from ^3.10.0 to ^5.2.2 (PR #465) - Bump react-router-dom from ^6.30.1 to ^7.13.1 (PR #463) - Bump lucide-react from ^0.462.0 to ^0.577.0 (PR #462) - Bump postcss from ^8.5.6 to ^8.5.8 (PR #470) - Bump actions/cache from v4 to v5 in playwright.yml (PR #456) - Bump docker/login-action from v3 to v4 in publish-docker.yml (PR #457) - Bump docker/metadata-action from v5 to v6 in publish-docker.yml (PR #458) - Bump docker/build-push-action from v6 to v7 in publish-docker.yml (PR #460) - Bump rojopolis/spellcheck-github-actions from 0.58.0 to 0.59.0 in spellcheck.yml (PR #459) Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Add workflow to close superseded Dependabot PRs (#456-#470) as duplicates of #473 Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Add workflow_dispatch trigger so maintainers can manually run close-superseded-prs workflow Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Delete .github/workflows/close-superseded-prs.yml * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove obsolete BrowserRouter future prop for react-router-dom v7 Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Refresh root package lock for CI Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Resolve staging conflicts Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> Co-authored-by: Guy Korland Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: replace unnecessary dynamic import of vendorConfig with static import (#474) vendorConfig.ts is already statically imported by SettingsContext, useApiKeyValidation, Settings, and chat service, so the dynamic import() in ChatInterface.tsx provided no code-splitting benefit and triggered a Vite build warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add AGENTS.md project guide and CLAUDE.md symlink (#477) Provide a single-file onboarding reference covering architecture, tech stack, directory layout, build/test/lint commands, code conventions, environment variables, and CI/CD workflows. CLAUDE.md symlinks to AGENTS.md so both AI coding assistants and contributors find the same guide. Co-authored-by: Claude Opus 4.6 * feat: add support for postgres schema selection (#475) * feat: add support for postgres schema selection Add support for selecting a PostgreSQL schema instead of always using 'public'. The schema is extracted from the connection URL's options parameter (search_path), following PostgreSQL's native libpq format. Changes: - Add _parse_schema_from_url() to extract schema from connection URL - Thread schema parameter through all extraction methods with 'public' default - Add pg_namespace JOINs for correct cross-schema disambiguation - Add schema input field in DatabaseModal (PostgreSQL only) - Add comprehensive unit tests for URL schema parsing - Update documentation with custom schema configuration guide Based on PR #373 by sirudog with the following fixes: - Fix pg_namespace JOIN order in extract_columns_info to prevent duplicate rows when same-named tables exist across schemas - Fix regex to require '=' separator (prevents mis-capture edge cases) - Improve $user handling to loop through all schemas instead of only checking first two positions - Fix pylint line-too-long in test file Co-authored-by: sirudog <1550561+sirudog@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make parse_schema_from_url public to fix CI pylint Rename _parse_schema_from_url to parse_schema_from_url since the method is already documented for external use and tested directly. This eliminates W0212 (protected-access) warnings that cause CI pylint to fail with exit code 4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address review comments on PR #475 - Add constraint_schema qualifier to key_column_usage JOINs in extract_columns_info to prevent cross-schema constraint name collisions - Sanitize schema input in DatabaseModal to strip non-identifier characters before building the URL options - Add edge case tests: empty tokens, blank quoted tokens, repeated $user entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove accidentally committed build artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address copilot reviewer comments on PR #475 - Fix regex to capture search_path values with spaces after commas (e.g. $user, public) by matching up to next -c option or EOL - Set session search_path explicitly after connecting so sample queries resolve to the correct schema - Use versionless PostgreSQL docs link (/docs/current/) - Clarify case-sensitivity note for schema names in troubleshooting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: gitignore build artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: replace ReDoS-vulnerable regex in parse_schema_from_url Replace (.+?)(?=\s+-c|\s*$) with [^\s,]+(?:\s*,\s*[^\s,]+)* to eliminate polynomial backtracking flagged by CodeQL. The new pattern uses unambiguous character classes with no overlapping quantifiers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: validate schema input instead of silent sanitization, fix doc URL encoding - DatabaseModal: Show validation error for invalid schema characters instead of silently stripping them. Throw error on submit if invalid chars present. - docs: URL-encode the example URL to prevent copy/paste connection failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: revert doc URL to readable form to fix spellcheck The URL-encoded form (-csearch_path%3Dmy_schema) inside the Liquid capture block triggers spellcheck failures ('csearch', 'Dmy'). Reverted to readable form since Python's urlparse handles both formats fine. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add missing tech terms to spellcheck wordlist Add terms from AGENTS.md/CLAUDE.md (added in staging merge) to the spellcheck wordlist: config, docstring, dotenv, ESLint, HSTS, init, Middleware, monorepo, PRs, pylint, pytest, Radix, Zod, and error class names. Also fix DockerHub capitalization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: ensure DB connection cleanup on error and add cursor type hints - Wrap psycopg2 connection/cursor in try/finally so they are always closed, even when extract_tables_info or extract_relationships raises - Set conn/cursor to None after explicit close to avoid double-close in the finally block - Add Any type hints to cursor parameters on extract_tables_info, extract_columns_info, extract_foreign_keys, extract_relationships, and _execute_sample_query Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: increase timeout for multi-step E2E chat tests Mark three tests that perform multiple LLM round-trips with test.slow() to triple their timeout (60s → 180s), preventing spurious CI failures when LLM responses are slow: - multiple sequential queries maintain conversation history - switching databases clears chat history - duplicate record shows user-friendly error message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: sirudog <1550561+sirudog@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: require SECRET_TOKEN at startup to prevent auth bypass (#476) * fix: require SECRET_TOKEN at startup to prevent auth bypass The original verify_token() allowed None == None when SECRET_TOKEN was unset, silently disabling authentication. The server now refuses to start without SECRET_TOKEN configured, and validate_user() accepts the static token via constant-time comparison (hmac.compare_digest) as an alternative to DB-backed OAuth tokens. Co-Authored-By: Claude Opus 4.6 * fix: add pylint disable for wrong-import-position in conftest.py The imports must come after os.environ.setdefault() for SECRET_TOKEN, which is intentionally non-standard. Suppress the C0413 warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add SECRET_TOKEN to Playwright CI workflow env The 'Start FastAPI application' step was missing SECRET_TOKEN, causing the app to crash at startup with RuntimeError since the PR made it required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR #455 review comments (#478) - Pin uv version (0.7.12) and use --locked in CI workflows - Fix Dockerfile: split uv sync into deps-only + project install - Remove || true from make lint so pylint failures are not masked - Move max-line-length to [tool.pylint.format] (canonical section) - Fix docs: use 'uv sync' instead of 'uv add' for existing deps - Remove dead initial LLM_PROVIDER/AZURE_FLAG assignments in config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove SECRET_TOKEN static API key requirement (#479) Users create their own API tokens via /tokens/generate (stored in FalkorDB), so the static SECRET_TOKEN env var is redundant. Removes: - The SECRET_TOKEN module-level variable and hmac check in validate_user - The hmac import (no longer needed) - All references in .env.example, CI workflows, and test conftest Reverts the hard requirement introduced in #476. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: require authentication on validate-api-key endpoint (#481) * fix: require authentication on validate-api-key endpoint The POST /api/validate-api-key endpoint was missing the @token_required decorator, allowing unauthenticated users to proxy LLM API calls through the server. Add @token_required to match all other POST endpoints. Co-Authored-By: Claude Opus 4.6 * fix: suppress pylint unused-argument for decorated request param The @token_required decorator consumes the request argument before the function body, so pylint incorrectly flags it as unused. Co-Authored-By: Claude Opus 4.6 * fix: add 401 response to OpenAPI docs for validate-api-key Add responses={401: UNAUTHORIZED_RESPONSE} to match the convention used by all other @token_required endpoints. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 * chore(deps): bump graphiti-core in the uv group across 1 directory (#480) Bumps the uv group with 1 update in the / directory: [graphiti-core](https://github.com/getzep/graphiti). Updates `graphiti-core` from 0.28.1 to 0.28.2 - [Release notes](https://github.com/getzep/graphiti/releases) - [Commits](https://github.com/getzep/graphiti/compare/v0.28.1...v0.28.2) --- updated-dependencies: - dependency-name: graphiti-core dependency-version: 0.28.2 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * ci(workflows): pin GitHub Actions dependencies to commit SHAs (#503) Pin all third-party GitHub Actions to their full commit SHA instead of mutable version tags. This is a supply-chain security best practice that prevents tag-mutation attacks. Changed files: dependency-review.yml, playwright.yml, publish-docker.yml, pylint.yml, spellcheck.yml, tests.yml Total actions pinned: 17 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(deps): combine dependabot dependency updates (#504) Python dependencies: - uvicorn: 0.41.0 → 0.42.0 - litellm: 1.82.0 → 1.82.6 - authlib: 1.6.8 → 1.6.9 - fastmcp: 3.0.1 → 3.1.1 - pytest-asyncio: 1.2.0 → 1.3.0 GitHub Actions: - astral-sh/setup-uv: v5.4.2 → v7.6.0 - rojopolis/spellcheck-github-actions: 0.59.0 → 0.60.0 NPM dependencies (app/): - typescript-eslint: 8.53.0 → 8.57.0 - flatted: 3.3.3 → 3.4.2 - picomatch: 2.3.1 → 2.3.2 / 4.0.3 → 4.0.4 Replaces: #483, #484, #486, #487, #491, #493, #498, #499, #500, #501 Skipped major version bumps (require migration work): - #488 vite 7→8, #489 react-dom 18→19 - #490 tailwind-merge 2→3, #492 tailwindcss 3→4 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review comments - Fix CSRF docstring to reflect actual cookie-setting behavior - Normalize X-Forwarded-Proto (handle comma-separated, mixed-case) - Fix comment typo in create_app - Serialize CI workers to 1 to avoid shared-state flakiness - Fix CSRF comment in e2e apiRequests helper - Retry only successful final_result in connectDatabaseWithRetry - Add .env creation step in Playwright CI workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): upgrade pyjwt 2.11.0 → 2.12.1 (GHSA-752w-5fwx-jx9f) PyJWT 2.11.0 accepts unknown `crit` header extensions in violation of RFC 7515 §4.1.11, allowing security policy bypass. Closes the dependency-review CI failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): upgrade requests 2.32.5 → 2.33.1 (GHSA-gc5v-m9x4-r6x2) Insecure temp file reuse in extract_zipped_paths() utility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): upgrade cryptography 46.0.5 → 46.0.6 (GHSA-m959-cc7f-wv43) Incomplete DNS name constraint enforcement on peer names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): upgrade pygments 2.19.2 → latest (GHSA-5239-wwwm-4pmq) ReDoS via inefficient regex for GUID matching. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci(playwright): skip E2E tests for Dependabot PRs (#518) * ci(playwright): skip E2E tests for Dependabot PRs Dependabot PRs do not have access to repository secrets (AZURE_API_KEY, AZURE_API_BASE, AZURE_API_VERSION) required by the LLM-dependent E2E tests for database schema loading. This causes all Database Connection and Chat Feature tests to fail with 'Failed to load database schema'. Skip the Playwright job for Dependabot-authored PRs while preserving full coverage on push events (merge to staging/main) where secrets are available. Unit tests, pylint, dependency review, and spellcheck still run for all PRs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(agents): note Playwright skip for Dependabot PRs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci(spellcheck): add Dependabot to wordlist Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(deps): bump fastapi from 0.135.1 to 0.135.2 (#509) Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.135.1 to 0.135.2. - [Release notes](https://github.com/fastapi/fastapi/releases) - [Commits](https://github.com/fastapi/fastapi/compare/0.135.1...0.135.2) --- updated-dependencies: - dependency-name: fastapi dependency-version: 0.135.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * chore(deps-dev): bump pytest from 8.4.2 to 9.0.2 (#508) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.2 to 9.0.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.2) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * chore(deps): bump fastmcp in the uv group across 1 directory (#515) Bumps the uv group with 1 update in the / directory: [fastmcp](https://github.com/PrefectHQ/fastmcp). Updates `fastmcp` from 3.1.1 to 3.2.0 - [Release notes](https://github.com/PrefectHQ/fastmcp/releases) - [Changelog](https://github.com/PrefectHQ/fastmcp/blob/main/docs/changelog.mdx) - [Commits](https://github.com/PrefectHQ/fastmcp/compare/v3.1.1...v3.2.0) --- updated-dependencies: - dependency-name: fastmcp dependency-version: 3.2.0 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates (#516) Bumps the npm_and_yarn group with 3 updates in the /app directory: [lodash-es](https://github.com/lodash/lodash), [picomatch](https://github.com/micromatch/picomatch) and [flatted](https://github.com/WebReflection/flatted). Updates `lodash-es` from 4.17.23 to 4.18.1 - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) Updates `picomatch` from 2.3.1 to 2.3.2 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `picomatch` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `flatted` from 3.3.3 to 3.4.2 - [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2) --- updated-dependencies: - dependency-name: lodash-es dependency-version: 4.18.1 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: picomatch dependency-version: 4.0.4 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * chore(deps): bump the npm-minor-patch group in /app with 2 updates (#510) Bumps the npm-minor-patch group in /app with 2 updates: [@falkordb/canvas](https://github.com/FalkorDB/falkordb-canvas) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom). Updates `@falkordb/canvas` from 0.0.44 to 0.0.45 - [Release notes](https://github.com/FalkorDB/falkordb-canvas/releases) - [Commits](https://github.com/FalkorDB/falkordb-canvas/compare/v0.0.44...v0.0.45) Updates `react-router-dom` from 7.13.1 to 7.13.2 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.13.2/packages/react-router-dom) --- updated-dependencies: - dependency-name: "@falkordb/canvas" dependency-version: 0.0.45 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: react-router-dom dependency-version: 7.13.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates (#519) Bumps the npm_and_yarn group with 3 updates in the /app directory: [picomatch](https://github.com/micromatch/picomatch), [flatted](https://github.com/WebReflection/flatted) and [lodash](https://github.com/lodash/lodash). Updates `picomatch` from 2.3.1 to 2.3.2 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `picomatch` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2) Updates `flatted` from 3.3.3 to 3.4.2 - [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2) Updates `lodash` from 4.17.23 to 4.18.1 - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) --- updated-dependencies: - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: picomatch dependency-version: 4.0.4 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: lodash dependency-version: 4.18.1 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Guy Korland * fix(deps): regenerate package-lock.json and fix vulnerabilities Regenerated the root lockfile to sync with current app/package.json dependencies. Also ran npm audit fix to resolve: - lodash Code Injection and Prototype Pollution (high severity) - brace-expansion DoS (moderate severity) - picomatch ReDoS and method injection (high severity) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): bump litellm to ~=1.83.0 to fix critical vulnerabilities Addresses: - GHSA-jjhc-v7c2-5hh6: OIDC authentication bypass via cache key collision (critical) - GHSA-53mr-6c8q-9789: Privilege escalation via unrestricted proxy config (high) Both vulnerabilities are fixed in litellm 1.83.0+. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(deps): upgrade aiohttp to 3.13.5 to fix security vulnerabilities Addresses: - GHSA-w2fm-2cpv-w7v5: unlimited trailer headers (moderate) - GHSA-p998-jp59-783m: UNC SSRF/NTLMv2 credential theft (moderate) - GHSA-m5qp-6w8w-w647: multipart header size bypass (moderate) - GHSA-c427-h43c-vf67: duplicate Host headers (moderate) - GHSA-hcc4-c3v8-rx92: DoS via unbounded DNS cache (low) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(deps): bump astral-sh/setup-uv from 7.6.0 to 8.0.0 (#525) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.6.0 to 8.0.0. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/37802adc94f370d6bfd71619e3f0bf239e1f3b78...cec208311dfd045dd5311c1add060b2062131d57) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(snowflake): add Snowflake loader with key-pair auth and security hardening Snowflake database loader: - Full schema extraction (tables, columns, PKs, FKs, relationships) - Key-pair authentication support (bypasses MFA) - SHOW PRIMARY KEYS / SHOW IMPORTED KEYS for constraint discovery - Identifier validation and parameterized queries for SQL injection prevention - Connection timeouts (login: 30s, network: 60s) Frontend: - Snowflake option in DatabaseModal with manual/URL entry modes - Key-pair auth UI (password/keypair toggle with PEM textarea) - Custom API key/model passed through ChatService to backend Security: - @token_required on /validate-api-key endpoint - Vendor-specific API key format validation - Narrowed vendor allowlist for key validation - Upgraded fastmcp 3.0.1→3.2.0, litellm→1.83+, aiohttp→3.13.5 Other fixes: - load_dotenv() in config.py for reliable env loading - Memory gracefully disabled for non-Azure/OpenAI providers - Null-safe LLM description generation - Anthropic config fails fast without embeddings - python-dotenv as explicit dependency Tests: 39 tests (20 Snowflake loader + 19 settings route) Co-Authored-By: Claude Opus 4.6 (1M context) * fix(spellcheck): add DDL and DML to wordlist Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: remove redundant API key validation from text2sql LLM providers already reject invalid keys with auth errors. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use /settings prefix for settings router instead of /api Consistent with other routers (/graphs, /tokens, /database). Co-Authored-By: Claude Opus 4.6 (1M context) * fix: update test docstring to match /settings route path Co-Authored-By: Claude Opus 4.6 (1M context) * fix: restore staging code reverted during rebase - Restore app_factory.py from staging (CSRF, proxy header handling) with only our 2 changes (remove load_dotenv, /settings prefix) - Restore PostgreSQL schema field in DatabaseModal - Restore vendor prefix logic in ChatService.streamQuery - Restore static getVendorPrefix import in ChatInterface Co-Authored-By: Claude Opus 4.6 (1M context) * chore(deps): bump uvicorn from 0.42.0 to 0.44.0 (#536) Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.42.0 to 0.44.0. - [Release notes](https://github.com/Kludex/uvicorn/releases) - [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md) - [Commits](https://github.com/Kludex/uvicorn/compare/0.42.0...0.44.0) --- updated-dependencies: - dependency-name: uvicorn dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump fastmcp from 3.2.0 to 3.2.4 (#543) Bumps [fastmcp](https://github.com/PrefectHQ/fastmcp) from 3.2.0 to 3.2.4. - [Release notes](https://github.com/PrefectHQ/fastmcp/releases) - [Changelog](https://github.com/PrefectHQ/fastmcp/blob/main/docs/changelog.mdx) - [Commits](https://github.com/PrefectHQ/fastmcp/compare/v3.2.0...v3.2.4) --- updated-dependencies: - dependency-name: fastmcp dependency-version: 3.2.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix Docker build: align Python base to Debian trixie, pin FalkorDB image (#545) * Initial plan * Fix Docker build: align Python base to trixie and pin FalkorDB to v4.18.1 Agent-Logs-Url: https://github.com/FalkorDB/QueryWeaver/sessions/c0859d23-c001-47bd-8dc5-1285e253c7d0 Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> * Update Dockerfile --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> Co-authored-by: Guy Korland * fix(docker): resolve apt dependency errors on falkordb trixie base (#546) The falkordb/falkordb:latest base image (Debian trixie) ships without libtinfo6 and bash. The apt 3.0 solver refuses to install build-essential and friends because util-linux PreDepends on libtinfo6, and the NodeSource setup script requires bash. Install libtinfo6 in a separate apt step so the solver can satisfy the remaining packages, and add bash so the NodeSource installer can run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: Anchel135 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 Co-authored-by: Anchel123 <110421452+Anchel123@users.noreply.github.com> Co-authored-by: Claude <242468646+Claude@users.noreply.github.com> Co-authored-by: Gal Shubeli Co-authored-by: Udaykumar Dhokia Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: sirudog <1550561+sirudog@users.noreply.github.com> --- .dockerignore | 13 + .env.example | 58 +- .github/copilot-instructions.md | 43 +- .github/dependabot.yml | 2 +- .github/wordlist.txt | 31 +- .github/workflows/dependency-review.yml | 7 +- .github/workflows/playwright.yml | 57 +- .github/workflows/publish-docker.yml | 8 +- .github/workflows/pylint.yml | 16 +- .github/workflows/spellcheck.yml | 4 +- .github/workflows/tests.yml | 16 +- .gitignore | 5 +- AGENTS.md | 147 + CLAUDE.md | 1 + Dockerfile | 27 +- Makefile | 22 +- Pipfile | 30 - Pipfile.lock | 3289 --------------- README.md | 77 +- api/agents/analysis_agent.py | 13 +- api/agents/follow_up_agent.py | 15 +- api/agents/relevancy_agent.py | 13 +- api/agents/response_formatter_agent.py | 29 +- api/agents/utils.py | 31 +- api/app_factory.py | 16 +- api/auth/user_management.py | 10 +- api/config.py | 70 +- api/core/schema_loader.py | 4 + api/core/text2sql.py | 41 +- api/index.py | 6 +- api/loaders/postgres_loader.py | 117 +- api/loaders/snowflake_loader.py | 711 ++++ api/memory/graphiti_tool.py | 47 +- api/routes/settings.py | 114 + api/utils.py | 3 +- app/package-lock.json | 311 +- app/package.json | 12 +- app/src/App.tsx | 29 +- app/src/components/chat/ChatInterface.tsx | 34 +- app/src/components/layout/Sidebar.tsx | 4 +- app/src/components/modals/DatabaseModal.tsx | 373 +- app/src/components/modals/SettingsModal.tsx | 109 - app/src/contexts/SettingsContext.tsx | 93 + app/src/hooks/useApiKeyValidation.ts | 76 + app/src/pages/Settings.tsx | 197 +- app/src/services/chat.ts | 37 +- app/src/types/api.ts | 5 + app/src/utils/vendorConfig.ts | 43 + docs/postgres_loader.md | 66 +- docs/snowflake_loader.md | 173 + e2e/infra/api/apiRequests.ts | 4 +- e2e/logic/api/apiCalls.ts | 6 +- e2e/tests/chat.spec.ts | 3 + package-lock.json | 4022 +++++++++++-------- playwright.config.ts | 4 +- pyproject.toml | 75 + pytest.ini | 19 - setup_e2e_tests.sh | 15 +- tests/conftest.py | 8 +- tests/test_postgres_loader.py | 119 + tests/test_settings_route.py | 184 + tests/test_snowflake_loader.py | 404 ++ uv.lock | 2972 ++++++++++++++ 63 files changed, 8949 insertions(+), 5541 deletions(-) create mode 100644 .dockerignore create mode 100644 AGENTS.md create mode 120000 CLAUDE.md delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 api/loaders/snowflake_loader.py create mode 100644 api/routes/settings.py delete mode 100644 app/src/components/modals/SettingsModal.tsx create mode 100644 app/src/contexts/SettingsContext.tsx create mode 100644 app/src/hooks/useApiKeyValidation.ts create mode 100644 app/src/utils/vendorConfig.ts create mode 100644 docs/snowflake_loader.md create mode 100644 pyproject.toml delete mode 100644 pytest.ini create mode 100644 tests/test_settings_route.py create mode 100644 tests/test_snowflake_loader.py create mode 100644 uv.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..86b3638d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.venv +node_modules +dist +.git +.github +*.md +!README.md +__pycache__ +*.pyc +.env +.env.* +e2e +tests diff --git a/.env.example b/.env.example index 94b8210c..c37f9772 100644 --- a/.env.example +++ b/.env.example @@ -42,29 +42,61 @@ FALKORDB_URL=redis://localhost:6379/0 # REQUIRED - change to your FalkorDB URL # FALKORDB_PORT=6379 # ----------------------------- -# Optional API / secret tokens +# API / secret tokens # ----------------------------- -# API token for internal API access (optional) -# SECRET_TOKEN=your_secret_token +# Optional: static bearer token for internal / programmatic API access. +# When set, this token is accepted as a "master" API key. +# When unset, only user-generated tokens are accepted. # SECRET_TOKEN_ERP=your_erp_token # ----------------------------- # AI / LLM configuration (optional) # ----------------------------- -# The default is to use Azure OpenAI if all three variables are set. -# If the OPENAI_API_KEY is set, it will use OpenAI directly. +# QueryWeaver supports multiple AI providers. Set ONE of the following API keys. +# Provider selection precedence (first match wins): +# OLLAMA_MODEL > OPENAI_API_KEY > GEMINI_API_KEY > ANTHROPIC_API_KEY > COHERE_API_KEY > Azure (fallback) +# +# Optional: Override the default models (both must be from the SAME provider): +# COMPLETION_MODEL=provider/model-name +# EMBEDDING_MODEL=provider/model-name +# +# IMPORTANT: The provider must match your API key. If you set gemini/* models, +# you must have GEMINI_API_KEY set. Mismatched overrides may cause runtime errors. +# +# Examples (using Gemini - requires GEMINI_API_KEY): +# COMPLETION_MODEL=gemini/gemini-3-pro-preview +# EMBEDDING_MODEL=gemini/gemini-embedding-001 -# Azure OpenAI (example) -# AZURE_API_KEY=your_azure_api_key -# AZURE_API_BASE=https://your-resource.openai.azure.com/ -# AZURE_API_VERSION=2023-05-15 +# Examples (using OpenAI - requires OPENAI_API_KEY): +# COMPLETION_MODEL=openai/gpt-4.1 +# EMBEDDING_MODEL=openai/text-embedding-ada-002 -# OpenAI (example) +# OpenAI - uses openai/gpt-4.1 and openai/text-embedding-ada-002 # OPENAI_API_KEY=your_openai_api_key -# Optional: override default model names from api/config.py -# COMPLETION_MODEL=azure/gpt-4.1 -# EMBEDDING_MODEL=azure/text-embedding-ada-002 +# Google Gemini - uses gemini/gemini-3-pro-preview and gemini/gemini-embedding-001 +# GEMINI_API_KEY=your_gemini_api_key + +# Anthropic - uses anthropic/claude-sonnet-4-5-20250929 +# Note: Anthropic has no native embeddings. You MUST also set one of: +# VOYAGE_API_KEY or EMBEDDING_MODEL for embeddings (startup fails otherwise). +# ANTHROPIC_API_KEY=your_anthropic_api_key +# VOYAGE_API_KEY=your_voyage_api_key # Optional: for Voyage AI embeddings with Anthropic + +# Cohere - uses cohere/command-a-03-2025 and cohere/embed-v4.0 +# COHERE_API_KEY=your_cohere_api_key +# COHERE_MODEL=command-a-03-2025 +# COHERE_EMBEDDING_MODEL=embed-v4.0 + +# Local open-source model via Ollama (through LiteLLM) +# OLLAMA_MODEL=llama3.1 +# OLLAMA_EMBEDDING_MODEL=nomic-embed-text +# OLLAMA_API_BASE=http://localhost:11434 + +# Azure OpenAI (default fallback) - uses azure/gpt-4.1 and azure/text-embedding-ada-002 +# AZURE_API_KEY=your_azure_api_key +# AZURE_API_BASE=https://your-resource.openai.azure.com/ +# AZURE_API_VERSION=2023-05-15 # ----------------------------- # OAuth configuration (optional — uncomment to enable login flows) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 32367ffb..7f74723f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,7 +10,7 @@ QueryWeaver is an open-source Text2SQL tool that transforms natural language int - **Backend**: Python 3.12+, FastAPI 0.115.0+, FalkorDB (Redis-based graph database) - **AI/ML**: LiteLLM with Azure OpenAI/OpenAI integration for text-to-SQL generation - **Testing**: pytest for unit tests, Playwright for E2E testing -- **Dependencies**: pipenv for package management +- **Dependencies**: uv for package management - **Authentication**: authlib with Google/GitHub OAuth - **Deployment**: Docker support, Vercel configuration @@ -22,8 +22,9 @@ Follow this order for a reliable local setup; if you customize the steps, ensure ### 1. Initial Setup (recommended for new contributors) ```bash -# Install pipenv if not available -pip install pipenv +# Install uv if not available +pip install uv +# or visit https://docs.astral.sh/uv/getting-started/installation/ # Install dependencies (backend + frontend) and prepare dev tools # Recommended: use the Make helper which installs Python deps and frontend deps @@ -34,9 +35,9 @@ make install make setup-dev # OR manual steps if you prefer more granular control: -# pipenv sync --dev -# pipenv run playwright install chromium -# pipenv run playwright install-deps +# uv sync +# uv run playwright install chromium +# uv run playwright install-deps # Set up environment file cp .env.example .env @@ -51,9 +52,9 @@ Note: This project includes a TypeScript frontend in `app/` that must be built b make setup-dev # OR manual steps: -pipenv sync --dev -pipenv run playwright install chromium -pipenv run playwright install-deps +uv sync +uv run playwright install chromium +uv run playwright install-deps ``` ### 3. Testing Commands @@ -82,7 +83,7 @@ make docker-stop ```bash # Run pylint (can be run without FalkorDB) make lint -# OR manually: pipenv run pylint $(git ls-files '*.py') +# OR manually: uv run pylint $(git ls-files '*.py') ``` ### 5. Running the Application @@ -90,11 +91,11 @@ make lint ```bash # Development server with debug mode make run-dev -# OR manually: pipenv run uvicorn api.index:app --host "localhost" --port "5000" --reload +# OR manually: uv run uvicorn api.index:app --host "localhost" --port "5000" --reload # Production mode make run-prod -# OR manually: pipenv run uvicorn api.index:app --host "localhost" --port "5000" +# OR manually: uv run uvicorn api.index:app --host "localhost" --port "5000" ``` Important: If you're preparing a production deployment or have changed frontend code, run `make build-prod` (or `make build-dev` for a development build) first to produce the static bundle used by the app. @@ -183,8 +184,8 @@ make docker-falkordb **Error**: E2E tests fail with browser not found **Solution**: ```bash -pipenv run playwright install chromium -pipenv run playwright install-deps +uv run playwright install chromium +uv run playwright install-deps ``` ### 3. Missing Environment File @@ -197,10 +198,10 @@ cp .env.example .env ### 4. Import Errors During Testing **Error**: Module import failures in tests -**Solution**: Ensure you're using pipenv and dependencies are installed: +**Solution**: Ensure you're using uv and dependencies are installed: ```bash -pipenv sync --dev -pipenv run pytest tests/ -k "not e2e" +uv sync +uv run python -m pytest tests/ -k "not e2e" ``` ### 5. Port Conflicts @@ -242,7 +243,7 @@ tests/ ``` ### Configuration Files -- `Pipfile` & `Pipfile.lock`: Python dependencies +- `pyproject.toml` & `uv.lock`: Python dependencies - `pytest.ini`: Test configuration with custom markers - `Makefile`: Build and development commands - `.env.example`: Environment variable template @@ -268,7 +269,7 @@ The repository has comprehensive CI/CD in `.github/workflows/`: 2. **pylint.yml**: Code quality checks - Runs on every push - - Uses same Python/pipenv setup + - Uses same Python/uv setup 3. **e2e-tests.yml**: Dedicated E2E testing - Separate workflow for E2E tests @@ -281,8 +282,8 @@ The repository has comprehensive CI/CD in `.github/workflows/`: All workflows follow this pattern: ```yaml - Python 3.12 setup -- pipenv installation -- pipenv sync --dev +- uv installation +- uv sync - .env file creation with test values (use FALKORDB_URL in CI) - FalkorDB service startup (for tests requiring DB) - Playwright browser installation (for E2E tests) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bcb81510..8fa39381 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: target-branch: "staging" schedule: interval: "weekly" - - package-ecosystem: "pip" + - package-ecosystem: "uv" directory: "/" target-branch: "staging" schedule: diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 0bbe3fe9..5eacb0df 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -1,6 +1,8 @@ QueryWeaver FalkorDB OAuth +DDL +DML AGPL Affero nullability @@ -8,6 +10,7 @@ schemas psycopg html PostgreSQLLoader +PostgresLoader api postgres postgresql @@ -30,8 +33,8 @@ Nullability endcapture CVSS falkordb -pipenv -Pipenv +uv +pyproject README md UI @@ -71,9 +74,13 @@ namespace namespaced CSRF +Anthropic +Cohere LLM +Ollama OpenAI OpenAI's +DockerHub Dockerhub FDE github @@ -95,4 +102,22 @@ Sanitization JOINs subqueries subquery -TTL \ No newline at end of file +TTL + +config +docstring +dotenv +ESLint +GraphNotFoundError +HSTS +init +InternalError +InvalidArgumentError +Middleware +monorepo +PRs +pylint +pytest +Radix +Zod +Dependabot diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4e43cdf8..e0c3be3e 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -29,12 +29,9 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout repository' - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4 # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. with: comment-summary-in-pr: always - # fail-on-severity: moderate - # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later - # retry-on-snapshot-warnings: true diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 94891b53..c0d9d871 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -19,36 +19,41 @@ env: jobs: test: + # Skip for Dependabot PRs — the LLM-dependent E2E tests require Azure OpenAI + # secrets that are not available in Dependabot PR runs. Full E2E coverage runs + # on the merge-to-staging/main push event where secrets are accessible. + if: github.event_name != 'pull_request' || github.event.pull_request.user.login != 'dependabot[bot]' timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # Setup Python - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'pip' - cache-dependency-path: Pipfile.lock - + + # Install uv + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + version: "0.7.12" + # Setup Node.js - - uses: actions/setup-node@v6 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' cache-dependency-path: | package-lock.json app/package-lock.json - - # Install pipenv - - name: Install pipenv - run: pip install pipenv - + + # Install Python dependencies - name: Install Python dependencies - run: pipenv sync --dev + run: uv sync --locked # Install Node dependencies (root - for Playwright) - name: Install root dependencies @@ -57,7 +62,7 @@ jobs: # Cache Playwright browsers - name: Cache Playwright browsers id: playwright-cache - uses: actions/cache@v4 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: ~/.cache/ms-playwright key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }} @@ -107,12 +112,25 @@ jobs: echo "Testing FalkorDB connectivity from host..." docker ps -a + # Create CI .env so load_dotenv() finds the required variables + - name: Create CI .env + run: | + cat > .env < /tmp/uvicorn.log 2>&1 & + # Start server in background with uv + uv run uvicorn api.index:app --host localhost --port 5000 > /tmp/uvicorn.log 2>&1 & UVICORN_PID=$! + echo "pid=$UVICORN_PID" >> "$GITHUB_OUTPUT" echo "Started server with PID: $UVICORN_PID" # Wait for app to start echo "Waiting for application to start..." @@ -162,7 +180,7 @@ jobs: CI: true # Upload test results on failure - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 if: failure() with: name: playwright-report @@ -170,17 +188,18 @@ jobs: retention-days: 30 # Upload test screenshots on failure - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 if: failure() with: name: test-results path: test-results/ retention-days: 30 - # Cleanup - Stop all containers - - name: Cleanup Docker containers + # Cleanup - Stop all containers and background processes + - name: Cleanup if: always() run: | + kill ${{ steps.start-server.outputs.pid }} || true docker compose -f e2e/docker-compose.test.yml down -v || true docker stop falkordb-test || true docker rm falkordb-test || true diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 8eb2753b..265bd456 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -12,22 +12,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repo - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: falkordb/queryweaver - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . push: true diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 08930af1..57da534b 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -9,22 +9,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.12' - - name: Install pipenv - run: | - python -m pip install --upgrade pip - pip install pipenv + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + version: "latest" - name: Install dependencies run: | - pipenv sync --dev + uv sync - name: Run pylint run: | - pipenv run pylint $(git ls-files '*.py') \ No newline at end of file + uv run python -m pylint $(git ls-files '*.py') \ No newline at end of file diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 7fce97f2..ffcc1d88 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -10,9 +10,9 @@ jobs: spellcheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Spellcheck - uses: rojopolis/spellcheck-github-actions@0.58.0 + uses: rojopolis/spellcheck-github-actions@e3cd8e9aec4587ec73bc0e60745aafd45c37aa2e # 0.60.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bba98e22..f393b892 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,21 +29,21 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: '3.12' - - name: Install pipenv - run: | - python -m pip install --upgrade pip - pip install pipenv + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + version: "0.7.12" - name: Install dependencies run: | - pipenv sync --dev + uv sync --locked - name: Install frontend dependencies run: | @@ -63,7 +63,7 @@ jobs: - name: Run unit tests run: | - pipenv run pytest tests/ -k "not e2e" --verbose + uv run python -m pytest tests/ -k "not e2e" --verbose - name: Run lint run: | diff --git a/.gitignore b/.gitignore index a9e199cd..3bf6a813 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ demo_tokens.py /blob-report/ /playwright/.cache/ /playwright/.auth/ -e2e/.auth/ \ No newline at end of file +e2e/.auth/ +# Build artifacts +clients/python/queryweaver_client.egg-info/ +clients/ts/dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..e99aae12 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,147 @@ +# AGENTS.md — QueryWeaver (Text2SQL) + +## Project Overview + +QueryWeaver is an open-source Text2SQL tool that converts natural-language questions into SQL using graph-powered schema understanding backed by FalkorDB. It is a full-stack monorepo with a Python/FastAPI backend (`api/`) and a React/TypeScript frontend (`app/`). + +Repository: `FalkorDB/QueryWeaver` + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Backend | Python 3.12+, FastAPI, Uvicorn | +| Frontend | React 18, TypeScript 5.8, Vite 7, Tailwind CSS | +| Graph DB | FalkorDB | +| LLM | LiteLLM (multi-provider: OpenAI, Gemini, Anthropic, Cohere, Azure, Ollama) | +| Auth | OAuth 2.0 (Google, GitHub) via authlib | +| Package mgmt | uv (Python), npm (Node) | +| Testing | pytest (unit), Playwright (E2E) | +| Linting | pylint (Python), ESLint (TypeScript) | +| CI/CD | GitHub Actions | + +## Directory Structure + +``` +api/ Python backend (FastAPI) + agents/ AI agents (analysis, healing, relevancy, follow-up) + analyzers/ Code/syntax analyzers + auth/ OAuth handlers, user management + core/ Core text2sql logic, schema loading, errors + entities/ Data models / DTOs + loaders/ Database loaders (PostgreSQL, MySQL) + memory/ Conversation memory management + routes/ API endpoints (auth, graphs, database, tokens, settings) + sql_utils/ SQL sanitization + config.py LLM provider detection, configuration + app_factory.py FastAPI app init, middleware + index.py Application entry point +app/ React + Vite frontend + src/components/ React components + src/contexts/ React contexts (Auth, Chat, Database, Settings) + src/services/ API service layer + src/types/ TypeScript type definitions +tests/ Unit tests (pytest) +e2e/ End-to-end tests (Playwright) +docs/ Documentation +.github/workflows/ CI/CD pipelines +``` + +## Quick Reference + +### Install & Setup + +```bash +make install # uv sync + npm ci +make setup-dev # install + Playwright browsers +cp .env.example .env # configure environment +``` + +### Run + +```bash +make run-dev # dev server with hot reload (localhost:5000) +make run-prod # production server +make docker-falkordb # start FalkorDB in Docker +``` + +### Test + +```bash +make test-unit # pytest unit tests +make test-e2e # Playwright E2E (headless) +make test-e2e-headed # Playwright E2E (visible browser) +make test # build + unit + E2E +``` + +### Lint + +```bash +make lint # pylint + ESLint +make lint-frontend # ESLint only +``` + +### Build + +```bash +make build-dev # Vite dev build +make build-prod # Vite production build +``` + +## Code Conventions + +### Python (backend) + +- PEP 8 with **120-char line limit** +- Type hints throughout +- pylint for linting (docstring checks disabled) +- FastAPI routers split by domain under `api/routes/` +- Custom exceptions in `api/core/errors.py` (GraphNotFoundError, InternalError, InvalidArgumentError) +- Middleware: CSRF (double-submit cookies), HSTS headers +- Environment config via dotenv; see `api/config.py` for defaults and provider detection +- Run backend: `uv run uvicorn api.index:app` + +### TypeScript / React (frontend) + +- Strict TypeScript +- ESLint with `@typescript-eslint`; unused vars prefixed with `_` are allowed +- State management via React Context API +- API calls through service layer (`app/src/services/`) +- Styling with Tailwind CSS + Radix UI primitives +- Routing with React Router v7 +- Forms with React Hook Form + Zod validation +- Build tool: Vite (dev proxy to backend on port 5000) + +### Testing + +- **Unit tests** (`tests/`): pytest with markers `e2e`, `slow`, `auth`, `integration`, `unit` +- **E2E tests** (`e2e/`): Playwright with Page Object Model pattern; auth setup runs first +- E2E infra lives in `e2e/infra/`, page objects in `e2e/logic/pom/` +- Test data (SQL init scripts) in `e2e/test-data/` + +## Environment Variables + +Required: +- `FASTAPI_SECRET_KEY` — session secret +- `FALKORDB_URL` — FalkorDB connection string (e.g. `redis://localhost:6379/0`) + +LLM provider (set one): `OPENAI_API_KEY`, `GEMINI_API_KEY`, `ANTHROPIC_API_KEY`, `COHERE_API_KEY`, `AZURE_API_KEY`, or `OLLAMA_MODEL` + +Optional overrides: `COMPLETION_MODEL`, `EMBEDDING_MODEL` (must match provider) + +See `.env.example` for the full list. + +## CI/CD + +GitHub Actions workflows (`.github/workflows/`): +- **tests.yml** — unit tests + lint on push/PR to main/staging +- **playwright.yml** — dedicated Playwright E2E suite (skipped for Dependabot PRs; secrets unavailable) +- **pylint.yml** — Python linting +- **spellcheck.yml** — docs spellcheck +- **publish-docker.yml** — build & push Docker image to DockerHub +- **dependency-review.yml** — dependency security review + +## Branching + +- `main` — production branch, target for PRs +- `staging` — integration branch diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a9517927..f7ae3720 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Multi-stage build: Start with Python 3.12 base -FROM python:3.12-bookworm AS python-base +FROM python:3.12-trixie AS python-base # Main stage: Use FalkorDB base and copy Python 3.12 FROM falkordb/falkordb:latest @@ -15,7 +15,9 @@ COPY --from=python-base /usr/local /usr/local # Install netcat for wait loop in start.sh and system build tools needed for # compiling Python wheels (g++, make, libc-dev) -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN apt-get update && apt-get install -y --no-install-recommends libtinfo6 \ + && apt-get install -y --no-install-recommends \ + bash \ netcat-openbsd \ git \ build-essential \ @@ -28,14 +30,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app -# Install pipenv -RUN python3 -m pip install --no-cache-dir --break-system-packages pipenv +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv -# Copy Pipfile and Pipfile.lock -COPY Pipfile Pipfile.lock ./ +# Copy pyproject.toml, uv.lock, and README.md (needed by hatchling during install) +COPY pyproject.toml uv.lock* README.md ./ -# Install Python dependencies from Pipfile -RUN PIP_BREAK_SYSTEM_PACKAGES=1 pipenv sync --system +# Install packages into system Python (no virtualenv in container) +ENV UV_SYSTEM_PYTHON=1 + +# Ensure venv binaries are on PATH (uv sync always creates .venv) +ENV PATH="/app/.venv/bin:$PATH" + +# Install Python dependencies only (project itself installed after COPY) +RUN uv sync --frozen --no-dev --no-install-project # Install Node.js (Node 22) so we can build the frontend inside the image. # Use NodeSource setup script to get a recent Node version on Debian-based images. @@ -69,6 +77,9 @@ RUN npm --prefix ./app run build # Copy application code COPY . . +# Install the project package now that source code is available +RUN uv sync --frozen --no-dev + # Copy and make start.sh executable COPY start.sh /start.sh RUN chmod +x /start.sh diff --git a/Makefile b/Makefile index 4ebeda8b..54b5ac9a 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,13 @@ help: ## Show this help message @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST) install: ## Install dependencies - pipenv sync --dev - npm install --prefix ./app + uv sync + npm ci --prefix ./app setup-dev: install ## Set up development environment - pipenv run playwright install chromium - pipenv run playwright install-deps + uv run playwright install chromium + uv run playwright install-deps @echo "Development environment setup complete!" @echo "Don't forget to copy .env.example to .env and configure your settings" @@ -26,23 +26,23 @@ build-prod: test: build-dev test-unit test-e2e ## Run all tests test-unit: ## Run unit tests only - pipenv run python -m pytest tests/ -k "not e2e" --verbose + uv run python -m pytest tests/ -k "not e2e" --verbose test-e2e: build-dev ## Run E2E tests headless - pipenv run python -m pytest tests/e2e/ --browser chromium --video=on --screenshot=on + uv run python -m pytest tests/e2e/ --browser chromium --video=on --screenshot=on test-e2e-headed: build-dev ## Run E2E tests with browser visible - pipenv run python -m pytest tests/e2e/ --browser chromium --headed + uv run python -m pytest tests/e2e/ --browser chromium --headed test-e2e-debug: build-dev ## Run E2E tests with debugging enabled - pipenv run python -m pytest tests/e2e/ --browser chromium --slowmo=1000 + uv run python -m pytest tests/e2e/ --browser chromium --slowmo=1000 lint: ## Run linting (backend + frontend) @echo "Running backend lint (pylint)" - pipenv run pylint $(shell git ls-files '*.py') || true + uv run python -m pylint $(shell git ls-files '*.py') @echo "Running frontend lint (eslint)" make lint-frontend @@ -61,10 +61,10 @@ clean: ## Clean up test artifacts find . -name "*.pyo" -delete run-dev: build-dev ## Run development server - pipenv run uvicorn api.index:app --host $${HOST:-127.0.0.1} --port $${PORT:-5000} --reload + uv run uvicorn api.index:app --host $${HOST:-127.0.0.1} --port $${PORT:-5000} --reload run-prod: build-prod ## Run production server - pipenv run uvicorn api.index:app --host $${HOST:-0.0.0.0} --port $${PORT:-5000} + uv run uvicorn api.index:app --host $${HOST:-0.0.0.0} --port $${PORT:-5000} docker-falkordb: ## Start FalkorDB in Docker for testing docker run -d --name falkordb-test -p 6379:6379 falkordb/falkordb:latest diff --git a/Pipfile b/Pipfile deleted file mode 100644 index c1c7fa77..00000000 --- a/Pipfile +++ /dev/null @@ -1,30 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -fastapi = "~=0.135.0" -uvicorn = "~=0.41.0" -litellm = "~=1.82.0" -falkordb = "~=1.6.0" -psycopg2-binary = "~=2.9.11" -pymysql = "~=1.1.0" -authlib = "~=1.6.4" -itsdangerous = "~=2.2.0" -jsonschema = "~=4.26.0" -tqdm = "~=4.67.3" -python-multipart = "~=0.0.10" -jinja2 = "~=3.1.4" -graphiti-core = {ref = "staging", git = "git+https://github.com/FalkorDB/graphiti.git"} -fastmcp = ">=2.13.1" - -[dev-packages] -pytest = "~=8.4.2" -pylint = "~=4.0.3" -playwright = "~=1.58.0" -pytest-playwright = "~=0.7.1" -pytest-asyncio = "~=1.2.0" - -[requires] -python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index a01f66f2..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,3289 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "1fba6f108fd74d27ba11d829c89ee35b72233b52c5efefd802e877194ff152e8" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.12" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "aiofile": { - "hashes": [ - "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", - "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b" - ], - "markers": "python_version >= '3.8' and python_version < '4'", - "version": "==3.9.0" - }, - "aiohappyeyeballs": { - "hashes": [ - "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", - "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8" - ], - "markers": "python_version >= '3.9'", - "version": "==2.6.1" - }, - "aiohttp": { - "hashes": [ - "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", - "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", - "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", - "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", - "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", - "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", - "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", - "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", - "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", - "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", - "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", - "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", - "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", - "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", - "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", - "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", - "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", - "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", - "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", - "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", - "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", - "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", - "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", - "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", - "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", - "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", - "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", - "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", - "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", - "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", - "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", - "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", - "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", - "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", - "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", - "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", - "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", - "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", - "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", - "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", - "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", - "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", - "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", - "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", - "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", - "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", - "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", - "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", - "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", - "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", - "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", - "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", - "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", - "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", - "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", - "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", - "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", - "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", - "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", - "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", - "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", - "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", - "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", - "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", - "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", - "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", - "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", - "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", - "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", - "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", - "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", - "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", - "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", - "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", - "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", - "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", - "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", - "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", - "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", - "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", - "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", - "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", - "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", - "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", - "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", - "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", - "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", - "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", - "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", - "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", - "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", - "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", - "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", - "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", - "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", - "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", - "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", - "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", - "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", - "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", - "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", - "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", - "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", - "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", - "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", - "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", - "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", - "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", - "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", - "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", - "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", - "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", - "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", - "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", - "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", - "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", - "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", - "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", - "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", - "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940" - ], - "markers": "python_version >= '3.9'", - "version": "==3.13.3" - }, - "aiosignal": { - "hashes": [ - "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", - "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" - ], - "markers": "python_version >= '3.9'", - "version": "==1.4.0" - }, - "annotated-doc": { - "hashes": [ - "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", - "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" - ], - "markers": "python_version >= '3.8'", - "version": "==0.0.4" - }, - "annotated-types": { - "hashes": [ - "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", - "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" - ], - "markers": "python_version >= '3.8'", - "version": "==0.7.0" - }, - "anyio": { - "hashes": [ - "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", - "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c" - ], - "markers": "python_version >= '3.9'", - "version": "==4.12.1" - }, - "attrs": { - "hashes": [ - "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", - "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" - ], - "markers": "python_version >= '3.9'", - "version": "==25.4.0" - }, - "authlib": { - "hashes": [ - "sha256:41ae180a17cf672bc784e4a518e5c82687f1fe1e98b0cafaeda80c8e4ab2d1cb", - "sha256:97286fd7a15e6cfefc32771c8ef9c54f0ed58028f1322de6a2a7c969c3817888" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.6.8" - }, - "backoff": { - "hashes": [ - "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", - "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" - ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==2.2.1" - }, - "beartype": { - "hashes": [ - "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", - "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2" - ], - "markers": "python_version >= '3.10'", - "version": "==0.22.9" - }, - "cachetools": { - "hashes": [ - "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", - "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341" - ], - "markers": "python_version >= '3.10'", - "version": "==7.0.1" - }, - "caio": { - "hashes": [ - "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", - "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", - "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", - "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", - "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", - "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", - "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", - "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", - "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", - "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", - "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", - "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044" - ], - "markers": "python_version >= '3.10'", - "version": "==0.9.25" - }, - "certifi": { - "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" - ], - "markers": "python_version >= '3.7'", - "version": "==2026.2.25" - }, - "cffi": { - "hashes": [ - "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", - "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", - "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", - "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", - "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", - "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", - "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", - "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", - "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", - "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", - "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", - "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", - "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", - "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", - "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", - "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", - "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", - "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", - "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", - "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", - "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", - "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", - "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", - "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", - "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", - "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", - "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", - "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", - "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", - "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", - "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", - "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", - "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", - "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", - "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", - "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", - "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", - "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", - "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", - "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", - "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", - "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", - "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", - "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", - "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", - "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", - "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", - "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", - "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", - "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", - "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", - "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", - "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", - "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", - "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", - "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", - "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", - "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", - "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", - "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", - "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", - "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", - "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", - "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", - "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", - "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", - "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", - "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", - "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", - "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", - "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", - "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", - "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", - "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", - "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", - "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", - "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", - "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", - "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", - "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", - "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", - "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", - "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", - "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", - "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", - "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", - "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", - "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", - "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", - "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", - "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", - "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", - "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", - "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", - "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", - "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", - "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", - "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", - "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", - "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", - "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", - "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", - "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", - "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", - "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", - "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", - "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", - "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", - "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", - "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", - "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", - "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", - "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", - "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", - "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", - "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", - "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", - "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", - "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", - "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", - "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", - "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", - "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", - "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", - "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", - "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", - "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", - "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", - "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", - "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", - "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", - "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", - "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", - "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", - "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", - "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", - "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", - "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", - "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", - "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", - "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", - "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", - "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", - "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", - "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", - "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", - "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", - "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", - "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", - "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", - "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", - "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", - "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", - "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", - "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", - "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", - "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", - "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", - "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", - "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", - "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", - "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", - "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", - "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", - "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", - "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", - "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", - "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", - "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", - "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", - "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", - "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", - "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", - "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", - "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", - "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", - "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", - "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", - "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", - "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", - "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", - "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", - "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", - "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", - "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", - "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", - "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", - "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", - "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", - "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", - "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", - "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", - "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", - "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", - "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", - "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.4" - }, - "click": { - "hashes": [ - "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", - "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" - ], - "markers": "python_version >= '3.10'", - "version": "==8.3.1" - }, - "cryptography": { - "hashes": [ - "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", - "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", - "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", - "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", - "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", - "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", - "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", - "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", - "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", - "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", - "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", - "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", - "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", - "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", - "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", - "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", - "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", - "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", - "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", - "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", - "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", - "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", - "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", - "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", - "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", - "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", - "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", - "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", - "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", - "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", - "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", - "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", - "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", - "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", - "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", - "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", - "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", - "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", - "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", - "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", - "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", - "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", - "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", - "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", - "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", - "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", - "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", - "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", - "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87" - ], - "markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'", - "version": "==46.0.5" - }, - "cyclopts": { - "hashes": [ - "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", - "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5" - ], - "markers": "python_version >= '3.10'", - "version": "==4.6.0" - }, - "diskcache": { - "hashes": [ - "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", - "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19" - ], - "markers": "python_version >= '3'", - "version": "==5.6.3" - }, - "distro": { - "hashes": [ - "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", - "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2" - ], - "markers": "python_version >= '3.6'", - "version": "==1.9.0" - }, - "dnspython": { - "hashes": [ - "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", - "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f" - ], - "markers": "python_version >= '3.10'", - "version": "==2.8.0" - }, - "docstring-parser": { - "hashes": [ - "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", - "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708" - ], - "markers": "python_version >= '3.8'", - "version": "==0.17.0" - }, - "docutils": { - "hashes": [ - "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", - "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de" - ], - "markers": "python_version >= '3.9'", - "version": "==0.22.4" - }, - "email-validator": { - "hashes": [ - "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", - "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426" - ], - "markers": "python_version >= '3.8'", - "version": "==2.3.0" - }, - "exceptiongroup": { - "hashes": [ - "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", - "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "falkordb": { - "hashes": [ - "sha256:0f190e9d6104595fd51ece4f1e7b5d49d62cfee346d94151d7986a138fd90d89", - "sha256:5c307d973f3fc3987a18478ebd5882f7e842d4225463a8ef5e026970ebfba8c6" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==1.6.0" - }, - "fastapi": { - "hashes": [ - "sha256:31e2ddc78d6406c6f7d5d7b9996a057985e2600fbe7e9ba6ace8205d48dff688", - "sha256:bd37903acf014d1284bda027096e460814dca9699f9dacfe11c275749d949f4d" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==0.135.0" - }, - "fastmcp": { - "hashes": [ - "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", - "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==3.0.2" - }, - "fastuuid": { - "hashes": [ - "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", - "sha256:0737606764b29785566f968bd8005eace73d3666bd0862f33a760796e26d1ede", - "sha256:089c18018fdbdda88a6dafd7d139f8703a1e7c799618e33ea25eb52503d28a11", - "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", - "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", - "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", - "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", - "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", - "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", - "sha256:139d7ff12bb400b4a0c76be64c28cbe2e2edf60b09826cbfd85f33ed3d0bbe8b", - "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00", - "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", - "sha256:193ca10ff553cf3cc461572da83b5780fc0e3eea28659c16f89ae5202f3958d4", - "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219", - "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", - "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", - "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", - "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", - "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", - "sha256:2dce5d0756f046fa792a40763f36accd7e466525c5710d2195a038f93ff96346", - "sha256:2ec3d94e13712a133137b2805073b65ecef4a47217d5bac15d8ac62376cefdb4", - "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", - "sha256:2fc37479517d4d70c08696960fad85494a8a7a0af4e93e9a00af04d74c59f9e3", - "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", - "sha256:3964bab460c528692c70ab6b2e469dd7a7b152fbe8c18616c58d34c93a6cf8d4", - "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", - "sha256:448aa6833f7a84bfe37dd47e33df83250f404d591eb83527fa2cac8d1e57d7f3", - "sha256:47c821f2dfe95909ead0085d4cb18d5149bca704a2b03e03fb3f81a5202d8cea", - "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6", - "sha256:5816d41f81782b209843e52fdef757a361b448d782452d96abedc53d545da722", - "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a", - "sha256:6fbc49a86173e7f074b1a9ec8cf12ca0d54d8070a85a06ebf0e76c309b84f0d0", - "sha256:73657c9f778aba530bc96a943d30e1a7c80edb8278df77894fe9457540df4f85", - "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", - "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", - "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", - "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d", - "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a", - "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", - "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8", - "sha256:84b0779c5abbdec2a9511d5ffbfcd2e53079bf889824b32be170c0d8ef5fc74c", - "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", - "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", - "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", - "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", - "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", - "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", - "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", - "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", - "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", - "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", - "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4", - "sha256:af5967c666b7d6a377098849b07f83462c4fedbafcf8eb8bc8ff05dcbe8aa209", - "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470", - "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", - "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", - "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", - "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe", - "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", - "sha256:c0eb25f0fd935e376ac4334927a59e7c823b36062080e2e13acbaf2af15db836", - "sha256:c3091e63acf42f56a6f74dc65cfdb6f99bfc79b5913c8a9ac498eb7ca09770a8", - "sha256:c501561e025b7aea3508719c5801c360c711d5218fc4ad5d77bf1c37c1a75779", - "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", - "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", - "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", - "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", - "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", - "sha256:d31f8c257046b5617fc6af9c69be066d2412bdef1edaa4bdf6a214cf57806105", - "sha256:d55b7e96531216fc4f071909e33e35e5bfa47962ae67d9e84b00a04d6e8b7173", - "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", - "sha256:de01280eabcd82f7542828ecd67ebf1551d37203ecdfd7ab1f2e534edb78d505", - "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", - "sha256:e0976c0dff7e222513d206e06341503f07423aceb1db0b83ff6851c008ceee06", - "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", - "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", - "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", - "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", - "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d" - ], - "markers": "python_version >= '3.8'", - "version": "==0.14.0" - }, - "filelock": { - "hashes": [ - "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", - "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3" - ], - "markers": "python_version >= '3.10'", - "version": "==3.25.0" - }, - "frozenlist": { - "hashes": [ - "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", - "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", - "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", - "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", - "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", - "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", - "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", - "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", - "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", - "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", - "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", - "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", - "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", - "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", - "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", - "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", - "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", - "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", - "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", - "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", - "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", - "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", - "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", - "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", - "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", - "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", - "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", - "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", - "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", - "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", - "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", - "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", - "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", - "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", - "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", - "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", - "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", - "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", - "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", - "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", - "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", - "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", - "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", - "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", - "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", - "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", - "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", - "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", - "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", - "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", - "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", - "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", - "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", - "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", - "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", - "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", - "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", - "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", - "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", - "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", - "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", - "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", - "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", - "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", - "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", - "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", - "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", - "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", - "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", - "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", - "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", - "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", - "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", - "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", - "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", - "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", - "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", - "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", - "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", - "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", - "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", - "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", - "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", - "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", - "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", - "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", - "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", - "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", - "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", - "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", - "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", - "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", - "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", - "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", - "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", - "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", - "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", - "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", - "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", - "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", - "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", - "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", - "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", - "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", - "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", - "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", - "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", - "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", - "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", - "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", - "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", - "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", - "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", - "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", - "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", - "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", - "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", - "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", - "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", - "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", - "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", - "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", - "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", - "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", - "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", - "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", - "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", - "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", - "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", - "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd" - ], - "markers": "python_version >= '3.9'", - "version": "==1.8.0" - }, - "fsspec": { - "hashes": [ - "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", - "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437" - ], - "markers": "python_version >= '3.10'", - "version": "==2026.2.0" - }, - "graphiti-core": { - "git": "git+https://github.com/FalkorDB/graphiti.git", - "markers": "python_version >= '3.10' and python_version < '4'", - "ref": "6e3c54ae2891e10a24d698a2f0733d65d148edf4" - }, - "h11": { - "hashes": [ - "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", - "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" - ], - "markers": "python_version >= '3.8'", - "version": "==0.16.0" - }, - "hf-xet": { - "hashes": [ - "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", - "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", - "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", - "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", - "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", - "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", - "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", - "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", - "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", - "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", - "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", - "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", - "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", - "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", - "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", - "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", - "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", - "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", - "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", - "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", - "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", - "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", - "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", - "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", - "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f" - ], - "markers": "python_version >= '3.8'", - "version": "==1.3.2" - }, - "httpcore": { - "hashes": [ - "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", - "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" - ], - "markers": "python_version >= '3.8'", - "version": "==1.0.9" - }, - "httpx": { - "hashes": [ - "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", - "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" - ], - "markers": "python_version >= '3.8'", - "version": "==0.28.1" - }, - "httpx-sse": { - "hashes": [ - "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", - "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.3" - }, - "huggingface-hub": { - "hashes": [ - "sha256:c9c0b3ab95a777fc91666111f3b3ede71c0cdced3614c553a64e98920585c4ee", - "sha256:f281838db29265880fb543de7a23b0f81d3504675de82044307ea3c6c62f799d" - ], - "markers": "python_full_version >= '3.9.0'", - "version": "==1.5.0" - }, - "idna": { - "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" - ], - "markers": "python_version >= '3.8'", - "version": "==3.11" - }, - "importlib-metadata": { - "hashes": [ - "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", - "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151" - ], - "markers": "python_version >= '3.9'", - "version": "==8.7.1" - }, - "itsdangerous": { - "hashes": [ - "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", - "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.2.0" - }, - "jaraco.classes": { - "hashes": [ - "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", - "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" - ], - "markers": "python_version >= '3.8'", - "version": "==3.4.0" - }, - "jaraco.context": { - "hashes": [ - "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", - "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda" - ], - "markers": "python_version >= '3.9'", - "version": "==6.1.0" - }, - "jaraco.functools": { - "hashes": [ - "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", - "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb" - ], - "markers": "python_version >= '3.9'", - "version": "==4.4.0" - }, - "jeepney": { - "hashes": [ - "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", - "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" - ], - "markers": "python_version >= '3.7'", - "version": "==0.9.0" - }, - "jinja2": { - "hashes": [ - "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", - "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.1.6" - }, - "jiter": { - "hashes": [ - "sha256:00203f47c214156df427b5989de74cb340c65c8180d09be1bf9de81d0abad599", - "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", - "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", - "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", - "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", - "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", - "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", - "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", - "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", - "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", - "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", - "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", - "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", - "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", - "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", - "sha256:19cd6f85e1dc090277c3ce90a5b7d96f32127681d825e71c9dce28788e39fc0c", - "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", - "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", - "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", - "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", - "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", - "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", - "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", - "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", - "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", - "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", - "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", - "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", - "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", - "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", - "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", - "sha256:4397ee562b9f69d283e5674445551b47a5e8076fdde75e71bfac5891113dc543", - "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", - "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", - "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", - "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", - "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", - "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", - "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", - "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", - "sha256:6207fc61c395b26fffdcf637a0b06b4326f35bfa93c6e92fe1a166a21aeb6731", - "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", - "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", - "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", - "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", - "sha256:6eeb7db8bc77dc20476bc2f7407a23dbe3d46d9cc664b166e3d474e1c1de4baa", - "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", - "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", - "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", - "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", - "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", - "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", - "sha256:7c26ad6967c9dcedf10c995a21539c3aa57d4abad7001b7a84f621a263a6b605", - "sha256:7f90023f8f672e13ea1819507d2d21b9d2d1c18920a3b3a5f1541955a85b5504", - "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", - "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", - "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", - "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", - "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", - "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", - "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", - "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", - "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", - "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", - "sha256:9ffda299e417dc83362963966c50cb76d42da673ee140de8a8ac762d4bb2378b", - "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", - "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", - "sha256:a576f5dce9ac7de5d350b8e2f552cf364f32975ed84717c35379a51c7cb198bd", - "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", - "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", - "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", - "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", - "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", - "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", - "sha256:b22945be8425d161f2e536cdae66da300b6b000f1c0ba3ddf237d1bfd45d21b8", - "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", - "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", - "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", - "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", - "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", - "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", - "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", - "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", - "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", - "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", - "sha256:dc3ce84cfd4fa9628fe62c4f85d0d597a4627d4242cfafac32a12cc1455d00f7", - "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", - "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", - "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", - "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", - "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", - "sha256:ed0240dd1536a98c3ab55e929c60dfff7c899fecafcb7d01161b21a99fc8c363", - "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", - "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", - "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", - "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", - "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", - "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", - "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", - "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", - "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", - "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f" - ], - "markers": "python_version >= '3.9'", - "version": "==0.13.0" - }, - "jsonref": { - "hashes": [ - "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", - "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9" - ], - "markers": "python_version >= '3.7'", - "version": "==1.1.0" - }, - "jsonschema": { - "hashes": [ - "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", - "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==4.26.0" - }, - "jsonschema-path": { - "hashes": [ - "sha256:5f5ff183150030ea24bb51cf1ddac9bf5dbf030272e2792a7ffe8262f7eea2a5", - "sha256:9c3d88e727cc4f1a88e51dbbed4211dbcd815d27799d2685efd904435c3d39e7" - ], - "markers": "python_version >= '3.10' and python_full_version < '4.0.0'", - "version": "==0.4.2" - }, - "jsonschema-specifications": { - "hashes": [ - "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", - "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" - ], - "markers": "python_version >= '3.9'", - "version": "==2025.9.1" - }, - "keyring": { - "hashes": [ - "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", - "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b" - ], - "markers": "python_version >= '3.9'", - "version": "==25.7.0" - }, - "litellm": { - "hashes": [ - "sha256:5496b5d4532cccdc7a095c21cbac4042f7662021c57bc1d17be4e39838929e80", - "sha256:d388f52447daccbcaafa19a3e68d17b75f1374b5bf2cde680d65e1cd86e50d22" - ], - "index": "pypi", - "markers": "python_version >= '3.9' and python_version < '4.0'", - "version": "==1.82.0" - }, - "markdown-it-py": { - "hashes": [ - "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", - "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" - ], - "markers": "python_version >= '3.10'", - "version": "==4.0.0" - }, - "markupsafe": { - "hashes": [ - "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", - "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", - "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", - "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", - "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", - "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", - "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", - "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", - "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", - "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", - "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", - "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", - "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", - "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", - "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", - "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", - "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", - "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", - "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", - "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", - "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", - "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", - "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", - "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", - "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", - "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", - "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", - "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", - "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", - "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", - "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", - "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", - "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", - "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", - "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", - "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", - "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", - "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", - "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", - "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", - "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", - "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", - "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", - "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", - "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", - "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", - "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", - "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", - "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", - "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", - "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", - "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", - "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", - "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", - "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", - "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", - "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", - "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", - "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", - "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", - "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", - "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", - "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", - "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", - "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", - "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", - "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", - "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", - "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", - "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", - "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", - "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", - "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", - "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", - "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", - "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", - "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", - "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", - "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", - "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", - "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", - "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", - "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", - "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", - "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", - "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", - "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", - "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", - "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" - ], - "markers": "python_version >= '3.9'", - "version": "==3.0.3" - }, - "mcp": { - "hashes": [ - "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", - "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66" - ], - "markers": "python_version >= '3.10'", - "version": "==1.26.0" - }, - "mdurl": { - "hashes": [ - "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.1.2" - }, - "more-itertools": { - "hashes": [ - "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", - "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd" - ], - "markers": "python_version >= '3.9'", - "version": "==10.8.0" - }, - "multidict": { - "hashes": [ - "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", - "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", - "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", - "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", - "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", - "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", - "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", - "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", - "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", - "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", - "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", - "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", - "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", - "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", - "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", - "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", - "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", - "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", - "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", - "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", - "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", - "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", - "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", - "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", - "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", - "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", - "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", - "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", - "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", - "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", - "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", - "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", - "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", - "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", - "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", - "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", - "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", - "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", - "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", - "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", - "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", - "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", - "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", - "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", - "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", - "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", - "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", - "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", - "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", - "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", - "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", - "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", - "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", - "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", - "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", - "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", - "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", - "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", - "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", - "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", - "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", - "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", - "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", - "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", - "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", - "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", - "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", - "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", - "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", - "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", - "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", - "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", - "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", - "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", - "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", - "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", - "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", - "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", - "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", - "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", - "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", - "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", - "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", - "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", - "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", - "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", - "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", - "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", - "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", - "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", - "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", - "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", - "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", - "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", - "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", - "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", - "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", - "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", - "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", - "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", - "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", - "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", - "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", - "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", - "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", - "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", - "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", - "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", - "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", - "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", - "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", - "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", - "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", - "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", - "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", - "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", - "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", - "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", - "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", - "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", - "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", - "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", - "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", - "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", - "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", - "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", - "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", - "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", - "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", - "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", - "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", - "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", - "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", - "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", - "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", - "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", - "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", - "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", - "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", - "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", - "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", - "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", - "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", - "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", - "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", - "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" - ], - "markers": "python_version >= '3.9'", - "version": "==6.7.1" - }, - "neo4j": { - "hashes": [ - "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", - "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84" - ], - "markers": "python_version >= '3.10'", - "version": "==6.1.0" - }, - "numpy": { - "hashes": [ - "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", - "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", - "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", - "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", - "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", - "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", - "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", - "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", - "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", - "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", - "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", - "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", - "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", - "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", - "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", - "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", - "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", - "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", - "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", - "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", - "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", - "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", - "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", - "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", - "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", - "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", - "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", - "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", - "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", - "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", - "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", - "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", - "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", - "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", - "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", - "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", - "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", - "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", - "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", - "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", - "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", - "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", - "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", - "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", - "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", - "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", - "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", - "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", - "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", - "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", - "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", - "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", - "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", - "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", - "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", - "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", - "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", - "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", - "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", - "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", - "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", - "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", - "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", - "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", - "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", - "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", - "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", - "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", - "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", - "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", - "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", - "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1" - ], - "markers": "python_version >= '3.11'", - "version": "==2.4.2" - }, - "openai": { - "hashes": [ - "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", - "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94" - ], - "markers": "python_version >= '3.9'", - "version": "==2.24.0" - }, - "openapi-pydantic": { - "hashes": [ - "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", - "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d" - ], - "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==0.5.1" - }, - "opentelemetry-api": { - "hashes": [ - "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", - "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c" - ], - "markers": "python_version >= '3.9'", - "version": "==1.39.1" - }, - "packaging": { - "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" - ], - "markers": "python_version >= '3.8'", - "version": "==26.0" - }, - "pathable": { - "hashes": [ - "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", - "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1" - ], - "markers": "python_version >= '3.10' and python_version < '4.0'", - "version": "==0.5.0" - }, - "platformdirs": { - "hashes": [ - "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", - "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291" - ], - "markers": "python_version >= '3.10'", - "version": "==4.9.2" - }, - "posthog": { - "hashes": [ - "sha256:2ddcacdef6c4afb124ebfcf27d7be58388943a7e24f8d4a51a52732c9b90bad6", - "sha256:55f7580265d290936ac4c112a4e2031a41743be4f90d4183ac9f85b721ff13ae" - ], - "markers": "python_version >= '3.10'", - "version": "==7.9.3" - }, - "propcache": { - "hashes": [ - "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", - "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", - "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", - "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", - "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", - "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", - "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", - "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", - "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", - "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", - "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", - "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", - "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", - "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", - "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", - "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", - "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", - "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", - "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", - "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", - "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", - "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", - "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", - "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", - "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", - "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", - "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", - "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", - "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", - "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", - "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", - "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", - "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", - "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", - "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", - "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", - "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", - "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", - "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", - "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", - "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", - "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", - "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", - "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", - "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", - "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", - "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", - "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", - "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", - "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", - "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", - "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", - "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", - "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", - "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", - "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", - "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", - "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", - "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", - "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", - "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", - "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", - "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", - "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", - "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", - "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", - "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", - "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", - "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", - "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", - "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", - "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", - "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", - "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", - "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", - "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", - "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", - "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", - "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", - "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", - "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", - "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", - "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", - "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", - "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", - "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", - "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", - "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", - "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", - "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", - "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", - "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", - "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", - "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", - "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", - "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", - "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", - "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", - "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", - "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", - "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", - "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", - "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", - "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", - "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", - "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", - "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", - "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", - "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", - "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", - "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", - "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", - "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", - "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", - "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", - "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", - "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", - "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", - "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", - "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", - "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", - "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.1" - }, - "psycopg2-binary": { - "hashes": [ - "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", - "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", - "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", - "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", - "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", - "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", - "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", - "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", - "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", - "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", - "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", - "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", - "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", - "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", - "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", - "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", - "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", - "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", - "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", - "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", - "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", - "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", - "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", - "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", - "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", - "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", - "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", - "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", - "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", - "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", - "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", - "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", - "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", - "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", - "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", - "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", - "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", - "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", - "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", - "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", - "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", - "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", - "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", - "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", - "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", - "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", - "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", - "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", - "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", - "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", - "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", - "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", - "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", - "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", - "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", - "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", - "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", - "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", - "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", - "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", - "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", - "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", - "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", - "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", - "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", - "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", - "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==2.9.11" - }, - "py-key-value-aio": { - "extras": [ - "filetree", - "keyring", - "memory" - ], - "hashes": [ - "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", - "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55" - ], - "markers": "python_version >= '3.10'", - "version": "==0.4.4" - }, - "pycparser": { - "hashes": [ - "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", - "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" - ], - "markers": "python_version >= '3.10'", - "version": "==3.0" - }, - "pydantic": { - "extras": [ - "email" - ], - "hashes": [ - "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", - "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" - ], - "markers": "python_version >= '3.9'", - "version": "==2.12.5" - }, - "pydantic-core": { - "hashes": [ - "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", - "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", - "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", - "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", - "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", - "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", - "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", - "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", - "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", - "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", - "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", - "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", - "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", - "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", - "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", - "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", - "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", - "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", - "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", - "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", - "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", - "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", - "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", - "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", - "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", - "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", - "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", - "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", - "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", - "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", - "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", - "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", - "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", - "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", - "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", - "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", - "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", - "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", - "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", - "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", - "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", - "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", - "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", - "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", - "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", - "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", - "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", - "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", - "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", - "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", - "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", - "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", - "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", - "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", - "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", - "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", - "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", - "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", - "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", - "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", - "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", - "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", - "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", - "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", - "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", - "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", - "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", - "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", - "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", - "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", - "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", - "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", - "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", - "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", - "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", - "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", - "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", - "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", - "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", - "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", - "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", - "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", - "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", - "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", - "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", - "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", - "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", - "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", - "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", - "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", - "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", - "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", - "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", - "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", - "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", - "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", - "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", - "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", - "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", - "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", - "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", - "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", - "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", - "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", - "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", - "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", - "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", - "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", - "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", - "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", - "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", - "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", - "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", - "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", - "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", - "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", - "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", - "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", - "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", - "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", - "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" - ], - "markers": "python_version >= '3.9'", - "version": "==2.41.5" - }, - "pydantic-settings": { - "hashes": [ - "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", - "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237" - ], - "markers": "python_version >= '3.10'", - "version": "==2.13.1" - }, - "pygments": { - "hashes": [ - "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", - "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" - ], - "markers": "python_version >= '3.8'", - "version": "==2.19.2" - }, - "pyjwt": { - "extras": [ - "crypto" - ], - "hashes": [ - "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", - "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469" - ], - "markers": "python_version >= '3.9'", - "version": "==2.11.0" - }, - "pymysql": { - "hashes": [ - "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", - "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.1.2" - }, - "pyperclip": { - "hashes": [ - "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", - "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273" - ], - "version": "==1.11.0" - }, - "python-dateutil": { - "hashes": [ - "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", - "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==2.9.0.post0" - }, - "python-dotenv": { - "hashes": [ - "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", - "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" - ], - "markers": "python_version >= '3.10'", - "version": "==1.2.2" - }, - "python-multipart": { - "hashes": [ - "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", - "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==0.0.22" - }, - "pytz": { - "hashes": [ - "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", - "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" - ], - "version": "==2025.2" - }, - "pyyaml": { - "hashes": [ - "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", - "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", - "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", - "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", - "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", - "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", - "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", - "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", - "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", - "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", - "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", - "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", - "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", - "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", - "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", - "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", - "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", - "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", - "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", - "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", - "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", - "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", - "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", - "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", - "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", - "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", - "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", - "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", - "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", - "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", - "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", - "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", - "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", - "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", - "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", - "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", - "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", - "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", - "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", - "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", - "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", - "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", - "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", - "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", - "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", - "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", - "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", - "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", - "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", - "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", - "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", - "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", - "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", - "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", - "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", - "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", - "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", - "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", - "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", - "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", - "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", - "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", - "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", - "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", - "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", - "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", - "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", - "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", - "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", - "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", - "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", - "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", - "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" - ], - "markers": "python_version >= '3.8'", - "version": "==6.0.3" - }, - "redis": { - "hashes": [ - "sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497", - "sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26" - ], - "markers": "python_version >= '3.10'", - "version": "==7.2.0" - }, - "referencing": { - "hashes": [ - "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", - "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" - ], - "markers": "python_version >= '3.10'", - "version": "==0.37.0" - }, - "regex": { - "hashes": [ - "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", - "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", - "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", - "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", - "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", - "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", - "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", - "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", - "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", - "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", - "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", - "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", - "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", - "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", - "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", - "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", - "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", - "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", - "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", - "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", - "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", - "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", - "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", - "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", - "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", - "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", - "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", - "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", - "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", - "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", - "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", - "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", - "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", - "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", - "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", - "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", - "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", - "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", - "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", - "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", - "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", - "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", - "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", - "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", - "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", - "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", - "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", - "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", - "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", - "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", - "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", - "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", - "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", - "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", - "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", - "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", - "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", - "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", - "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", - "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", - "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", - "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", - "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", - "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", - "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", - "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", - "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", - "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", - "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", - "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", - "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", - "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", - "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", - "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", - "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", - "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", - "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", - "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", - "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", - "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", - "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", - "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", - "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", - "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", - "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", - "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", - "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", - "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", - "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", - "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", - "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", - "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", - "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", - "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", - "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", - "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", - "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", - "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", - "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", - "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", - "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", - "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", - "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", - "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", - "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", - "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", - "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", - "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", - "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", - "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", - "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", - "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", - "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", - "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61" - ], - "markers": "python_version >= '3.10'", - "version": "==2026.2.28" - }, - "requests": { - "hashes": [ - "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", - "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" - ], - "markers": "python_version >= '3.9'", - "version": "==2.32.5" - }, - "rich": { - "hashes": [ - "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", - "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==14.3.3" - }, - "rich-rst": { - "hashes": [ - "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", - "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a" - ], - "version": "==1.3.2" - }, - "rpds-py": { - "hashes": [ - "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", - "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", - "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", - "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", - "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", - "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", - "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", - "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", - "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", - "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", - "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", - "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", - "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", - "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", - "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", - "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", - "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", - "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", - "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", - "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", - "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", - "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", - "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", - "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", - "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", - "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", - "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", - "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", - "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", - "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", - "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", - "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", - "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", - "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", - "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", - "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", - "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", - "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", - "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", - "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", - "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", - "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", - "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", - "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", - "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", - "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", - "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", - "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", - "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", - "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", - "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", - "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", - "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", - "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", - "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", - "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", - "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", - "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", - "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", - "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", - "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", - "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", - "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", - "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", - "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", - "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", - "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", - "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", - "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", - "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", - "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", - "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", - "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", - "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", - "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", - "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", - "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", - "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", - "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", - "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", - "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", - "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", - "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", - "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", - "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", - "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", - "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", - "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", - "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", - "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", - "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", - "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", - "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", - "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", - "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", - "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", - "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", - "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", - "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", - "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", - "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", - "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", - "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", - "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", - "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", - "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", - "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", - "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", - "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", - "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", - "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", - "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", - "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", - "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", - "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5" - ], - "markers": "python_version >= '3.10'", - "version": "==0.30.0" - }, - "secretstorage": { - "hashes": [ - "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", - "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be" - ], - "markers": "python_version >= '3.10'", - "version": "==3.5.0" - }, - "shellingham": { - "hashes": [ - "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", - "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" - ], - "markers": "python_version >= '3.7'", - "version": "==1.5.4" - }, - "six": { - "hashes": [ - "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", - "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.17.0" - }, - "sniffio": { - "hashes": [ - "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", - "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "sse-starlette": { - "hashes": [ - "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", - "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422" - ], - "markers": "python_version >= '3.9'", - "version": "==3.2.0" - }, - "starlette": { - "hashes": [ - "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", - "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933" - ], - "markers": "python_version >= '3.10'", - "version": "==0.52.1" - }, - "tenacity": { - "hashes": [ - "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", - "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a" - ], - "markers": "python_version >= '3.10'", - "version": "==9.1.4" - }, - "tiktoken": { - "hashes": [ - "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", - "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", - "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", - "sha256:09eb4eae62ae7e4c62364d9ec3a57c62eea707ac9a2b2c5d6bd05de6724ea179", - "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", - "sha256:15d875454bbaa3728be39880ddd11a5a2a9e548c29418b41e8fd8a767172b5ec", - "sha256:20cf97135c9a50de0b157879c3c4accbb29116bcf001283d26e073ff3b345946", - "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", - "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", - "sha256:2cff3688ba3c639ebe816f8d58ffbbb0aa7433e23e08ab1cade5d175fc973fb3", - "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", - "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", - "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", - "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", - "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", - "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", - "sha256:4c9614597ac94bb294544345ad8cf30dac2129c05e2db8dc53e082f355857af7", - "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", - "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", - "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", - "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", - "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", - "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", - "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", - "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", - "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", - "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", - "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", - "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", - "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", - "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", - "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", - "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", - "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", - "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", - "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", - "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", - "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", - "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", - "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", - "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", - "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", - "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", - "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", - "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", - "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", - "sha256:d51d75a5bffbf26f86554d28e78bfb921eae998edc2675650fd04c7e1f0cdc1e", - "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", - "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", - "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", - "sha256:df37684ace87d10895acb44b7f447d4700349b12197a526da0d4a4149fde074c", - "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", - "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", - "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", - "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", - "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", - "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd" - ], - "markers": "python_version >= '3.9'", - "version": "==0.12.0" - }, - "tokenizers": { - "hashes": [ - "sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113", - "sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37", - "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", - "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", - "sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee", - "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", - "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", - "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", - "sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012", - "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", - "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", - "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", - "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", - "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", - "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", - "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", - "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", - "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", - "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", - "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", - "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", - "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", - "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", - "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5" - ], - "markers": "python_version >= '3.9'", - "version": "==0.22.2" - }, - "tqdm": { - "hashes": [ - "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", - "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==4.67.3" - }, - "typer": { - "hashes": [ - "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", - "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45" - ], - "markers": "python_version >= '3.10'", - "version": "==0.24.1" - }, - "typer-slim": { - "hashes": [ - "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", - "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34" - ], - "markers": "python_version >= '3.10'", - "version": "==0.24.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", - "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" - ], - "markers": "python_version >= '3.9'", - "version": "==4.15.0" - }, - "typing-inspection": { - "hashes": [ - "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", - "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.2" - }, - "urllib3": { - "hashes": [ - "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", - "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" - ], - "markers": "python_version >= '3.9'", - "version": "==2.6.3" - }, - "uvicorn": { - "hashes": [ - "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", - "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==0.41.0" - }, - "watchfiles": { - "hashes": [ - "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", - "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", - "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", - "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", - "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", - "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", - "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", - "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", - "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", - "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", - "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", - "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", - "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", - "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", - "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", - "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", - "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", - "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", - "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", - "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", - "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", - "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", - "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", - "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", - "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", - "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", - "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", - "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", - "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", - "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", - "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", - "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", - "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", - "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", - "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", - "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", - "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", - "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", - "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", - "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", - "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", - "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", - "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", - "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", - "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", - "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", - "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", - "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", - "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", - "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", - "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", - "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", - "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", - "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", - "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", - "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", - "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", - "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", - "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", - "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", - "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", - "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", - "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", - "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", - "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", - "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", - "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", - "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", - "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", - "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", - "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", - "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", - "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", - "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", - "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", - "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", - "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", - "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", - "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", - "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", - "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", - "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", - "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", - "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", - "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", - "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", - "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", - "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", - "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", - "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", - "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", - "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", - "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", - "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", - "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", - "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", - "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", - "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", - "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", - "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", - "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", - "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", - "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", - "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", - "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", - "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", - "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", - "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", - "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf" - ], - "markers": "python_version >= '3.9'", - "version": "==1.1.1" - }, - "websockets": { - "hashes": [ - "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", - "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", - "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", - "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", - "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", - "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", - "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", - "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", - "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", - "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", - "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", - "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", - "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", - "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", - "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", - "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", - "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", - "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", - "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", - "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", - "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", - "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", - "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", - "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", - "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", - "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", - "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", - "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", - "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", - "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", - "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", - "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", - "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", - "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", - "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", - "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", - "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", - "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", - "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", - "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", - "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", - "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", - "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", - "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", - "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", - "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", - "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", - "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", - "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", - "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", - "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", - "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", - "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", - "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", - "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", - "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", - "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", - "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", - "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", - "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", - "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4" - ], - "markers": "python_version >= '3.10'", - "version": "==16.0" - }, - "yarl": { - "hashes": [ - "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", - "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", - "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", - "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", - "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", - "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", - "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", - "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", - "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", - "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", - "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", - "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", - "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", - "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", - "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", - "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", - "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", - "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", - "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", - "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", - "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", - "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", - "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", - "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", - "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", - "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", - "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", - "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", - "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", - "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", - "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", - "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", - "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", - "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", - "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", - "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", - "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", - "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", - "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", - "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", - "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", - "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", - "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", - "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", - "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", - "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", - "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", - "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", - "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", - "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", - "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", - "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", - "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", - "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", - "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", - "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", - "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", - "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", - "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", - "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", - "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", - "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", - "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", - "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", - "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", - "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", - "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", - "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", - "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", - "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", - "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", - "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", - "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", - "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", - "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", - "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", - "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", - "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", - "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", - "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", - "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", - "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", - "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", - "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", - "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", - "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", - "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", - "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", - "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", - "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", - "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", - "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", - "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", - "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", - "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", - "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", - "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", - "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", - "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", - "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", - "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", - "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", - "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", - "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", - "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", - "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", - "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", - "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", - "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", - "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", - "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", - "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", - "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", - "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", - "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", - "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", - "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", - "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", - "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", - "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", - "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", - "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", - "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", - "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", - "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", - "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", - "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", - "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d" - ], - "markers": "python_version >= '3.10'", - "version": "==1.23.0" - }, - "zipp": { - "hashes": [ - "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" - ], - "markers": "python_version >= '3.9'", - "version": "==3.23.0" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", - "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0" - ], - "markers": "python_full_version >= '3.10.0'", - "version": "==4.0.4" - }, - "certifi": { - "hashes": [ - "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", - "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7" - ], - "markers": "python_version >= '3.7'", - "version": "==2026.2.25" - }, - "charset-normalizer": { - "hashes": [ - "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", - "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", - "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", - "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", - "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", - "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", - "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", - "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", - "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", - "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", - "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", - "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", - "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", - "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", - "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", - "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", - "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", - "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", - "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", - "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", - "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", - "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", - "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", - "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", - "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", - "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", - "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", - "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", - "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", - "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", - "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", - "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", - "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", - "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", - "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", - "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", - "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", - "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", - "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", - "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", - "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", - "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", - "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", - "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", - "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", - "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", - "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", - "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", - "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", - "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", - "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", - "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", - "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", - "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", - "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", - "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", - "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", - "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", - "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", - "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", - "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", - "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", - "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", - "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", - "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", - "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", - "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", - "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", - "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", - "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", - "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", - "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", - "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", - "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", - "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", - "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", - "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", - "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", - "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", - "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", - "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", - "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", - "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", - "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", - "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", - "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", - "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", - "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", - "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", - "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", - "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", - "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", - "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", - "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", - "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", - "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", - "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", - "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", - "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", - "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", - "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", - "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", - "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", - "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", - "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", - "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", - "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", - "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", - "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", - "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", - "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", - "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", - "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.4" - }, - "dill": { - "hashes": [ - "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", - "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa" - ], - "markers": "python_version >= '3.9'", - "version": "==0.4.1" - }, - "greenlet": { - "hashes": [ - "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", - "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", - "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", - "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", - "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", - "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", - "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", - "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", - "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", - "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", - "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", - "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", - "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", - "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", - "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", - "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", - "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", - "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", - "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", - "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", - "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", - "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", - "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", - "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", - "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", - "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", - "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", - "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", - "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", - "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", - "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", - "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", - "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", - "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", - "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", - "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", - "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", - "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", - "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", - "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", - "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", - "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", - "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", - "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", - "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", - "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", - "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", - "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", - "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", - "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", - "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", - "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", - "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9" - ], - "markers": "python_version >= '3.10'", - "version": "==3.3.2" - }, - "idna": { - "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" - ], - "markers": "python_version >= '3.8'", - "version": "==3.11" - }, - "iniconfig": { - "hashes": [ - "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", - "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" - ], - "markers": "python_version >= '3.10'", - "version": "==2.3.0" - }, - "isort": { - "hashes": [ - "sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9", - "sha256:fddea59202f231e170e52e71e3510b99c373b6e571b55d9c7b31b679c0fed47c" - ], - "markers": "python_full_version >= '3.10.0'", - "version": "==8.0.0" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "packaging": { - "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" - ], - "markers": "python_version >= '3.8'", - "version": "==26.0" - }, - "platformdirs": { - "hashes": [ - "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", - "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291" - ], - "markers": "python_version >= '3.10'", - "version": "==4.9.2" - }, - "playwright": { - "hashes": [ - "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", - "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", - "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", - "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", - "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", - "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", - "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", - "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.58.0" - }, - "pluggy": { - "hashes": [ - "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", - "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" - ], - "markers": "python_version >= '3.9'", - "version": "==1.6.0" - }, - "pyee": { - "hashes": [ - "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", - "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228" - ], - "markers": "python_version >= '3.8'", - "version": "==13.0.1" - }, - "pygments": { - "hashes": [ - "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", - "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" - ], - "markers": "python_version >= '3.8'", - "version": "==2.19.2" - }, - "pylint": { - "hashes": [ - "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", - "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c" - ], - "index": "pypi", - "markers": "python_full_version >= '3.10.0'", - "version": "==4.0.5" - }, - "pytest": { - "hashes": [ - "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", - "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==8.4.2" - }, - "pytest-asyncio": { - "hashes": [ - "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", - "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.2.0" - }, - "pytest-base-url": { - "hashes": [ - "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", - "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6" - ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" - }, - "pytest-playwright": { - "hashes": [ - "sha256:247b61123b28c7e8febb993a187a07e54f14a9aa04edc166f7a976d88f04c770", - "sha256:8084e015b2b3ecff483c2160f1c8219b38b66c0d4578b23c0f700d1b0240ea38" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==0.7.2" - }, - "python-slugify": { - "hashes": [ - "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", - "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856" - ], - "markers": "python_version >= '3.7'", - "version": "==8.0.4" - }, - "requests": { - "hashes": [ - "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", - "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" - ], - "markers": "python_version >= '3.9'", - "version": "==2.32.5" - }, - "text-unidecode": { - "hashes": [ - "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", - "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" - ], - "version": "==1.3" - }, - "tomlkit": { - "hashes": [ - "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", - "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064" - ], - "markers": "python_version >= '3.9'", - "version": "==0.14.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", - "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" - ], - "markers": "python_version >= '3.9'", - "version": "==4.15.0" - }, - "urllib3": { - "hashes": [ - "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", - "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" - ], - "markers": "python_version >= '3.9'", - "version": "==2.6.3" - } - } -} diff --git a/README.md b/README.md index c7798a3e..6d0f1a49 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ docker run -p 5000:5000 -it \ falkordb/queryweaver ``` -> Note: To use OpenAI directly instead of Azure OpenAI, replace `AZURE_API_KEY` with `OPENAI_API_KEY` in the above command. +> Note: QueryWeaver supports multiple AI providers. You can use `OPENAI_API_KEY`, `GEMINI_API_KEY`, `ANTHROPIC_API_KEY`, or `AZURE_API_KEY`. See the [AI/LLM configuration](#aillm-configuration) section for details. > For a full list of configuration options, consult `.env.example`. @@ -231,6 +231,7 @@ with requests.post(url, headers=headers, json={"chat": ["Count orders last week" continue obj = json.loads(part) print('STREAM:', obj) +``` Notes & tips - Graph IDs are namespaced per-user. When calling the API directly use the plain graph id (the server will namespace by the authenticated user). For uploaded files the `database` field determines the saved graph id. @@ -245,7 +246,7 @@ Follow these steps to run and develop QueryWeaver from source. ### Prerequisites - Python 3.12+ -- pipenv +- uv (Python package manager) - A FalkorDB instance (local or remote) - Node.js and npm (for the React frontend) @@ -263,11 +264,11 @@ make install make run-dev ``` -If you prefer to set up manually or need a custom environment, use Pipenv: +If you prefer to set up manually or need a custom environment, use uv: ```bash # Install Python (backend) and frontend dependencies -pipenv sync --dev +uv sync # Create a local environment file cp .env.example .env @@ -277,7 +278,7 @@ cp .env.example .env ### Run the app locally ```bash -pipenv run uvicorn api.index:app --host 0.0.0.0 --port 5000 --reload +uv run uvicorn api.index:app --host 0.0.0.0 --port 5000 --reload ``` The server will be available at http://localhost:5000 @@ -326,49 +327,63 @@ APP_ENV=development ### AI/LLM configuration -QueryWeaver uses AI models for Text2SQL conversion and supports both Azure OpenAI and OpenAI directly. +QueryWeaver supports multiple AI providers. Set one API key and QueryWeaver auto-detects which provider to use. + +**Priority order:** Ollama > OpenAI > Gemini > Anthropic > Cohere > Azure (default) -#### Default: Azure OpenAI +| Provider | API Key | Default Models | +|----------|---------|----------------| +| Ollama | `OLLAMA_MODEL` | `ollama/`, `ollama/nomic-embed-text` | +| OpenAI | `OPENAI_API_KEY` | `openai/gpt-4.1`, `openai/text-embedding-ada-002` | +| Google Gemini | `GEMINI_API_KEY` | `gemini/gemini-3-pro-preview`, `gemini/gemini-embedding-001` | +| Anthropic | `ANTHROPIC_API_KEY` | `anthropic/claude-sonnet-4-5-20250929`, `voyage/voyage-3`* | +| Cohere | `COHERE_API_KEY` | `cohere/command-a-03-2025`, `cohere/embed-v4.0` | +| Azure OpenAI | `AZURE_API_KEY` | `azure/gpt-4.1`, `azure/text-embedding-ada-002` | -By default, QueryWeaver is configured to use Azure OpenAI. You need to set all three Azure credentials: +\* Anthropic has no native embeddings. You must set `VOYAGE_API_KEY` or `EMBEDDING_MODEL` for embeddings, otherwise startup will fail with an error. + +**Optional: Override default models** ```bash -AZURE_API_KEY=your_azure_api_key -AZURE_API_BASE=https://your-resource.openai.azure.com/ -AZURE_API_VERSION=2024-12-01-preview +COMPLETION_MODEL=gemini/gemini-3-pro-preview +EMBEDDING_MODEL=gemini/gemini-embedding-001 ``` -#### Alternative: OpenAI directly +Both must match your API key's provider. -To use OpenAI directly instead of Azure, simply set the `OPENAI_API_KEY` environment variable: +#### Docker examples with AI configuration +Using OpenAI: ```bash -OPENAI_API_KEY=your_openai_api_key +docker run -p 5000:5000 -it \ + -e FASTAPI_SECRET_KEY=your_secret_key \ + -e OPENAI_API_KEY=your_openai_api_key \ + falkordb/queryweaver ``` -When `OPENAI_API_KEY` is provided, QueryWeaver automatically switches to use OpenAI's models: -- Embedding model: `openai/text-embedding-ada-002` -- Completion model: `openai/gpt-4.1` - -This configuration is handled automatically in `api/config.py` - you only need to provide the appropriate API key. - -#### Docker examples with AI configuration +Using Google Gemini: +```bash +docker run -p 5000:5000 -it \ + -e FASTAPI_SECRET_KEY=your_secret_key \ + -e GEMINI_API_KEY=your_gemini_api_key \ + falkordb/queryweaver +``` -Using Azure OpenAI: +Using Anthropic: ```bash docker run -p 5000:5000 -it \ -e FASTAPI_SECRET_KEY=your_secret_key \ - -e AZURE_API_KEY=your_azure_api_key \ - -e AZURE_API_BASE=https://your-resource.openai.azure.com/ \ - -e AZURE_API_VERSION=2024-12-01-preview \ + -e ANTHROPIC_API_KEY=your_anthropic_api_key \ falkordb/queryweaver ``` -Using OpenAI directly: +Using Azure OpenAI: ```bash docker run -p 5000:5000 -it \ -e FASTAPI_SECRET_KEY=your_secret_key \ - -e OPENAI_API_KEY=your_openai_api_key \ + -e AZURE_API_KEY=your_azure_api_key \ + -e AZURE_API_BASE=https://your-resource.openai.azure.com/ \ + -e AZURE_API_VERSION=2024-12-01-preview \ falkordb/queryweaver ``` @@ -378,9 +393,9 @@ docker run -p 5000:5000 -it \ ### Prerequisites -- Install dev dependencies: `pipenv sync --dev` +- Install dev dependencies: `uv sync` - Start FalkorDB (see `make docker-falkordb`) -- Install Playwright browsers: `pipenv run playwright install` +- Install Playwright browsers: `uv run playwright install` ### Quick commands @@ -412,7 +427,7 @@ make test-e2e-headed ### Test types -- Unit tests: focus on individual modules and utilities. Run with `make test-unit` or `pipenv run pytest tests/ -k "not e2e"`. +- Unit tests: focus on individual modules and utilities. Run with `make test-unit` or `uv run python -m pytest tests/ -k "not e2e"`. - End-to-end (E2E) tests: run via Playwright and exercise UI flows, OAuth, file uploads, schema processing, chat queries, and API endpoints. Use `make test-e2e`. See `tests/e2e/README.md` for full E2E test instructions. @@ -424,7 +439,7 @@ GitHub Actions run unit and E2E tests on pushes and pull requests. Failures capt ## Troubleshooting - FalkorDB connection issues: start the DB helper `make docker-falkordb` or check network/host settings. -- Playwright/browser failures: install browsers with `pipenv run playwright install` and ensure system deps are present. +- Playwright/browser failures: install browsers with `uv run playwright install` and ensure system deps are present. - Missing environment variables: copy `.env.example` and fill required values. - **OAuth "mismatching_state: CSRF Warning!" errors**: Set `APP_ENV=production` (or `staging`) in your environment for HTTPS deployments, or `APP_ENV=development` for HTTP development environments. This ensures session cookies are configured correctly for your deployment type. diff --git a/api/agents/analysis_agent.py b/api/agents/analysis_agent.py index 2eeff49a..050f42c1 100644 --- a/api/agents/analysis_agent.py +++ b/api/agents/analysis_agent.py @@ -1,9 +1,7 @@ """Analysis agent for analyzing user queries and generating database analysis.""" from typing import List -from litellm import completion -from api.config import Config -from .utils import BaseAgent, parse_response +from .utils import BaseAgent, parse_response, run_completion class AnalysisAgent(BaseAgent): @@ -38,13 +36,10 @@ def get_analysis( # pylint: disable=too-many-arguments, too-many-positional-arg instructions, memory_context, database_type, user_rules_spec ) self.messages.append({"role": "user", "content": prompt}) - completion_result = completion( - model=Config.COMPLETION_MODEL, - messages=self.messages, - temperature=0, - ) - response = completion_result.choices[0].message.content + response = run_completion( + self.messages, self.custom_model, self.custom_api_key, temperature=0 + ) analysis = parse_response(response) if isinstance(analysis["ambiguities"], list): analysis["ambiguities"] = [ diff --git a/api/agents/follow_up_agent.py b/api/agents/follow_up_agent.py index 3798f3b5..b39661ec 100644 --- a/api/agents/follow_up_agent.py +++ b/api/agents/follow_up_agent.py @@ -1,8 +1,6 @@ """Follow-up agent for generating helpful questions when queries fail or are off-topic.""" -from litellm import completion -from api.config import Config -from .utils import BaseAgent +from .utils import BaseAgent, run_completion FOLLOW_UP_GENERATION_PROMPT = """ @@ -70,14 +68,11 @@ def generate_follow_up_question( ) try: - completion_result = completion( - model=Config.COMPLETION_MODEL, - messages=[{"role": "user", "content": prompt}], - temperature=0.9 + response = run_completion( + [{"role": "user", "content": prompt}], + self.custom_model, self.custom_api_key, temperature=0.9 ) - - response = completion_result.choices[0].message.content.strip() - return response + return response.strip() except Exception: # pylint: disable=broad-exception-caught # Fallback response if LLM call fails diff --git a/api/agents/relevancy_agent.py b/api/agents/relevancy_agent.py index 9317ec60..7db4f987 100644 --- a/api/agents/relevancy_agent.py +++ b/api/agents/relevancy_agent.py @@ -1,9 +1,7 @@ """Relevancy agent for determining relevancy of queries to database schema.""" import json -from litellm import completion -from api.config import Config -from .utils import BaseAgent, parse_response +from .utils import BaseAgent, parse_response, run_completion RELEVANCY_PROMPT = """ @@ -82,12 +80,9 @@ async def get_answer(self, user_question: str, database_desc: dict) -> dict: ), } ) - completion_result = completion( - model=Config.COMPLETION_MODEL, - messages=self.messages, - temperature=0, - ) - answer = completion_result.choices[0].message.content + answer = run_completion( + self.messages, self.custom_model, self.custom_api_key, temperature=0 + ) self.messages.append({"role": "assistant", "content": answer}) return parse_response(answer) diff --git a/api/agents/response_formatter_agent.py b/api/agents/response_formatter_agent.py index 198450b0..9e9dfe87 100644 --- a/api/agents/response_formatter_agent.py +++ b/api/agents/response_formatter_agent.py @@ -1,8 +1,7 @@ """Response formatter agent for generating user-readable responses from SQL query results.""" from typing import List, Dict -from litellm import completion -from api.config import Config +from .utils import run_completion RESPONSE_FORMATTER_PROMPT = """ @@ -43,8 +42,20 @@ class ResponseFormatterAgent: # pylint: disable=too-few-public-methods """Agent for generating user-readable responses from SQL query results.""" - def __init__(self): - """Initialize the response formatter agent.""" + def __init__(self, queries_history: List[str] = None, result_history: List[str] = None, + custom_api_key: str = None, custom_model: str = None): + """Initialize the response formatter agent. + + Args: + queries_history: List of previous user queries (for context) + result_history: List of previous results (for context) + custom_api_key: Optional custom API key for LLM calls + custom_model: Optional custom model name for LLM calls + """ + self.queries_history = queries_history or [] + self.result_history = result_history or [] + self.custom_api_key = custom_api_key + self.custom_model = custom_model def format_response(self, user_query: str, sql_query: str, query_results: List[Dict], db_description: str = "") -> str: @@ -64,14 +75,10 @@ def format_response(self, user_query: str, sql_query: str, messages = [{"role": "user", "content": prompt}] - completion_result = completion( - model=Config.COMPLETION_MODEL, - messages=messages, - temperature=0.3, # Slightly higher temperature for more natural responses - top_p=1, + response = run_completion( + messages, self.custom_model, self.custom_api_key, + temperature=0.3 # Slightly higher temperature for more natural responses ) - - response = completion_result.choices[0].message.content return response.strip() def _build_response_prompt(self, user_query: str, sql_query: str, diff --git a/api/agents/utils.py b/api/agents/utils.py index ceafff23..bc28c99f 100644 --- a/api/agents/utils.py +++ b/api/agents/utils.py @@ -1,13 +1,37 @@ """Utility functions for agents.""" import json -from typing import Any, Dict +from typing import Any, Dict, List + +from litellm import completion +from api.config import Config + + +def run_completion(messages: List[Dict[str, str]], custom_model: str = None, + custom_api_key: str = None, **kwargs) -> str: + """Run an LLM completion with optional custom model/key overrides. + + Returns the content string from the first choice. + """ + completion_args = { + "model": custom_model if custom_model else Config.COMPLETION_MODEL, + "messages": messages, + "top_p": 1, + **kwargs, + } + + if custom_api_key: + completion_args["api_key"] = custom_api_key + + result = completion(**completion_args) + return result.choices[0].message.content class BaseAgent: # pylint: disable=too-few-public-methods """Base class for agents.""" - def __init__(self, queries_history: list, result_history: list): + def __init__(self, queries_history: list, result_history: list, + custom_api_key: str = None, custom_model: str = None): """Initialize the agent with query and result history.""" if result_history is None: self.messages = [] @@ -17,6 +41,9 @@ def __init__(self, queries_history: list, result_history: list): self.messages.append({"role": "user", "content": query}) self.messages.append({"role": "assistant", "content": result}) + self.custom_api_key = custom_api_key + self.custom_model = custom_model + def parse_response(response: str) -> Dict[str, Any]: """ diff --git a/api/app_factory.py b/api/app_factory.py index 7c93cce9..3b95b5bf 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -5,7 +5,6 @@ import os import secrets -from dotenv import load_dotenv from fastapi import FastAPI, Request, HTTPException from fastapi.responses import RedirectResponse, JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles @@ -22,9 +21,8 @@ from api.routes.graphs import graphs_router from api.routes.database import database_router from api.routes.tokens import tokens_router +from api.routes.settings import settings_router -# Load environment variables from .env file -load_dotenv() logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) @@ -60,14 +58,17 @@ def _is_secure_request(request: Request) -> bool: """Determine if the request is over HTTPS.""" forwarded_proto = request.headers.get("x-forwarded-proto") if forwarded_proto: - return forwarded_proto == "https" + # Normalize: proxies may send comma-separated or mixed-case values + first_proto = forwarded_proto.split(",")[0].strip().lower() + return first_proto == "https" return request.url.scheme == "https" class CSRFMiddleware(BaseHTTPMiddleware): # pylint: disable=too-few-public-methods """Double Submit Cookie CSRF protection. - Sets a csrf_token cookie (readable by JS) on every response. + Ensures a csrf_token cookie (readable by JS) exists, setting it + on the response if the incoming request does not already carry one. State-changing requests must echo the cookie value back via the X-CSRF-Token header. Bearer-token authenticated requests and auth/login endpoints are exempt. @@ -129,10 +130,10 @@ def _ensure_csrf_cookie(self, request: Request, response): ) -def create_app(): +def create_app(): # pylint: disable=too-many-statements """Create and configure the FastAPI application.""" - # Create the FastAPI app instance just to set the o routes + # Create the FastAPI app instance with original routes # Will be merged with MCP app later if MCP is enabled app = FastAPI( title="QueryWeaver" @@ -143,6 +144,7 @@ def create_app(): app.include_router(graphs_router, prefix="/graphs") app.include_router(database_router) app.include_router(tokens_router, prefix="/tokens") + app.include_router(settings_router, prefix="/settings") diff --git a/api/auth/user_management.py b/api/auth/user_management.py index c7a82196..3b5cd00f 100644 --- a/api/auth/user_management.py +++ b/api/auth/user_management.py @@ -235,11 +235,13 @@ async def validate_user(request: Request) -> Tuple[Optional[Dict[str, Any]], boo try: api_token = get_token(request) - if api_token: - db_info = await _get_user_info(api_token) + if not api_token: + return None, False - if db_info: - return db_info, True + db_info = await _get_user_info(api_token) + + if db_info: + return db_info, True return None, False diff --git a/api/config.py b/api/config.py index 7063f067..b029970c 100644 --- a/api/config.py +++ b/api/config.py @@ -7,8 +7,13 @@ import logging import dataclasses from typing import Union + +from dotenv import load_dotenv from litellm import embedding +# Ensure .env is loaded before Config reads os.getenv() at class definition time +load_dotenv() + # Configure litellm logging to prevent sensitive data leakage def configure_litellm_logging(): """Configure litellm to suppress completion logs.""" @@ -58,19 +63,70 @@ def get_vector_size(self) -> int: return size +def _with_prefix(model: str, provider: str) -> str: + """Ensure a model string has exactly one provider prefix.""" + prefix = f"{provider}/" + return prefix + model.removeprefix(prefix) + + +SUPPORTED_VENDORS = ("openai", "anthropic", "gemini", "azure", "ollama", "cohere") + + @dataclasses.dataclass class Config: """ Configuration class for the text2sql module. """ - AZURE_FLAG = True - if not os.getenv("OPENAI_API_KEY"): - EMBEDDING_MODEL_NAME = "azure/text-embedding-ada-002" - COMPLETION_MODEL = "azure/gpt-4.1" - else: + + # User-provided overrides via env vars + _user_completion = os.getenv("COMPLETION_MODEL", "") + _user_embedding = os.getenv("EMBEDDING_MODEL", "") + + # Determine the provider and models based on available API keys + # Priority: Ollama > OpenAI > Gemini > Anthropic > Cohere > Azure (default) + if os.getenv("OLLAMA_MODEL"): + LLM_PROVIDER = "ollama" + AZURE_FLAG = False + COMPLETION_MODEL = _user_completion or _with_prefix( + os.getenv("OLLAMA_MODEL"), "ollama") + EMBEDDING_MODEL_NAME = _user_embedding or _with_prefix( + os.getenv("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text"), "ollama") + elif os.getenv("OPENAI_API_KEY"): + LLM_PROVIDER = "openai" + AZURE_FLAG = False + COMPLETION_MODEL = _user_completion or "openai/gpt-4.1" + EMBEDDING_MODEL_NAME = _user_embedding or "openai/text-embedding-ada-002" + elif os.getenv("GEMINI_API_KEY"): + LLM_PROVIDER = "gemini" AZURE_FLAG = False - EMBEDDING_MODEL_NAME = "openai/text-embedding-ada-002" - COMPLETION_MODEL = "openai/gpt-4.1" + COMPLETION_MODEL = _user_completion or "gemini/gemini-3-pro-preview" + EMBEDDING_MODEL_NAME = _user_embedding or "gemini/gemini-embedding-001" + elif os.getenv("ANTHROPIC_API_KEY"): + LLM_PROVIDER = "anthropic" + AZURE_FLAG = False + COMPLETION_MODEL = _user_completion or "anthropic/claude-sonnet-4-5-20250929" + if _user_embedding: + EMBEDDING_MODEL_NAME = _user_embedding + elif os.getenv("VOYAGE_API_KEY"): + EMBEDDING_MODEL_NAME = "voyage/voyage-3" + else: + raise ValueError( + "ANTHROPIC_API_KEY is set, but Anthropic has no native embeddings. " + "Set EMBEDDING_MODEL or VOYAGE_API_KEY for embeddings." + ) + elif os.getenv("COHERE_API_KEY"): + LLM_PROVIDER = "cohere" + AZURE_FLAG = False + COMPLETION_MODEL = _user_completion or _with_prefix( + os.getenv("COHERE_MODEL", "command-a-03-2025"), "cohere") + EMBEDDING_MODEL_NAME = _user_embedding or _with_prefix( + os.getenv("COHERE_EMBEDDING_MODEL", "embed-v4.0"), "cohere") + else: + # Default to Azure + LLM_PROVIDER = "azure" + AZURE_FLAG = True + COMPLETION_MODEL = _user_completion or "azure/gpt-4.1" + EMBEDDING_MODEL_NAME = _user_embedding or "azure/text-embedding-ada-002" DB_MAX_DISTINCT: int = 100 # pylint: disable=invalid-name DB_UNIQUENESS_THRESHOLD: float = 0.5 # pylint: disable=invalid-name diff --git a/api/core/schema_loader.py b/api/core/schema_loader.py index bb4dcedb..b1568514 100644 --- a/api/core/schema_loader.py +++ b/api/core/schema_loader.py @@ -13,6 +13,7 @@ from api.loaders.base_loader import BaseLoader from api.loaders.postgres_loader import PostgresLoader from api.loaders.mysql_loader import MySQLLoader +from api.loaders.snowflake_loader import SnowflakeLoader # Use the same delimiter as in the JavaScript frontend for streaming chunks MESSAGE_DELIMITER = "|||FALKORDB_MESSAGE_BOUNDARY|||" @@ -44,6 +45,9 @@ def _step_detect_db_type(steps_counter: int, url: str) -> tuple[type[BaseLoader] elif url.startswith("mysql://"): db_type = "mysql" loader = MySQLLoader + elif url.startswith("snowflake://"): + db_type = "snowflake" + loader = SnowflakeLoader else: raise InvalidArgumentError("Invalid database URL format") diff --git a/api/core/text2sql.py b/api/core/text2sql.py index efdca397..65bc0b4f 100644 --- a/api/core/text2sql.py +++ b/api/core/text2sql.py @@ -15,10 +15,12 @@ from api.agents import AnalysisAgent, RelevancyAgent, ResponseFormatterAgent, FollowUpAgent from api.agents.healer_agent import HealerAgent from api.config import Config +from api.config import SUPPORTED_VENDORS from api.extensions import db from api.graph import find, get_db_description, get_user_rules from api.loaders.postgres_loader import PostgresLoader from api.loaders.mysql_loader import MySQLLoader +from api.loaders.snowflake_loader import SnowflakeLoader from api.memory.graphiti_tool import MemoryTool from api.sql_utils import SQLIdentifierQuoter, DatabaseSpecificQuoter @@ -45,6 +47,8 @@ class ChatRequest(BaseModel): chat: list[str] result: list[str] | None = None instructions: str | None = None + custom_api_key: str | None = None + custom_model: str | None = None use_user_rules: bool = True # If True, fetch rules from database; if False, don't use rules use_memory: bool = True @@ -58,6 +62,8 @@ class ConfirmRequest(BaseModel): sql_query: str confirmation: str = "" chat: list = [] + custom_api_key: str | None = None + custom_model: str | None = None def get_database_type_and_loader(db_url: str): @@ -79,6 +85,8 @@ def get_database_type_and_loader(db_url: str): return 'postgresql', PostgresLoader if db_url_lower.startswith('mysql://'): return 'mysql', MySQLLoader + if db_url_lower.startswith('snowflake://'): + return 'snowflake', SnowflakeLoader # Default to PostgresLoader for backward compatibility return 'postgresql', PostgresLoader @@ -248,9 +256,25 @@ async def generate(): # pylint: disable=too-many-locals,too-many-branches,too-m logging.info("Starting query processing pipeline for query: %s", sanitize_query(queries_history[-1])) # nosemgrep - agent_rel = RelevancyAgent(queries_history, result_history) - agent_an = AnalysisAgent(queries_history, result_history) - follow_up_agent = FollowUpAgent(queries_history, result_history) + # Extract custom API key and model from chat_data + custom_api_key = chat_data.custom_api_key + custom_model = chat_data.custom_model + + # Validate custom model format (vendor/model) + if custom_model: + parts = custom_model.split("/", 1) + if len(parts) != 2 or not parts[0] or not parts[1]: + raise InvalidArgumentError( + "Invalid model format. Expected 'vendor/model' (e.g. 'openai/gpt-4.1')" + ) + if parts[0] not in SUPPORTED_VENDORS: + raise InvalidArgumentError( + f"Unsupported vendor '{parts[0]}'. Supported: {', '.join(SUPPORTED_VENDORS)}" + ) + + agent_rel = RelevancyAgent(queries_history, result_history, custom_api_key, custom_model) + agent_an = AnalysisAgent(queries_history, result_history, custom_api_key, custom_model) + follow_up_agent = FollowUpAgent(queries_history, result_history, custom_api_key, custom_model) step = {"type": "reasoning_step", "final_response": False, @@ -576,7 +600,9 @@ def execute_sql(sql: str): "message": f"Step {step_num}: Generating user-friendly response"} yield json.dumps(step) + MESSAGE_DELIMITER - response_agent = ResponseFormatterAgent() + response_agent = ResponseFormatterAgent( + queries_history, result_history, custom_api_key, custom_model + ) user_readable_response = response_agent.format_response( user_query=queries_history[-1], sql_query=answer_an["sql_query"], @@ -715,6 +741,8 @@ async def execute_destructive_operation( # pylint: disable=too-many-statements sql_query = confirm_data.sql_query if hasattr(confirm_data, 'sql_query') else "" queries_history = confirm_data.chat if hasattr(confirm_data, 'chat') else [] + custom_api_key = confirm_data.custom_api_key + custom_model = confirm_data.custom_model if not sql_query: raise InvalidArgumentError("No SQL query provided") @@ -723,6 +751,7 @@ async def execute_destructive_operation( # pylint: disable=too-many-statements async def generate_confirmation(): # pylint: disable=too-many-locals,too-many-statements # Create memory tool for saving query results memory_tool = await MemoryTool.create(user_id, graph_id) + result_history = [] # Initialize result_history for this context if confirmation == "CONFIRM": try: @@ -823,7 +852,9 @@ async def generate_confirmation(): # pylint: disable=too-many-locals,too-many-s "message": f"Step {step_num}: Generating user-friendly response"} yield json.dumps(step) + MESSAGE_DELIMITER - response_agent = ResponseFormatterAgent() + response_agent = ResponseFormatterAgent( + queries_history, result_history, custom_api_key, custom_model + ) user_readable_response = response_agent.format_response( user_query=queries_history[-1] if queries_history else "Destructive operation", sql_query=sql_query, diff --git a/api/index.py b/api/index.py index 829e3e0a..1bb7d061 100644 --- a/api/index.py +++ b/api/index.py @@ -1,6 +1,10 @@ """Main entry point for the text2sql API.""" -from api.app_factory import create_app +# Load .env before any app imports that read os.getenv at module level +from dotenv import load_dotenv +load_dotenv() + +from api.app_factory import create_app # pylint: disable=wrong-import-position app = create_app() diff --git a/api/loaders/postgres_loader.py b/api/loaders/postgres_loader.py index be0be497..9d58f4e2 100644 --- a/api/loaders/postgres_loader.py +++ b/api/loaders/postgres_loader.py @@ -5,6 +5,7 @@ import decimal import logging from typing import AsyncGenerator, Dict, Any, List, Tuple +from urllib.parse import urlparse, parse_qs, unquote import psycopg2 from psycopg2 import sql @@ -52,7 +53,7 @@ class PostgresLoader(BaseLoader): @staticmethod def _execute_sample_query( - cursor, table_name: str, col_name: str, sample_size: int = 3 + cursor: Any, table_name: str, col_name: str, sample_size: int = 3 ) -> List[Any]: """ Execute query to get random sample values for a column. @@ -96,6 +97,48 @@ def _serialize_value(value): return None return value + @staticmethod + def parse_schema_from_url(connection_url: str) -> str: + """ + Parse the search_path from the connection URL's options parameter. + + The options parameter follows PostgreSQL's libpq format: + postgresql://user:pass@host:port/db?options=-csearch_path%3Dschema_name + + Args: + connection_url: PostgreSQL connection URL + + Returns: + The first schema from search_path, or 'public' if not specified + """ + try: + parsed = urlparse(connection_url) + query_params = parse_qs(parsed.query) + + options = query_params.get('options', []) + if not options: + return 'public' + + options_str = unquote(options[0]) + + # Parse -c search_path=value from options + # Format can be: -csearch_path=schema or -c search_path=schema + # Match comma-separated schema tokens (supports spaces after commas). + match = re.search(r'-c\s*search_path\s*=\s*([^\s,]+(?:\s*,\s*[^\s,]+)*)', options_str, re.IGNORECASE) + if match: + search_path = match.group(1) + schemas = search_path.split(',') + for s in schemas: + s = s.strip().strip('"\'') + if s and s != '$user': + return s + return 'public' + + return 'public' + + except Exception: # pylint: disable=broad-exception-caught + return 'public' + @staticmethod async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, str], None]: """ @@ -103,16 +146,29 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s Args: connection_url: PostgreSQL connection URL in format: - postgresql://username:password@host:port/database + postgresql://username:password@host:port/database + Optionally with schema via options parameter: + postgresql://...?options=-csearch_path%3Dschema_name Returns: Tuple[bool, str]: Success status and message """ + conn = None + cursor = None try: + # Parse schema from connection URL (defaults to 'public') + schema = PostgresLoader.parse_schema_from_url(connection_url) + # Connect to PostgreSQL database conn = psycopg2.connect(connection_url) cursor = conn.cursor() + # Set the session search_path to the parsed schema so unqualified + # table references (e.g. in sample queries) resolve correctly. + cursor.execute( + sql.SQL("SET search_path TO {}").format(sql.Identifier(schema)) + ) + # Extract database name from connection URL db_name = connection_url.split('/')[-1] if '?' in db_name: @@ -120,15 +176,17 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s # Get all table information yield True, "Extracting table information..." - entities = PostgresLoader.extract_tables_info(cursor) + entities = PostgresLoader.extract_tables_info(cursor, schema) yield True, "Extracting relationship information..." # Get all relationship information - relationships = PostgresLoader.extract_relationships(cursor) + relationships = PostgresLoader.extract_relationships(cursor, schema) - # Close database connection + # Close database connection before graph loading cursor.close() + cursor = None conn.close() + conn = None yield True, "Loading data into graph..." # Load data into graph @@ -144,21 +202,27 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s except Exception as e: # pylint: disable=broad-exception-caught logging.error("Error loading PostgreSQL schema: %s", e) yield False, "Failed to load PostgreSQL database schema" + finally: + if cursor is not None: + cursor.close() + if conn is not None: + conn.close() @staticmethod - def extract_tables_info(cursor) -> Dict[str, Any]: + def extract_tables_info(cursor: Any, schema: str = 'public') -> Dict[str, Any]: """ Extract table and column information from PostgreSQL database. Args: cursor: Database cursor + schema: Database schema to extract tables from (default: 'public') Returns: Dict containing table information """ entities = {} - # Get all tables in public schema + # Get all tables in the specified schema cursor.execute(""" SELECT table_name, table_comment FROM information_schema.tables t @@ -166,13 +230,14 @@ def extract_tables_info(cursor) -> Dict[str, Any]: SELECT schemaname, tablename, description as table_comment FROM pg_tables pt JOIN pg_class pc ON pc.relname = pt.tablename + JOIN pg_namespace pn ON pn.oid = pc.relnamespace AND pn.nspname = pt.schemaname JOIN pg_description pd ON pd.objoid = pc.oid AND pd.objsubid = 0 - WHERE pt.schemaname = 'public' + WHERE pt.schemaname = %s ) tc ON tc.tablename = t.table_name - WHERE t.table_schema = 'public' + WHERE t.table_schema = %s AND t.table_type = 'BASE TABLE' ORDER BY t.table_name; - """) + """, (schema, schema)) tables = cursor.fetchall() @@ -180,10 +245,10 @@ def extract_tables_info(cursor) -> Dict[str, Any]: table_name = table_name.strip() # Get column information for this table - columns_info = PostgresLoader.extract_columns_info(cursor, table_name) + columns_info = PostgresLoader.extract_columns_info(cursor, table_name, schema) # Get foreign keys for this table - foreign_keys = PostgresLoader.extract_foreign_keys(cursor, table_name) + foreign_keys = PostgresLoader.extract_foreign_keys(cursor, table_name, schema) # Generate table description table_description = table_comment if table_comment else f"Table: {table_name}" @@ -201,13 +266,14 @@ def extract_tables_info(cursor) -> Dict[str, Any]: return entities @staticmethod - def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: + def extract_columns_info(cursor: Any, table_name: str, schema: str = 'public') -> Dict[str, Any]: """ Extract column information for a specific table. Args: cursor: Database cursor table_name: Name of the table + schema: Database schema (default: 'public') Returns: Dict containing column information @@ -230,7 +296,9 @@ def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name + AND tc.constraint_schema = ku.constraint_schema WHERE tc.table_name = %s + AND tc.table_schema = %s AND tc.constraint_type = 'PRIMARY KEY' ) pk ON pk.column_name = c.column_name LEFT JOIN ( @@ -238,16 +306,19 @@ def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name + AND tc.constraint_schema = ku.constraint_schema WHERE tc.table_name = %s + AND tc.table_schema = %s AND tc.constraint_type = 'FOREIGN KEY' ) fk ON fk.column_name = c.column_name - LEFT JOIN pg_class pc ON pc.relname = c.table_name + LEFT JOIN pg_namespace pn ON pn.nspname = c.table_schema + LEFT JOIN pg_class pc ON pc.relname = c.table_name AND pc.relnamespace = pn.oid LEFT JOIN pg_attribute pa ON pa.attrelid = pc.oid AND pa.attname = c.column_name LEFT JOIN pg_description pgd ON pgd.objoid = pc.oid AND pgd.objsubid = pa.attnum WHERE c.table_name = %s - AND c.table_schema = 'public' + AND c.table_schema = %s ORDER BY c.ordinal_position; - """, (table_name, table_name, table_name)) + """, (table_name, schema, table_name, schema, table_name, schema)) columns = cursor.fetchall() columns_info = {} @@ -289,13 +360,14 @@ def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: return columns_info @staticmethod - def extract_foreign_keys(cursor, table_name: str) -> List[Dict[str, str]]: + def extract_foreign_keys(cursor: Any, table_name: str, schema: str = 'public') -> List[Dict[str, str]]: """ Extract foreign key information for a specific table. Args: cursor: Database cursor table_name: Name of the table + schema: Database schema (default: 'public') Returns: List of foreign key dictionaries @@ -315,8 +387,8 @@ def extract_foreign_keys(cursor, table_name: str) -> List[Dict[str, str]]: AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = %s - AND tc.table_schema = 'public'; - """, (table_name,)) + AND tc.table_schema = %s; + """, (table_name, schema)) foreign_keys = [] for constraint_name, column_name, foreign_table, foreign_column in cursor.fetchall(): @@ -330,12 +402,13 @@ def extract_foreign_keys(cursor, table_name: str) -> List[Dict[str, str]]: return foreign_keys @staticmethod - def extract_relationships(cursor) -> Dict[str, List[Dict[str, str]]]: + def extract_relationships(cursor: Any, schema: str = 'public') -> Dict[str, List[Dict[str, str]]]: """ Extract all relationship information from the database. Args: cursor: Database cursor + schema: Database schema (default: 'public') Returns: Dict containing relationship information @@ -355,9 +428,9 @@ def extract_relationships(cursor) -> Dict[str, List[Dict[str, str]]]: ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = 'public' + AND tc.table_schema = %s ORDER BY tc.table_name, tc.constraint_name; - """) + """, (schema,)) relationships = {} for (table_name, constraint_name, column_name, diff --git a/api/loaders/snowflake_loader.py b/api/loaders/snowflake_loader.py new file mode 100644 index 00000000..7685daa9 --- /dev/null +++ b/api/loaders/snowflake_loader.py @@ -0,0 +1,711 @@ +"""Snowflake loader for loading database schemas into FalkorDB graphs.""" + +import base64 +import datetime +import decimal +import logging +import re +from typing import AsyncGenerator, Dict, Any, List, Tuple +from urllib.parse import urlparse, parse_qs + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +import tqdm +import snowflake.connector +from snowflake.connector import DictCursor + +from api.loaders.base_loader import BaseLoader +from api.loaders.graph_loader import load_to_graph + + +class SnowflakeQueryError(Exception): + """Exception raised for Snowflake query execution errors.""" + + +class SnowflakeConnectionError(Exception): + """Exception raised for Snowflake connection errors.""" + + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") + + +class SnowflakeLoader(BaseLoader): + """ + Loader for Snowflake databases that connects and extracts schema information. + """ + + # DDL operations that modify database schema # pylint: disable=duplicate-code + SCHEMA_MODIFYING_OPERATIONS = { + 'CREATE', 'ALTER', 'DROP', 'RENAME', 'TRUNCATE' + } + + # More specific patterns for schema-affecting operations + SCHEMA_PATTERNS = [ # pylint: disable=duplicate-code + r'^\s*CREATE\s+TABLE', + r'^\s*CREATE\s+INDEX', + r'^\s*CREATE\s+UNIQUE\s+INDEX', + r'^\s*ALTER\s+TABLE', + r'^\s*DROP\s+TABLE', + r'^\s*DROP\s+INDEX', + r'^\s*RENAME\s+TABLE', + r'^\s*TRUNCATE\s+TABLE', + r'^\s*CREATE\s+VIEW', + r'^\s*DROP\s+VIEW', + r'^\s*CREATE\s+DATABASE', + r'^\s*DROP\s+DATABASE', + r'^\s*CREATE\s+SCHEMA', + r'^\s*DROP\s+SCHEMA', + ] + + @staticmethod + def _validate_identifier(identifier: str, identifier_type: str = "identifier") -> None: + """ + Validate that an identifier (table, column, database, schema name) is safe. + + Args: + identifier: The identifier to validate + identifier_type: Type of identifier for error messages + + Raises: + ValueError: If identifier contains invalid characters + """ + # Allow alphanumeric, underscore, dollar sign, and limit to reasonable length + # Snowflake identifiers can contain these characters when quoted + if not re.match(r'^[A-Za-z0-9_$]+$', identifier): + raise ValueError( + f"Invalid {identifier_type}: {identifier!r}. " + "Only alphanumeric characters, underscore, and dollar sign are allowed." + ) + if len(identifier) > 255: + raise ValueError(f"Invalid {identifier_type}: exceeds maximum length of 255") + + @staticmethod + def _quote_identifier(identifier: str) -> str: + """ + Safely quote a Snowflake identifier by escaping double quotes. + + Args: + identifier: The identifier to quote + + Returns: + Quoted identifier safe for SQL interpolation + """ + # Escape any existing double quotes by doubling them + escaped = identifier.replace('"', '""') + return f'"{escaped}"' + + @staticmethod + def _execute_sample_query( + cursor, table_name: str, col_name: str, sample_size: int = 3 + ) -> List[Any]: + """ + Execute query to get random sample values for a column. + Snowflake implementation using SAMPLE for random sampling. + """ + # Validate identifiers to prevent SQL injection + SnowflakeLoader._validate_identifier(table_name, "table name") + SnowflakeLoader._validate_identifier(col_name, "column name") + + # Validate sample_size is a positive integer + if not isinstance(sample_size, int) or sample_size <= 0: + raise ValueError(f"sample_size must be a positive integer, got {sample_size!r}") + + # Quote identifiers safely + quoted_table = SnowflakeLoader._quote_identifier(table_name) + quoted_col = SnowflakeLoader._quote_identifier(col_name) + + # Oversample by 10x to increase the chance of getting sample_size + # distinct non-null values after filtering (Snowflake's SAMPLE clause + # returns approximate row counts, and rows may contain NULLs or duplicates) + sample_rows = sample_size * 10 + + query = f""" + SELECT DISTINCT {quoted_col} + FROM {quoted_table} SAMPLE ({sample_rows} ROWS) + WHERE {quoted_col} IS NOT NULL + LIMIT %s; + """ + cursor.execute(query, (sample_size,)) + + sample_results = cursor.fetchall() + # DictCursor returns dicts; extract the column value by name + return [row[col_name] for row in sample_results if row[col_name] is not None] + + @staticmethod + def _serialize_value(value): + """ + Convert non-JSON serializable values to JSON serializable format. + + Args: + value: The value to serialize + + Returns: + JSON serializable version of the value + """ + if isinstance(value, (datetime.date, datetime.datetime)): + return value.isoformat() + if isinstance(value, datetime.time): + return value.isoformat() + if isinstance(value, decimal.Decimal): + return float(value) + if value is None: + return None + return value + + @staticmethod + def _parse_snowflake_url(connection_url: str) -> Dict[str, Any]: # pylint: disable=too-many-locals + """ + Parse Snowflake connection URL into components. + + Supports two authentication modes: + - Password: snowflake://user:pass@account/db/schema?warehouse=WH + - Key-pair: snowflake://user@account/db/schema?warehouse=WH&private_key=BASE64_PEM + (optionally with &private_key_passphrase=PASSPHRASE) + + Args: + connection_url: Snowflake connection URL + + Returns: + Dict with connection parameters for snowflake.connector.connect() + """ + if not connection_url.startswith('snowflake://'): + raise ValueError( + "Invalid Snowflake URL format. Expected " + "snowflake://username:password@account/database/schema?warehouse=warehouse_name" + ) + + parsed = urlparse(connection_url) + + if not parsed.username: + raise ValueError("Snowflake URL must include username") + + username = parsed.username + password = parsed.password or "" + + if not parsed.hostname: + raise ValueError("Snowflake URL must include account") + account = parsed.hostname + + path_parts = [p for p in parsed.path.split('/') if p] + if len(path_parts) < 1: + raise ValueError("Snowflake URL must include database name") + + database = path_parts[0] + schema = path_parts[1] if len(path_parts) > 1 else "PUBLIC" + + query_params = parse_qs(parsed.query) + warehouse = query_params.get('warehouse', ['COMPUTE_WH'])[0] + + # Validate all identifiers + SnowflakeLoader._validate_identifier(database, "database") + SnowflakeLoader._validate_identifier(schema, "schema") + SnowflakeLoader._validate_identifier(warehouse, "warehouse") + + conn_params: Dict[str, Any] = { + 'user': username, + 'account': account, + 'database': database, + 'schema': schema, + 'warehouse': warehouse, + 'login_timeout': 30, + 'network_timeout': 60, + } + + # Check for key-pair authentication + private_key_b64 = query_params.get('private_key', [None])[0] + if private_key_b64: + passphrase = query_params.get('private_key_passphrase', [None])[0] + passphrase_bytes = passphrase.encode() if passphrase else None + + try: + # Handle both standard and URL-safe base64 (browsers may + # convert '+' to spaces when URL-encoding query params) + cleaned_b64 = private_key_b64.replace(' ', '+') + pem_bytes = base64.b64decode(cleaned_b64) + private_key = serialization.load_pem_private_key( + pem_bytes, + password=passphrase_bytes, + backend=default_backend(), + ) + conn_params['private_key'] = private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + except Exception as e: + raise ValueError(f"Failed to load private key: {e}") from e + else: + conn_params['password'] = password + + return conn_params + + @staticmethod + async def load(prefix: str, connection_url: str) -> AsyncGenerator[ + tuple[bool, str], None + ]: + """ + Load the graph data from a Snowflake database into the graph database. + + Args: + connection_url: Snowflake connection URL in format: + snowflake://username:password@account/database/schema?warehouse=warehouse_name + + Returns: + Tuple[bool, str]: Success status and message + """ + try: + # Parse connection URL + conn_params = SnowflakeLoader._parse_snowflake_url(connection_url) + + # Connect to Snowflake database + conn = snowflake.connector.connect(**conn_params) + cursor = conn.cursor(DictCursor) + + # Get database and schema name + db_name = conn_params['database'] + # Snowflake stores unquoted identifiers in UPPERCASE; + # INFORMATION_SCHEMA lookups require the canonical form. + schema_name = conn_params['schema'].upper() + + # Get all table information + yield True, "Extracting table information..." + entities = SnowflakeLoader.extract_tables_info(cursor, db_name, schema_name) + + # Get all relationship information + yield True, "Extracting relationship information..." + relationships = SnowflakeLoader.extract_relationships(cursor, db_name, schema_name) + + # Close database connection + cursor.close() + conn.close() + + # Load data into graph + yield True, "Loading data into graph..." + await load_to_graph(f"{prefix}_{db_name}", entities, relationships, + db_name=db_name, db_url=connection_url) + + yield True, (f"Snowflake schema loaded successfully. " + f"Found {len(entities)} tables.") + + except snowflake.connector.Error as e: + logging.error("Snowflake error: %s", e) + yield False, f"Snowflake error: {e}" + except Exception as e: # pylint: disable=broad-exception-caught + logging.error("Error loading Snowflake schema: %s", e) + yield False, f"Failed to load Snowflake database schema: {e}" + + @staticmethod + def extract_tables_info(cursor, db_name: str, schema_name: str) -> Dict[str, Any]: + """ + Extract table and column information from Snowflake database. + + Args: + cursor: Database cursor + db_name: Database name + schema_name: Schema name + + Returns: + Dict containing table information + """ + # Validate identifiers to prevent SQL injection + SnowflakeLoader._validate_identifier(db_name, "database name") + SnowflakeLoader._validate_identifier(schema_name, "schema name") + + entities = {} + + # Get all tables in the schema + # Use quoted identifiers for database name, parameterize schema_name + quoted_db = SnowflakeLoader._quote_identifier(db_name) + cursor.execute(f""" + SELECT TABLE_NAME, COMMENT + FROM {quoted_db}.INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = %s + AND TABLE_TYPE = 'BASE TABLE' + ORDER BY TABLE_NAME; + """, (schema_name,)) + + tables = cursor.fetchall() + + for table_info in tqdm.tqdm(tables, desc="Extracting table information"): + table_name = table_info['TABLE_NAME'] + table_comment = table_info['COMMENT'] + + # Get column information for this table + columns_info = SnowflakeLoader.extract_columns_info( + cursor, db_name, schema_name, table_name + ) + + # Get foreign keys for this table + foreign_keys = SnowflakeLoader.extract_foreign_keys( + cursor, db_name, schema_name, table_name + ) + + # Generate table description + table_description = table_comment if table_comment else f"Table: {table_name}" + + # Get column descriptions for batch embedding + col_descriptions = [col_info['description'] for col_info in columns_info.values()] + + entities[table_name] = { + 'description': table_description, + 'columns': columns_info, + 'foreign_keys': foreign_keys, + 'col_descriptions': col_descriptions + } + + return entities + + @staticmethod + def extract_columns_info( # pylint: disable=too-many-locals + cursor, db_name: str, schema_name: str, table_name: str + ) -> Dict[str, Any]: + """ + Extract column information for a specific table. + + Args: + cursor: Database cursor + db_name: Database name + schema_name: Schema name + table_name: Name of the table + + Returns: + Dict containing column information + """ + # Validate identifiers to prevent SQL injection + SnowflakeLoader._validate_identifier(db_name, "database name") + SnowflakeLoader._validate_identifier(schema_name, "schema name") + SnowflakeLoader._validate_identifier(table_name, "table name") + + quoted_db = SnowflakeLoader._quote_identifier(db_name) + + cursor.execute(f""" + SELECT + COLUMN_NAME, + DATA_TYPE, + IS_NULLABLE, + COLUMN_DEFAULT, + COMMENT + FROM {quoted_db}.INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %s + AND TABLE_NAME = %s + ORDER BY ORDINAL_POSITION; + """, (schema_name, table_name)) + + columns = cursor.fetchall() + columns_info = {} + + # Get primary key information using Snowflake's SHOW command + quoted_table = SnowflakeLoader._quote_identifier(table_name) + quoted_schema = SnowflakeLoader._quote_identifier(schema_name) + cursor.execute(f"SHOW PRIMARY KEYS IN TABLE {quoted_db}.{quoted_schema}.{quoted_table}") + primary_keys = {row['column_name'] for row in cursor.fetchall()} + + # Get foreign key columns using Snowflake's SHOW IMPORTED KEYS + cursor.execute(f"SHOW IMPORTED KEYS IN TABLE {quoted_db}.{quoted_schema}.{quoted_table}") + foreign_keys_cols = {row['fk_column_name'] for row in cursor.fetchall()} + + for col_info in columns: + col_name = col_info['COLUMN_NAME'] + data_type = col_info['DATA_TYPE'] + is_nullable = col_info['IS_NULLABLE'] + column_default = col_info['COLUMN_DEFAULT'] + column_comment = col_info['COMMENT'] + + # Determine key type + if col_name in primary_keys: + key_type = 'PRIMARY KEY' + elif col_name in foreign_keys_cols: + key_type = 'FOREIGN KEY' + else: + key_type = 'NONE' + + # Generate column description + description_parts = [] + if column_comment: + description_parts.append(column_comment) + else: + description_parts.append(f"Column {col_name} of type {data_type}") + + if key_type != 'NONE': + description_parts.append(f"({key_type})") + + if is_nullable == 'NO': + description_parts.append("(NOT NULL)") + + if column_default is not None: + description_parts.append(f"(Default: {column_default})") + + # Extract sample values for the column (stored separately, not in description) + sample_values = SnowflakeLoader.extract_sample_values_for_column( + cursor, table_name, col_name + ) + + columns_info[col_name] = { + 'type': data_type, + 'null': is_nullable, + 'key': key_type, + 'description': ' '.join(description_parts), + 'default': column_default, + 'sample_values': sample_values + } + + return columns_info + + @staticmethod + def extract_foreign_keys( + cursor, db_name: str, schema_name: str, table_name: str + ) -> List[Dict[str, str]]: + """ + Extract foreign key information for a specific table. + + Args: + cursor: Database cursor + db_name: Database name + schema_name: Schema name + table_name: Name of the table + + Returns: + List of foreign key dictionaries + """ + # Validate identifiers to prevent SQL injection + SnowflakeLoader._validate_identifier(db_name, "database name") + SnowflakeLoader._validate_identifier(schema_name, "schema name") + SnowflakeLoader._validate_identifier(table_name, "table name") + + quoted_db = SnowflakeLoader._quote_identifier(db_name) + quoted_schema = SnowflakeLoader._quote_identifier(schema_name) + quoted_table = SnowflakeLoader._quote_identifier(table_name) + + # Use Snowflake's SHOW IMPORTED KEYS for foreign key information + cursor.execute(f"SHOW IMPORTED KEYS IN TABLE {quoted_db}.{quoted_schema}.{quoted_table}") + + foreign_keys = [] + for fk_info in cursor.fetchall(): + foreign_keys.append({ + 'constraint_name': fk_info['fk_name'], + 'column': fk_info['fk_column_name'], + 'referenced_table': fk_info['pk_table_name'], + 'referenced_column': fk_info['pk_column_name'] + }) + + return foreign_keys + + @staticmethod + def extract_relationships( + cursor, db_name: str, schema_name: str + ) -> Dict[str, List[Dict[str, str]]]: + """ + Extract all relationship information from the database. + + Args: + cursor: Database cursor + db_name: Database name + schema_name: Schema name + + Returns: + Dict containing relationship information + """ + # Validate identifiers to prevent SQL injection + SnowflakeLoader._validate_identifier(db_name, "database name") + SnowflakeLoader._validate_identifier(schema_name, "schema name") + + quoted_db = SnowflakeLoader._quote_identifier(db_name) + quoted_schema = SnowflakeLoader._quote_identifier(schema_name) + + # Use Snowflake's SHOW IMPORTED KEYS for each table to get relationships + cursor.execute(f""" + SELECT TABLE_NAME + FROM {quoted_db}.INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = %s + AND TABLE_TYPE = 'BASE TABLE' + ORDER BY TABLE_NAME; + """, (schema_name,)) + tables = [row['TABLE_NAME'] for row in cursor.fetchall()] + + relationships = {} + for tbl in tables: + SnowflakeLoader._validate_identifier(tbl, "table name") + quoted_table = SnowflakeLoader._quote_identifier(tbl) + cursor.execute( + f"SHOW IMPORTED KEYS IN TABLE {quoted_db}.{quoted_schema}.{quoted_table}" + ) + for rel_info in cursor.fetchall(): + constraint_name = rel_info['fk_name'] + + if constraint_name not in relationships: + relationships[constraint_name] = [] + + relationships[constraint_name].append({ + 'from': rel_info['fk_table_name'], + 'to': rel_info['pk_table_name'], + 'source_column': rel_info['fk_column_name'], + 'target_column': rel_info['pk_column_name'], + 'note': f'Foreign key constraint: {constraint_name}' + }) + + return relationships + + @staticmethod + def is_schema_modifying_query(sql_query: str) -> Tuple[bool, str]: + """ + Check if a SQL query modifies the database schema. + + Args: + sql_query: The SQL query to check + + Returns: + Tuple of (is_schema_modifying, operation_type) + """ + if not sql_query or not sql_query.strip(): + return False, "" + + # Clean and normalize the query + normalized_query = sql_query.strip().upper() + + # Check for basic DDL operations + first_word = normalized_query.split()[0] if normalized_query.split() else "" + if first_word in SnowflakeLoader.SCHEMA_MODIFYING_OPERATIONS: + # Additional pattern matching for more precise detection + for pattern in SnowflakeLoader.SCHEMA_PATTERNS: + if re.match(pattern, normalized_query, re.IGNORECASE): + return True, first_word + + # If it's a known DDL operation but doesn't match specific patterns, + # still consider it schema-modifying (better safe than sorry) + return True, first_word + + return False, "" + + @staticmethod + async def refresh_graph_schema(graph_id: str, db_url: str) -> Tuple[bool, str]: + """ + Refresh the graph schema by clearing existing data and reloading from the database. + + Args: + graph_id: The graph ID to refresh + db_url: Database connection URL + + Returns: + Tuple of (success, message) + """ + try: + logging.info("Schema modification detected. Refreshing graph schema.") + + # Import here to avoid circular imports + from api.extensions import db # pylint: disable=import-error,import-outside-toplevel + + # Clear existing graph data + # Drop current graph before reloading + graph = db.select_graph(graph_id) + await graph.delete() + + # Extract prefix from graph_id (remove database name part) + # graph_id format is typically "prefix_database_name" + parts = graph_id.split('_') + if len(parts) >= 2: + # Reconstruct prefix by joining all parts except the last one + prefix = '_'.join(parts[:-1]) + else: + prefix = graph_id + + # Reuse the existing load method to reload the schema + success = False + message = "" + async for progress_tuple in SnowflakeLoader.load(prefix, db_url): + success, message = progress_tuple + + if success: + logging.info("Graph schema refreshed successfully.") + return True, message + + logging.error("Schema refresh failed") + return False, "Failed to reload schema" + + except Exception as e: # pylint: disable=broad-exception-caught + # Log the error and return failure + logging.error("Error refreshing graph schema: %s", str(e)) + error_msg = "Error refreshing graph schema" + logging.error(error_msg) + return False, error_msg + + @staticmethod + def execute_sql_query(sql_query: str, db_url: str) -> List[Dict[str, Any]]: + """ + Execute a SQL query on the Snowflake database and return the results. + + Args: + sql_query: The SQL query to execute + db_url: Snowflake connection URL in format: + snowflake://username:password@account/database/schema?warehouse=warehouse_name + + Returns: + List of dictionaries containing the query results + """ + try: + # Parse connection URL + conn_params = SnowflakeLoader._parse_snowflake_url(db_url) + + # Connect to Snowflake database + conn = snowflake.connector.connect(**conn_params) + cursor = conn.cursor(DictCursor) + + # Execute the SQL query + cursor.execute(sql_query) + + # Check if the query returns results (SELECT queries) + if cursor.description is not None: + # This is a SELECT query or similar that returns rows + results = cursor.fetchall() + result_list = [] + for row in results: + # Serialize each value to ensure JSON compatibility + serialized_row = { + key: SnowflakeLoader._serialize_value(value) + for key, value in row.items() + } + result_list.append(serialized_row) + else: + # This is an INSERT, UPDATE, DELETE, or other non-SELECT query + # Return information about the operation + affected_rows = cursor.rowcount + sql_type = sql_query.strip().split()[0].upper() + + if sql_type in ['INSERT', 'UPDATE', 'DELETE']: + result_list = [{ + "operation": sql_type, + "affected_rows": affected_rows, + "status": "success" + }] + else: + # For other types of queries (CREATE, DROP, etc.) + result_list = [{ + "operation": sql_type, + "status": "success" + }] + + # Commit the transaction for write operations + conn.commit() + + # Close database connection + cursor.close() + conn.close() + + return result_list + + except snowflake.connector.Error as e: + # Rollback in case of error + if 'conn' in locals(): + conn.rollback() + cursor.close() + conn.close() + logging.error("Snowflake query execution error: %s", e) + raise SnowflakeQueryError(f"Snowflake query execution error: {str(e)}") from e + except Exception as e: + # Rollback in case of error + if 'conn' in locals(): + conn.rollback() + cursor.close() + conn.close() + logging.error("Error executing SQL query: %s", e) + raise SnowflakeQueryError(f"Error executing SQL query: {str(e)}") from e diff --git a/api/memory/graphiti_tool.py b/api/memory/graphiti_tool.py index 3c944545..00a1b3e6 100644 --- a/api/memory/graphiti_tool.py +++ b/api/memory/graphiti_tool.py @@ -65,6 +65,7 @@ def __init__(self, user_id: str, graph_id: str): # Create Graphiti client with Azure OpenAI configuration self.graphiti_client = create_graphiti_client(falkor_driver) + self.memory_enabled = self.graphiti_client is not None self.user_id = user_id self.graph_id = graph_id @@ -82,6 +83,9 @@ async def create(cls, user_id: str, graph_id: str, use_direct_entities: bool = T """Async factory to construct and initialize the tool.""" self = cls(user_id, graph_id) + if not self.memory_enabled: + return self + await self._ensure_entity_nodes_direct(user_id, graph_id) @@ -301,16 +305,18 @@ async def add_new_memory(self, conversation: Dict[str, Any], history: Tuple[List async def save_query_memory(self, query: str, sql_query: str, success: bool, error: Optional[str] = None) -> bool: """ Save individual query memory directly to the database node. - + Args: query: The user's natural language query sql_query: The generated SQL query success: Whether the query execution was successful error: Error message if the query failed - + Returns: bool: True if memory was saved successfully, False otherwise """ + if not self.memory_enabled: + return False try: database_name = self.graph_id database_node_name = f"Database {database_name}" @@ -396,6 +402,8 @@ async def retrieve_similar_queries(self, query: str, limit: int = 5) -> List[Dic Returns: A list of similar query metadata. """ + if not self.memory_enabled: + return [] try: database_name = self.graph_id @@ -457,10 +465,12 @@ async def search_user_summary(self, limit: int = 5) -> str: Args: query: Natural language query to search for limit: Maximum number of results to return - + Returns: List of user node summaries with metadata """ + if not self.memory_enabled: + return "" try: driver = self.graphiti_client.driver query = """ @@ -508,14 +518,16 @@ async def extract_episode_from_rel(self, rel_result): async def search_database_facts(self, query: str, limit: int = 5, episode_limit: int = 3) -> str: """ Search for database-specific facts and interaction history using database node as center. - + Args: query: Natural language query to search for database facts limit: Maximum number of results to return - + Returns: String containing all relevant database facts with time relevancy information """ + if not self.memory_enabled: + return "" try: driver = self.graphiti_client.driver query = """ @@ -568,15 +580,18 @@ async def search_memories(self, query: str, user_limit: int = 5, database_limit: """ Run both user summary and database facts searches concurrently for better performance. Also builds a comprehensive memory context string for the analysis agent. - + Args: query: Natural language query to search for database facts user_limit: Maximum number of results for user summary search database_limit: Maximum number of results for database facts search - + Returns: - Dict containing user_summary, database_facts, similar_queries, and memory_context + A formatted memory context string combining user summary, database facts, + and similar query history, or empty string if memory is disabled. """ + if not self.memory_enabled: + return "" try: # Run both searches concurrently using asyncio.gather user_summary_task = self.search_user_summary(limit=user_limit) @@ -819,10 +834,10 @@ def create_graphiti_client(falkor_driver: FalkorDriver) -> Graphiti: client=llm_client_azure, ), ) - else: # Fallback to default OpenAI config but use Config's embedding model - # Extract just the model name without provider prefix for Graphiti + elif Config.LLM_PROVIDER == "openai": + # OpenAI provider — use OpenAIEmbedder with configured model embedding_model_name = extract_embedding_model_name(Config.EMBEDDING_MODEL_NAME) - + graphiti_client = Graphiti( graph_driver=falkor_driver, embedder=OpenAIEmbedder( @@ -832,6 +847,16 @@ def create_graphiti_client(falkor_driver: FalkorDriver) -> Graphiti: ) ), ) + else: + # Non-OpenAI/Azure providers (Gemini, Anthropic, Ollama, Cohere): + # Graphiti memory requires OpenAI-compatible embeddings. + # Memory is not supported for these providers. + logging.warning( + "Memory is only supported with Azure or OpenAI providers. " + "Current provider: %s. Memory will be disabled.", + getattr(Config, 'LLM_PROVIDER', 'unknown') + ) + return None return graphiti_client diff --git a/api/routes/settings.py b/api/routes/settings.py new file mode 100644 index 00000000..554081d2 --- /dev/null +++ b/api/routes/settings.py @@ -0,0 +1,114 @@ +"""Settings and configuration routes for the text2sql API.""" + +import logging +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from litellm import completion + +from api.auth.user_management import token_required +from api.routes.tokens import UNAUTHORIZED_RESPONSE + +settings_router = APIRouter(tags=["Settings"]) + + +def _sanitize_for_log(value: str) -> str: + """Remove control characters that could enable log injection.""" + return str(value).replace("\r", "").replace("\n", "").replace("\t", " ") + + +class ValidateKeyRequest(BaseModel): + """Request model for API key validation.""" + api_key: str + vendor: str = "openai" + model: str = "gpt-3.5-turbo" + + +@settings_router.post("/validate-api-key", responses={401: UNAUTHORIZED_RESPONSE}) +@token_required +async def validate_api_key(request: Request, data: ValidateKeyRequest): # pylint: disable=too-many-return-statements,unused-argument + """ + Validate an AI provider API key by making a simple test request. + This endpoint does not store the key, it only validates it. + Supports: openai, google, anthropic + """ + api_key = data.api_key.strip() + vendor = data.vendor.lower() + model = data.model + + if not api_key: + return JSONResponse( + content={"valid": False, "error": "API key is required"}, + status_code=400 + ) + + # Validate vendor — only key-based vendors can be validated via API call + validatable_vendors = ("openai", "anthropic", "gemini", "cohere") + if vendor not in validatable_vendors: + allowed = ", ".join(validatable_vendors) + return JSONResponse( + content={"valid": False, "error": f"Unsupported vendor for key validation. Supported: {allowed}"}, + status_code=400 + ) + + # Validate model is not empty + if not model or not model.strip(): + return JSONResponse( + content={"valid": False, "error": "Model name is required"}, + status_code=400 + ) + + # Validate key format based on vendor + if vendor == "openai" and not api_key.startswith('sk-'): + return JSONResponse( + content={"valid": False, "error": "Invalid OpenAI API key format"}, + status_code=400 + ) + if vendor == "anthropic" and not api_key.startswith('sk-ant-'): + return JSONResponse( + content={"valid": False, "error": "Invalid Anthropic API key format"}, + status_code=400 + ) + + try: + # Construct model name for LiteLLM (vendor/model format) + full_model_name = f"{vendor}/{model}" + + test_response = completion( + model=full_model_name, + messages=[{"role": "user", "content": "test"}], + max_tokens=1, + api_key=api_key, + ) + + # If we get here without exception, the key is valid + if test_response and test_response.choices: + return JSONResponse( + content={"valid": True}, + status_code=200 + ) + return JSONResponse( + content={"valid": False, "error": "Invalid API key"}, + status_code=401 + ) + + except Exception as e: # pylint: disable=broad-except + error_lower = str(e).lower() + logging.warning("API key validation failed for vendor=%s", + _sanitize_for_log(vendor)) + + # Return generic messages — never expose exception details + if "invalid" in error_lower or "authentication" in error_lower: + return JSONResponse( + content={"valid": False, "error": "Invalid API key"}, + status_code=401 + ) + if "quota" in error_lower or "rate" in error_lower: + return JSONResponse( + content={"valid": False, "error": "API quota exceeded or rate limited"}, + status_code=429 + ) + return JSONResponse( + content={"valid": False, "error": "Failed to validate API key"}, + status_code=500 + ) diff --git a/api/utils.py b/api/utils.py index 845ca604..e6979876 100644 --- a/api/utils.py +++ b/api/utils.py @@ -98,7 +98,8 @@ def create_combined_description( # pylint: disable=too-many-locals if isinstance(batch_response, Exception): table_info[table_name]["description"] = table_name else: - content = batch_response.choices[0].message["content"].strip() + msg_content = batch_response.choices[0].message["content"] + content = msg_content.strip() if msg_content else table_name table_info[table_name]["description"] = content return table_info diff --git a/app/package-lock.json b/app/package-lock.json index 33c4e227..13db7eed 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,8 +8,8 @@ "name": "queryweaver-app", "version": "0.1.0", "dependencies": { - "@falkordb/canvas": "^0.0.41", - "@hookform/resolvers": "^3.10.0", + "@falkordb/canvas": "^0.0.45", + "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", @@ -46,7 +46,7 @@ "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", - "lucide-react": "^0.462.0", + "lucide-react": "^0.577.0", "next-themes": "^0.3.0", "preact": "^10.28.4", "react": "^18.3.1", @@ -54,7 +54,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.71.2", "react-resizable-panels": "^2.1.9", - "react-router-dom": "^6.30.1", + "react-router-dom": "^7.13.2", "recharts": "^2.15.4", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", @@ -74,10 +74,10 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^17.3.0", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "typescript-eslint": "^8.38.0", + "typescript-eslint": "^8.57.0", "vite": "^7.3.0" } }, @@ -674,9 +674,9 @@ } }, "node_modules/@falkordb/canvas": { - "version": "0.0.41", - "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.41.tgz", - "integrity": "sha512-r2DC2Mo9naASr3DDmVKCQVY/S50Hn2bGlmKaFxDRFcn5btiuuvPBqJ3krtC+uz5ZPmad8EoXRRlcQ6KwKJhUZw==", + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@falkordb/canvas/-/canvas-0.0.45.tgz", + "integrity": "sha512-BIb24rfnaLCxO2UH7L4cNYLikmOTXlVR4I/FIpEG/d9ZEp3F+9U/ugaLYOcYe1pmZzT72YnzWUuvdhNE3zK99A==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -724,10 +724,15 @@ "license": "MIT" }, "node_modules/@hookform/resolvers": { - "version": "3.10.0", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, "peerDependencies": { - "react-hook-form": "^7.0.0" + "react-hook-form": "^7.55.0" } }, "node_modules/@humanfs/core": { @@ -2333,13 +2338,6 @@ "version": "1.1.1", "license": "MIT" }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "dev": true, @@ -2695,6 +2693,12 @@ "win32" ] }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.15.8", "dev": true, @@ -3192,15 +3196,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/type-utils": "8.53.0", - "@typescript-eslint/utils": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -3213,13 +3219,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.53.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -3227,14 +3235,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -3245,17 +3255,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.0", - "@typescript-eslint/types": "^8.53.0", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -3270,12 +3282,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3286,7 +3300,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -3301,13 +3317,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0", - "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -3319,12 +3337,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -3336,16 +3356,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.53.0", - "@typescript-eslint/tsconfig-utils": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/visitor-keys": "8.53.0", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -3361,39 +3383,56 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.53.0", - "@typescript-eslint/types": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3403,17 +3442,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3423,6 +3464,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.11.0", "dev": true, @@ -3464,7 +3518,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3663,9 +3719,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001775", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", - "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "dev": true, "funding": [ { @@ -3803,6 +3859,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -4670,7 +4739,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4765,9 +4836,9 @@ } }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -5014,15 +5085,15 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -5041,10 +5112,12 @@ } }, "node_modules/lucide-react": { - "version": "0.462.0", + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/merge2": { @@ -5227,7 +5300,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -5251,7 +5326,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -5566,31 +5643,41 @@ } }, "node_modules/react-router": { - "version": "6.30.3", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-router-dom": { - "version": "6.30.3", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" + "react-router": "7.13.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/react-smooth": { @@ -5811,7 +5898,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5821,6 +5910,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -6032,7 +6127,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -6052,7 +6149,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -6094,14 +6193,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.53.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.53.0", - "@typescript-eslint/parser": "8.53.0", - "@typescript-eslint/typescript-estree": "8.53.0", - "@typescript-eslint/utils": "8.53.0" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6111,7 +6212,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -6328,7 +6429,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/app/package.json b/app/package.json index 6322ceab..ddcdb637 100644 --- a/app/package.json +++ b/app/package.json @@ -11,8 +11,8 @@ "preview": "vite preview" }, "dependencies": { - "@falkordb/canvas": "^0.0.41", - "@hookform/resolvers": "^3.10.0", + "@falkordb/canvas": "^0.0.45", + "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", @@ -49,7 +49,7 @@ "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", - "lucide-react": "^0.462.0", + "lucide-react": "^0.577.0", "next-themes": "^0.3.0", "preact": "^10.28.4", "react": "^18.3.1", @@ -57,7 +57,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.71.2", "react-resizable-panels": "^2.1.9", - "react-router-dom": "^6.30.1", + "react-router-dom": "^7.13.2", "recharts": "^2.15.4", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", @@ -77,10 +77,10 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^17.3.0", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "typescript-eslint": "^8.38.0", + "typescript-eslint": "^8.57.0", "vite": "^7.3.0" }, "overrides": { diff --git a/app/src/App.tsx b/app/src/App.tsx index 25699055..1746e1be 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { AuthProvider } from "@/contexts/AuthContext"; import { DatabaseProvider } from "@/contexts/DatabaseContext"; +import { SettingsProvider } from "@/contexts/SettingsContext"; import { ChatProvider } from "@/contexts/ChatContext"; import Index from "./pages/Index"; import Settings from "./pages/Settings"; @@ -15,19 +16,21 @@ const App = () => ( - - - - - - } /> - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - - - - + + + + + + + } /> + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + + diff --git a/app/src/components/chat/ChatInterface.tsx b/app/src/components/chat/ChatInterface.tsx index 75548563..6f43691a 100644 --- a/app/src/components/chat/ChatInterface.tsx +++ b/app/src/components/chat/ChatInterface.tsx @@ -3,6 +3,7 @@ import { cn } from "@/lib/utils"; import { useToast } from "@/components/ui/use-toast"; import { useDatabase } from "@/contexts/DatabaseContext"; import { useAuth } from "@/contexts/AuthContext"; +import { useSettings } from "@/contexts/SettingsContext"; import { useChat } from "@/contexts/ChatContext"; import LoadingSpinner from "@/components/ui/loading-spinner"; import { Skeleton } from "@/components/ui/skeleton"; @@ -10,6 +11,8 @@ import ChatMessage from "./ChatMessage"; import QueryInput from "./QueryInput"; import SuggestionCards from "../SuggestionCards"; import { ChatService } from "@/services/chat"; +import type { ConfirmRequest } from "@/types/api"; +import { getVendorPrefix } from "@/utils/vendorConfig"; interface ChatMessageData { id: string; @@ -53,6 +56,7 @@ const ChatInterface = ({ }: ChatInterfaceProps) => { const { toast } = useToast(); const { selectedGraph } = useDatabase(); + const { vendor, apiKey, modelName, isApiKeyValid } = useSettings(); const { messages, setMessages, conversationHistory, isProcessing, setIsProcessing } = useChat(); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); @@ -146,13 +150,15 @@ const ChatInterface = ({ explanation?: string; isValid?: boolean; } = {}; - // Stream the query for await (const message of ChatService.streamQuery({ query, database: selectedGraph.id, history: historySnapshot, - use_user_rules: useRulesFromDatabase, // Backend fetches from DB when true + customApiKey: isApiKeyValid ? apiKey : undefined, + customModel: isApiKeyValid ? modelName : undefined, + customVendor: isApiKeyValid ? vendor : undefined, + use_user_rules: useRulesFromDatabase, use_memory: useMemory, })) { @@ -323,15 +329,27 @@ const ChatInterface = ({ let finalContent = ""; let queryResults: any[] | null = null; + // Build confirm request with custom credentials if available + const confirmRequest: ConfirmRequest = { + sql_query: confirmMessage.confirmationData.sqlQuery, + confirmation: 'CONFIRM', + chat: confirmMessage.confirmationData.chatHistory, + use_user_rules: useRulesFromDatabase, + }; + if (isApiKeyValid && apiKey) { + confirmRequest.custom_api_key = apiKey; + if (modelName && vendor) { + const vendorPrefix = getVendorPrefix(vendor); + confirmRequest.custom_model = modelName.startsWith(`${vendorPrefix}/`) + ? modelName + : `${vendorPrefix}/${modelName}`; + } + } + // Stream the confirmation response for await (const message of ChatService.streamConfirmOperation( selectedGraph.id, - { - sql_query: confirmMessage.confirmationData.sqlQuery, - confirmation: 'CONFIRM', - chat: confirmMessage.confirmationData.chatHistory, - use_user_rules: useRulesFromDatabase, // Backend fetches from DB when true - } + confirmRequest )) { if (message.type === 'status' || message.type === 'reasoning' || message.type === 'reasoning_step') { // Add reasoning steps diff --git a/app/src/components/layout/Sidebar.tsx b/app/src/components/layout/Sidebar.tsx index bdabc0fc..56acef9e 100644 --- a/app/src/components/layout/Sidebar.tsx +++ b/app/src/components/layout/Sidebar.tsx @@ -6,7 +6,7 @@ import { BookOpen, LifeBuoy, Waypoints, - Settings, + Sliders, } from 'lucide-react'; import { Tooltip, @@ -141,7 +141,7 @@ const Sidebar = ({ className, onSchemaClick, isSchemaOpen, isCollapsed = false, diff --git a/app/src/components/modals/DatabaseModal.tsx b/app/src/components/modals/DatabaseModal.tsx index 35feae26..e3a7f47a 100644 --- a/app/src/components/modals/DatabaseModal.tsx +++ b/app/src/components/modals/DatabaseModal.tsx @@ -29,6 +29,15 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { const [database, setDatabase] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [schema, setSchema] = useState(""); + const [schemaError, setSchemaError] = useState(""); + // Snowflake-specific fields + const [account, setAccount] = useState(""); + const [snowflakeSchema, setSnowflakeSchema] = useState("PUBLIC"); + const [warehouse, setWarehouse] = useState("COMPUTE_WH"); + const [authMode, setAuthMode] = useState<'password' | 'keypair'>('password'); + const [privateKey, setPrivateKey] = useState(""); + const [privateKeyPassphrase, setPrivateKeyPassphrase] = useState(""); const [isConnecting, setIsConnecting] = useState(false); const [connectionSteps, setConnectionSteps] = useState([]); const { refreshGraphs } = useDatabase(); @@ -73,13 +82,32 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { return; } } else { - if (!selectedDatabase || !host || !port || !database || !username) { - toast({ - title: "Missing Information", - description: "Please fill in all required fields", - variant: "destructive", - }); - return; + if (selectedDatabase === 'snowflake') { + if (!account || !database || !username) { + toast({ + title: "Missing Information", + description: "Please fill in all required fields (account, database, username)", + variant: "destructive", + }); + return; + } + if (authMode === 'keypair' && !privateKey) { + toast({ + title: "Missing Information", + description: "Please paste your private key in PEM format", + variant: "destructive", + }); + return; + } + } else { + if (!selectedDatabase || !host || !port || !database || !username) { + toast({ + title: "Missing Information", + description: "Please fill in all required fields", + variant: "destructive", + }); + return; + } } } @@ -90,11 +118,37 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { // Build the connection URL let dbUrl = connectionUrl; if (connectionMode === 'manual') { - const protocol = selectedDatabase === 'mysql' ? 'mysql' : 'postgresql'; - const builtUrl = new URL(`${protocol}://${host}:${port}/${database}`); - builtUrl.username = username; - builtUrl.password = password; - dbUrl = builtUrl.toString(); + if (selectedDatabase === 'snowflake') { + // Build Snowflake URL: snowflake://user@account/database/schema?warehouse=WH + const builtUrl = new URL(`snowflake://${account}/${database}/${snowflakeSchema}`); + builtUrl.username = username; + if (authMode === 'keypair' && privateKey) { + // Base64-encode the PEM key for safe URL transport + builtUrl.searchParams.set('private_key', btoa(privateKey)); + if (privateKeyPassphrase) { + builtUrl.searchParams.set('private_key_passphrase', privateKeyPassphrase); + } + } else { + builtUrl.password = password; + } + builtUrl.searchParams.set('warehouse', warehouse); + dbUrl = builtUrl.toString(); + } else { + const protocol = selectedDatabase === 'mysql' ? 'mysql' : 'postgresql'; + const builtUrl = new URL(`${protocol}://${host}:${port}/${database}`); + builtUrl.username = username; + builtUrl.password = password; + + // Append schema option for PostgreSQL if provided + if (selectedDatabase === 'postgresql' && schema.trim()) { + if (/[^a-zA-Z0-9_]/.test(schema.trim())) { + throw new Error('Schema name can only contain letters, digits, and underscores'); + } + builtUrl.searchParams.set('options', `-csearch_path=${schema.trim()}`); + } + + dbUrl = builtUrl.toString(); + } } // Make streaming request @@ -177,6 +231,14 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { setDatabase(""); setUsername(""); setPassword(""); + setSchema(""); + setSchemaError(""); + setAccount(""); + setSnowflakeSchema("PUBLIC"); + setWarehouse("COMPUTE_WH"); + setAuthMode('password'); + setPrivateKey(""); + setPrivateKeyPassphrase(""); setConnectionSteps([]); }, 1000); } else { @@ -239,7 +301,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { Connect to Database - Connect to PostgreSQL or MySQL database using a connection URL or manual entry.{" "} + Connect to PostgreSQL, MySQL, or Snowflake database using a connection URL or manual entry.{" "}
{ MySQL + +
+
+ Snowflake +
+
@@ -315,75 +383,248 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { placeholder={ selectedDatabase === 'postgresql' ? 'postgresql://username:password@host:5432/database' - : 'mysql://username:password@host:3306/database' + : selectedDatabase === 'mysql' + ? 'mysql://username:password@host:3306/database' + : 'snowflake://username:password@account/database/schema?warehouse=warehouse_name' } value={connectionUrl} onChange={(e) => setConnectionUrl(e.target.value)} className="bg-muted border-border font-mono text-sm focus-visible:ring-purple-500" />

- Enter your database connection string + {selectedDatabase === 'snowflake' + ? 'Enter your Snowflake connection string (schema defaults to PUBLIC, warehouse to COMPUTE_WH)' + : 'Enter your database connection string'}

)} {selectedDatabase && connectionMode === 'manual' && ( <> -
- - setHost(e.target.value)} - className="bg-muted border-border focus-visible:ring-purple-500" - /> -
+ {selectedDatabase === 'snowflake' ? ( + <> +
+ + setAccount(e.target.value)} + className="bg-muted border-border focus-visible:ring-purple-500" + /> +

+ Your Snowflake account identifier (e.g., myorg-account) +

+
-
- - setPort(e.target.value)} - className="bg-muted border-border focus-visible:ring-purple-500" - /> -
+
+ + setDatabase(e.target.value)} + className="bg-muted border-border focus-visible:ring-purple-500" + /> +
-
- - setDatabase(e.target.value)} - className="bg-muted border-border focus-visible:ring-purple-500" - /> -
+
+ + setSnowflakeSchema(e.target.value)} + className="bg-muted border-border focus-visible:ring-purple-500" + /> +

+ Defaults to PUBLIC if not specified +

+
-
- - setUsername(e.target.value)} - className="bg-muted border-border focus-visible:ring-purple-500" - /> -
+
+ + setWarehouse(e.target.value)} + className="bg-muted border-border focus-visible:ring-purple-500" + /> +

+ Defaults to COMPUTE_WH if not specified +

+
-
- - setPassword(e.target.value)} - className="bg-muted border-border focus-visible:ring-purple-500" - /> -
+
+ + setUsername(e.target.value)} + className="bg-muted border-border focus-visible:ring-purple-500" + /> +
+ + {/* Auth Mode Toggle */} +
+ +
+ + +
+
+ + {authMode === 'password' ? ( +
+ + setPassword(e.target.value)} + className="bg-muted border-border focus-visible:ring-purple-500" + /> +
+ ) : ( + <> +
+ +