diff --git a/pyproject.toml b/pyproject.toml index 6f17cd39..fe388eba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ classifiers = [ "Framework :: Hatch", ] +[project.optional-dependencies] +lakebase = ["psycopg[binary]>=3.2.0"] + [build-system] requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"] build-backend = "hatchling.build" @@ -54,6 +57,7 @@ dev = [ "docutils>=0.22.4", "fastapi>=0.119.0", "mypy>=1.18.2", + "psycopg[binary]>=3.2.0", "pydantic-settings>=2.11.0", "pytest>=8.4.2", "pytest-asyncio>=0.24.0", diff --git a/src/apx/cli/dev/commands.py b/src/apx/cli/dev/commands.py index b2e78ae2..a4878145 100644 --- a/src/apx/cli/dev/commands.py +++ b/src/apx/cli/dev/commands.py @@ -439,6 +439,11 @@ def dev_mcp(): This command should be run from the project root directory. """ + # Load .env file if it exists (for DATABRICKS_CONFIG_PROFILE, etc.) + dotenv_path = Path.cwd() / ".env" + if dotenv_path.exists(): + load_dotenv(dotenv_path) + from apx.mcp import run_mcp_server run_mcp_server() diff --git a/src/apx/mcp/__init__.py b/src/apx/mcp/__init__.py index df85fce3..2e99a8b5 100644 --- a/src/apx/mcp/__init__.py +++ b/src/apx/mcp/__init__.py @@ -11,6 +11,7 @@ # Import tools to register them with the mcp instance # These imports must come after mcp is available from apx.mcp import common as _common # noqa: F401 +from apx.mcp import lakebase as _lakebase # noqa: F401 from apx.mcp import sdk as _sdk # noqa: F401 # Track initialization state diff --git a/src/apx/mcp/lakebase.py b/src/apx/mcp/lakebase.py new file mode 100644 index 00000000..632aecbd --- /dev/null +++ b/src/apx/mcp/lakebase.py @@ -0,0 +1,603 @@ +"""Databricks Lakebase MCP tools for database instance management and SQL operations. + +This module provides MCP (Model Context Protocol) tools for: +- Listing and describing Lakebase database instances +- Getting and setting instance capacity (autoscaling) +- Running SQL queries against Lakebase instances +- Schema introspection (tables and columns) + +The tools follow the same patterns as Neon MCP for PostgreSQL operations, +adapted for Databricks Lakebase instances. + +Note: SQL execution and schema introspection require the 'lakebase' extra: + uv add apx[lakebase] +""" + +import asyncio +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from typing import Any +from uuid import UUID + +from databricks.sdk import WorkspaceClient +from pydantic import JsonValue +from databricks.sdk.errors import NotFound +from sqlalchemy import create_engine, event, text + +from apx.mcp.server import mcp +from apx.models import ( + LakebaseCapacityInfo, + LakebaseCapacityUpdateResponse, + LakebaseColumnInfo, + LakebaseInstanceInfo, + LakebaseListInstancesResponse, + LakebaseSqlResult, + LakebaseTableInfo, + LakebaseTableSchemaResponse, + LakebaseTablesResponse, + McpErrorResponse, +) + +# Check if psycopg is available for PostgreSQL connectivity +def _check_psycopg_available() -> bool: + try: + import psycopg # noqa: F401 + + return True + except ImportError: + return False + + +_psycopg_available = _check_psycopg_available() + +PSYCOPG_INSTALL_MSG = ( + "The psycopg PostgreSQL driver is not installed. " + "Install the 'lakebase' extra to enable SQL functionality:\n" + " uv add apx[lakebase]" +) + +# Available Lakebase capacity tiers +LAKEBASE_CAPACITIES = ["CU_1", "CU_2", "CU_4", "CU_8", "CU_16"] + +# Default database name for Lakebase +DEFAULT_DATABASE_NAME = "databricks_postgres" + +# Default port for Lakebase PostgreSQL +DEFAULT_PORT = 5432 + + +def _get_workspace_client() -> WorkspaceClient: + """Get a WorkspaceClient for Lakebase operations. + + Uses environment-based authentication (DATABRICKS_CONFIG_PROFILE or + service principal credentials). + """ + return WorkspaceClient() + + +def _get_lakebase_engine( + ws: WorkspaceClient, + instance_name: str, + database_name: str = DEFAULT_DATABASE_NAME, + port: int = DEFAULT_PORT, +) -> Any: + """Create a SQLAlchemy engine for a Lakebase instance. + + Args: + ws: WorkspaceClient for authentication + instance_name: Name of the database instance + database_name: Database name within the instance + port: PostgreSQL port + + Returns: + SQLAlchemy Engine configured for the Lakebase instance + + Raises: + ImportError: If psycopg is not installed (requires apx[lakebase] extra) + """ + if not _psycopg_available: + raise ImportError(PSYCOPG_INSTALL_MSG) + + instance = ws.database.get_database_instance(instance_name) + + username = ws.config.client_id or ws.current_user.me().user_name + host = instance.read_write_dns + url = f"postgresql+psycopg://{username}:@{host}:{port}/{database_name}" + + engine = create_engine( + url, + connect_args={"sslmode": "require"}, + ) + + # Set up dynamic password retrieval for token-based auth + def before_connect( + dialect: Any, conn_rec: Any, cargs: Any, cparams: dict[str, Any] + ) -> None: + cred = ws.database.generate_database_credential(instance_names=[instance_name]) + cparams["password"] = cred.token + + event.listen(engine, "do_connect", before_connect) + + return engine + + +def _serialize_row_value(value: Any) -> JsonValue: + """Convert a database value to a JSON-serializable type. + + Handles common PostgreSQL types that are not natively JSON-serializable: + - datetime, date, time -> ISO format string + - timedelta -> string representation + - Decimal -> float + - UUID -> string + - bytes -> UTF-8 decoded string + - memoryview -> UTF-8 decoded string + + Args: + value: Any value from a database row + + Returns: + A JSON-serializable value (str, int, float, bool, list, dict, or None) + """ + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, (datetime, date, time)): + return value.isoformat() + if isinstance(value, timedelta): + return str(value) + if isinstance(value, Decimal): + return float(value) + if isinstance(value, UUID): + return str(value) + if isinstance(value, (bytes, memoryview)): + return bytes(value).decode("utf-8", errors="replace") + if isinstance(value, (list, tuple)): + return [_serialize_row_value(v) for v in value] + if isinstance(value, dict): + return {k: _serialize_row_value(v) for k, v in value.items()} + # Fallback for any other type + return str(value) + + +def _serialize_row(columns: list[str], row: tuple[Any, ...]) -> dict[str, JsonValue]: + """Convert a database row to a JSON-serializable dictionary. + + Args: + columns: List of column names + row: Tuple of values from the database + + Returns: + Dictionary mapping column names to JSON-serializable values + """ + return {col: _serialize_row_value(val) for col, val in zip(columns, row)} + + +# ============================================================================ +# MCP Tools - Instance Management +# ============================================================================ + + +@mcp.tool() +async def list_database_instances() -> LakebaseListInstancesResponse | McpErrorResponse: + """List all Databricks Lakebase database instances. + + Returns a list of all database instances accessible to the current user, + including their state, capacity, and endpoint information. + + Returns: + LakebaseListInstancesResponse with all instances, or McpErrorResponse on error + """ + try: + ws = _get_workspace_client() + + def fetch_instances() -> list[LakebaseInstanceInfo]: + instances = list(ws.database.list_database_instances()) + return [ + LakebaseInstanceInfo( + name=inst.name or "", + state=str(inst.state) if inst.state else "UNKNOWN", + capacity=str(inst.capacity) if inst.capacity else "UNKNOWN", + read_write_dns=inst.read_write_dns, + read_only_dns=inst.read_only_dns, + creator=inst.creator, + created_time=( + str(inst.creation_time) if inst.creation_time else None + ), + ) + for inst in instances + ] + + instances = await asyncio.to_thread(fetch_instances) + return LakebaseListInstancesResponse( + instances=instances, total_count=len(instances) + ) + except Exception as e: + return McpErrorResponse(error=f"Failed to list database instances: {e!s}") + + +@mcp.tool() +async def get_database_instance( + instance_name: str, +) -> LakebaseInstanceInfo | McpErrorResponse: + """Get detailed information about a specific Lakebase database instance. + + Args: + instance_name: Name of the database instance to retrieve + + Returns: + LakebaseInstanceInfo with instance details, or McpErrorResponse if not found + """ + try: + ws = _get_workspace_client() + + def fetch_instance() -> LakebaseInstanceInfo: + inst = ws.database.get_database_instance(instance_name) + return LakebaseInstanceInfo( + name=inst.name or instance_name, + state=str(inst.state) if inst.state else "UNKNOWN", + capacity=str(inst.capacity) if inst.capacity else "UNKNOWN", + read_write_dns=inst.read_write_dns, + read_only_dns=inst.read_only_dns, + creator=inst.creator, + created_time=str(inst.creation_time) if inst.creation_time else None, + ) + + return await asyncio.to_thread(fetch_instance) + except NotFound: + return McpErrorResponse( + error=f"Database instance '{instance_name}' not found. " + "Use list_database_instances to see available instances." + ) + except Exception as e: + return McpErrorResponse(error=f"Failed to get database instance: {e!s}") + + +# ============================================================================ +# MCP Tools - Capacity/Autoscaling +# ============================================================================ + + +@mcp.tool() +async def get_instance_capacity( + instance_name: str, +) -> LakebaseCapacityInfo | McpErrorResponse: + """Get current capacity and available scaling options for a Lakebase instance. + + Args: + instance_name: Name of the database instance + + Returns: + LakebaseCapacityInfo with current and available capacities + """ + try: + ws = _get_workspace_client() + + def fetch_capacity() -> LakebaseCapacityInfo: + inst = ws.database.get_database_instance(instance_name) + return LakebaseCapacityInfo( + instance_name=instance_name, + current_capacity=str(inst.capacity) if inst.capacity else "UNKNOWN", + available_capacities=LAKEBASE_CAPACITIES, + ) + + return await asyncio.to_thread(fetch_capacity) + except NotFound: + return McpErrorResponse(error=f"Database instance '{instance_name}' not found.") + except Exception as e: + return McpErrorResponse(error=f"Failed to get instance capacity: {e!s}") + + +@mcp.tool() +async def set_instance_capacity( + instance_name: str, + capacity: str, + confirm: bool = False, +) -> LakebaseCapacityUpdateResponse | McpErrorResponse: + """Set the capacity (scaling tier) for a Lakebase instance. + + This is a write operation that modifies the instance configuration. + The capacity change may take a few minutes to complete. + + Available capacity tiers: CU_1, CU_2, CU_4, CU_8, CU_16 + + Args: + instance_name: Name of the database instance + capacity: Target capacity tier (CU_1, CU_2, CU_4, CU_8, CU_16) + confirm: Must be True to apply changes (safety check) + + Returns: + LakebaseCapacityUpdateResponse with status, or McpErrorResponse on error + """ + if capacity not in LAKEBASE_CAPACITIES: + return McpErrorResponse( + error=f"Invalid capacity '{capacity}'. " + f"Must be one of: {', '.join(LAKEBASE_CAPACITIES)}" + ) + + if not confirm: + return McpErrorResponse( + error=f"Safety check: Set confirm=True to change capacity from current " + f"value to '{capacity}'. This operation may affect running workloads." + ) + + try: + ws = _get_workspace_client() + + def update_capacity() -> LakebaseCapacityUpdateResponse: + from databricks.sdk.service.database import DatabaseInstance + + # Get current capacity first + inst = ws.database.get_database_instance(instance_name) + previous_capacity = str(inst.capacity) if inst.capacity else "UNKNOWN" + + # Update the instance capacity + # Create a DatabaseInstance with the new capacity + update_inst = DatabaseInstance(name=instance_name, capacity=capacity) + ws.database.update_database_instance( + name=instance_name, + database_instance=update_inst, + update_mask="capacity", + ) + + return LakebaseCapacityUpdateResponse( + status="success", + instance_name=instance_name, + previous_capacity=previous_capacity, + new_capacity=capacity, + message=f"Capacity change initiated. The instance will scale to " + f"{capacity}. This may take a few minutes to complete.", + ) + + return await asyncio.to_thread(update_capacity) + except NotFound: + return McpErrorResponse(error=f"Database instance '{instance_name}' not found.") + except Exception as e: + return McpErrorResponse(error=f"Failed to set instance capacity: {e!s}") + + +# ============================================================================ +# MCP Tools - SQL Execution +# ============================================================================ + + +@mcp.tool() +async def run_lakebase_sql( + instance_name: str, + sql: str, + database_name: str = DEFAULT_DATABASE_NAME, + read_only: bool = True, +) -> LakebaseSqlResult | McpErrorResponse: + """Execute SQL against a Lakebase database instance. + + By default, runs in read-only mode for safety. Set read_only=False for + write operations (INSERT, UPDATE, DELETE, DDL). + + Args: + instance_name: Name of the database instance + sql: SQL statement to execute + database_name: Database name within the instance (default: databricks_postgres) + read_only: If True, executes in a read-only transaction (default: True) + + Returns: + LakebaseSqlResult with query results or affected row count + + Examples: + - SELECT queries: Returns columns and rows + - INSERT/UPDATE/DELETE: Returns rows_affected count (requires read_only=False) + - DDL statements: Returns success status (requires read_only=False) + """ + try: + ws = _get_workspace_client() + + def execute_sql() -> LakebaseSqlResult: + engine = _get_lakebase_engine(ws, instance_name, database_name) + + with engine.connect() as conn: + if read_only: + # Start a read-only transaction + conn.execute(text("SET TRANSACTION READ ONLY")) + + result = conn.execute(text(sql)) + + if result.returns_rows: + columns = list(result.keys()) + rows = [_serialize_row(columns, row) for row in result.fetchall()] + return LakebaseSqlResult( + success=True, + columns=columns, + rows=rows, + ) + else: + if not read_only: + conn.commit() + return LakebaseSqlResult( + success=True, + rows_affected=result.rowcount, + ) + + return await asyncio.to_thread(execute_sql) + except NotFound: + return McpErrorResponse(error=f"Database instance '{instance_name}' not found.") + except Exception as e: + error_msg = str(e) + # Check if it's a read-only transaction error + if "read-only transaction" in error_msg.lower() and read_only: + return LakebaseSqlResult( + success=False, + error=f"Cannot execute write operation in read-only mode. " + f"Set read_only=False to allow writes. Original error: {error_msg}", + ) + return LakebaseSqlResult(success=False, error=error_msg) + + +# ============================================================================ +# MCP Tools - Schema Introspection +# ============================================================================ + + +@mcp.tool() +async def get_lakebase_tables( + instance_name: str, + database_name: str = DEFAULT_DATABASE_NAME, +) -> LakebaseTablesResponse | McpErrorResponse: + """List all tables in a Lakebase database. + + Returns tables from all schemas except system schemas (pg_catalog, + information_schema). + + Args: + instance_name: Name of the database instance + database_name: Database name within the instance (default: databricks_postgres) + + Returns: + LakebaseTablesResponse with list of tables + """ + try: + ws = _get_workspace_client() + + def fetch_tables() -> LakebaseTablesResponse: + engine = _get_lakebase_engine(ws, instance_name, database_name) + + query = """ + SELECT + table_schema, + table_name, + table_type + FROM information_schema.tables + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY table_schema, table_name + """ + + with engine.connect() as conn: + result = conn.execute(text(query)) + tables = [ + LakebaseTableInfo( + table_schema=row[0], + table_name=row[1], + table_type=row[2], + ) + for row in result.fetchall() + ] + + return LakebaseTablesResponse( + instance_name=instance_name, + database_name=database_name, + tables=tables, + total_count=len(tables), + ) + + return await asyncio.to_thread(fetch_tables) + except NotFound: + return McpErrorResponse(error=f"Database instance '{instance_name}' not found.") + except Exception as e: + return McpErrorResponse(error=f"Failed to get tables: {e!s}") + + +@mcp.tool() +async def describe_lakebase_table( + instance_name: str, + table_name: str, + table_schema: str = "public", + database_name: str = DEFAULT_DATABASE_NAME, +) -> LakebaseTableSchemaResponse | McpErrorResponse: + """Get detailed schema information for a table in a Lakebase database. + + Returns column definitions, primary key, and index information. + + Args: + instance_name: Name of the database instance + table_name: Name of the table to describe + table_schema: Schema containing the table (default: public) + database_name: Database name within the instance (default: databricks_postgres) + + Returns: + LakebaseTableSchemaResponse with column and constraint details + """ + try: + ws = _get_workspace_client() + + def fetch_schema() -> LakebaseTableSchemaResponse: + engine = _get_lakebase_engine(ws, instance_name, database_name) + + # Query for column information + columns_query = """ + SELECT + column_name, + data_type, + is_nullable, + column_default, + ordinal_position + FROM information_schema.columns + WHERE table_schema = :schema AND table_name = :table + ORDER BY ordinal_position + """ + + # Query for primary key columns + pk_query = """ + SELECT kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_schema = :schema + AND tc.table_name = :table + ORDER BY kcu.ordinal_position + """ + + # Query for indexes + indexes_query = """ + SELECT indexname + FROM pg_indexes + WHERE schemaname = :schema AND tablename = :table + """ + + with engine.connect() as conn: + # Fetch columns + result = conn.execute( + text(columns_query), {"schema": table_schema, "table": table_name} + ) + columns = [ + LakebaseColumnInfo( + column_name=row[0], + data_type=row[1], + is_nullable=row[2] == "YES", + column_default=row[3], + ordinal_position=row[4], + ) + for row in result.fetchall() + ] + + if not columns: + raise ValueError( + f"Table '{table_schema}.{table_name}' not found or has no columns" + ) + + # Fetch primary key + pk_result = conn.execute( + text(pk_query), {"schema": table_schema, "table": table_name} + ) + primary_key = [row[0] for row in pk_result.fetchall()] + + # Fetch indexes + idx_result = conn.execute( + text(indexes_query), {"schema": table_schema, "table": table_name} + ) + indexes = [row[0] for row in idx_result.fetchall()] + + return LakebaseTableSchemaResponse( + instance_name=instance_name, + database_name=database_name, + table_schema=table_schema, + table_name=table_name, + columns=columns, + primary_key=primary_key, + indexes=indexes, + ) + + return await asyncio.to_thread(fetch_schema) + except NotFound: + return McpErrorResponse(error=f"Database instance '{instance_name}' not found.") + except ValueError as e: + return McpErrorResponse(error=str(e)) + except Exception as e: + return McpErrorResponse(error=f"Failed to describe table: {e!s}") diff --git a/src/apx/models.py b/src/apx/models.py index f3f09dcd..3e383371 100644 --- a/src/apx/models.py +++ b/src/apx/models.py @@ -664,3 +664,114 @@ class McpDatabricksAppsLogsResponse(CommandResult): app_name: str resolved_from_databricks_yml: bool = False + + +# ============================================================================ +# Lakebase MCP Response Models +# ============================================================================ + + +class LakebaseInstanceInfo(BaseModel): + """Information about a Databricks Lakebase database instance.""" + + name: str = Field(description="Instance name") + state: str = Field(description="Instance state (RUNNING, STOPPED, PENDING, etc.)") + capacity: str = Field(description="Current capacity tier (CU_1, CU_2, etc.)") + read_write_dns: str | None = Field( + default=None, description="Read-write DNS endpoint" + ) + read_only_dns: str | None = Field( + default=None, description="Read-only DNS endpoint" + ) + creator: str | None = Field(default=None, description="Creator of the instance") + created_time: str | None = Field(default=None, description="Creation timestamp") + + +class LakebaseListInstancesResponse(BaseModel): + """Response for list_database_instances tool.""" + + instances: list[LakebaseInstanceInfo] = Field( + default_factory=list, description="List of database instances" + ) + total_count: int = Field(description="Total number of instances") + + +class LakebaseCapacityInfo(BaseModel): + """Capacity/scaling information for a Lakebase instance.""" + + instance_name: str = Field(description="Name of the database instance") + current_capacity: str = Field(description="Current capacity tier") + available_capacities: list[str] = Field( + default_factory=list, description="Available capacity options" + ) + + +class LakebaseCapacityUpdateResponse(BaseModel): + """Response for set_instance_capacity tool.""" + + status: str = Field(description="Status of the operation (success, error)") + instance_name: str = Field(description="Name of the instance") + previous_capacity: str = Field(description="Previous capacity tier") + new_capacity: str = Field(description="New capacity tier") + message: str | None = Field(default=None, description="Additional status message") + + +class LakebaseSqlResult(BaseModel): + """Result of SQL execution against a Lakebase instance.""" + + success: bool = Field(description="Whether the query executed successfully") + rows_affected: int | None = Field( + default=None, description="Number of rows affected (for write operations)" + ) + columns: list[str] = Field( + default_factory=list, description="Column names in result" + ) + rows: list[dict[str, JsonValue]] = Field( + default_factory=list, description="Result rows as list of dicts" + ) + error: str | None = Field(default=None, description="Error message if failed") + + +class LakebaseTableInfo(BaseModel): + """Information about a table in a Lakebase database.""" + + table_schema: str = Field(description="Schema name (e.g., 'public')") + table_name: str = Field(description="Table name") + table_type: str = Field(description="Table type (BASE TABLE, VIEW, etc.)") + + +class LakebaseTablesResponse(BaseModel): + """Response for get_lakebase_tables tool.""" + + instance_name: str = Field(description="Name of the database instance") + database_name: str = Field(description="Name of the database") + tables: list[LakebaseTableInfo] = Field( + default_factory=list, description="List of tables" + ) + total_count: int = Field(description="Total number of tables") + + +class LakebaseColumnInfo(BaseModel): + """Information about a column in a table.""" + + column_name: str = Field(description="Column name") + data_type: str = Field(description="Data type") + is_nullable: bool = Field(description="Whether the column allows NULL") + column_default: str | None = Field(default=None, description="Default value") + ordinal_position: int = Field(description="Position of column in table") + + +class LakebaseTableSchemaResponse(BaseModel): + """Response for describe_lakebase_table tool.""" + + instance_name: str = Field(description="Name of the database instance") + database_name: str = Field(description="Name of the database") + table_schema: str = Field(description="Schema name") + table_name: str = Field(description="Table name") + columns: list[LakebaseColumnInfo] = Field( + default_factory=list, description="List of columns" + ) + primary_key: list[str] = Field( + default_factory=list, description="Primary key column names" + ) + indexes: list[str] = Field(default_factory=list, description="Index names") diff --git a/src/apx/templates/addons/cursor/.cursor/rules/project.mdc.jinja2 b/src/apx/templates/addons/cursor/.cursor/rules/project.mdc.jinja2 index 9eae4d14..35e25e5a 100644 --- a/src/apx/templates/addons/cursor/.cursor/rules/project.mdc.jinja2 +++ b/src/apx/templates/addons/cursor/.cursor/rules/project.mdc.jinja2 @@ -13,6 +13,7 @@ alwaysApply: true - If agent has access to native browser tool, use it to verify changes on the frontend. If such tool is not present or is not working, use playwright mcp to automate browser actions (e.g. screenshots, clicks, etc.). - Avoid unnecessary restarts of the development servers - **Databricks SDK:** Use the apx MCP tools (`search_databricks_sdk`, `get_method_spec`, `get_model_spec`) to look up Databricks SDK methods and models instead of guessing or hallucinating API signatures. +- **Lakebase:** Use the apx MCP Lakebase tools (`list_database_instances`, `get_database_instance`, `get_lakebase_tables`, `describe_lakebase_table`, `run_lakebase_sql`, `get_instance_capacity`, `set_instance_capacity`) for Databricks Lakebase database operations. Use `read_only=True` (default) for SELECT queries; only set `read_only=False` for writes when explicitly requested. Never change instance capacity without user confirmation. ## Package Management - **Frontend:** Always use `bun` (never `npm`) diff --git a/uv.lock b/uv.lock index 3ac566a5..de9394d7 100644 --- a/uv.lock +++ b/uv.lock @@ -55,6 +55,11 @@ dependencies = [ { name = "websockets" }, ] +[package.optional-dependencies] +lakebase = [ + { name = "psycopg", extra = ["binary"] }, +] + [package.dev-dependencies] dev = [ { name = "basedpyright" }, @@ -62,6 +67,7 @@ dev = [ { name = "docutils" }, { name = "fastapi" }, { name = "mypy" }, + { name = "psycopg", extra = ["binary"] }, { name = "pydantic-settings" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -95,6 +101,7 @@ requires-dist = [ { name = "keyring", specifier = ">=25.6.0" }, { name = "mcp", specifier = ">=1.0.0" }, { name = "psutil", specifier = ">=6.1.0" }, + { name = "psycopg", extras = ["binary"], marker = "extra == 'lakebase'", specifier = ">=3.2.0" }, { name = "python-dotenv", specifier = ">=1.1.1" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "rich", specifier = ">=14.2.0" }, @@ -106,6 +113,7 @@ requires-dist = [ { name = "watchfiles", specifier = ">=1.1.1" }, { name = "websockets", specifier = ">=15.0" }, ] +provides-extras = ["lakebase"] [package.metadata.requires-dev] dev = [ @@ -114,6 +122,7 @@ dev = [ { name = "docutils", specifier = ">=0.22.4" }, { name = "fastapi", specifier = ">=0.119.0" }, { name = "mypy", specifier = ">=1.18.2" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2.0" }, { name = "pydantic-settings", specifier = ">=2.11.0" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, @@ -531,6 +540,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, @@ -541,6 +551,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, @@ -551,6 +562,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -561,6 +573,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -1007,6 +1020,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/8d/8a9a45c8b655851f216c1d44f68e3533dc8d2c752ccd0f61f1aa73be4893/psutil-7.1.1-cp37-abi3-win_arm64.whl", hash = "sha256:5457cf741ca13da54624126cd5d333871b454ab133999a9a103fb097a7d7d21a", size = 243944, upload-time = "2025-10-19T15:44:20.666Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" }, + { url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" }, + { url = "https://files.pythonhosted.org/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" }, + { url = "https://files.pythonhosted.org/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" }, + { url = "https://files.pythonhosted.org/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" }, + { url = "https://files.pythonhosted.org/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" }, + { url = "https://files.pythonhosted.org/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" }, + { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" }, + { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" }, + { url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" }, + { url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" }, + { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" }, + { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" }, + { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" }, + { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" }, + { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" }, + { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" }, + { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" }, + { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" }, + { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -1865,6 +1947,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"