diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 000000000..948406100 --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,21 @@ +# Wren Engine Skills + +Claude Code skills for the Wren Engine CLI. + +## Available skills + +| Skill | Trigger | Description | +|-------|---------|-------------| +| [`wren-query`](wren-query/SKILL.md) | `/wren-query [sql]` | Run, dry-run, or validate a SQL query through the Wren semantic CLI | + +## Usage + +Skills are invoked via slash commands in Claude Code: + +``` +/wren-query SELECT order_id FROM "orders" LIMIT 5 +/wren-query --dry-plan SELECT * FROM "orders" +/wren-query --validate SELECT * FROM "NonExistent" +``` + +See each skill's `SKILL.md` for full details. diff --git a/.claude/skills/wren-query/SKILL.md b/.claude/skills/wren-query/SKILL.md new file mode 100644 index 000000000..b188810e8 --- /dev/null +++ b/.claude/skills/wren-query/SKILL.md @@ -0,0 +1,95 @@ +--- +name: wren-query +description: > + Run, dry-plan, or validate a SQL query through the Wren semantic CLI. + Use when the user asks to query a data source using wren, run wren --sql, + dry-plan SQL through MDL, or test a wren query against MySQL/Postgres/etc. +argument-hint: "[sql query]" +allowed-tools: Read, Bash(uv run wren *), Bash(wren *) +--- + +The user wants to run a Wren CLI command. $ARGUMENTS is the SQL query or instruction. + +## What to do + +1. **Check for `~/.wren/mdl.json` and `~/.wren/connection_info.json`** using Read or Glob. + - If either is missing, tell the user what's needed and show the format below. + - If both exist, proceed directly. + +2. **Run the appropriate command** based on what the user asked: + +| Intent | Command | +|--------|---------| +| Execute and return results | `uv run wren --sql '...'` | +| Translate to native SQL (no DB) | `uv run wren dry-plan --sql '...'` | +| Validate without fetching rows | `uv run wren dry-run --sql '...'` | +| Check SQL is valid | `uv run wren validate --sql '...'` | + +If `wren` is installed globally (not via uv), use `wren` directly instead of `uv run wren`. + +3. **Show the result** and explain what happened. + +--- + +## Required files + +Both files are auto-discovered from `~/.wren/`. + +### mdl.json — semantic model +```json +{ + "catalog": "wren", + "schema": "public", + "models": [ + { + "name": "orders", + "tableReference": { "schema": "mydb", "table": "orders" }, + "columns": [ + { "name": "order_id", "type": "integer" }, + { "name": "total", "type": "double" }, + { "name": "status", "type": "varchar" } + ], + "primaryKey": "order_id" + } + ] +} +``` + +### connection_info.json — connection info (include `datasource` field) +```json +{ + "datasource": "mysql", + "host": "localhost", + "port": 3306, + "database": "mydb", + "user": "root", + "password": "secret" +} +``` + +Supported datasource values: `mysql`, `postgres`, `bigquery`, `snowflake`, +`clickhouse`, `trino`, `mssql`, `databricks`, `redshift`, `oracle`, `duckdb`. + +--- + +## Override flags + +When needed, flags can override the defaults: + +```bash +wren --sql '...' --mdl other-mdl.json --connection-file prod-connection_info.json +wren --sql '...' --output csv # table (default) | csv | json +wren --sql '...' --limit 100 +``` + +--- + +## Common errors + +| Error | Fix | +|-------|-----| +| `mdl.json not found` | Create `~/.wren/mdl.json` | +| `connection_info.json not found` | Create `~/.wren/connection_info.json` with a `datasource` field | +| `datasource key not found` | Add `"datasource": "mysql"` to connection_info.json | +| `unknown datasource 'X'` | Check spelling; see supported values above | +| Connection refused | Confirm the DB is running and host/port are correct | diff --git a/.github/workflows/wren-ci.yml b/.github/workflows/wren-ci.yml index c8dc167b8..d1cc031d7 100644 --- a/.github/workflows/wren-ci.yml +++ b/.github/workflows/wren-ci.yml @@ -69,9 +69,20 @@ jobs: - name: Run unit tests run: uv run pytest tests/unit/ -v -m unit - test-postgres: - name: postgres tests + test-connector: + name: ${{ matrix.datasource }} tests runs-on: ubuntu-latest + strategy: + matrix: + include: + - datasource: postgres + extra: postgres + test_file: tests/connectors/test_postgres.py + marker: postgres + - datasource: mysql + extra: mysql + test_file: tests/connectors/test_mysql.py + marker: mysql defaults: run: working-directory: wren @@ -82,6 +93,11 @@ jobs: python-version: "3.11" - name: Install uv uses: astral-sh/setup-uv@v4 + - name: Install system dependencies for mysqlclient + if: matrix.datasource == 'mysql' + run: | + sudo apt-get update + sudo apt-get install -y default-libmysqlclient-dev pkg-config - name: Cache Cargo uses: actions/cache@v4 with: @@ -103,6 +119,6 @@ jobs: - name: Install dependencies run: | uv lock --find-links ../wren-core-py/target/wheels/ --upgrade-package wren-core-py - uv sync --extra postgres --extra dev --find-links ../wren-core-py/target/wheels/ - - name: Run postgres tests - run: uv run pytest tests/connectors/test_postgres.py -v -m postgres + uv sync --extra ${{ matrix.extra }} --extra dev --find-links ../wren-core-py/target/wheels/ + - name: Run ${{ matrix.datasource }} tests + run: uv run pytest ${{ matrix.test_file }} -v -m ${{ matrix.marker }} diff --git a/wren/README.md b/wren/README.md index 2e7a5e105..ae5b9aa37 100644 --- a/wren/README.md +++ b/wren/README.md @@ -1,42 +1,91 @@ -# wren +# wren-engine Wren Engine CLI and Python SDK — semantic SQL layer for 20+ data sources. +Translate natural SQL queries through an MDL (Modeling Definition Language) semantic layer and execute them against your database. + ## Installation ```bash -pip install wren-engine +pip install wren-engine[mysql] # MySQL +pip install wren-engine[postgres] # PostgreSQL +pip install wren-engine[duckdb] # DuckDB (local files) +pip install wren-engine[all] # All connectors +``` + +## Quick start + +**1. Create `~/.wren/mdl.json`** — your semantic model: + +```json +{ + "catalog": "wren", + "schema": "public", + "models": [ + { + "name": "orders", + "tableReference": { "schema": "mydb", "table": "orders" }, + "columns": [ + { "name": "order_id", "type": "integer" }, + { "name": "customer_id", "type": "integer" }, + { "name": "total", "type": "double" }, + { "name": "status", "type": "varchar" } + ], + "primaryKey": "order_id" + } + ] +} +``` + +**2. Create `~/.wren/connection_info.json`** — your connection: + +```json +{ + "datasource": "mysql", + "host": "localhost", + "port": 3306, + "database": "mydb", + "user": "root", + "password": "secret" +} +``` + +**3. Run queries** — `wren` auto-discovers both files from `~/.wren`: + +```bash +wren --sql 'SELECT order_id FROM "orders" LIMIT 10' ``` -## Usage +For the full CLI reference and per-datasource `connection_info.json` formats, see [`docs/cli.md`](docs/cli.md) and [`docs/connections.md`](docs/connections.md). + +--- + +## Python SDK ```python +import base64, orjson from wren import WrenEngine, DataSource + +manifest = { ... } # your MDL dict +manifest_str = base64.b64encode(orjson.dumps(manifest)).decode() + +with WrenEngine(manifest_str, DataSource.mysql, {"host": "...", ...}) as engine: + result = engine.query('SELECT * FROM "orders" LIMIT 10') + print(result.to_pandas()) ``` -See the [Wren Engine documentation](https://getwren.ai) for details. +--- ## Running tests -Install dev dependencies first: - ```bash just install-dev ``` | Command | What it runs | Docker needed | |---------|-------------|---------------| -| `just test-unit` | Unit tests (transpile, dry-plan, context manager) | No | -| `just test-duckdb` | DuckDB connector tests — generates TPCH data via `dbgen` | No | -| `just test-postgres` | PostgreSQL connector tests — spins up a container | Yes | +| `just test-unit` | Unit tests | No | +| `just test-duckdb` | DuckDB connector tests | No | +| `just test-postgres` | PostgreSQL connector tests | Yes | +| `just test-mysql` | MySQL connector tests | Yes | | `just test` | All tests | Yes | - -Run a specific connector via marker: - -```bash -just test-connector postgres -``` - -To add tests for a new connector, subclass `WrenQueryTestSuite` in -`tests/connectors/test_.py` and provide a class-scoped `engine` fixture. -All base tests are inherited automatically. diff --git a/wren/docs/cli.md b/wren/docs/cli.md new file mode 100644 index 000000000..2bb353e61 --- /dev/null +++ b/wren/docs/cli.md @@ -0,0 +1,64 @@ +# CLI reference + +## Default command — query + +Running `wren --sql '...'` executes a query and prints the result. This is the same as `wren query --sql '...'`. + +```bash +wren --sql 'SELECT COUNT(*) FROM "orders"' +wren --sql 'SELECT * FROM "orders" LIMIT 5' --output csv +wren --sql 'SELECT * FROM "orders"' --limit 100 --output json +``` + +Output formats: `table` (default), `csv`, `json`. + +## `wren query` + +Execute SQL and return results. + +```bash +wren query --sql 'SELECT order_id, total FROM "orders" ORDER BY total DESC LIMIT 5' +``` + +## `wren dry-plan` + +Translate MDL SQL to the native dialect SQL for your data source. No database connection required. + +```bash +wren dry-plan --sql 'SELECT order_id FROM "orders"' +``` + +## `wren dry-run` + +Validate SQL against the live database without returning rows. Prints `OK` on success. + +```bash +wren dry-run --sql 'SELECT * FROM "orders" LIMIT 1' +``` + +## `wren validate` + +Same as `dry-run` but prints `Valid` / `Invalid: `. + +```bash +wren validate --sql 'SELECT * FROM "NonExistent"' +# Invalid: table not found ... +``` + +## Overriding defaults + +All flags are optional when `~/.wren/mdl.json` and `~/.wren/connection_info.json` exist: + +```bash +wren --sql '...' \ + --mdl /path/to/other-mdl.json \ + --connection-file /path/to/prod-connection_info.json \ + --datasource postgres +``` + +Or pass connection info inline: + +```bash +wren --sql 'SELECT COUNT(*) FROM "orders"' \ + --connection-info '{"datasource":"mysql","host":"localhost","port":3306,"database":"mydb","user":"root","password":"secret"}' +``` diff --git a/wren/docs/connections.md b/wren/docs/connections.md new file mode 100644 index 000000000..ce870dd9a --- /dev/null +++ b/wren/docs/connections.md @@ -0,0 +1,91 @@ +# Connection formats + +The `connection_info.json` file (or `--connection-info` / `--connection-file` flags) requires a `datasource` field plus the connector-specific fields below. + +## MySQL + +```json +{ + "datasource": "mysql", + "host": "localhost", + "port": 3306, + "database": "mydb", + "user": "root", + "password": "secret" +} +``` + +## PostgreSQL + +```json +{ + "datasource": "postgres", + "host": "localhost", + "port": 5432, + "database": "mydb", + "user": "postgres", + "password": "secret" +} +``` + +## BigQuery + +```json +{ + "datasource": "bigquery", + "project_id": "my-gcp-project", + "dataset_id": "my_dataset", + "credentials": "" +} +``` + +## Snowflake + +```json +{ + "datasource": "snowflake", + "user": "myuser", + "password": "secret", + "account": "myorg-myaccount", + "database": "MYDB", + "schema": "PUBLIC" +} +``` + +## Redshift (standard) + +```json +{ + "datasource": "redshift", + "host": "my-cluster.xxxx.us-east-1.redshift.amazonaws.com", + "port": 5439, + "database": "dev", + "user": "awsuser", + "password": "secret" +} +``` + +## Redshift (IAM) + +```json +{ + "datasource": "redshift", + "redshift_type": "redshift_iam", + "cluster_identifier": "my-cluster", + "database": "dev", + "user": "awsuser", + "region": "us-east-1", + "access_key_id": "AKIA...", + "access_key_secret": "secret" +} +``` + +## DuckDB (local files) + +```json +{ + "datasource": "duckdb", + "url": "/path/to/data", + "format": "parquet" +} +``` diff --git a/wren/justfile b/wren/justfile index 702f48b8a..c3b8034d7 100644 --- a/wren/justfile +++ b/wren/justfile @@ -53,6 +53,9 @@ test-duckdb: test-postgres: uv run pytest tests/connectors/test_postgres.py -v -m postgres +test-mysql: + uv run pytest tests/connectors/test_mysql.py -v -m mysql + test-connector marker: uv run pytest tests/connectors/ -v -m {{ marker }} diff --git a/wren/pyproject.toml b/wren/pyproject.toml index c51ac91ac..8a5315ca7 100644 --- a/wren/pyproject.toml +++ b/wren/pyproject.toml @@ -11,47 +11,54 @@ requires-python = ">=3.11" license = { text = "Apache-2.0" } keywords = ["wrenai", "wren", "sql", "semantic", "mdl", "datafusion"] classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ - "wren-core-py>=0.1", - "ibis-framework>=10", - "duckdb>=1.0", - "sqlglot>=27", - "typer>=0.12", - "pydantic>=2", - "pyarrow>=14", - "pyarrow-hotfix>=0.6", - "loguru>=0.7", - "opendal>=0.45", - "pandas>=2", - "boto3>=1.26", - # Transitive dependency constraints for security patches - "requests>=2.33.0", - "pyasn1>=0.6.3", - "pyopenssl>=26.0.0", + "wren-core-py>=0.1", + "ibis-framework>=10", + "duckdb>=1.0", + "sqlglot>=27", + "typer>=0.12", + "pydantic>=2", + "pyarrow>=14", + "pyarrow-hotfix>=0.6", + "loguru>=0.7", + "opendal>=0.45", + "pandas>=2", + "boto3>=1.26", + # Transitive dependency constraints for security patches + "requests>=2.33.0", + "pyasn1>=0.6.3", + "pyopenssl>=26.0.0", ] [project.optional-dependencies] -postgres = ["psycopg>=3", "ibis-framework[postgres]"] -mysql = ["mysqlclient>=2.2", "ibis-framework[mysql]"] -bigquery = ["ibis-framework[bigquery]", "google-auth"] -snowflake = ["ibis-framework[snowflake]"] -clickhouse = ["ibis-framework[clickhouse]"] -trino = ["ibis-framework[trino]", "trino>=0.321"] -mssql = ["ibis-framework[mssql]"] -databricks = ["databricks-sql-connector", "databricks-sdk"] -redshift = ["redshift_connector"] -spark = ["pyspark>=3.5"] -athena = ["ibis-framework[athena]"] -oracle = ["ibis-framework[oracle]", "oracledb"] -all = ["wren-engine[postgres,mysql,bigquery,snowflake,clickhouse,trino,mssql,databricks,redshift,athena,oracle]"] -dev = ["pytest>=8", "ruff>=0.4", "orjson>=3", "testcontainers[postgres]>=4"] +postgres = ["psycopg>=3", "ibis-framework[postgres]"] +mysql = ["mysqlclient>=2.2", "ibis-framework[mysql]"] +bigquery = ["ibis-framework[bigquery]", "google-auth"] +snowflake = ["ibis-framework[snowflake]"] +clickhouse = ["ibis-framework[clickhouse]"] +trino = ["ibis-framework[trino]", "trino>=0.321"] +mssql = ["ibis-framework[mssql]"] +databricks = ["databricks-sql-connector", "databricks-sdk"] +redshift = ["redshift_connector"] +spark = ["pyspark>=3.5"] +athena = ["ibis-framework[athena]"] +oracle = ["ibis-framework[oracle]", "oracledb"] +all = [ + "wren[postgres,mysql,bigquery,snowflake,clickhouse,trino,mssql,databricks,redshift,athena,oracle]", +] +dev = [ + "pytest>=8", + "ruff>=0.4", + "orjson>=3", + "testcontainers[postgres,mysql]>=4", +] [project.scripts] wren = "wren.cli:app" diff --git a/wren/src/wren/cli.py b/wren/src/wren/cli.py index b19338fe4..7fc069275 100644 --- a/wren/src/wren/cli.py +++ b/wren/src/wren/cli.py @@ -8,71 +8,135 @@ import typer -app = typer.Typer(name="wren", help="Wren Engine CLI", no_args_is_help=True) +app = typer.Typer(name="wren", help="Wren Engine CLI", no_args_is_help=False) +_WREN_HOME = Path.home() / ".wren" +_DEFAULT_MDL = _WREN_HOME / "mdl.json" +_DEFAULT_CONN = _WREN_HOME / "connection_info.json" -def _load_connection_info( + +# ── File discovery helpers ───────────────────────────────────────────────── + + +def _require_mdl(mdl: str | None) -> str: + """Return mdl arg if given, else auto-discover mdl.json from ~/.wren.""" + if mdl is not None: + return mdl + if _DEFAULT_MDL.exists(): + return str(_DEFAULT_MDL) + typer.echo( + f"Error: --mdl not specified and '{_DEFAULT_MDL}' not found.", + err=True, + ) + raise typer.Exit(1) + + +def _load_manifest(mdl: str) -> str: + """Load MDL from a file path or treat as base64 string directly.""" + path = Path(mdl).expanduser() + if path.exists(): + import base64 # noqa: PLC0415 + + content = path.read_bytes() + if path.suffix.lower() == ".json": + # Raw JSON file — base64-encode it for WrenEngine + return base64.b64encode(content).decode() + # Non-.json file — assume it already contains a base64-encoded MDL string + return content.decode().strip() + # Not a file path — treat as a raw base64 string passed directly + return mdl + + +def _load_conn( connection_info: str | None, - connection_file_path: str | None, + connection_file: str | None, + *, + required: bool = True, ) -> dict: + """Load connection dict from inline JSON or file, with ~/.wren auto-discovery. + + If neither --connection-info nor --connection-file is given, looks for + connection_info.json in ~/.wren. Raises typer.Exit(1) if required=True and nothing + is found. + """ if connection_info: try: - return json.loads(connection_info) + conn = json.loads(connection_info) except json.JSONDecodeError as e: typer.echo(f"Error: invalid JSON in --connection-info: {e}", err=True) raise typer.Exit(1) - if connection_file_path: - path = Path(connection_file_path) - if not path.exists(): + if not isinstance(conn, dict): typer.echo( - f"Error: connection file not found: {connection_file_path}", err=True + "Error: --connection-info must decode to a JSON object.", err=True ) raise typer.Exit(1) + return conn + + path_str = connection_file or ( + str(_DEFAULT_CONN) if _DEFAULT_CONN.exists() else None + ) + if path_str: + path = Path(path_str).expanduser() + if not path.exists(): + typer.echo(f"Error: connection file not found: {path_str}", err=True) + raise typer.Exit(1) try: - return json.loads(path.read_text()) + conn = json.loads(path.read_text()) except json.JSONDecodeError as e: - typer.echo(f"Error: invalid JSON in {connection_file_path}: {e}", err=True) + typer.echo(f"Error: invalid JSON in {path_str}: {e}", err=True) + raise typer.Exit(1) + if not isinstance(conn, dict): + typer.echo(f"Error: {path_str} must contain a JSON object.", err=True) raise typer.Exit(1) + return conn + + if required: + typer.echo( + f"Error: --connection-file not specified and '{_DEFAULT_CONN}' not found.", + err=True, + ) + raise typer.Exit(1) + return {} + + +def _resolve_datasource(explicit: str | None, conn_dict: dict) -> str: + """Return datasource: use explicit --datasource arg first, then pop from conn dict. + + Note: mutates conn_dict by removing the 'datasource' key so it is not + forwarded as an unknown field to WrenEngine / the connector. + """ + if explicit: + conn_dict.pop("datasource", None) + return explicit + ds = conn_dict.pop("datasource", None) + if ds: + return ds typer.echo( - "Error: either --connection-info or --connection-file must be provided", + "Error: --datasource not specified and 'datasource' key not found in connection info.", err=True, ) raise typer.Exit(1) -def _load_manifest(mdl: str) -> str: - """Load MDL from a file path or treat as base64 string directly.""" - path = Path(mdl) - if path.exists(): - import base64 # noqa: PLC0415 - - content = path.read_bytes() - # If it's a JSON file, base64-encode it - if mdl.endswith(".json"): - return base64.b64encode(content).decode() - # Otherwise assume it's already base64 - return content.decode().strip() - # Treat as inline base64 string - return mdl - - -def _make_engine( - sql: str, - datasource: str, - mdl: str, +def _build_engine( + datasource: str | None, + mdl: str | None, connection_info: str | None, connection_file: str | None, + *, + conn_required: bool = True, ): from wren.engine import WrenEngine # noqa: PLC0415 from wren.model.data_source import DataSource # noqa: PLC0415 - manifest_str = _load_manifest(mdl) - conn_dict = _load_connection_info(connection_info, connection_file) + manifest_str = _load_manifest(_require_mdl(mdl)) + conn_dict = _load_conn(connection_info, connection_file, required=conn_required) + ds_str = _resolve_datasource(datasource, conn_dict) try: - ds = DataSource(datasource.lower()) + ds = DataSource(ds_str.lower()) except ValueError: - typer.echo(f"Error: unknown datasource '{datasource}'", err=True) + typer.echo(f"Error: unknown datasource '{ds_str}'", err=True) raise typer.Exit(1) return WrenEngine( @@ -80,125 +144,201 @@ def _make_engine( ) -# ── Common options ───────────────────────────────────────────────────────── +# ── Shared option types ──────────────────────────────────────────────────── -SqlArg = Annotated[str, typer.Option("--sql", "-s", help="SQL query to execute")] DatasourceOpt = Annotated[ - str, - typer.Option("--datasource", "-d", help="Data source name (e.g. postgres, duckdb)"), + Optional[str], + typer.Option( + "--datasource", + "-d", + help="Data source (e.g. mysql, postgres). Defaults to 'datasource' field in connection_info.json.", + ), ] MdlOpt = Annotated[ - str, typer.Option("--mdl", "-m", help="Path to MDL JSON file or base64 MDL string") + Optional[str], + typer.Option( + "--mdl", + "-m", + help=f"Path to MDL JSON file or base64 string. Defaults to {_DEFAULT_MDL}.", + ), ] ConnInfoOpt = Annotated[ - Optional[str], typer.Option("--connection-info", help="JSON connection info string") + Optional[str], + typer.Option("--connection-info", help="Inline JSON connection string"), ] ConnFileOpt = Annotated[ Optional[str], - typer.Option("--connection-file", help="Path to JSON connection info file"), + typer.Option( + "--connection-file", + help=f"Path to JSON connection file. Defaults to {_DEFAULT_CONN}.", + ), ] LimitOpt = Annotated[ Optional[int], typer.Option("--limit", "-l", help="Max rows to return") ] +OutputOpt = Annotated[ + str, typer.Option("--output", "-o", help="Output format: json|csv|table") +] + + +# ── Default command (no subcommand = query) ──────────────────────────────── + + +@app.callback(invoke_without_command=True) +def main( + ctx: typer.Context, + sql: Annotated[ + Optional[str], + typer.Option( + "--sql", "-s", help="SQL query to execute (runs query by default)" + ), + ] = None, + datasource: DatasourceOpt = None, + mdl: MdlOpt = None, + connection_info: ConnInfoOpt = None, + connection_file: ConnFileOpt = None, + limit: LimitOpt = None, + output: OutputOpt = "table", +) -> None: + """Wren Engine CLI. + + Run with --sql to execute a query using mdl.json and connection_info.json from + ~/.wren. Use a subcommand (query / dry-run / dry-plan / validate) + for explicit control. + + connection_info.json format: + + \b + { + "datasource": "mysql", + "host": "localhost", + "port": 3306, + "database": "mydb", + "user": "root", + "password": "secret" + } + """ + if ctx.invoked_subcommand is not None: + return + if sql is None: + typer.echo(ctx.get_help()) + return + with _build_engine(datasource, mdl, connection_info, connection_file) as engine: + try: + result = engine.query(sql, limit=limit) + except Exception as e: + typer.echo(f"Error: {e}", err=True) + raise typer.Exit(1) + _print_result(result, output) + + +# ── Subcommands ──────────────────────────────────────────────────────────── @app.command() def query( - sql: SqlArg, - datasource: DatasourceOpt, - mdl: MdlOpt, + sql: Annotated[str, typer.Option("--sql", "-s", help="SQL query to execute")], + datasource: DatasourceOpt = None, + mdl: MdlOpt = None, connection_info: ConnInfoOpt = None, connection_file: ConnFileOpt = None, limit: LimitOpt = None, - output: Annotated[ - str, typer.Option("--output", "-o", help="Output format: json|csv|table") - ] = "table", + output: OutputOpt = "table", ): """Execute a SQL query through the Wren semantic layer.""" - engine = _make_engine(sql, datasource, mdl, connection_info, connection_file) - try: - result = engine.query(sql, limit=limit) - except Exception as e: - typer.echo(f"Error: {e}", err=True) - raise typer.Exit(1) - finally: - engine.close() - + with _build_engine(datasource, mdl, connection_info, connection_file) as engine: + try: + result = engine.query(sql, limit=limit) + except Exception as e: + typer.echo(f"Error: {e}", err=True) + raise typer.Exit(1) _print_result(result, output) @app.command(name="dry-run") def dry_run( - sql: SqlArg, - datasource: DatasourceOpt, - mdl: MdlOpt, + sql: Annotated[str, typer.Option("--sql", "-s", help="SQL query to validate")], + datasource: DatasourceOpt = None, + mdl: MdlOpt = None, connection_info: ConnInfoOpt = None, connection_file: ConnFileOpt = None, ): """Dry-run a SQL query (parse + validate, no results returned).""" - engine = _make_engine(sql, datasource, mdl, connection_info, connection_file) - try: - engine.dry_run(sql) - typer.echo("OK") - except Exception as e: - typer.echo(f"Error: {e}", err=True) - raise typer.Exit(1) - finally: - engine.close() + with _build_engine(datasource, mdl, connection_info, connection_file) as engine: + try: + engine.dry_run(sql) + typer.echo("OK") + except Exception as e: + typer.echo(f"Error: {e}", err=True) + raise typer.Exit(1) @app.command(name="dry-plan") def dry_plan( - sql: SqlArg, - datasource: DatasourceOpt, - mdl: MdlOpt, + sql: Annotated[str, typer.Option("--sql", "-s", help="SQL query to plan")], + datasource: DatasourceOpt = None, + mdl: MdlOpt = None, + connection_file: ConnFileOpt = None, ): """Plan SQL through MDL and print the expanded SQL (no DB required).""" from wren.engine import WrenEngine # noqa: PLC0415 from wren.model.data_source import DataSource # noqa: PLC0415 - manifest_str = _load_manifest(mdl) + manifest_str = _load_manifest(_require_mdl(mdl)) + # Read datasource from connection_info.json only when --datasource is not given + conn_dict = ( + _load_conn(None, connection_file, required=False) + if connection_file is not None or datasource is None + else {} + ) + ds_str = _resolve_datasource(datasource, conn_dict) + try: - ds = DataSource(datasource.lower()) + ds = DataSource(ds_str.lower()) except ValueError: - typer.echo(f"Error: unknown datasource '{datasource}'", err=True) + typer.echo(f"Error: unknown datasource '{ds_str}'", err=True) raise typer.Exit(1) - engine = WrenEngine(manifest_str=manifest_str, data_source=ds, connection_info={}) - try: - result = engine.dry_plan(sql) - typer.echo(result) - except Exception as e: - typer.echo(f"Error: {e}", err=True) - raise typer.Exit(1) - finally: - engine.close() + with WrenEngine( + manifest_str=manifest_str, data_source=ds, connection_info={} + ) as engine: + try: + result = engine.dry_plan(sql) + typer.echo(result) + except Exception as e: + typer.echo(f"Error: {e}", err=True) + raise typer.Exit(1) @app.command() def validate( - sql: SqlArg, - datasource: DatasourceOpt, - mdl: MdlOpt, + sql: Annotated[str, typer.Option("--sql", "-s", help="SQL query to validate")], + datasource: DatasourceOpt = None, + mdl: MdlOpt = None, connection_info: ConnInfoOpt = None, connection_file: ConnFileOpt = None, ): """Validate SQL can be planned and dry-run against the data source.""" - engine = _make_engine(sql, datasource, mdl, connection_info, connection_file) - try: - engine.dry_run(sql) - typer.echo("Valid") - except Exception as e: - typer.echo(f"Invalid: {e}", err=True) - raise typer.Exit(1) - finally: - engine.close() + with _build_engine(datasource, mdl, connection_info, connection_file) as engine: + try: + engine.dry_run(sql) + typer.echo("Valid") + except Exception as e: + typer.echo(f"Invalid: {e}", err=True) + raise typer.Exit(1) # ── Output formatting ────────────────────────────────────────────────────── def _print_result(table, output: str) -> None: + output = output.lower() + if output not in {"json", "csv", "table"}: + typer.echo( + f"Error: unsupported output format '{output}'. Use json, csv, or table.", + err=True, + ) + raise typer.Exit(1) if output == "json": try: df = table.to_pandas() @@ -212,7 +352,6 @@ def _print_result(table, output: str) -> None: except Exception: typer.echo(str(table)) else: - # Default: table format via pandas try: df = table.to_pandas() typer.echo(df.to_string(index=False)) diff --git a/wren/tests/conftest.py b/wren/tests/conftest.py index 9377f569d..86fb2e387 100644 --- a/wren/tests/conftest.py +++ b/wren/tests/conftest.py @@ -5,5 +5,10 @@ def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line("markers", "unit: unit tests — no database required") - config.addinivalue_line("markers", "duckdb: DuckDB connector tests — no Docker required") - config.addinivalue_line("markers", "postgres: PostgreSQL connector tests — requires Docker") + config.addinivalue_line( + "markers", "duckdb: DuckDB connector tests — no Docker required" + ) + config.addinivalue_line( + "markers", "postgres: PostgreSQL connector tests — requires Docker" + ) + config.addinivalue_line("markers", "mysql: MySQL connector tests — requires Docker") diff --git a/wren/tests/connectors/test_mysql.py b/wren/tests/connectors/test_mysql.py new file mode 100644 index 000000000..9e8eade77 --- /dev/null +++ b/wren/tests/connectors/test_mysql.py @@ -0,0 +1,92 @@ +"""MySQL connector tests. + +Uses testcontainers to spin up a real MySQL instance. +TPCH data is generated via DuckDB's built-in extension and loaded via pymysql. +""" + +from __future__ import annotations + +import base64 +from urllib.parse import urlparse + +import duckdb +import orjson +import pytest +from testcontainers.mysql import MySqlContainer + +from tests.suite.manifests import make_tpch_manifest +from tests.suite.query import WrenQueryTestSuite +from wren import WrenEngine +from wren.model.data_source import DataSource + +pytestmark = pytest.mark.mysql + +_SCHEMA = "test" # MySqlContainer default database name + + +def _load_tpch(conn_str: str) -> None: + """Generate TPCH sf=0.01 via DuckDB and bulk-load into MySQL.""" + import pymysql # noqa: PLC0415 + + with duckdb.connect() as duck: + duck.execute("INSTALL tpch; LOAD tpch; CALL dbgen(sf=0.01)") + orders_rows = duck.execute( + "SELECT o_orderkey, o_custkey, o_orderstatus, " + "cast(o_totalprice as double), o_orderdate FROM orders" + ).fetchall() + customer_rows = duck.execute( + "SELECT c_custkey, c_name FROM customer" + ).fetchall() + + parsed = urlparse(conn_str) + conn = pymysql.connect( + host=parsed.hostname, + port=parsed.port, + user=parsed.username, + password=parsed.password, + database=parsed.path.lstrip("/"), + autocommit=True, + ) + with conn: + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE orders ( + o_orderkey INT PRIMARY KEY, + o_custkey INT NOT NULL, + o_orderstatus CHAR(1) NOT NULL, + o_totalprice DOUBLE NOT NULL, + o_orderdate DATE NOT NULL + ) + """) + cur.executemany( + "INSERT INTO orders VALUES (%s, %s, %s, %s, %s)", orders_rows + ) + cur.execute(""" + CREATE TABLE customer ( + c_custkey INT PRIMARY KEY, + c_name VARCHAR(25) NOT NULL + ) + """) + cur.executemany("INSERT INTO customer VALUES (%s, %s)", customer_rows) + + +class TestMySQL(WrenQueryTestSuite): + manifest = make_tpch_manifest(table_catalog=None, table_schema=_SCHEMA) + + @pytest.fixture(scope="class") + def engine(self) -> WrenEngine: # type: ignore[override] + with MySqlContainer("mysql:8") as mysql: + url = mysql.get_connection_url() + _load_tpch(url) + + parsed = urlparse(url) + conn_info = { + "host": parsed.hostname, + "port": parsed.port, + "database": parsed.path.lstrip("/"), + "user": parsed.username, + "password": parsed.password or "", + } + manifest_str = base64.b64encode(orjson.dumps(self.manifest)).decode() + with WrenEngine(manifest_str, DataSource.mysql, conn_info) as e: + yield e diff --git a/wren/uv.lock b/wren/uv.lock index e21b44a91..a29e3033c 100644 --- a/wren/uv.lock +++ b/wren/uv.lock @@ -688,6 +688,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578 }, ] +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890 }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120 }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363 }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046 }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156 }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649 }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472 }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389 }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645 }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358 }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217 }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792 }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250 }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875 }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467 }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001 }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081 }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331 }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120 }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238 }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219 }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268 }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774 }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277 }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455 }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961 }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221 }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650 }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295 }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163 }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371 }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160 }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181 }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713 }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034 }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437 }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617 }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189 }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225 }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581 }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907 }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857 }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010 }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086 }, +] + [[package]] name = "grpcio" version = "1.78.0" @@ -1661,6 +1713,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, @@ -1703,6 +1763,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726 }, ] +[[package]] +name = "pymysql" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300 }, +] + +[package.optional-dependencies] +rsa = [ + { name = "cryptography" }, +] + [[package]] name = "pyodbc" version = "5.3.0" @@ -2053,6 +2127,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016 }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184 }, + { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555 }, + { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057 }, + { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431 }, + { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646 }, + { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956 }, + { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627 }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737 }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020 }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983 }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690 }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738 }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546 }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484 }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599 }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825 }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200 }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876 }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045 }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700 }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851 }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525 }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611 }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812 }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335 }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095 }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401 }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528 }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523 }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312 }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565 }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205 }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519 }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611 }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326 }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453 }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209 }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198 }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202 }, +] + [[package]] name = "sqlglot" version = "29.0.1" @@ -2087,6 +2214,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl", hash = "sha256:03dfef4797b31c82e7b762a454b6afec61a2a512ad54af47ab41e4fa5415f891", size = 125640 }, ] +[package.optional-dependencies] +mysql = [ + { name = "pymysql", extra = ["rsa"] }, + { name = "sqlalchemy" }, +] + [[package]] name = "thrift" version = "0.20.0" @@ -2341,7 +2474,7 @@ dev = [ { name = "orjson" }, { name = "pytest" }, { name = "ruff" }, - { name = "testcontainers" }, + { name = "testcontainers", extra = ["mysql"] }, ] mssql = [ { name = "ibis-framework", extra = ["mssql"] }, @@ -2423,7 +2556,7 @@ requires-dist = [ { name = "requests", specifier = ">=2.33.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, { name = "sqlglot", specifier = ">=27" }, - { name = "testcontainers", extras = ["postgres"], marker = "extra == 'dev'", specifier = ">=4" }, + { name = "testcontainers", extras = ["mysql", "postgres"], marker = "extra == 'dev'", specifier = ">=4" }, { name = "trino", marker = "extra == 'all'", specifier = ">=0.321" }, { name = "trino", marker = "extra == 'trino'", specifier = ">=0.321" }, { name = "typer", specifier = ">=0.12" },